/** * 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://github.com/givanz/Vvveb */ // Simple JavaScript Templating and buildParams // John Resig - https://johnresig.com/ - MIT Licensed (function(){ let cache = {}; let startTag = "{%"; let endTag = "%}"; let re1 = new RegExp(`((^|${endTag})[^\t]*)'`,"g"); let re2 = new RegExp(`\t=(.*?)${endTag}`,"g"); this.tmpl = function tmpl(str, data){ // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. let fn = /^[-a-zA-Z0-9]+$/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "let p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript str .replace(/[\r\t\n]/g, " ") .split(startTag).join("\t") .replace(re1, "$1\r") .replace(re2, "',$1,'") .split("\t").join("');") .split(endTag).join("p.push('") .split("\r").join("\\'") + "');}return p.join('');"); // Provide some basic currying to the user return data ? fn( data ) : fn; }; })(); function buildParams( prefix, obj, add ) { let rbracket = /\[\]$/; if ( Array.isArray( obj ) ) { // Serialize array item. for(const key in obj) { let v = obj[key]; if ( rbracket.test( prefix ) ) { // Treat each array item as a scalar. add( prefix, v ); } else { // Item is non-scalar (array or object), encode its numeric index. buildParams( prefix + "[" + ( typeof v === "object" && v != null ? key : "" ) + "]", v, add ); } } } else if ( typeof obj === "object" ) { // Serialize object item. for (const name in obj ) { buildParams( prefix + "[" + name + "]", obj[ name ], add ); } } else { // Serialize scalar item. add( prefix, obj ); } } // Serialize an array of form elements or a set of // key/values into a query string function nestedFormData( a ) { let prefix, s = [], add = function( key, valueOrFunction ) { // If value is a function, invoke it and use its return value let value = typeof valueOrFunction === "function" ? valueOrFunction() : valueOrFunction; s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value == null ? "" : value ); }; if ( a == null ) { return ""; } // If an array was passed in, assume that it is an array of form elements. if ( Array.isArray( a ) || ( Object.is( a ) ) ) { // Serialize the form elements for(const key in object) { let v = object[key]; //jQuery.each( a, function() { add( key, v ); }; } else { // If traditional, encode the "old" way (the way 1.3.2 or older // did it), otherwise encode params recursively. for (const prefix in a ) { buildParams( prefix, a[ prefix ], add ); } } // Return the resulting serialization return s.join( "&" ); }; let delay = (function(){ let timer = 0; return function(callback, ms){ clearTimeout (timer); timer = setTimeout(callback, ms); }; })(); function isElement(obj){ return (typeof obj==="object") && (obj.nodeType===1) && (typeof obj.style === "object") && (typeof obj.ownerDocument ==="object")/* && obj.tagName != "BODY"*/; } function generateElements(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.children; } function offset(el) { box = el.getBoundingClientRect(); docElem = document.documentElement; return { top: box.top + window.pageYOffset - docElem.clientTop, left: box.left + window.pageXOffset - docElem.clientLeft }; } if (Vvveb === undefined) var Vvveb = {}; Vvveb.defaultComponent = "_base"; Vvveb.preservePropertySections = true; //icon = use component icon when dragging | html = use component html to create draggable element Vvveb.dragIcon = 'icon'; //if empty the html of the component is used to view dropping in real time but for large elements it can jump around for this you can set a html placeholder with this option Vvveb.dragElementStyle = "background:limegreen;width:100%;height:3px;border:1px solid limegreen;box-shadow:0px 0px 2px 1px rgba(0,0,0,0.14);overflow:hidden;"; Vvveb.dragHtml = '
'; Vvveb.baseUrl = document.currentScript?document.currentScript.src.replace(/[^\/]*?\.js$/,''):''; Vvveb.imgBaseUrl = Vvveb.baseUrl; Vvveb.ComponentsGroup = {}; Vvveb.SectionsGroup = {}; Vvveb.BlocksGroup = {}; Vvveb.StylesGroup = {}; Vvveb.Components = { _components: {}, _nodesLookup: {}, _attributesLookup: {}, _classesLookup: {}, _classesRegexLookup: {}, componentPropertiesElement: "#right-panel .component-properties", componentPropertiesDefaultSection: "content", get: function(type) { return this._components[type]; }, updateProperty: function(type, key, value) { let properties = this._components[type]["properties"]; for (property in properties) { if (key == properties[property]["key"]) { return this._components[type]["properties"][property] = Object.assign(properties[property], value); } } }, getProperty: function(type, key) { let properties = this._components[type] ? this._components[type]["properties"] : []; for (property in properties) { if (key == properties[property]["key"]) { return properties[property]; } } }, add: function(type, data) { data.type = type; this._components[type] = data; if (data.nodes) { for (let i in data.nodes) { this._nodesLookup[ data.nodes[i] ] = data; } } if (data.attributes) { if (data.attributes.constructor === Array) { for (let i in data.attributes) { this._attributesLookup[ data.attributes[i] ] = data; } } else { for (let i in data.attributes) { if (typeof this._attributesLookup[i] === 'undefined') { this._attributesLookup[i] = {}; } if (typeof this._attributesLookup[i][ data.attributes[i] ] === 'undefined') { this._attributesLookup[i][ data.attributes[i] ] = {}; } this._attributesLookup[i][ data.attributes[i] ] = data; } } } if (data.classes) { for (let i in data.classes) { this._classesLookup[ data.classes[i] ] = data; } } if (data.classesRegex) { for (let i in data.classesRegex) { this._classesRegexLookup[ data.classesRegex[i] ] = data; } } }, extend: function(inheritType, type, data) { let newData = data; if (inheritData = this._components[inheritType]) { newData = {...inheritData, ...data}; newData.properties = (data.properties ? data.properties : []).concat(inheritData.properties ? inheritData.properties : []); } //remove duplicates /* newData.properties = newData.properties.filter((value, index, self) => index === self.findIndex((t) => ( t.key === value.key )) );*/ //sort by order newData.properties.sort(function (a,b) { if (typeof a.sort === "undefined") a.sort = 0; if (typeof b.sort === "undefined") b.sort = 0; if (a.sort < b.sort) return -1; if (a.sort > b.sort) return 1; return 0; }); this.add(type, newData); }, matchNode: function(node) { let component = {}; if (!node || !node.tagName) return false; if (node.attributes && node.attributes.length) { //search for attributes for (let i in node.attributes) { if (node.attributes[i]) { attr = node.attributes[i].name; value = node.attributes[i].value; if (attr in this._attributesLookup) { component = this._attributesLookup[ attr ]; //currently we check that is not a component by looking at name attribute //if we have a collection of objects it means that attribute value must be checked if (typeof component["name"] === "undefined") { if (value in component) { return component[value]; } } else { return component; } } } } for (let i in node.attributes) { attr = node.attributes[i].name; value = node.attributes[i].value; //check for node classes if (attr == "class") { classes = value.split(" "); for (j in classes) { if (classes[j] in this._classesLookup) return this._classesLookup[ classes[j] ]; } for (regex in this._classesRegexLookup) { regexObj = new RegExp(regex); if (regexObj.exec(value)) { return this._classesRegexLookup[ regex ]; } } } } } tagName = node.tagName.toLowerCase(); if (tagName in this._nodesLookup) return this._nodesLookup[ tagName ]; return false; //return false; }, renderProperties: function(component, properties, componentsPanelSections, container) { let fn = function(component, property) { if (property.input) { property.input.addEventListener('propertyChange', (event) => { element = selectedElement = Vvveb.Builder.selectedEl; let value = event.detail.value, input = event.detail.input, origEvent = event.detail.origEvent; if (property.element) element = property.element; if (property.child) element = element.querySelector(property.child); if (property.parent) element = element.parent(property.parent); if (property.onChange) { let ret = property.onChange(element, value, input, component, origEvent); //if on change returns an object then is returning the dom node otherwise is returning the new value if (typeof ret == "object") { element = ret; } else { value = ret; } }/* else */ if (property.htmlAttr) { oldValue = element.getAttribute(property.htmlAttr); if (property.htmlAttr == "class" && property.validValues) { if (property.validValues) { element.classList.remove(...property.validValues.filter(v => v)); } if (value) { element.classList.add(...value.split(" ")); } } else if (property.htmlAttr == "style") { //keep old style for undo oldStyle = window.FrameDocument.getElementById("vvvebjs-styles").textContent; element = Vvveb.StyleManager.setStyle(element, property.key, value); } else if (property.htmlAttr == "innerHTML") { element = Vvveb.ContentManager.setHtml(element, value); } else if (property.htmlAttr == "innerText") { element = Vvveb.ContentManager.setText(element, value); } else { //if value is empty then remove attribute useful for attributes without values like disabled if (value) { element.setAttribute(property.htmlAttr, value); } else { element.removeAttribute(property.htmlAttr); } } if (property.htmlAttr == "style") { mutation = { type: 'style', target: element, attributeName: property.htmlAttr, oldValue: oldStyle, newValue: window.FrameDocument.getElementById("vvvebjs-styles").textContent}; Vvveb.Undo.addMutation(mutation); } else { Vvveb.Undo.addMutation({ type: 'attributes', target: element, attributeName: property.htmlAttr, oldValue: oldValue, newValue: value }); } } if (component && component.onChange) { element = component.onChange(element, property, value, input); } if (property.child || property.parent) { Vvveb.Builder.selectNode(selectedElement); } else { Vvveb.Builder.selectNode(element); } return element; }); } return property.input; }; let element; let defaultSection = this.componentPropertiesDefaultSection; let section; if (componentsPanelSections) { section = componentsPanelSections[defaultSection].querySelector('.section[data-section="default"]'); } let nodeElement = Vvveb.Builder.selectedEl; for (let i in properties) { let property = properties[i]; let element = nodeElement; if (property.beforeInit) property.beforeInit(element); if (property.element) element = property.element; if (property.child) element = element.querySelector(property.child) ?? element; if (property.parent) element = element.closest(property.parent) ?? element; if (property.data) { property.data["key"] = property.key; } else { property.data = {"key" : property.key}; } if (typeof property.group === 'undefined') property.group = null; property.input = property.inputtype.init(property.data, element); let value; if (property.init) { property.inputtype.setValue(property.init(element)); } else if (property.htmlAttr) { if (property.htmlAttr == "style") { //value = element.css(property.key);//jquery css returns computed style value = Vvveb.StyleManager.getStyle(element, property.key);//getStyle returns declared style } else if (property.htmlAttr == "innerHTML") { value = Vvveb.ContentManager.getHtml(element); } else if (property.htmlAttr == "innerText") { value = Vvveb.ContentManager.getText(element); } else { value = element.getAttribute(property.htmlAttr); } //if attribute is class check if one of valid values is included as class to set the select if (value && property.htmlAttr == "class" && property.validValues) { let valid = value.split(" ").filter(function(el) { return property.validValues.indexOf(el) != -1 }); if (valid && valid.length) { value = valid[0]; } else { value = ""; } } if (!value && property.defaultValue) { value = property.defaultValue; } property.inputtype.setValue(value); } else { if (!value && property.defaultValue) { value = property.defaultValue; } property.inputtype.setValue(value); } fn(component, property); let propertySection = defaultSection; if (property.section) { propertySection = property.section; } if (property.inputtype == SectionInput) { section = componentsPanelSections[propertySection].querySelector('.section[data-section="' + property.key + '"]'); if (Vvveb.preservePropertySections && section) { section.replaceChildren(); } else { componentsPanelSections[propertySection].append(property.input); section = componentsPanelSections[propertySection].querySelector('.section[data-section="' + property.key + '"]'); } } else { let row = generateElements(tmpl('vvveb-property', property))[0]; row.querySelector('.input').append(property.input); if (container) { container.append(row); } else { section.append(row); } } if (property.inputtype.afterInit) { property.inputtype.afterInit(property.input); } if (property.afterInit) { property.afterInit(element, property.input); } } }, render: function(type, panel = false) { let component = this._components[type]; if (!component) return; if (!panel) { //panel = document.querySelector(this.componentPropertiesElement); panel = this.componentPropertiesElement; } let defaultSection = this.componentPropertiesDefaultSection; let componentsPanelSections = {}; document.querySelectorAll(panel + " .tab-pane").forEach((el, i) => { let sectionName = el.dataset.section; componentsPanelSections[sectionName] = el; for (const item of el.querySelectorAll( 'label:not([data-header="default"]) + input,' + 'label:not([data-header="default"]),' + '.section:not([data-section="default"])' )) { item.remove(); } }); let section = componentsPanelSections[defaultSection].querySelector('.section[data-section="default"]'); if (!(Vvveb.preservePropertySections && section)) { let template = tmpl("vvveb-input-sectioninput", {key:"default", header:component.name}); componentsPanelSections[defaultSection].replaceChildren(); componentsPanelSections[defaultSection].append(generateElements(template)[0]); section = componentsPanelSections[defaultSection].querySelector(".section"); } componentsPanelSections[defaultSection].querySelector('[data-header="default"] span').innerHTML = component.name; section.replaceChildren(); if (component.beforeInit) component.beforeInit(Vvveb.Builder.selectedEl); this.renderProperties(component, component.properties, componentsPanelSections); if (component.init) component.init(Vvveb.Builder.selectedEl); } }; Vvveb.Blocks = { _blocks: {}, get: function(type) { return this._blocks[type]; }, add: function(type, data) { data.type = type; this._blocks[type] = data; }, }; Vvveb.Sections = { _sections: {}, get: function(type) { return this._sections[type]; }, add: function(type, data) { data.type = type; this._sections[type] = data; }, }; Vvveb.Styles = { _styles: {}, get: function(type) { return this._styles[type]; }, add: function(type, data) { data.type = type; this._styles[type] = data; }, }; Vvveb.WysiwygEditor = { isActive: false, oldValue: '', doc:false, editorSetStyle: function (tag, style = {}, toggle = false) { let iframeWindow = Vvveb.Builder.iframe.contentWindow; let selection = iframeWindow.getSelection(); let element = this.element; let range; if (!tag) { tag = "span"; } if (selection.rangeCount > 0) { //check if the whole text is inside an existing node to use the node directly if ((selection.baseNode && selection.baseNode.nextSibling == null && selection.baseNode.previousSibling == null && selection.anchorOffset == 0 && selection.focusOffset == selection.baseNode.length) || (selection.anchorOffset == selection.focusOffset)) { element = selection.baseNode.parentNode; } else { element = document.createElement(tag); range = selection.getRangeAt(0); try { range.surroundContents(element); range.selectNodeContents(element.childNodes[0], 0); } catch (e) { let content = range.extractContents(); element.appendChild(content); range.insertNode(element); range.selectNodeContents(element); } } } if (element && style) { for (name in style) { if ( !style[name] || (toggle && element.style.getPropertyValue(name))) { element.style.removeProperty(name); } else { element.style.setProperty(name, style[name]); } } } //if edited text is an empty span remove the span if (element.tagName == "SPAN" && element.style.length == 0 && element.attributes.length <= 1) { let textNode = iframeWindow.document.createTextNode(element.innerText); element.replaceWith(textNode); element = textNode; range = iframeWindow.document.createRange(); range.selectNodeContents(element); selection.removeAllRanges(); selection.addRange(range); } //select link element to edit link etc if (tag == "a") { Vvveb.Builder.selectNode(element); Vvveb.Builder.loadNodeComponent(element); } return element; }, init: function(doc) { this.doc = doc; let self = this; document.getElementById("bold-btn").addEventListener("click", function (e) { //doc.execCommand('bold',false,null); //self.editorSetStyle("b", {"font-weight" : "bold"}, true); self.editorSetStyle(false, {"font-weight" : "bold"}, true); e.preventDefault(); return false; }); document.getElementById("italic-btn").addEventListener("click", function (e) { //doc.execCommand('italic',false,null); //self.editorSetStyle("i", {"font-style" : "italic"}, true); self.editorSetStyle(false, {"font-style" : "italic"}, true); e.preventDefault(); return false; }); document.getElementById("underline-btn").addEventListener("click", function (e) { //doc.execCommand('underline',false,null); //self.editorSetStyle("u", {"text-decoration" : "underline"}, true); self.editorSetStyle(false, {"text-decoration" : "underline"}, true); e.preventDefault(); return false; }); document.getElementById("strike-btn").addEventListener("click", function (e) { //doc.execCommand('strikeThrough',false,null); //self.editorSetStyle("strike", {"text-decoration" : "line-through"}, true); self.editorSetStyle(false, {"text-decoration" : "line-through"}, true); e.preventDefault(); return false; }); document.getElementById("link-btn").addEventListener("click", function (e) { //doc.execCommand('createLink',false,"#"); self.editorSetStyle("a"); e.preventDefault(); return false; }); document.getElementById("fore-color").addEventListener("change", function (e) { //doc.execCommand('foreColor',false,this.value); self.editorSetStyle(false, {"color" : this.value}); e.preventDefault(); return false; }); document.getElementById("back-color").addEventListener("change", function (e) { //doc.execCommand('hiliteColor',false,this.value); self.editorSetStyle(false, {"background-color" : this.value}); e.preventDefault(); return false; }); document.getElementById("font-size").addEventListener("change", function (e) { //doc.execCommand('fontSize',false,this.value); self.editorSetStyle(false, {"font-size" : this.value}); e.preventDefault(); return false; }); let sizes = ""; for (i = 1;i <= 128; i++) { sizes += ""; } document.getElementById("font-size").innerHTML = sizes; document.getElementById("font-family").addEventListener("change", function (e) { let option = this.options[this.selectedIndex]; let element = self.editorSetStyle(false, {"font-family" : this.value}); Vvveb.FontsManager.addFont(option.dataset.provider, this.value, element); //doc.execCommand('fontName',false,this.value); e.preventDefault(); return false; }); document.getElementById("justify-btn").addEventListener("click", function (e) { //let command = "justify" + this.dataset.value; //doc.execCommand(command,false,"#"); self.editorSetStyle(false, {"text-align" : e.srcElement.dataset.value}); e.preventDefault(); return false; }); doc.addEventListener('keydown', event => { if (event.key === 'Enter') { let target = event.target.closest("[contenteditable]"); if (target) { doc.execCommand('insertLineBreak'); event.preventDefault(); } } }) }, undo: function(element) { this.doc.execCommand('undo',false,null); }, redo: function(element) { this.doc.execCommand('redo',false,null); }, edit: function(element) { element.setAttribute("contenteditable", true); element.setAttribute("spellcheckker", false); document.getElementById("wysiwyg-editor").style.display = "block"; this.element = element; this.isActive = true; this.oldValue = element.innerHTML; document.getElementById("font-family").value = Vvveb.StyleManager.getStyle(element,'font-family'); document.getElementById("fore-color").value = Vvveb.StyleManager.getStyle(element,'color'); document.getElementById("back-color").value = Vvveb.StyleManager.getStyle(element,'background-color'); element.focus(); }, destroy: function(element) { element.removeAttribute("contenteditable"); element.removeAttribute("spellcheckker"); document.getElementById("wysiwyg-editor").style.display = "none"; this.isActive = false; node = this.element; Vvveb.Undo.addMutation({type:'characterData', target: node, oldValue: this.oldValue, newValue: node.innerHTML}); } } Vvveb.Builder = { component : {}, dragMoveMutation : false, isPreview : false, runJsOnSetHtml : false, designerMode : false, highlightEnabled : false, selectPadding: 0, leftPanelWidth: 275, ignoreClasses: ["clearfix", "masonry", "has-shadow"], init: function(url, callback) { let self = this; self.loadControlGroups(); self.loadBlockGroups(); self.loadSectionGroups(); self.loadStylesGroups(); self.selectedEl = null; self.highlightEl = null; self.initCallback = callback; self.documentFrame = document.querySelector("#iframe-wrapper > iframe"); self.canvas = document.getElementById("canvas"); self._loadIframe(url + (url.indexOf('?') > -1 ? '&r=':'?r=') + Math.random()); self._initDragdrop(); self._initBox(); self.dragElement = null; self.highlightEnabled = true; self.leftPanelWidth = document.getElementById("left-panel").clientWidth; }, /* controls */ loadControlGroups : function() { let componentsList = document.querySelectorAll(".components-list"); let item = {}, component = {}; let count = 0; componentsList.forEach(function (list, i) { let type = list.dataset.type; list.replaceChildren(); count ++; for (group in Vvveb.ComponentsGroup) { list.append(generateElements( `
    1. `)[0]); //list.append('
      1. '); let componentsSubList = list.querySelector('li[data-section="' + group + '"] ol'); components = Vvveb.ComponentsGroup[ group ]; for (i in components) { const componentType = components[i]; component = Vvveb.Components.get(componentType); if (component) { item = generateElements(`
      2. ${component.name}
      3. `)[0]; if (component.image) { item.style.backgroundImage = "url(" + Vvveb.imgBaseUrl + component.image + ")"; item.style.backgroundRepeat = "no-repeat"; } componentsSubList.append(item); } } } }); }, loadSectionGroups : function() { let sectionsList = document.querySelectorAll(".sections-list"); let item = {}; sectionsList.forEach(function (list, i) { let type = list.dataset.type; list.replaceChildren(); for (group in Vvveb.SectionsGroup) { list.append(generateElements( `
        1. `)[0]); let sectionsSubList = list.querySelector('li[data-section="' + group + '"] ol'); let sections = Vvveb.SectionsGroup[ group ]; for (i in sections) { const sectionType = sections[i]; const section = Vvveb.Sections.get(sectionType); if (section) { item = generateElements(`
        2. ${section.name}
        3. `)[0]; if (section.image) { let image = ((section.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + section.image; item.querySelector("img").setAttribute("src", image); } sectionsSubList.append(item) } } } }); }, loadBlockGroups : function() { let blocksList = document.querySelectorAll(".blocks-list"); let item = {}; blocksList.forEach(function (list, i) { let type = list.dataset.type; list.replaceChildren(); for (group in Vvveb.BlocksGroup) { list.append(generateElements( `
          1. `)[0]); let blocksSubList = list.querySelector('li[data-section="' + group + '"] ol'); blocks = Vvveb.BlocksGroup[ group ]; for (i in blocks) { const blockType = blocks[i]; const block = Vvveb.Blocks.get(blockType); if (block) { item = generateElements(`
          2. ${block.name}
          3. `)[0]; if (block.image) { let image = ((block.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + block.image; item.querySelector("img").setAttribute("src", image); } blocksSubList.append(item); } } } }); }, loadStylesGroups : function() { let stylesList = document.querySelectorAll(".styles-list"); let item = {}; stylesList.forEach(function (list, i) { let type = list.dataset.type; list.replaceChildren(); for (group in Vvveb.StylesGroup) { list.append(generateElements( `
            1. `)[0]); let stylesSubList = list.querySelector('li[data-section="' + group + '"] ol'); styles = Vvveb.StylesGroup[ group ]; for (i in styles) { const styleType = styles[i]; const style = Vvveb.Blocks.get(styleType); if (style) { item = generateElements(`
            2. ${style.name}
            3. `)[0]; if (style.image) { let image = ((style.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + style.image; item.querySelector("img").setAttribute("src", image); } stylesSubList.append(item); } } } }); }, loadUrl : function(url, callback) { let self = this; document.getElementById("select-box").style.display = "none"; self.initCallback = callback; if (Vvveb.Builder.iframe.src != url) Vvveb.Builder.iframe.src = url; }, /* iframe */ _loadIframe : function(url) { let self = this; self.iframe = this.documentFrame; self.iframe.src = url; return this.documentFrame.addEventListener("load", function() { window.FrameWindow = self.iframe.contentWindow; window.FrameDocument = self.iframe.contentWindow.document; let addSectionBox = document.getElementById("add-section-box"); let highlightBox = document.getElementById("highlight-box"); let SelectBox = document.getElementById("select-box"); highlightBox.style.display = "none"; window.FrameWindow.addEventListener("beforeunload", function(event) { if (Vvveb.Undo.undoIndex >= 0) { let dialogText = "You have unsaved changes"; event.returnValue = dialogText; return dialogText; } }); window.FrameWindow.addEventListener("unload", function(event) { document.querySelector(".loading-message").classList.add("active"); Vvveb.Undo.reset(); }); //prevent accidental clicks on links when editing text window.FrameDocument.addEventListener("click", function(event) { if (Vvveb.WysiwygEditor.isActive && event.target.closest("a")) { event.preventDefault(); return false; } }); selectBoxPosition = function(event) { let pos; let target = self.selectedEl;// ?? self.highlightEl; highlightBox.style.display = "none"; if (target) { pos = offset(target); SelectBox.style.top = (pos.top - (self.frameDoc.scrollTop ?? 0) - self.selectPadding) + "px"; SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px"; SelectBox.style.width = ((target.offsetWidth ?? target.clientWidth) + self.selectPadding * 2) + "px"; SelectBox.style.height = ((target.offsetHeight ?? target.clientHeight) + self.selectPadding * 2) + "px"; } } window.FrameWindow.addEventListener("scroll", selectBoxPosition); window.FrameWindow.addEventListener("resize", selectBoxPosition); Vvveb.WysiwygEditor.init(window.FrameDocument); Vvveb.StyleManager.init(window.FrameDocument); Vvveb.ColorPaletteManager.init(window.FrameDocument); if (self.initCallback) self.initCallback(); return self._frameLoaded(); }); }, _frameLoaded : function() { let self = Vvveb.Builder; self.frameDoc = window.FrameDocument; self.frameHtml = window.FrameDocument.querySelector("html"); self.frameBody = window.FrameDocument.querySelector("body"); self.frameHead = window.FrameDocument.querySelector("head"); //insert editor helpers like non editable areas self.frameHead.append(generateElements('')[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"); //enable save button only if changes are made let setSaveButtonState = function (e) { if (Vvveb.Undo.hasChanges()){ document.querySelectorAll("#top-panel .save-btn").forEach(e => e.removeAttribute("disabled")); } else { document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true")); } }; Vvveb.Builder.frameBody.addEventListener("vvveb.undo.add", setSaveButtonState); Vvveb.Builder.frameBody.addEventListener("vvveb.undo.restore", setSaveButtonState); }, _getElementType: function(el) { //search for component attribute let componentName = ''; let componentAttribute = ''; if (el.attributes) { for (let j = 0; j < el.attributes.length; j++){ let nodeName = el.attributes[j].nodeName; if (nodeName.indexOf('data-component') > -1) { componentName = nodeName.replace('data-component-', ''); return [componentName, "component"]; } if (nodeName.indexOf('data-v-component-') > -1) { componentName = nodeName.replace('data-v-component-', ''); return [componentName,"component"]; } if (nodeName.indexOf('data-v-') > -1) { componentAttribute = (componentAttribute ? componentAttribute + " - " : "") + nodeName.replace('data-v-', '') + " "; } } } if (componentAttribute != '') return [componentAttribute, "attribute"]; if (el.id) { componentName = "#" + el.id; } else { componentName = (el.className && (typeof el.className == "string")) ? "." + el.className.split(" ")[0] : ""; } return [componentName, el.tagName]; }, loadNodeComponent: function(node) { const data = Vvveb.Components.matchNode(node); let component; if (data) component = data.type; else component = Vvveb.defaultComponent; Vvveb.component = Vvveb.Components.get(component); Vvveb.Components.render(component); this.selectedComponent = component; //if component properties is loaded in left panel tab instead of right panel show tab let propertiesTab = document.querySelector(".component-properties-tab a"); if (propertiesTab.offsetParent) {//if properites tab is enabled/visible propertiesTab.style.display = ""; const bsTab = bootstrap.Tab.getOrCreateInstance(propertiesTab); bsTab.show(); } }, reloadComponent: function() { Vvveb.Components.render(this.selectedComponent); }, moveNodeUp: function(node) { if (!node) { node = Vvveb.Builder.selectedEl; } const oldParent = node.parentNode; const oldNextSibling = node.nextSibling; const next = node.previousElementSibling; if (next) { next.before(node); } else { node.parentNode.before(node); } Vvveb.Builder.selectNode(node); const newParent = node.parentNode; const newNextSibling = node.nextSibling; Vvveb.Undo.addMutation({type: 'move', target: node, oldParent: oldParent, newParent: newParent, oldNextSibling: oldNextSibling, newNextSibling: newNextSibling}); }, moveNodeDown: function(node) { if (!node) { node = Vvveb.Builder.selectedEl; } const oldParent = node.parentNode; const oldNextSibling = node.nextSibling; const next = node.nextElementSibling; if (next) { next.after(node); } else { node.parentNode.after(node); } Vvveb.Builder.selectNode(node); const newParent = node.parentNode; const newNextSibling = node.nextSibling; Vvveb.Undo.addMutation({type: 'move', target: node, oldParent: oldParent, newParent: newParent, oldNextSibling: oldNextSibling, newNextSibling: newNextSibling}); }, cloneNode: function(node) { if (!node) { node = Vvveb.Builder.selectedEl; } const clone = node.cloneNode(true); node.after(clone); node.click(); Vvveb.Undo.addMutation({type: 'childList', target: node.parentNode, addedNodes: [clone], nextSibling: node.nextSibling}); }, selectNode: function(node) { let SelectBox = document.getElementById("select-box"); if (!node) { SelectBox.style.display = "none"; return; } let self = this; let SelectActions = document.getElementById("select-actions"); let AddSectionBtn = document.getElementById("add-section-btn"); let elementType = this._getElementType(node); if (self.texteditEl && (self.selectedEl != node)) { Vvveb.WysiwygEditor.destroy(self.texteditEl); self.selectPadding = 0; SelectBox.classList.remove("text-edit"); SelectActions.style.display = ""; self.texteditEl = null; } if (elementType[1] == "BODY") { SelectActions.style.display = "none"; AddSectionBtn.style.display = "none"; } else { SelectActions.style.display = ""; AddSectionBtn.style.display = ""; } let target = node; self.selectedEl = target; try { let pos = offset(target); let top = (pos.top - (self.frameDoc.scrollTop ?? 0) - self.selectPadding); SelectBox.style.top = top + "px"; SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px"; SelectBox.style.width = ((target.offsetWidth ?? target.clientWidth) + self.selectPadding * 2) + "px"; SelectBox.style.height = ((target.offsetHeight ?? target.clientHeight) + self.selectPadding * 2) + "px"; SelectBox.style.display = "block"; //move actions toolbar to bottom if there is no space on top if (top < 30) { SelectActions.style.top = "unset"; SelectActions.style.bottom = "-25px"; } else { SelectActions.style.top = ""; SelectActions.style.bottom = ""; } Vvveb.Breadcrumb.loadBreadcrumb(target); } catch(err) { console.log(err); return false; } document.querySelector("#highlight-name .type").innerHTML = elementType[0]; document.querySelector("#highlight-name .name").innerHTML = elementType[1]; window.dispatchEvent(new CustomEvent("vvveb.Builder.selectNode", {detail: {target}})); }, /* iframe highlight */ _initHighlight: function() { let self = Vvveb.Builder; let highlightMove = function(event) { if (self.highlightEnabled == true && event.target && isElement(event.target)) { self.highlightEl = target = event.target; let pos = offset(target); let height = target.offsetHeight; let halfHeight = Math.max(height / 2, 5); let width = target.offsetWidth; let halfWidth = Math.max(width / 2, 5); let prepend = true; let x = event.x; let y = event.y; if (self.isResize) { if (!self.initialPosition) { self.initialPosition = {x,y}; } let deltaX = x - self.initialPosition.x; let deltaY = y - self.initialPosition.y; pos = offset(self.selectedEl); width = self.initialSize.width; height = self.initialSize.height; switch (self.resizeHandler) { // top case "top-left": height -= deltaY; width -= deltaX; break; case "top-center": height -= deltaY; break; case "top-right": height -= deltaY; width += deltaX; break; // center case "center-left": width -= deltaX; break; case "center-right": width += deltaX; break; // bottom case "bottom-left": width -= deltaX; height += deltaY; break; case "bottom-center": height += deltaY; break; case "bottom-right": width += deltaX; height += deltaY; break; } if (self.resizeMode == "css") { self.selectedEl.style.width = width + "px"; self.selectedEl.style.height = height + "px"; } else { self.selectedEl.setAttribute("width", width); self.selectedEl.setAttribute("height", height); } let SelectBox = document.getElementById("select-box"); SelectBox.style.top = pos.top - (self.frameDoc.scrollTop ?? 0) + "px"; SelectBox.style.left = pos.left - (self.frameDoc.scrollLeft ?? 0) + "px"; SelectBox.style.width = width + "px"; SelectBox.style.height = self.selectedEl.offsetHeight + "px"; SelectBox.style.display = "block"; } else if (self.isDragging) { let noChildren = { input: true, textarea: true, img: true, svg: true, iframe: true, embed: true, col: true, area: true, hr: true, br: true, wbr: true }; let parent = self.highlightEl; if (self.dragType == "section") { let closest = parent.closest("section, header, footer, body"); if (closest) { parent = closest; } noChildren.section = true; } let parentTagName = parent.tagName.toLowerCase(); let isVattribute = false; //check if node is a data-v-attribute dynamic node that will override the content if added inside if (parent.childElementCount == 0) { for (let attr of parent.attributes) { if (attr.name.startsWith("data-v-") && !attr.name.startsWith("data-v-component-")) { isVattribute = true; break; } } } try { if ((pos.top < (y - halfHeight)) || (pos.left < (x - halfWidth))) { if (noChildren[parentTagName] || 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] || isVattribute) { parent.parentNode.insertBefore(self.dragElement, parent); } else { parent.prepend(self.dragElement); } prepend = false; }; if (self.designerMode) { let parentOffset = offset(self.dragElement.offsetParent); self.dragElement.style.position = "absolute"; self.dragElement.style.x = x - (parentOffset.left - self.frameDoc.scrollLeft); self.dragElement.style.y = y - (parentOffset.top - self.frameDoc.scrollTop); } } catch(err) { console.log(err); return false; } if (!self.designerMode && self.iconDrag) { self.iconDrag.style.top = (y + 60) + "px"; self.iconDrag.style.left = (x + self.leftPanelWidth + 10) + "px"; } }// else //uncomment else to disable parent highlighting when dragging { //if text editor is open check if the highlighted element is not inside the editor if (Vvveb.WysiwygEditor.isActive ) { if (self.texteditEl.contains(event.target)) { return true; } } document.getElementById("highlight-box").setAttribute("style", `top:${pos.top - (self.frameDoc.scrollTop ?? 0)}px; left:${pos.left - (self.frameDoc.scrollLeft ?? 0)}px; width:${width}px; height:${height}px; display:${event.target.hasAttribute('contenteditable') ? "none":"block"}; border:${self.isDragging ? "1px dashed #0d6efd":""}; `); if (height < 50) { document.getElementById("section-actions").classList.add("outside"); } else { document.getElementById("section-actions").classList.remove("outside"); } let elementType = self._getElementType(event.target); document.querySelector("#highlight-name .type").innerHTML = elementType[0]; document.querySelector("#highlight-name .name").innerHTML = elementType[1]; } } }; self.frameBody.addEventListener("mousemove", highlightMove); let highlightUp = function(event) { self.isResize = false; document.querySelectorAll("#section-actions, #highlight-name").forEach(el => el.style.display = ""); if (self.isDragging) { self.isDragging = false; Vvveb.Builder.highlightEnabled = true; if (self.iconDrag) self.iconDrag.remove(); document.getElementById("component-clone")?.remove(); if (self.dragMoveMutation === false) { if (self.component.dragHtml || Vvveb.dragHtml) { //if dragHtml is set for dragging then set real component html if (self.component) { let html = self.component.html.replace('RANDOM_ID', Math.floor(Math.random() * 1000)); newElement = generateElements(html)[0]; self.dragElement.replaceWith(newElement); self.dragElement = newElement; } } if (self.component.afterDrop) self.dragElement = self.component.afterDrop(self.dragElement); } else { self.selectedEl.classList.remove("is-dragged"); self.dragElement.replaceWith(self.selectedEl); self.dragElement = self.selectedEl; } const node = self.dragElement; self.selectNode(node); Vvveb.TreeList.loadComponents(); Vvveb.TreeList.selectComponent(node); self.loadNodeComponent(node); if (self.dragType == "section") { node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); } if (self.dragMoveMutation === false) { Vvveb.Undo.addMutation({type: 'childList', target: node.parentNode, addedNodes: [node], nextSibling: node.nextSibling}); } else { self.dragMoveMutation.newParent = node.parentNode; self.dragMoveMutation.newNextSibling = node.nextSibling; Vvveb.Undo.addMutation(self.dragMoveMutation); self.dragMoveMutation = false; } } }; self.frameBody.addEventListener("mouseup", highlightUp); let highlightDbClick = function(event) { if (Vvveb.Builder.isPreview == false) { if (!Vvveb.WysiwygEditor.isActive) { self.selectPadding = 10; self.texteditEl = target = event.target; Vvveb.WysiwygEditor.edit(self.texteditEl); _updateSelectBox = function(event) { if (!self.texteditEl) return; let pos = offset(self.selectedEl); let SelectBox = document.getElementById("select-box"); SelectBox.style.top = (pos.top - (self.frameDoc.scrollTop ?? 0) - self.selectPadding) + "px"; SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px"; SelectBox.style.width = (self.texteditEl.offsetWidth + (self.selectPadding * 2)) + "px"; SelectBox.style.height = (self.texteditEl.offsetHeight + (self.selectPadding * 2)) + "px"; SelectBox.style.display = "block"; }; //update select box when the text size is changed self.texteditEl.addEventListener("blur", _updateSelectBox); self.texteditEl.addEventListener("keyup", _updateSelectBox); self.texteditEl.addEventListener("paste", _updateSelectBox); self.texteditEl.addEventListener("input", _updateSelectBox); _updateSelectBox(); document.getElementById("select-box").classList.add("text-edit") document.getElementById("select-actions").style.display = "none"; document.getElementById("highlight-box").style.display = "none"; } } }; self.frameBody.addEventListener("dblclick", highlightDbClick); let highlightClick = function(event) { if (Vvveb.Builder.isPreview == false){ if (event.target) { if (Vvveb.WysiwygEditor.isActive ) { if (self.texteditEl.contains(event.target)) { return true; } } self.selectNode(event.target); Vvveb.TreeList.selectComponent(event.target); self.loadNodeComponent(event.target); if (Vvveb.component.resizable) { document.getElementById("select-box").classList.add("resizable"); self.resizeMode = Vvveb.component.resizeMode; } else { document.getElementById("select-box").classList.remove("resizable"); } document.getElementById("add-section-box").style.display = "none"; event.preventDefault(); return false; } } }; self.frameBody.addEventListener("click", highlightClick); }, _initBox: function() { let self = this; document.getElementById("drag-btn").addEventListener("mousedown", function(event) { //self.dragElement = self.selectedEl.setAttribute("style",Vvveb.dragElementStyle); if (event.which == 1) {//left click self.isDragging = true; document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(el => el.style.display = ""); if (self.designerMode) { self.dragElement = self.selectedEl; } else { self.selectedEl.style.position = ""; self.selectedEl.style.top = ""; self.selectedEl.style.left = ""; self.selectedEl.classList.add("is-dragged"); self.dragElement = generateElements(Vvveb.dragHtml)[0]; } const node = self.selectedEl; self.dragMoveMutation = {type: 'move', target: node, oldParent: node.parentNode, oldNextSibling: node.nextSibling}; //self.selectNode(false); event.preventDefault(); return false; } }); let resizeDown = function(event) { if (event.which == 1) {//left click document.querySelector("#section-actions, #highlight-name, #highlight-box").style.display = "none"; self.isResize = true; self.initialSize = {"width" : self.selectedEl.offsetWidth, "height" : self.selectedEl.offsetHeight}; self.initialPosition = false; self.resizeHandler = this.className; event.preventDefault(); return false; } }; document.querySelectorAll(".resize > div").forEach(e => e.addEventListener("mousedown", resizeDown)); document.getElementById("down-btn").addEventListener("click", function(event) { document.getElementById("select-box").style.display = "none"; Vvveb.Builder.moveNodeDown(); event.preventDefault(); return false; }); document.getElementById("up-btn").addEventListener("click", function(event) { document.getElementById("select-box").style.display = "none"; Vvveb.Builder.moveNodeUp(); event.preventDefault(); return false; }); document.getElementById("clone-btn").addEventListener("click", function(event) { Vvveb.Builder.cloneNode(); event.preventDefault(); return false; }); document.getElementById("parent-btn").addEventListener("click", function(event) { const node = self.selectedEl.parentNode; self.selectNode(node); self.loadNodeComponent(node); Vvveb.TreeList.selectComponent(node); event.preventDefault(); return false; }); document.getElementById("save-reusable-btn").addEventListener("click", function(event) { const node = self.selectedEl; let type = 'block'; if (node.tagName.toLowerCase() == 'section') { type = 'section'; } const name = prompt("Enter name for new reusable " + type, ''); if (name) { Vvveb.Builder.saveElement(node, type, name); } event.preventDefault(); return false; }); let codeEditorOldValue; document.getElementById("edit-code-btn").addEventListener("click", function(event) { let value = Vvveb.Builder.selectedEl.innerHTML; Vvveb.ModalCodeEditor.show(); Vvveb.ModalCodeEditor.setValue(value); codeEditorOldValue = value; event.preventDefault(); return false; }); let onSave = function(event) { Vvveb.Builder.selectedEl.innerHTML = event.detail; const node = Vvveb.Builder.selectedEl; Vvveb.Undo.addMutation({type:'characterData', target: node, oldValue: codeEditorOldValue, newValue: node.innerHTML}); Vvveb.Builder.selectNode(node); }; window.addEventListener("vvveb.ModalCodeEditor.save", onSave); document.getElementById("translate-code-btn")?.addEventListener("click", function(event) { let text = Vvveb.Builder.selectedEl.innerHTML.trim(); // uncomment to use outerHTML, not recommended //let text = selectedEl.outerHTML; fetch(namespaceUrl + "/translate&action=get", {method: "POST", body: new URLSearchParams({text, csrf: document.getElementById('csrf')?.value})}) .then((response) => { if (!response.ok) { throw new Error(response) } return response.json() }) .then((data) => { for (code in data) { let translation = data[code]; document.querySelector("#lang-" + code + "-editor textarea").value = translation; } }) .catch(error => { console.log(error.statusText); displayToast("danger", "Error", "Error loading translations!"); }); Vvveb.ModalTranslateEditor.show(); Vvveb.ModalTranslateEditor.setValue(text); event.preventDefault(); return false; }); let onTranslateSave = function(event) { let data = Object.fromEntries(new FormData(document.getElementById("translateForm"))); data["csrf"] = document.getElementById('csrf')?.value; fetch(namespaceUrl + "/translate&action=save", {method: "POST", body: new URLSearchParams(data)}) .then((response) => { if (!response.ok) { throw new Error(response) } return response.json() }) .then((data) => { let bg = "success"; if (data.success || text == "success") { } else { bg = "danger"; } displayToast(bg, "Save", data.message ?? data); }) .catch(error => { console.log(error.statusText); displayToast("danger", "Error", "Error saving translations!"); }); /* Vvveb.Builder.frameBody.querySelectorAll("form").forEach(f => { console.log( JSON.stringify(Object.fromEntries(new FormData(f))) ); }); */ Vvveb.Builder.selectedEl.innerHTML = event.detail; //selectedEl.outerHTML = text; }; //window.removeEventListener("vvveb.ModalTranslateEditor.save", onTranslateSave); window.addEventListener("vvveb.ModalTranslateEditor.save", onTranslateSave); document.getElementById("delete-btn").addEventListener("click", function(event) { document.getElementById("select-box").style.display = "none"; const node = self.selectedEl; Vvveb.Undo.addMutation({type: 'childList', target: node.parentNode, removedNodes: [node], nextSibling: node.nextSibling}); self.selectedEl.remove(); Vvveb.TreeList.loadComponents(); Vvveb.SectionList.loadSections(); event.preventDefault(); return false; }); let addSectionBox = document.getElementById("add-section-box"); let addSectionElement = {}; document.getElementById("add-section-btn").addEventListener("click", function(event) { addSectionElement = self.highlightEl; addSectionBox.style.display = "block"; 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; //check if box is out of viewport and move inside if (left < 0) left = 0; if (top < 0) top = 0; 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; 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"].includes(tagName)) { while (element = element.parentElement) { tagName = element.tagName.toLowerCase(); if (["section", "footer", "header"].includes(tagName)) { after = true; break; } } if (!element) { element = addSectionElement; } } if (after) { element.after(node); } else { element.append(node); } if (component.afterDrop) { node = component.afterDrop(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(".components-list li ol li"); if (element) { let html = Vvveb.Components.get(element.dataset.type); addSectionComponent(html, (document.querySelector("[name='add-section-insert-mode']:checked").value == "after")); addSectionBox.style.display = "none"; } }); addSectionBox.addEventListener("click", function(event) { let element = event.target.closest(".blocks-list li ol li"); if (element) { let html = Vvveb.Blocks.get(element.dataset.type); addSectionComponent(html, (document.querySelector("[name='add-section-insert-mode']:checked").value == "after")); addSectionBox.style.display = "none"; } }); addSectionBox.addEventListener("click", function(event) { let element = event.target.closest(".sections-list li ol li"); if (element) { let html = Vvveb.Sections.get(element.dataset.type); addSectionComponent(html, (document.querySelector("[name='add-section-insert-mode']:checked").value == "after")); addSectionBox.style.display = "none"; } }); }, /* drag and drop */ _initDragdrop : function() { let self = this; self.isDragging = false; document.addEventListener("mousedown", function(event) { let element = event.target.closest(".drag-elements-sidepane ul > li > ol > li[data-drag-type]"); let html; if (element && event.which == 1) {//left click document.getElementById("component-clone")?.remove(); document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(e => e.style.display = "none"); self.dragType = element.dataset.dragType; if (self.dragType == "component") { self.component = Vvveb.Components.get(element.dataset.type); } else if (self.dragType == "section") { self.component = Vvveb.Sections.get(element.dataset.type); } else if (self.dragType == "block") { self.component = Vvveb.Blocks.get(element.dataset.type); } if (self.component.dragHtml) { html = self.component.dragHtml; } else if (Vvveb.dragHtml) { html = Vvveb.dragHtml; } else { html = self.component.html.replace('RANDOM_ID', Math.floor(Math.random() * 1000)); } self.dragElement = generateElements(html)[0]; //self.dragElement.css("border", "1px dashed #4285f4"); if (self.component.dragStart) self.dragElement = self.component.dragStart(self.dragElement); self.isDragging = true; if (Vvveb.dragIcon == 'html') { self.iconDrag = generateElements(html)[0]; self.iconDrag.setAttribute("id", "dragElement-clone"); self.iconDrag.style.position = "absolute"; } else if (self.designerMode == false) { self.iconDrag = document.createElement("img"); self.iconDrag.setAttribute("id", "dragElement-clone"); self.iconDrag.setAttribute("src", element.style.backgroundImage.replace(/^url\(['"](.+)['"]\)/, '$1')); self.iconDrag.style.zIndex = "100"; self.iconDrag.style.position = "absolute"; self.iconDrag.style.width = "64px"; self.iconDrag.style.height = "64px"; self.iconDrag.style.top = event.y + "px"; self.iconDrag.style.left = event.x + "px"; } document.body.append(self.iconDrag); event.preventDefault(); return false; } }); document.addEventListener('mouseup', function(event) { if (self.iconDrag && self.isDragging == true) { self.isDragging = false; document.getElementById("component-clone")?.remove(); document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(el => el.style.display = ""); self.iconDrag.remove(); if(self.dragElement){ self.dragElement.remove(); } } }); document.addEventListener('mousemove', function(event) { if (self.iconDrag && self.isDragging == true) { let x = (event.clientX || event.clientX); let y = (event.clientY || event.clientY); self.iconDrag.style.left = (x - 60) + "px"; self.iconDrag.style.top = (y - 30) + "px"; const elementMouseIsOver = document.elementFromPoint(x - 60, y - 40); //if drag elements hovers over iframe switch to iframe mouseover handler return; if (elementMouseIsOver && elementMouseIsOver.tagName == 'IFRAME') { self.frameBody.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, })); //self.frameBody.trigger("mousemove", event); event.stopPropagation(); self.selectNode(false); } } }); document.addEventListener("mouseup", function(event) { let element = event.target.closest(".drag-elements-sidepane ul > ol > li > li"); if (element) { self.isDragging = false; document.getElementById("component-clone")?.remove() document.querySelectorAll("#section-actions, #highlight-name, #select-box").forEach(el => el.style.display = ""); } }); }, removeHelpers: function (html, keepHelperAttributes = false) { //tags like stylesheets or scripts html = html.replace(/<[^>]+?data-vvveb-helpers.+?>/gi, ""); //attributes if (!keepHelperAttributes) { html = html.replace(/\s*data-vvveb-\w+(=["'].*?["'])?\s*/gi, ""); } html = html.replaceAll("vvveb-hidden", "").replaceAll("data-vvvebjs-editor", ""); return html; }, getHtml: function(keepHelperAttributes = true, filter = true) { let doc = window.FrameDocument; let hasDoctpe = (doc.doctype !== null); let html = ""; doc.querySelectorAll("[contenteditable]").forEach(e => e.removeAttribute("contenteditable")); doc.querySelectorAll("[spellcheckker]").forEach(e => e.removeAttribute("spellcheckker")); doc.querySelectorAll('script[src^="chrome-extension://"]').forEach(e => e.remove()); doc.querySelectorAll('script[src^="moz-extension://"]').forEach(e => e.remove()); // scroll page to top to avoid saving the page in a different state // like saving with sticky classes set for navbar etc // this.iframe.contentWindow.scrollTo(0,0); if (filter) { window.dispatchEvent(new CustomEvent("vvveb.getHtml.before", {detail: doc})); } if (hasDoctpe) html = "\n"; Vvveb.FontsManager.cleanUnusedFonts(); html += doc.documentElement.outerHTML; html = this.removeHelpers(html, keepHelperAttributes); if (filter) { window.dispatchEvent(new CustomEvent("vvveb.getHtml.after", {detail: doc})); window.dispatchEvent(new CustomEvent("vvveb.getHtml.filter", {detail: html})); } return html; }, setHtml: function(html) { //documentElement.innerHTML resets 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("= 0 && end >= 0) { if (outerHtml) return html.slice(start, end + 3 + tag.length); else return html.slice(html.indexOf(">", start) + 1, end); } else { return html; } } if (this.runJsOnSetHtml) { this.frameBody.innerHTML = getTag(html, "body"); } else { window.FrameDocument.body.innerHTML = getTag(html, "body"); } //use outerHTML if you want to set body tag attributes //window.FrameDocument.body.outerHTML = getTag(html, "body", true); //set head html only if changed to avoid page flicker let headHtml = getTag(html, "head"); if (window.FrameDocument.head.innerHTML != headHtml) { window.FrameDocument.head.innerHTML = headHtml; } }, saveElement: function(element, type, name, callback) { if (type == 'section') { Vvveb.Sections.add('reusable/'+ name, { name, image: "img/logo-small.png", html: element.outerHTML}); if (Vvveb.SectionsGroup["Reusable"] === undefined) { Vvveb.SectionsGroup["Reusable"] = []; } Vvveb.SectionsGroup["Reusable"].push('reusable/'+ name); Vvveb.Builder.loadSectionGroups(); } else { Vvveb.Blocks.add('reusable/'+ name, { name, image: "img/logo-small.png", html: element.outerHTML}); if (Vvveb.BlocksGroup["Reusable"] === undefined) { Vvveb.BlocksGroup["Reusable"] = []; } Vvveb.BlocksGroup["Reusable"].push('reusable/'+ name); Vvveb.Builder.loadBlockGroups(); } let data = {type, name, html:element.outerHTML, csrf: document.getElementById('csrf')?.value}; fetch(saveReusableUrl, {method: "POST", body: new URLSearchParams(data)}) .then((response) => { if (!response.ok) { throw new Error(response) } return response.json() }) .then((data) => { if (callback) callback(data); let bg = "success"; if (data.success || text == "success") { } else { bg = "danger"; } displayToast(bg, "Save", data.message ?? data); }) .catch(error => { console.log(error.statusText); displayToast("danger", "Error", "Error saving!"); }); }, saveAjax: function(data, saveUrl, callback, error ) { if (!data["file"]) { data["file"] = Vvveb.FileManager.getCurrentFileName(); } if (!data["url"]) { data["url"] = Vvveb.FileManager.getPageData('url'); } data["elements"] = Vvveb.ChangeManager.getChangedElements(); data["csrf"] = document.getElementById('csrf')?.value; if (!data["startTemplateUrl"]) { data["html"] = this.getHtml(); } //data['elements'] = new URLSearchParams(data['elements']); return fetch(saveUrl, { method: "POST", headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, body: nestedFormData(data) }) .then((response) => { if (!response.ok) { return Promise.reject(response); } return response.json(); }) .then((data) => { if (callback) callback(data); Vvveb.Undo.reset(); document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true")); }) .catch((err) => { if (error) error(err); let message = err?.statusText ?? "Error saving!"; displayToast("danger", "Error", message); if (err.hasOwnProperty('text')) err.text().then( errorMessage => { let message = errorMessage.substr(0, 200); displayToast("danger", "Error", message); }); }); }, setDesignerMode: function(designerMode = false) { this.designerMode = designerMode; } }; Vvveb.ModalCodeEditor = { modal: false, modalId: 'codeEditorModal', name: 'ModalCodeEditor', editor: false, init: function(modal = false, editor = false) { if (modal) { this.modal = modal; } else { this.modal = document.getElementById(this.modalId); } if (editor) { this.editor = editor; } else { this.editor = this.modal.querySelector('textarea'); } let self = this; this.modal.querySelector('.save-btn').addEventListener("click", function(event) { window.dispatchEvent(new CustomEvent("vvveb." + self.name + ".save", {detail: self.getValue()})); self.hide(); return false; }); }, show: function(value) { if (!this.modal) { this.init(); } const bsModal = bootstrap.Modal.getOrCreateInstance(this.modal); return bsModal.show(); }, hide: function(value) { const bsModal = bootstrap.Modal.getOrCreateInstance(this.modal); return bsModal.hide(); }, getValue: function() { return this.editor.value;; }, setValue: function(value) { if (!this.modal) { this.init(); } //enable save button document.querySelectorAll("#top-panel .save-btn").forEach(e => e.removeAttribute("disabled")); this.editor.value = value; }, } //Vvveb.ModalTranslateEditor = structuredClone(Vvveb.ModalCodeEditor); Vvveb.ModalTranslateEditor = Object.assign({}, Vvveb.ModalCodeEditor); Vvveb.ModalTranslateEditor.modalId = 'translateEditorModal'; Vvveb.ModalTranslateEditor.name = 'ModalTranslateEditor'; Vvveb.CodeEditor = { isActive: false, oldValue: '', doc:false, textarea:false, init: function(doc) { this.textarea = document.querySelector("#vvveb-code-editor textarea"); this.textarea.value = Vvveb.Builder.getHtml(); this.textarea.addEventListener("keyup", e => { delay(() => Vvveb.Builder.setHtml(this.value), 1000); }); //load code on document changes Vvveb.Builder.frameBody.addEventListener("vvveb.undo.add", () => Vvveb.CodeEditor.setValue()); Vvveb.Builder.frameBody.addEventListener("vvveb.undo.restore", () => Vvveb.CodeEditor.setValue()); //load code when a new url is loaded Vvveb.Builder.documentFrame.addEventListener("load", () => Vvveb.CodeEditor.setValue()); this.isActive = true; }, setValue: function(value) { if (this.isActive) { this.textarea.value = Vvveb.Builder.getHtml(); } }, destroy: function(element) { //this.isActive = false; }, toggle: function() { if (this.isActive != true) { this.isActive = true; return this.init(); } this.isActive = false; this.destroy(); } } Vvveb.CssEditor = { isActive: false, oldValue: '', doc:false, textarea:false, init: function(doc) { this.textarea = document.getElementById("css-editor") this.textarea.value = Vvveb.StyleManager.getCss(); let self = this; /* document.querySelectorAll('[href="#csscode-tab"]').forEach( t => t.addEventListener("click", e => { self.textarea.value = Vvveb.StyleManager.getCss(); })); */ this.textarea.addEventListener("keyup", e => { delay(() => Vvveb.StyleManager.setCss(self.textarea.value), 1000); }); }, getValue: function() { return this.textarea.value; }, setValue: function(value, updateStyles = true) { this.textarea.value = value; if (updateStyles) { Vvveb.StyleManager.setCss(value); } }, destroy: function() { }, toggle: function() { if (this.isActive != true) { this.isActive = true; return this.init(); } this.isActive = false; this.destroy(); } } function displayToast(type, title, message, position = 'bottom', id = null) { if (!id) { id = position + "-toast"; } let toast = document.getElementById(id); let header = toast.querySelector(".toast-header"); toast.classList.remove("bottom-0", "top-0"); toast.classList.add(position + "-0"); toast.querySelector(".toast-body .message").innerHTML = message; header.classList.remove("danger", "success"); header.classList.add("bg-" + type); header.querySelector("strong").textContent = title; let toastDisplay = toast.cloneNode(true); toast.parentNode.appendChild(toastDisplay); let delay = 3000; if (type == "danger") { delay = 10000; } let bsToast = new bootstrap.Toast(toastDisplay, {animation:true, delay}); toastDisplay.addEventListener('hidden.bs.toast', () => { toastDisplay.remove(); }); bsToast.show(); } Vvveb.Gui = { init: function() { document.querySelectorAll("[data-vvveb-action]").forEach(function (el,i) { const on = el.dataset.vvvebOn ?? "click"; el.addEventListener(on, Vvveb.Gui[el.dataset.vvvebAction]); }); this.shortcuts(); }, shortcuts: function() { let self = this; handleShortcuts = function(e) { if (e.ctrlKey) { switch (e.key) { case 's': e.preventDefault(); let btn = document.querySelector('.save-btn'); let url = btn.dataset.vvvebUrl; self.saveAjax(null, url, document.querySelector('.save-btn')); return; case 'z': e.preventDefault(); self.undo(); return; case 'Z': case 'y': e.preventDefault(); self.redo(); return; case 'L': e.preventDefault(); self.toggleTreeList(); return; case 'e': e.preventDefault(); self.toggleEditor(); return; case 'P': e.preventDefault(); self.newPage(); return; } } } //handle shortcuts from main window and iframe also document.addEventListener('keydown', handleShortcuts); window.addEventListener('vvveb.iframe.loaded', () => { Vvveb.Builder.frameBody.addEventListener('keydown', handleShortcuts); }); }, undo : function () { if (Vvveb.WysiwygEditor.isActive) { Vvveb.WysiwygEditor.undo(); } else { Vvveb.Undo.undo(); } Vvveb.Builder.selectNode(); }, redo : function () { if (Vvveb.WysiwygEditor.isActive) { Vvveb.WysiwygEditor.redo(); } else { Vvveb.Undo.redo(); } Vvveb.Builder.selectNode(); }, //show modal with html content save : function () { document.getElementById('textarea-modal textarea').value = Vvveb.Builder.getHtml(); document.getElementById('textarea-modal').modal(); }, //show offcanvas with page changes showChanges : function () { let base, folder, filename, page; filename = Vvveb.FileManager.getPageData('file'); base = (base = /[^\/]+$/.exec(filename)) ? base[0] : ''; folder = (folder = /(.+)\//.exec(filename)) ? folder[1] : '/'; page = Vvveb.FileManager.getCurrentUrl(); page = (page = /[^\/]+\?$/.exec(page)) ? page[0] : ''; page = page.replace(".html", ""); page = page ? page : "new-template"; page += ".html"; let safeOffcanvas = document.querySelector("#save-offcanvas"); let element; if (element = safeOffcanvas.querySelector("[data-v-filename]")) { element.textContent = filename; } if (element = safeOffcanvas.querySelector("input[data-v-filename]")) { element.value = filename; } if (element = safeOffcanvas.querySelector("[data-v-basename]")) { element.value = page; } if (element = safeOffcanvas.querySelector("[data-v-theme-folders]")) { element.value = folder; } Vvveb.ChangeManager.render(); }, //post html content through ajax to save to filesystem/db saveAjax : function (event, saveUrl = null, saveBtn = null) { let btn = saveBtn ?? this; saveUrl = saveUrl ?? this.dataset.vvvebUrl; let file = Vvveb.FileManager.getPageData('file'); //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") { file = document.querySelector("#save-offcanvas [name=folder]").value + "/" + document.querySelector("#save-offcanvas [name=file]").value; } } btn.querySelector(".loading").classList.remove("d-none"); btn.querySelector(".button-text").classList.add("d-none"); return Vvveb.Builder.saveAjax({file}, saveUrl, (data) => { //use toast to show save status let bg = "success"; if (data.success || data == "success") { document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true")); } else { bg = "danger"; } displayToast(bg, "Save", data.message ?? 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"); }, (error) => { btn.querySelector(".loading").classList.add("d-none"); btn.querySelector(".button-text").classList.remove("d-none"); let message = error?.statusText ?? "Error saving!"; displayToast("danger", "Error", message); }); }, download : function () { const filename = /[^\/]+$/.exec(Vvveb.Builder.iframe.src)[0]; const uriContent = "data:application/octet-stream," + encodeURIComponent(Vvveb.Builder.getHtml()); let link = document.createElement('a'); if ('download' in link) { link.dataset.download = filename; link.href = uriContent; link.target = "_blank"; document.body.appendChild(link); const result = link.click(); document.body.removeChild(link); link.remove(); } else { location.href = uriContent; } }, viewport : function () { document.getElementById("canvas").setAttribute("class", this.dataset.view); document.getElementById("iframe1").removeAttribute("style"); document.querySelectorAll(".responsive-btns .active").forEach(e => e.classList.remove("active")); if (this.dataset.view) this.classList.add("active"); }, toggleEditor : function () { document.getElementById("vvveb-builder").classList.toggle("bottom-panel-expand"); document.getElementById("toggleEditorJsExecute").classList.toggle("d-none"); //hide breadcrumb when showing the editor document.querySelector(".breadcrumb-navigator .breadcrumb").classList.toggle("d-none"); document.querySelector(".breadcrumb-navigator .nav-tabs").classList.toggle("d-none"); Vvveb.CodeEditor.toggle(); Vvveb.CssEditor.toggle(); }, toggleHidden : function () { Vvveb.Builder.frameBody.classList.toggle("vvveb-hidden"); }, toggleEditorJsExecute : function () { Vvveb.Builder.runJsOnSetHtml = this.checked; }, preview : function () { (Vvveb.Builder.isPreview == true)?Vvveb.Builder.isPreview = false:Vvveb.Builder.isPreview = true; document.getElementById("iframe-layer").classList.toggle("d-none"); document.getElementById("vvveb-builder").classList.toggle("preview"); }, fullscreen : function () { launchFullScreen(document); // the whole page }, search : function () { let searchText = this.value; let panel = this.parentNode.parentNode.querySelector("div > ul"); panel.querySelectorAll("li ol li").forEach(function (el, i) { el.style.display = "none"; if (el.dataset.search.indexOf(searchText) > -1) el.style.display = ""; }); }, clearSearch : function (e) { let input = this.parentNode.querySelector("input"); input.value = ""; input.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, cancelable: true, })); }, expand : function (e) { this.parentNode.parentNode.parentNode.querySelectorAll('input.header_check[type="checkbox"]').forEach(e => e.checked = true); }, collapse : function (e) { this.parentNode.parentNode.parentNode.querySelectorAll('input.header_check[type="checkbox"]').forEach(e => e.checked = false); }, //Pages, file/components tree newPage : function () { let newPageModal = document.getElementById('new-page-modal'); let form = newPageModal.querySelector("form"); const bsModal = bootstrap.Modal.getOrCreateInstance(newPageModal); bsModal.show(); let submitForm = function(e) { let data = {}; this.querySelectorAll("input[type=text],input[type=checkbox]:checked,input[type=radio]:checked,input[name=image],textarea,select:not(:disabled)").forEach( (el, i) => { if (el.offsetParent || el.name == 'image') data[el.name] = el.value; }); if (data['file']) { data['title'] = data['file'].replace('/', '').replace('.html', ''); //let name = data['name'] = data['folder'].replace('/', '_') + "-" + data['title']; if (!data['name']) { data['name'] = data['title']; } data['url'] = data['folder'] + "/" + data['file']; data['url'] = Vvveb.themeBaseUrl + data['url']; } e.preventDefault(); return Vvveb.Builder.saveAjax(data, this.action, function (savedData) { if (savedData?.success) { data.title = data.name; if (typeof savedData === 'object' && savedData !== null) { data.name = savedData.name ?? data.name; data.url = savedData.url ?? data.url; data.file = savedData.file ?? (data.folder + "/" + data.file); data.title = savedData.title ?? data.title; } let page = Vvveb.FileManager.addPage(data.name, data); Vvveb.FileManager.loadPage(data.name); Vvveb.FileManager.scrollToPage(page); bsModal.hide(); } else { let message = savedData?.message ?? "Error saving!"; displayToast("danger", "Error", message); } }); }; if (!form.dataset.init) { form.addEventListener("submit", submitForm); form.dataset.init = true; } }, setDesignerMode : function () { //aria-pressed attribute is updated after action is called and we check for false instead of true let designerMode = this.attributes["aria-pressed"].value == "true"; Vvveb.Builder.setDesignerMode(designerMode); }, //layout togglePanel: function (panel, cssVar) { panel = document.querySelector(panel); let body = document.querySelector("body"); let prevValue = getComputedStyle(body).getPropertyValue(cssVar); let visible = false; if (prevValue !== "0px") { panel.dataset.layoutToggle = prevValue; body.style.setProperty(cssVar, "0px"); panel.style.display = "none"; visible = false; } else { prevValue= panel.dataset.layoutToggle; body.style.setProperty(cssVar, ""); panel.style.display = ""; visible = true; } return visible; }, toggleFileManager: function () { Vvveb.Gui.togglePanel("#filemanager", "--builder-filemanager-height"); }, toggleLeftColumn: function () { Vvveb.Gui.togglePanel("#left-panel", "--builder-left-panel-width"); }, toggleRightColumn: function (rightColumnEnabled = null) { rightColumnEnabled = Vvveb.Gui.togglePanel("#right-panel", "--builder-right-panel-width"); document.getElementById("vvveb-builder").classList.toggle("no-right-panel"); document.querySelector(".component-properties-tab").classList.toggle("d-none"); Vvveb.Components.componentPropertiesElement = (rightColumnEnabled ? "#right-panel" :"#left-panel #properties") + " .component-properties"; let componentTab = document.querySelector("#components-tab"); if (document.getElementById("properties").offsetParent) { const bsTab = bootstrap.Tab.getOrCreateInstance(componentTab); componentTab.style.display = ""; bsTab.show(); } }, toggleTreeList: function () { let treeList = document.getElementById("tree-list"); treeList.classList.toggle("d-none"); if (!treeList.offsetParent) { document.getElementById("toggle-tree-list").classList.remove("active"); } }, treeListRight: function () { let treeList = document.getElementById("tree-list"); let btnIcon = document.querySelector("[data-vvveb-action='treeListRight'] i"); if (treeList.style.height) { treeList.style.height = ""; treeList.style.right = ""; treeList.style.top = ""; treeList.style.left = ""; treeList.style.width = ""; btnIcon.className = "icon-stop-outline"; } else { treeList.style.height = "100vh"; treeList.style.height = "calc(100vh - 35px)"; treeList.style.right = "0"; treeList.style.top = "35px"; treeList.style.left = "auto"; treeList.style.width = "300px"; btnIcon.className = "icon-remove-outline"; } }, zoomChange: function () { let wrapper = document.getElementById("iframe-wrapper"); let scale = ""; let height = ""; if (this.value != "100") { scale = "scale(" + this.value + "%)"; height = ((100 / this.value) * 100) + "%"; } wrapper.style.transform = scale; wrapper.style.height = height; }, setState: function () { Vvveb.StyleManager.setState(this.value); Vvveb.Builder.reloadComponent(); } } Vvveb.StyleManager = { styles:{}, cssContainer:false, mobileWidth: '320px', tabletWidth: '768px', doc:false, inlineCSS:false, currentElement:null, currentSelector:null, state:"",//hover, active etc init: function(doc) { if (doc) { this.doc = doc; let style = false; let _style = false; //check if editor style is present for (let i = 0; i < doc.styleSheets.length; i++) { _style = doc.styleSheets[i]; if (_style.ownerNode.id && _style.ownerNode.id == "vvvebjs-styles") { style = _style.ownerNode; break; } } document.getElementById("vvvebjs-styles-css")?.remove(); //if style element does not exist create it if (!style) { style = generateElements('')[0]; doc.head.append(style); return this.cssContainer = style; } //if it exists this.cssContainer = style; this.loadCss(); return this.cssContainer; } }, loadCss: function() { let style = this.cssContainer.sheet; //if style exist then load all css styles for editor for (let j = 0; j < style.cssRules.length; j++) { const media = (typeof style.cssRules[j].media === "undefined") ? "desktop" : (style.cssRules[j].media[0] === "screen and (max-width: 1220px)") ? "tablet" : (style.cssRules[j].media[0] === "screen and (max-width: 320px)") ? "mobile" : "desktop"; const selector = (media === "desktop") ? style.cssRules[j].selectorText : style.cssRules[j].cssRules[0].selectorText; const styles = (media === "desktop") ? style.cssRules[j].style : style.cssRules[j].cssRules[0].style; if (media) { this.styles[media] = this.styles[media] ?? {}; if (selector) { this.styles[media][selector] = {}; for (let k = 0; k < styles.length; k++) { const property = styles[k]; const value = styles[property]; this.styles[media][selector][property] = value; } } } } }, getSelectorForElement: function(element) { if (!element) return ''; let currentElement = element; let selector = []; while (currentElement.parentElement) { let elementSelector = ""; let classSelector = Array.from(currentElement.classList).map(function (className) { if (Vvveb.Builder.ignoreClasses.indexOf(className) == -1) { return "." + className; } }).join(""); //element (tag) selector let tag = currentElement.tagName.toLowerCase(); //exclude top most element body unless the parent element is body if (tag == "body" && selector.length > 1) { break; } //stop at a unique element (with id) if (currentElement.id) { elementSelector = "#" + currentElement.id; selector.push(elementSelector); break; } else if (classSelector) { //class selector elementSelector = classSelector; } else { //element selector elementSelector = tag } if (elementSelector) { selector.push(elementSelector); } currentElement = currentElement.parentElement; } return selector.reverse().join(" > "); }, setState: function(state) { this.state = state; }, addSelectorState: function(selector) { return selector + (this.state ? ":" + this.state : ""); }, setStyle: function(element, styleProp, value) { let selector; if (typeof(element) == "string") { selector = element; } else { let node = element; //if propert is set with inline style attribute then override it and don't save to css //inline text editor sets properties like font-size inline that can't be later overriten from css if (node.style && node.style[styleProp]) { node.style[styleProp] = value; return element; } selector = this.getSelectorForElement(node); } if (this.inlineCSS) { element.style[styleProp] = value; return element; } selector = this.addSelectorState(selector); const media = document.getElementById("canvas").classList.contains("tablet") ? "tablet" : document.getElementById("canvas").classList.contains("mobile") ? "mobile" : "desktop"; //styles[media][selector][styleProp] = value if (!this.styles[media]) { this.styles[media] = {}; } if (!this.styles[media][selector]) { this.styles[media][selector] = {}; } if (!this.styles[media][selector][styleProp]) { this.styles[media][selector][styleProp] = {}; } this.styles[media][selector][styleProp] = value; this.generateCss(media); window.dispatchEvent(new CustomEvent("vvveb.StyleManager.setStyle", {detail: {element, styleProp, value}})); return element; //uncomment bellow code to set css in element's style attribute //return element.css(styleProp, value); }, setCss: function (css) { this.cssContainer.innerHTML = css; this.loadCss(); }, getCss: function (css) { return this.cssContainer.innerHTML; }, generateCss: function (media) { //let css = ""; //for (selector in this.styles[media]) { // css += `${selector} {`; // for (property in this.styles[media][selector]) { // value = this.styles[media][selector][property]; // css += `${property}: ${value};`; // } // css += '}'; //} //this.cssContainer.innerHTML = css; //return element; //refresh container element to avoid issues with changes from code editor this.cssContainer = this.doc.getElementById("vvvebjs-styles"); let css = ""; for (media in this.styles) { if (media === "tablet" || media === "mobile") { css += `@media screen and (max-width: ${(media === 'tablet') ? this.tabletWidth : this.mobileWidth}){\n\n` } for (selector in this.styles[media]) { css += `${selector} {\n`; for (property in this.styles[media][selector]) { const value = this.styles[media][selector][property]; css += `\t${property}: ${value};\n`; } css += '}\n\n'; } if (media === "tablet" || media === "mobile") { css += `}\n\n` } } return this.cssContainer.innerHTML = css; }, _getCssStyle: function(element, styleProp){ let value = "", el, selector, media; el = element; if (el != this.currentElement) { selector = this.getSelectorForElement(el); this.currentElement = el; this.currentSelector = selector } else { selector = this.currentSelector; } selector = this.addSelectorState(selector); media = document.getElementById("canvas").classList.contains("tablet") ? "tablet" : document.getElementById("canvas").classList.contains("mobile") ? "mobile" : "desktop"; if (el.style && el.style.length > 0 && el.style[styleProp]) {//check inline value = el.style[styleProp]; } else if (this.styles[media] !== undefined && this.styles[media][selector] !== undefined && this.styles[media][selector][styleProp] !== undefined) {//check defined css value = this.styles[media][selector][styleProp]; if (styleProp == 'font-family') { } } else if (window.getComputedStyle) { value = document.defaultView.getDefaultComputedStyle ? document.defaultView.getDefaultComputedStyle(el,null).getPropertyValue(styleProp) : window.getComputedStyle(el,null).getPropertyValue(styleProp); } return value; }, getStyle: function(element,styleProp){ return this._getCssStyle(element, styleProp); } } Vvveb.ContentManager = { getAttr: function(element, attrName) { return element.getAttribute(attrName); }, setAttr: function(element, attrName, value) { return element.setAttribute(attrName, value); }, setHtml: function(element, html) { return element.innerHTML = html; }, getHtml: function(element) { return element.innerHTML; }, setText: function(element, text) { return element.textContent = text; }, getText: function(element) { return element.textContent; }, }; function getNodeTree (node, parent, allowedComponents, idToNode = {}) { function getNodeTreeTraverse (node, parent, id = '') { if (node.hasChildNodes()) { for (let j = 0; j < node.childNodes.length; j++) { const child = node.childNodes[j]; //skip text and comments nodes if (child.nodeType == 3 || child.nodeType == 8) { continue; } let element; if (child && child["attributes"] != undefined && (matchChild = Vvveb.Components.matchNode(child))) { if (Array.isArray(allowedComponents) && allowedComponents.indexOf(matchChild.type) == -1) { element = getNodeTreeTraverse(child, parent); continue; } let title = ""; //if (matchChild.type === "elements/section") { title = child.id ? child.id : (child.title ? child.title : child.ariaLabel ?? ""); //} element = { name: matchChild.name, image: matchChild.image, type: matchChild.type, title, node: child, id: id + '-' + j, children: [] }; element.children = []; parent.push(element); idToNode[id + '-' + j] = child; element = getNodeTreeTraverse(child, element.children, id + '-' + j); } else { element = getNodeTreeTraverse(child, parent, id + '-' + j); } } } return false; } getNodeTreeTraverse(node, parent, '1'); } function drawComponentsTree(tree) { let j = 1; let prefix = Math.floor(Math.random() * 100); function drawComponentsTreeTraverse(tree) { let list = document.createElement("ol"); j++; for (i in tree) { let node = tree[i]; let id = node.id; let li; if (!id) { id = prefix + '-' + j + '-' + i; } let title = (node.title ? friendlyName(node.title.substr(0, 21)) : ""); if (title) { title = ` - ${title}`; } if (tree[i].children.length > 0) { li = generateElements('
            4. \ \ \
            5. ')[0]; li.append(drawComponentsTreeTraverse(node.children)); } else { li = generateElements('
            6. \ \ \
            7. ')[0]; } li._treeNode = node.node; list.append(li); } return list; } return drawComponentsTreeTraverse(tree); } let selected = null; let dragover = null; Vvveb.SectionList = { selector: '.sections-container', allowedComponents: {}, init: function(allowedComponents = {}) { this.allowedComponents = allowedComponents; document.querySelector(this.selector).addEventListener("click", function (e) { let element = e.target.closest(":scope > div .controls"); if (element) { let node = element.parentNode._node; if (node) { node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); //node.click(); Vvveb.Builder.selectNode(node); Vvveb.Builder.loadNodeComponent(node); } } }); document.querySelector(this.selector).addEventListener("dblclick", function (e) { let element = e.target.closest(":scope > div"); if (element) { const node = element._node; node.click(); } }); document.querySelector(this.selector).addEventListener("click", function (e) { let element = e.target.closest("li[data-component] label"); if (element) { let node = element.parentNode._node; if (node) { node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); node.click(); } } }); document.querySelector(this.selector).addEventListener("mouseenter", function (e) { let element = e.target.closest("li[data-component] label"); if (element) { const node = document.querySelector(element.parentNode._node); node.css("outline","1px dashed blue"); } }); document.querySelector(this.selector).addEventListener("mouseleave", function (e){ let element = e.target.closest("li[data-component] label"); if (element) { const node = document.querySelector(element.parentNode._node); node.css("outline",""); if (node.getAttribute("style") == "") node.removeAttribute("style"); } }); document.querySelector(this.selector).addEventListener("dragstart", this.dragStart); document.querySelector(this.selector).addEventListener("dragover", this.dragOver); document.querySelector(this.selector).addEventListener("dragend", this.dragEnd); document.querySelector(this.selector).addEventListener("click", function (e) { let element = e.target.closest(".delete-btn"); if (element) { let section = element.closest(".section-item"); let node = section._node; node.remove(); section.remove(); Vvveb.TreeList.loadComponents(); e.stopPropagation(); e.preventDefault(); } }); let sectionIn; let img = document.querySelector(".block-preview img"); document.querySelector(".sections-list").addEventListener("mouseover", function (e) { let element = e.target.closest("li[data-type]"); if (element) { if (sectionIn != element) { let src = element.querySelector("img").getAttribute("src"); sectionIn = element; img.setAttribute("src", src); img.style.display = ""; } } else { sectionIn = element; img.setAttribute("src", ""); img.style.display = "none"; } }) /* document.querySelector(this.selector).addEventListener("click", ".up-btn", function (e) { let section = e.target.closest(".section-item"); let node = section._node; Vvveb.Builder.moveNodeUp(node); Vvveb.Builder.moveNodeUp(section); e.preventDefault(); }); document.querySelector(this.selector).addEventListener("click", ".down-btn", function (e) { let section = e.target.closest(".section-item"); let node = section._node; Vvveb.Builder.moveNodeDown(node); Vvveb.Builder.moveNodeDown(section); e.preventDefault(); }); */ let self = this; document.querySelector(".sections-list").addEventListener("click", function (e) { let element = e.target.closest(".add-section-btn"); if (element) { let item = element.closest("li"); let section = Vvveb.Sections.get(item.dataset.type); let node = generateElements(section.html)[0]; let sectionType = node.tagName.toLowerCase(); let afterSection = Vvveb.Builder.frameBody.querySelector(":scope > " + sectionType + ":last-of-type"); if (afterSection) { afterSection.after(node); } else { if (sectionType == "nav") { afterSection = Vvveb.Builder.frameBody.querySelector(":scope > nav:first,> header:last-of-type"); if (afterSection) { afterSection.before(node); } else { Vvveb.Builder.frameBody.append(node); } } else if (sectionType != "footer") { afterSection = Vvveb.Builder.frameBody.querySelector("body > footer:last-of-type"); if (afterSection) { afterSection.before(node); } else { Vvveb.Builder.frameBody.append(node); } } else { Vvveb.Builder.frameBody.append(node); } } //node.click(); Vvveb.Builder.selectNode(node); Vvveb.Builder.loadNodeComponent(node); /* Vvveb.Builder.frameHtml.animate({ scrollTop: node.offset().top }, 1000); delay(() => node.click(), 1000); */ Vvveb.Undo.addMutation({type: 'childList', target: node.parentNode, addedNodes: [node], nextSibling: node.nextSibling}); self.loadSections(); Vvveb.TreeList.loadComponents(); Vvveb.TreeList.selectComponent(node); node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); e.preventDefault(); } }); document.querySelector(this.selector).addEventListener("click", function (e) { let element = e.target.closest(".properties-btn"); if (element) { let section = element.closest(".section-item"); let node = section._node; node.click(); e.preventDefault(); } }); }, getSections: function() { let sections = []; let sectionList = window.FrameDocument.body.querySelectorAll(':scope > section, :scope > header, :scope > footer, :scope > main, :scope > nav'); sectionList.forEach(function (node, i) { let id = node.id ? node.id : (node.title ? node.title : node.ariaLabel ?? node.className); if (!id) { id = 'section-' + Math.floor(Math.random() * 1000); } let section = { name: id.replace(/[^\w+]+/g,' '), id: node.id, type: node.tagName.toLowerCase(), node: node }; sections.push(section); }); return sections; }, loadComponents: function(sectionListItem, section, allowedComponents = {}) { let tree = []; getNodeTree(section, tree, allowedComponents); let html = drawComponentsTree(tree); document.querySelector("ol", sectionListItem).replaceWith(html); }, addSection: function(data) { let section = generateElements(tmpl("vvveb-section", data))[0]; section._node = data.node; document.querySelector(this.selector).append(section); //this.loadComponents(section, data.node, this.allowedComponents); }, loadSections: function() { let sections = this.getSections(); let container = document.querySelector(this.selector); container.replaceChildren(); for (i in sections) { this.addSection(sections[i]); } }, //drag and drop dragOver: function(e) { let element = e.target.closest("div"); if (element) { if (e.target != dragover && e.target.className == "section-item") { if (dragover) { dragover.classList.remove("drag-over"); } dragover = e.target; dragover.classList.add("drag-over"); } } }, dragEnd: function (e) { let element = e.target.closest("div"); if (element) { if (dragover) { let parent = selected.parentNode; let selectedNode = selected._node; let replaceNode = dragover._node; if ((dragover.offsetTop > selected.offsetTop)) { //replace section item list parent.insertBefore(selected, dragover.nextElementSibling); //replace section replaceNode.parentNode.insertBefore(selectedNode, replaceNode.nextElementSibling); } else { //replace section item list parent.insertBefore(selected, dragover); //replace section replaceNode.parentNode.insertBefore(selectedNode, replaceNode); } dragover.classList.remove("drag-over"); let node = selectedNode; Vvveb.Undo.addMutation({type: 'move', target: node, oldParent: node.parentNode, oldNextSibling: node.nextSibling}); } selected = null; dragover = null; } }, dragStart: function (e) { let element = e.target.closest("div"); if (element) { selected = e.target } }, } Vvveb.TreeList = { selector: '#tree-list', container: null, tree: [], idToNode : {}, init: function() { // header move this.container = document.querySelector(this.selector); let header = this.container.querySelector(".header"); let isDown = false; let offset = [0,0]; let self = this; header.addEventListener('mousedown', function(e) { if (e.which == 1) {//left click isDown = true; offset = [ self.container.offsetLeft - e.clientX, self.container.offsetTop - e.clientY ]; } }, true); document.addEventListener('mouseup', function() { isDown = false; }, true); document.addEventListener('mousemove', function(event) { if (isDown) { event.preventDefault(); let left = Math.max(event.clientX + offset[0], 0); let top = Math.max(event.clientY + offset[1], 0); if (left >= 0 && (left < (window.innerWidth - self.container.clientWidth))) self.container.style.left = left + "px"; if (top >= 0 && (top < (window.innerHeight - self.container.clientHeight))) self.container.style.top = top + "px"; } }); document.querySelector(this.selector).addEventListener("click", function (e) { let element = e.target.closest("li[data-component]"); if (element) { const node = element._treeNode; node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); //node.click(); Vvveb.Builder.selectNode(node); Vvveb.Builder.loadNodeComponent(node); document.querySelector(self.selector + " .active")?.classList.remove("active"); element.querySelector("label").classList.add("active"); } }) document.querySelector(this.selector).addEventListener("mousemove", function (e) { let element = e.target.closest("li[data-component]"); if (element) { const node = element._treeNode; node.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, })); } }) }, selectComponent: function(node) { let id; for (const i in this.idToNode) { if (node == this.idToNode[i]) { id = i; break; } } if (id) { let element = document.getElementById("id" + id); this.container.querySelector(".active")?.classList.remove("active"); //collapse all let checkboxes = this.container.querySelectorAll("input[type=checkbox]:checked"); for (let i = 0, len = checkboxes.length; i < len; i++) { checkboxes[i].checked = false; let label = checkboxes[i].labels[0]; if (label) { label.classList.remove("active"); } } //expand parents if (element) { let parent = element; let current = element; while (parent = current.closest("li")) { current = parent.parentNode; let input = parent.querySelector("input"); if (input && input.hasAttribute("type") && input.type == "checkbox") { input.checked = true; } } element.checked = true; element.labels[0].classList.add("active"); element.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); } } return false; }, loadComponents: function() { let list = this.container.querySelector(".tree > ol"); //if navigator not visible don't load if (list.offsetParent === null) return; this.tree = []; this.idToNode = {}; getNodeTree(window.FrameDocument.body, this.tree, {}, this.idToNode); let ol = drawComponentsTree(this.tree); list.replaceWith(ol); //list.replaceWith(html); }, } Vvveb.FileManager = { tree:false, pages:{}, currentPage: false, allowedComponents: {}, init: function(allowedComponents = {}) { this.allowedComponents = allowedComponents; this.tree = document.querySelector("#filemanager .tree > ol"); this.tree.replaceChildren(); this.tree.addEventListener("click", function (e) { let element = event.target.closest("a"); if (element) { e.stopImmediatePropagation(); if (element.classList.contains('view')) return; e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = event.target.closest(".delete"); if (element) { Vvveb.FileManager.deletePage(element.closest("li"), e); e.stopImmediatePropagation(); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = event.target.closest(".rename"); if (element) { Vvveb.FileManager.renamePage(element.closest("li"), e, false); e.stopImmediatePropagation(); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = event.target.closest(".duplicate"); if (element) { Vvveb.FileManager.renamePage(element.closest("li"), e, true); e.stopImmediatePropagation(); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = event.target.closest("li[data-page] label"); if (element) { let page = element.parentNode.dataset.page; if (page) Vvveb.FileManager.loadPage(page, allowedComponents); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = event.target.closest("li[data-component] label"); if (element) { const node = e.currentTarget.parentNode._node; node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); node.click(); } }); this.tree.addEventListener("mouseenter", function (e) { let element = event.target.closest("li[data-component] label"); if (element) { const node = e.currentTarget.parentNode._node; node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); node.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, })); //node.trigger("mousemove"); } }); }, clear: function() { this.pages = {}; this.currentPage = false; this.tree.replaceChildren(); }, deletePage: function(element, e) { let page = element.dataset; let post_id = element.dataset.post_id ?? 0; let name; let _self = this; if (post_id) { name = element.querySelector('label span')?.textContent; } else { name = page.file; } if (confirm(`Are you sure you want to delete "${name}"?`)) { //allow event to change page or cancel by setting page to false window.dispatchEvent(new CustomEvent("vvveb.FileManager.deletePage", { detail: page })); if (page) { fetch(deleteFileUrl, {method: "POST", body: new URLSearchParams({file:page.file, post_id, csrf: document.getElementById('csrf')?.value})}) .then((response) => { if (!response.ok) { return Promise.reject(response); } return response.json() }) .then((data) => { let bg = "success"; if (data.success) { document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true")); } else { bg = "danger"; } displayToast(bg, "Delete", data.message ?? data); }) .catch(error => { let message = error.statusText ?? "Error deleting page!"; displayToast("danger", "Error", message); error.text().then( errorMessage => { let message = errorMessage.substr(0, 200); displayToast("danger", "Error", message); }) }); element.remove(); } } }, renamePage: function(element, e, duplicate = false) { let page = element.dataset; let post_id = element.dataset.post_id ?? 0; let product_id = element.dataset.product_id ?? 0; let newfile; let name; let _self = this; name = element.querySelector('label span')?.textContent.replace('.html', ''); if (post_id || product_id) { name = prompt(`Enter new name for "${name}"`, name); if (name) { newfile = page.file; } } else { name = newfile = prompt(`Enter new file name for "${page.file}"`, name.toLowerCase()); } if (newfile && name) { //allow event to change page or newfile or cancel by setting page to false window.dispatchEvent(new CustomEvent("vvveb.FileManager.renamePage", { detail: {page, newfile} })); if (page) { fetch(renameFileUrl, {method: "POST", body: new URLSearchParams({file:page.file, newfile:newfile, name, duplicate, post_id, product_id, csrf: document.getElementById('csrf')?.value})}) .then((response) => { if (!response.ok) { return Promise.reject(response); } return response.json() }) .then((data) => { let bg = "success"; if (data.success) { //document.querySelectorAll("#top-panel .save-btn").forEach(e => e.setAttribute("disabled", "true")); } else { bg = "danger"; } newfile = data.newfile ?? newfile; displayToast(bg, "Rename", data.message ?? data); let baseName = newfile.replace('.html', ''); let newName = data.name ?? name ?? friendlyName(newfile.replace(/.*[\/\\]+/, '')).replace('.html', ''); if (duplicate) { let addPage = _self.pages[page.page]; addPage["name"] = data.name ?? baseName; addPage["file"] = newfile; addPage["title"] = newName; addPage["url"] = data.url; addPage["post_id"] = data.post_id; addPage["product_id"] = data.product_id; if (data.newfile) { //addPage["url"] = Vvveb.themeBaseUrl + addPage["url"] //addPage["url"] = page.url.substring(0, page.url.lastIndexOf("/") + 1) + addPage["url"]; } let newPage = Vvveb.FileManager.addPage(baseName, addPage, page.page); Vvveb.FileManager.scrollToPage(newPage); } else { _self.pages[page.page]["file"] = newfile; _self.pages[page.page]["title"] = newName; page.url = data.url ?? page.url.replace(page.file, newfile); page.file = newfile; element.querySelector(":scope > label span").innerHTML = newName; element.querySelector(":scope > label a.view").setAttribute("href", page.url); _self.pages[page.page]["url"] = page.url; _self.pages[page.page]["file"] = page.file; } }) .catch(error => { let message = error.statusText ?? "Error renaming page!"; displayToast("danger", "Error", message); error.text().then( errorMessage => { let message = errorMessage.substr(0, 200); displayToast("danger", "Error", message); }) }); } } }, addPage: function(name, data, afterPage = false) { //allow event to change name or cancel by setting name to false window.dispatchEvent(new CustomEvent("vvveb.FileManager.addPage", { detail: [name, data], })); if (!name) { return false; } this.pages[name] = data; data['name'] = name; let folder = this.tree; if (data.folder) { if ((data.folder && data.folder != "/") && !(folder = folder.querySelector('li[data-folder="' + data.folder + '"]'))) { data.folderTitle = friendlyName(data.folder);//data.folder[0].toUpperCase() + data.folder.slice(1); folder = generateElements(tmpl("vvveb-filemanager-folder", data))[0]; this.tree.append(folder); } folder = folder.querySelector("ol"); } let page = generateElements(tmpl("vvveb-filemanager-page", data))[0]; if (afterPage && (afterPage = folder.querySelector('[data-page="' + afterPage + '"]'))) { afterPage.after(page); } else { folder.append(page); } return page; }, addPages: function(pages) { for (page in pages) { this.addPage(pages[page]['name'], pages[page]); } }, addComponent: function(name, url, title, page) { document.querySelector("[data-page='" + page + "'] > ol", this.tree).append( tmpl("vvveb-filemanager-component", {name:name, url:url, title:title})); }, loadComponents: function(allowedComponents = {}) { let tree = []; getNodeTree(window.FrameDocument.body, tree, allowedComponents); let html = drawComponentsTree(tree); document.querySelector("[data-page='" + this.currentPage + "'] > ol", this.tree).replaceWith(html); }, getCurrentUrl: function() { if (this.currentPage) { return this.pages[this.currentPage]['url']; } }, getCurrentPage: function() { return this.currentPage; }, getPageData: function(key) { if (this.currentPage) { return this.pages[this.currentPage][key]; } }, getCurrentFileName: function() { if (this.currentPage) { let folder = this.pages[this.currentPage]['folder']; folder = folder ? folder + '/': ''; return folder + this.pages[this.currentPage]['file']; } }, reloadCurrentPage: function() { if (this.currentPage) return this.loadPage(this.currentPage); }, loadPage: function(name, allowedComponents = false, disableCache = true, loadComponents = false) { let url = this.pages[name]['url'] ?? ""; if (!url) { return; } let page = this.tree.querySelector("[data-page='" + name + "']"); //remove active from current active page this.tree.querySelector("[data-page].active")?.classList.remove("active"); //set loaded page as active page.classList.add("active"); //open parent folder if closed page.closest("[data-folder]")?.querySelector("input[type=checkbox]").setAttribute("checked", true); this.currentPage = name; document.querySelector(".btn-preview-url").setAttribute("href", url); //allow event to change page or url or cancel by setting url to false let self = this; window.dispatchEvent(new CustomEvent("vvveb.FileManager.loadPage", { detail: self.pages[name], })); if (url) { Vvveb.Builder.loadUrl(url + (disableCache ? (url.indexOf('?') > -1 ? '&r=':'?r=') + Math.random():''), function () { if (loadComponents) { Vvveb.FileManager.loadComponents(allowedComponents); } Vvveb.SectionList.loadSections(allowedComponents); Vvveb.TreeList.loadComponents(); Vvveb.StyleManager.init(); Vvveb.ChangeManager.setOriginalContent(); }); } }, scrollToPage: function(page) { page.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); }, } Vvveb.Breadcrumb = { tree:false, init: function() { this.tree = document.querySelector(".breadcrumb-navigator > .breadcrumb"); this.tree.replaceChildren(); this.tree.addEventListener("click", function (e) { let element = event.target.closest(".breadcrumb-item"); if (element) { let node = element._node; if (node) { //node.click(); Vvveb.Builder.selectNode(node); Vvveb.Builder.loadNodeComponent(node); node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); } e.preventDefault(); } }); let currentHoverNode; this.tree.addEventListener("mousemove", function (e) { if (event.target == currentHoverNode) return; currentHoverNode = event.target; let element = event.target.closest(".breadcrumb-item"); if (element) { let node = element._node; node.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, })); } }) }, addElement: function(data, element) { let li = generateElements(tmpl("vvveb-breadcrumb-navigaton-item", data))[0]; li._node = element; this.tree.prepend(li); }, loadBreadcrumb: function(element) { this.tree.replaceChildren(); let currentElement = element; while (currentElement.parentElement) { let elementType = Vvveb.Builder._getElementType(currentElement); let el = elementType[1].toLowerCase(); this.addElement({ "name": el + " " + elementType[0], "className": "el-" + el }, currentElement); currentElement = currentElement.parentElement; } } } Vvveb.Revisions = { updateRevisionsDropdown: function (revisions) { let dropdown = document.querySelector(".revisions-dropdown"); let count = Object.keys(revisions).length; document.querySelector(".revisions-count").innerHTML = count; dropdown.replaceChildren(); for (let i in revisions) { let revision = revisions[i]; //revision['url'] = '' + revision['file'] + ".html"; dropdown.append(generateElements(tmpl('vvveb-revision-item', revision))[0]); } }, loadRevisions: function (template) { let self = this; if (template) { fetch(revisionsUrl + "&" + new URLSearchParams({template})) .then((response) => { if (!response.ok) { throw new Error(response) } return response.json() }) .then((data) => { self.updateRevisionsDropdown(data); }) .catch(error => { console.log(error.statusText); displayToast("danger", "Error", "Error loading revision!"); }); } }, init: function () { let self = this; window.addEventListener("vvveb.FileManager.loadPage", function(event) { let data = event.detail; self.loadRevisions(data['file'] ?? false); }); document.querySelector(".revisions-dropdown").addEventListener("click", function (event) { let element = event.target.closest(".btn-revision-load"); if (element) { let file = element.dataset.file; if (file) { fetch(revisionLoadUrl, { method: "POST", headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({file, csrf: document.getElementById('csrf')?.value}) }) .then((response) => { if (!response.ok) { throw new Error(response) } return response.text() }) .then((data) => { Vvveb.Builder.setHtml(data); displayToast("success", "Load", "Revision loaded"); Vvveb.Builder.frameBody.querySelectorAll("[data-aos]").forEach(e => e.classList.add("aos-init","aos-animate")); document.querySelectorAll("#top-panel .save-btn").forEach(e => e.removeAttribute("disabled")); Vvveb.SectionList.loadSections(); Vvveb.TreeList.loadComponents(); Vvveb.StyleManager.init(); Vvveb.ChangeManager.setOriginalContent(); }) .catch(error => { console.log(error.statusText); displayToast("danger", "Error", "Error loading revisions!"); }); } } }); document.querySelector(".revisions-dropdown").addEventListener("click", function (event) { if (confirm("Are you sure?")) { let element = event.target.closest(".btn-revision-delete"); if (element) { let item = element.closest(".dropdown-item"); let file = element.dataset.file; if (file) { fetch(revisionDeleteUrl, { method: "POST", headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({file, csrf: document.getElementById('csrf')?.value}) }) .then((response) => { if (!response.ok) { throw new Error(response) } return response.json() }) .then((data) => { let bg = "success"; if (data.success) { item.remove(); } else { bg = "danger"; } displayToast(bg, "Delete", data.message ?? data); let count = document.querySelector(".revisions-count"); count.innerHTML = Math.max(0, parseInt(count.innerHTML) - 1); }) .catch(error => { console.log(error.statusText); displayToast("danger", "Error", "Error deleting revision!"); }); } } } }); } } Vvveb.Server = { url: "", component: "", index: "", fullUpdate: false, userServerTemplate: false, ajax: function (callback) { let self = this; this.url = Vvveb.Builder.iframe.contentWindow.location.href; self.element.style.opacity = "0.85"; self.element.style.transition = "min-height 0.25s ease-out"; self.element.style.interpolateSize = "allow-keywords"; let data = new FormData(); data.append("_component_content", this.content); data.append("html", Vvveb.Builder.getHtml()); data.append("csrf", document.getElementById('csrf')?.value); fetch(this.url + '&_component_ajax=' + this.component + '&_component_id=' + this.index + '&_server_template=' + this.userServerTemplate + '&r=true',{ method: 'POST', body: data }) .then((response) => { if (!response.ok) { throw new Error(response) } return response.text() }) .then((data) => { if (data) { let newElement = generateElements(data)[0]; //set fixed height for parent to avoid page flicker let parent = self.element.parentNode; parent.style.minHeight = parent.clientHeight + "px"; //full update if (this.fullUpdate) { self.element.replaceWith(newElement); } else { self.element.innerHTML = newElement.innerHTML; self.element.style.minHeight = self.element.clientHeight + "px"; } Vvveb.Builder.selectNode(self.element); setTimeout(function () { //if (this.fullUpdate) if (parent) parent.style.minHeight = ""; self.element.style.minHeight = ""; //self.element.click(); Vvveb.Builder.selectNode(self.element); Vvveb.TreeList.loadComponents(); }, 500); } self.element.removeAttribute("style"); if (callback) callback(data); }) .catch(error => { console.log(error.statusText); }); }, //render a single component renderComponent:function (element, component, index) { if (component) { this.component = component; this.index = index; } else { this.component = element.attributes[0].replace("data-v-component-", ""); let selector = "[" + element.attributes[0] + "]"; 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; this.throttle = setTimeout(function () { clearTimeout(this.throttle); self.ajax(); }, 500); } return element; }, //search for components inside element and render them all renderComponents() { } } Vvveb.FontsManager = { activeFonts:[], providers: {},//{"google":GoogleFontsManager}; addFontList: function(provider, groupName, fontList) { let fonts = {}; let fontNames = []; let fontSelect = generateElements("")[0]; for (const font in fontList) { fontNames.push({"text":font, "value":font, "data-provider": provider}); let option = new Option(font, font); option.dataset.provider = provider; //option.style.setProperty("font-family", font);//font preview if the fonts are loaded in editor fontSelect.append(option); } document.getElementById("font-family").append(fontSelect); let list = Vvveb.Components.getProperty("_base", "font-family"); if (list) { list.onChange = function (node,value, input, component) { let option = input.options[input.selectedIndex]; Vvveb.FontsManager.addFont(option.dataset.provider, value, node); return node; }; list.data.options.push({optgroup:groupName}); list.data.options = list.data.options.concat(fontNames); Vvveb.Components.updateProperty("_base", "font-family", {data:list.data}); //update default font list fontList = list.data.options; } }, addProvider: function(provider, Obj) { this.providers[provider] = Obj; }, //add also element so we can keep track of the used fonts to remove unused ones addFont: function(provider, fontFamily, element = false) { if (!provider) return; let providerObj = this.providers[provider]; if (providerObj) { providerObj.addFont(fontFamily); this.activeFonts.push({provider, fontFamily, element}); } }, removeFont: function(provider, fontFamily) { if (!provider) return; let providerObj = this.providers[provider]; if (provider!= "default" && providerObj) { providerObj.removeFont(fontFamily); } }, //check if the added fonts are still used for the elements they were set and remove unused ones cleanUnusedFonts: function (){ for (i in this.activeFonts) { let elementFont = this.activeFonts[i]; if (elementFont.element) { if (Vvveb.StyleManager.getStyle(elementFont.element,'font-family').replaceAll('"','') != elementFont.fontFamily) { this.removeFont(elementFont.provider, elementFont.fontFamily); } } } } }; Vvveb.ColorPalette = { colors: {}, getAll: function() { return this.colors; }, add: function(name, color) { this.colors[name] = color; }, remove: function(color) { delete this.colors[name]; }, } function friendlyName(name) { name = name.replaceAll("--bs-","").replace(/[-_]/g, " ").trim(); return name = name[0].toUpperCase() + name.slice(1); } Vvveb.ColorPaletteManager = { cssVars: {"font": {}, "color" : {}, "dimensions": {}}, getType: function (type) { return this.cssVars[type]; }, getAllCSSVariableNames: function (styleSheets = document.styleSheets, selector){ for(let i = 0; i < styleSheets.length; i++){ try{ let cssRules = styleSheets[i].cssRules; for( let j = 0; j < cssRules.length; j++){ try{ let style = cssRules[j].style; if (selector && cssRules[j].selectorText && cssRules[j].selectorText != selector) continue; for(let k = 0; k < style.length; k++){ let name = style[k]; let value = style.getPropertyValue(name).trim(); let type = ""; if(name.startsWith("--")){ //ignore bootstrap rgb variables if (name.endsWith("-rgb")) continue; //ignore variables depending on other variables if (value.startsWith("var(")) continue; let friendlyName = name.replace("--bs-","").replaceAll("-", " "); if (value.startsWith("#")) { type = "color"; } else if (value.indexOf('"') >= 0 || value.indexOf("'") >= 0) { type = "font"; } else if (value.endsWith('em') > 0 || value.endsWith('px') > 0) { type = "dimensions"; } else if (!isNaN(parseFloat(value))) { type = "dimensions"; } if (type) { if (!this.cssVars[type]) this.cssVars[type] = {}; this.cssVars[type][name] = {value, type, friendlyName}; } } } } catch (error) {} } } catch (error) {} } return this.cssVars; }, getCssWithVars: function (styleSheets = document.styleSheets, vars){ let cssVars = {}; let css = ""; let cssStyles = ""; for(let i = 0; i < styleSheets.length; i++){ try{ let cssRules = styleSheets[i].cssRules; for( let j = 0; j < cssRules.length; j++){ try{ let style = cssRules[j].style; //if (selector && cssRules[j].selectorText && cssRules[j].selectorText != selector) continue; cssStyles = ""; for(let k = 0; k < style.length; k++){ let name = style[k]; let value = style.getPropertyValue(name); if(name.startsWith('--bs-btn-')) { for (v in vars) { if (value == vars[v]) { cssVars[name] = v; cssStyles += name + ":var(" + v + ");\n"; } } } } if (cssStyles) { css += cssRules[j].selectorText + "{\n" css += cssStyles; css += "}\n"; } } catch (error) {} } } catch (error) {} } return cssVars; }, init: function(document) { Vvveb.Components.render("config/bootstrap", "#configuration .component-properties"); //apply current theme color palette //let colors = Vvveb.ColorPaletteManager.getType("color"); let colors = this.cssVars.color; for (const name in colors) { let color = colors[name].value; if (color[0] == "#" && color.length == 7) {//add only valid hex color values 7 char long //add color as name to keep values unique Vvveb.ColorPalette.add(color, color); } } }, }; Vvveb.Config = { components :[], blocks :[], plugins :[], load: function(url = 'default.json') { $.getJSON( url, function( data ) { }); } } // Toggle fullscreen function launchFullScreen(document) { if(document.documentElement.requestFullScreen) { if (document.FullScreenElement) document.exitFullScreen(); else document.documentElement.requestFullScreen(); //mozilla } else if(document.documentElement.mozRequestFullScreen) { if (document.mozFullScreenElement) document.mozCancelFullScreen(); else document.documentElement.mozRequestFullScreen(); //webkit } else if(document.documentElement.webkitRequestFullScreen) { if (document.webkitFullscreenElement) document.webkitExitFullscreen(); else document.documentElement.webkitRequestFullScreen(); //ie } else if(document.documentElement.msRequestFullscreen) { if (document.msFullScreenElement) document.msExitFullscreen(); else document.documentElement.msRequestFullscreen(); } } let fontList = [{ value: "", text: "Default" }, { value: "Arial, Helvetica, sans-serif", text: "Arial" }, { value: "'Lucida Sans Unicode', 'Lucida Grande', sans-serif", text: 'Lucida Grande' }, { value: "'Palatino Linotype', 'Book Antiqua', Palatino, serif", text: 'Palatino Linotype' }, { value: "'Times New Roman', Times, serif", text: 'Times New Roman' }, { value: "Georgia, serif", text: "Georgia, serif" }, { value: "Tahoma, Geneva, sans-serif", text: "Tahoma" }, { value: "'Comic Sans MS', cursive, sans-serif", text: 'Comic Sans' }, { value: "Verdana, Geneva, sans-serif", text: 'Verdana' }, { value: "Impact, Charcoal, sans-serif", text: 'Impact' }, { value: "'Arial Black', Gadget, sans-serif", text: 'Arial Black' }, { value: "'Trebuchet MS', Helvetica, sans-serif", text: 'Trebuchet' }, { value: "'Courier New', Courier, monospace", text: 'Courier New' }, { value: "'Brush Script MT', sans-serif", text: 'Brush Script' }];