VvebOIDC/public/js/vvvebjs/builder.js

4698 lines
133 KiB
JavaScript

/**
* Vvveb
*
* Copyright (C) 2021 Ziadin Givan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* https://github.com/givanz/Vvveb
*/
// Simple JavaScript Templating and buildParams
// John Resig - https://johnresig.com/ - MIT Licensed
(function(){
let cache = {};
let startTag = "{%";
let endTag = "%}";
let re1 = new RegExp(`((^|${endTag})[^\t]*)'`,"g");
let re2 = new RegExp(`\t=(.*?)${endTag}`,"g");
this.tmpl = function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
let fn = /^[-a-zA-Z0-9]+$/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"let p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split(startTag).join("\t")
.replace(re1, "$1\r")
.replace(re2, "',$1,'")
.split("\t").join("');")
.split(endTag).join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');");
// Provide some basic currying to the user
return data ? fn( data ) : fn;
};
})();
function buildParams( prefix, obj, add ) {
let rbracket = /\[\]$/;
if ( Array.isArray( obj ) ) {
// Serialize array item.
for(const key in obj) {
let v = obj[key];
if ( rbracket.test( prefix ) ) {
// Treat each array item as a scalar.
add( prefix, v );
} else {
// Item is non-scalar (array or object), encode its numeric index.
buildParams(
prefix + "[" + ( typeof v === "object" && v != null ? key : "" ) + "]",
v,
add
);
}
}
} else if ( typeof obj === "object" ) {
// Serialize object item.
for (const name in obj ) {
buildParams( prefix + "[" + name + "]", obj[ name ], add );
}
} else {
// Serialize scalar item.
add( prefix, obj );
}
}
// Serialize an array of form elements or a set of
// key/values into a query string
function nestedFormData( a ) {
let prefix,
s = [],
add = function( key, valueOrFunction ) {
// If value is a function, invoke it and use its return value
let value = typeof valueOrFunction === "function" ?
valueOrFunction() :
valueOrFunction;
s[ s.length ] = encodeURIComponent( key ) + "=" +
encodeURIComponent( value == null ? "" : value );
};
if ( a == null ) {
return "";
}
// If an array was passed in, assume that it is an array of form elements.
if ( Array.isArray( a ) || ( Object.is( a ) ) ) {
// Serialize the form elements
for(const key in object) {
let v = object[key];
//jQuery.each( a, function() {
add( key, v );
};
} else {
// If traditional, encode the "old" way (the way 1.3.2 or older
// did it), otherwise encode params recursively.
for (const prefix in a ) {
buildParams( prefix, a[ prefix ], add );
}
}
// Return the resulting serialization
return s.join( "&" );
};
let delay = (function(){
let timer = 0;
return function(callback, ms){
clearTimeout (timer);
timer = setTimeout(callback, ms);
};
})();
function isElement(obj){
return (typeof obj==="object") &&
(obj.nodeType===1) && (typeof obj.style === "object") &&
(typeof obj.ownerDocument ==="object")/* && obj.tagName != "BODY"*/;
}
function generateElements(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.children;
}
function offset(el) {
box = el.getBoundingClientRect();
docElem = document.documentElement;
return {
top: box.top + window.pageYOffset - docElem.clientTop,
left: box.left + window.pageXOffset - docElem.clientLeft
};
}
if (Vvveb === undefined) var Vvveb = {};
Vvveb.defaultComponent = "_base";
Vvveb.preservePropertySections = true;
//icon = use component icon when dragging | html = use component html to create draggable element
Vvveb.dragIcon = 'icon';
//if empty the html of the component is used to view dropping in real time but for large elements it can jump around for this you can set a html placeholder with this option
Vvveb.dragElementStyle = "background:limegreen;width:100%;height:3px;border:1px solid limegreen;box-shadow:0px 0px 2px 1px rgba(0,0,0,0.14);overflow:hidden;";
Vvveb.dragHtml = '<div style="' + Vvveb.dragElementStyle + '"></div>';
Vvveb.baseUrl = document.currentScript?document.currentScript.src.replace(/[^\/]*?\.js$/,''):'';
Vvveb.imgBaseUrl = Vvveb.baseUrl;
Vvveb.ComponentsGroup = {};
Vvveb.SectionsGroup = {};
Vvveb.BlocksGroup = {};
Vvveb.StylesGroup = {};
Vvveb.Components = {
_components: {},
_nodesLookup: {},
_attributesLookup: {},
_classesLookup: {},
_classesRegexLookup: {},
componentPropertiesElement: "#right-panel .component-properties",
componentPropertiesDefaultSection: "content",
get: function(type) {
return this._components[type];
},
updateProperty: function(type, key, value) {
let properties = this._components[type]["properties"];
for (property in properties) {
if (key == properties[property]["key"]) {
return this._components[type]["properties"][property] =
Object.assign(properties[property], value);
}
}
},
getProperty: function(type, key) {
let properties = this._components[type] ? this._components[type]["properties"] : [];
for (property in properties) {
if (key == properties[property]["key"]) {
return properties[property];
}
}
},
add: function(type, data) {
data.type = type;
this._components[type] = data;
if (data.nodes) {
for (let i in data.nodes) {
this._nodesLookup[ data.nodes[i] ] = data;
}
}
if (data.attributes) {
if (data.attributes.constructor === Array) {
for (let i in data.attributes) {
this._attributesLookup[ data.attributes[i] ] = data;
}
} else {
for (let i in data.attributes) {
if (typeof this._attributesLookup[i] === 'undefined') {
this._attributesLookup[i] = {};
}
if (typeof this._attributesLookup[i][ data.attributes[i] ] === 'undefined') {
this._attributesLookup[i][ data.attributes[i] ] = {};
}
this._attributesLookup[i][ data.attributes[i] ] = data;
}
}
}
if (data.classes) {
for (let i in data.classes) {
this._classesLookup[ data.classes[i] ] = data;
}
}
if (data.classesRegex) {
for (let i in data.classesRegex) {
this._classesRegexLookup[ data.classesRegex[i] ] = data;
}
}
},
extend: function(inheritType, type, data) {
let newData = data;
if (inheritData = this._components[inheritType]) {
newData = {...inheritData, ...data};
newData.properties = (data.properties ? data.properties : []).concat(inheritData.properties ? inheritData.properties : []);
}
//remove duplicates
/*
newData.properties = newData.properties.filter((value, index, self) =>
index === self.findIndex((t) => (
t.key === value.key
))
);*/
//sort by order
newData.properties.sort(function (a,b) {
if (typeof a.sort === "undefined") a.sort = 0;
if (typeof b.sort === "undefined") b.sort = 0;
if (a.sort < b.sort)
return -1;
if (a.sort > b.sort)
return 1;
return 0;
});
this.add(type, newData);
},
matchNode: function(node) {
let component = {};
if (!node || !node.tagName) return false;
if (node.attributes && node.attributes.length) {
//search for attributes
for (let i in node.attributes) {
if (node.attributes[i]) {
attr = node.attributes[i].name;
value = node.attributes[i].value;
if (attr in this._attributesLookup) {
component = this._attributesLookup[ attr ];
//currently we check that is not a component by looking at name attribute
//if we have a collection of objects it means that attribute value must be checked
if (typeof component["name"] === "undefined") {
if (value in component) {
return component[value];
}
} else {
return component;
}
}
}
}
for (let i in node.attributes) {
attr = node.attributes[i].name;
value = node.attributes[i].value;
//check for node classes
if (attr == "class") {
classes = value.split(" ");
for (j in classes) {
if (classes[j] in this._classesLookup)
return this._classesLookup[ classes[j] ];
}
for (regex in this._classesRegexLookup) {
regexObj = new RegExp(regex);
if (regexObj.exec(value)) {
return this._classesRegexLookup[ regex ];
}
}
}
}
}
tagName = node.tagName.toLowerCase();
if (tagName in this._nodesLookup) return this._nodesLookup[ tagName ];
return false;
//return false;
},
renderProperties: function(component, properties, componentsPanelSections, container) {
let fn = function(component, property) {
if (property.input) {
property.input.addEventListener('propertyChange', (event) => {
element = selectedElement = Vvveb.Builder.selectedEl;
let value = event.detail.value, input = event.detail.input, origEvent = event.detail.origEvent;
if (property.element) element = property.element;
if (property.child) element = element.querySelector(property.child);
if (property.parent) element = element.parent(property.parent);
if (property.onChange) {
let ret = property.onChange(element, value, input, component, origEvent);
//if on change returns an object then is returning the dom node otherwise is returning the new value
if (typeof ret == "object") {
element = ret;
} else {
value = ret;
}
}/* else */
if (property.htmlAttr) {
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 = "<option value=''> - Font size - </option>";
for (i = 1;i <= 128; i++) {
sizes += "<option value='"+ i +"px'>"+ i +"</option>";
}
document.getElementById("font-size").innerHTML = sizes;
document.getElementById("font-family").addEventListener("change", function (e) {
let option = this.options[this.selectedIndex];
let element = self.editorSetStyle(false, {"font-family" : this.value});
Vvveb.FontsManager.addFont(option.dataset.provider, this.value, element);
//doc.execCommand('fontName',false,this.value);
e.preventDefault();
return false;
});
document.getElementById("justify-btn").addEventListener("click", function (e) {
//let command = "justify" + this.dataset.value;
//doc.execCommand(command,false,"#");
self.editorSetStyle(false, {"text-align" : e.srcElement.dataset.value});
e.preventDefault();
return false;
});
doc.addEventListener('keydown', event => {
if (event.key === 'Enter') {
let target = event.target.closest("[contenteditable]");
if (target) {
doc.execCommand('insertLineBreak');
event.preventDefault();
}
}
})
},
undo: function(element) {
this.doc.execCommand('undo',false,null);
},
redo: function(element) {
this.doc.execCommand('redo',false,null);
},
edit: function(element) {
element.setAttribute("contenteditable", true);
element.setAttribute("spellcheckker", false);
document.getElementById("wysiwyg-editor").style.display = "block";
this.element = element;
this.isActive = true;
this.oldValue = element.innerHTML;
document.getElementById("font-family").value = Vvveb.StyleManager.getStyle(element,'font-family');
document.getElementById("fore-color").value = Vvveb.StyleManager.getStyle(element,'color');
document.getElementById("back-color").value = Vvveb.StyleManager.getStyle(element,'background-color');
element.focus();
},
destroy: function(element) {
element.removeAttribute("contenteditable");
element.removeAttribute("spellcheckker");
document.getElementById("wysiwyg-editor").style.display = "none";
this.isActive = false;
node = this.element;
Vvveb.Undo.addMutation({type:'characterData',
target: node,
oldValue: this.oldValue,
newValue: node.innerHTML});
}
}
Vvveb.Builder = {
component : {},
dragMoveMutation : false,
isPreview : false,
runJsOnSetHtml : false,
designerMode : false,
highlightEnabled : false,
selectPadding: 0,
leftPanelWidth: 275,
ignoreClasses: ["clearfix", "masonry", "has-shadow"],
init: function(url, callback) {
let self = this;
self.loadControlGroups();
self.loadBlockGroups();
self.loadSectionGroups();
self.loadStylesGroups();
self.selectedEl = null;
self.highlightEl = null;
self.initCallback = callback;
self.documentFrame = document.querySelector("#iframe-wrapper > iframe");
self.canvas = document.getElementById("canvas");
self._loadIframe(url + (url.indexOf('?') > -1 ? '&r=':'?r=') + Math.random());
self._initDragdrop();
self._initBox();
self.dragElement = null;
self.highlightEnabled = true;
self.leftPanelWidth = document.getElementById("left-panel").clientWidth;
},
/* controls */
loadControlGroups : function() {
let componentsList = document.querySelectorAll(".components-list");
let item = {}, component = {};
let count = 0;
componentsList.forEach(function (list, i) {
let type = list.dataset.type;
list.replaceChildren();
count ++;
for (group in Vvveb.ComponentsGroup) {
list.append(generateElements(
`<li class="header" data-section="${group}" data-search="">
<label class="header" for="${type}_comphead_${group}${count}">
${group}<div class="header-arrow"></div>
</label>
<input class="header_check" type="checkbox" checked="true" id="${type}_comphead_${group}${count}">
<ol></ol>
</li>`)[0]);
//list.append('<li class="header clearfix" data-section="' + group + '" data-search=""><label class="header" for="' + type + '_comphead_' + group + count + '">' + group + ' <div class="header-arrow"></div>\
// </label><input class="header_check" type="checkbox" checked="true" id="' + type + '_comphead_' + group + count + '"> <ol></ol></li>');
let componentsSubList = list.querySelector('li[data-section="' + group + '"] ol');
components = Vvveb.ComponentsGroup[ group ];
for (i in components) {
const componentType = components[i];
component = Vvveb.Components.get(componentType);
if (component) {
item = generateElements(`<li data-section="${group}" data-drag-type="component" data-type="${componentType}" data-search="${component.name.toLowerCase()}">
<span>${component.name}</span>
</li>`)[0];
if (component.image) {
item.style.backgroundImage = "url(" + Vvveb.imgBaseUrl + component.image + ")";
item.style.backgroundRepeat = "no-repeat";
}
componentsSubList.append(item);
}
}
}
});
},
loadSectionGroups : function() {
let sectionsList = document.querySelectorAll(".sections-list");
let item = {};
sectionsList.forEach(function (list, i) {
let type = list.dataset.type;
list.replaceChildren();
for (group in Vvveb.SectionsGroup) {
list.append(generateElements(
`<li class="header" data-section="${group}" data-search="">
<label class="header" for="${type}_sectionhead_${group}">
${group}<div class="header-arrow"></div>
</label>
<input class="header_check" type="checkbox" checked="true" id="${type}_sectionhead_${group}">
<ol></ol>
</li>`)[0]);
let sectionsSubList = list.querySelector('li[data-section="' + group + '"] ol');
let sections = Vvveb.SectionsGroup[ group ];
for (i in sections) {
const sectionType = sections[i];
const section = Vvveb.Sections.get(sectionType);
if (section) {
item = generateElements(`<li data-section="${group}" data-drag-type="section" data-type="${sectionType}" data-search="${section.name.toLowerCase()}">
<span class="name">${section.name}</span>
<div class="add-section-btn" title="Add section"><i class="la la-plus"></i></div>
<img class="preview" src="" loading="lazy">
</li>`)[0];
if (section.image) {
let image = ((section.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + section.image;
item.querySelector("img").setAttribute("src", image);
}
sectionsSubList.append(item)
}
}
}
});
},
loadBlockGroups : function() {
let blocksList = document.querySelectorAll(".blocks-list");
let item = {};
blocksList.forEach(function (list, i) {
let type = list.dataset.type;
list.replaceChildren();
for (group in Vvveb.BlocksGroup) {
list.append(generateElements(
`<li class="header" data-section="${group}" data-search="">
<label class="header" for="${type}_blockhead_${group}">
${group}<div class="header-arrow"></div>
</label>
<input class="header_check" type="checkbox" checked="true" id="${type}_blockhead_${group}">
<ol></ol>
</li>`)[0]);
let blocksSubList = list.querySelector('li[data-section="' + group + '"] ol');
blocks = Vvveb.BlocksGroup[ group ];
for (i in blocks) {
const blockType = blocks[i];
const block = Vvveb.Blocks.get(blockType);
if (block) {
item = generateElements(`<li data-section="${group}" data-drag-type="block" data-type="${blockType}" data-search="${block.name.toLowerCase()}">
<span class="name">${block.name}</span>
<img class="preview" src="" loading="lazy">
</li>`)[0];
if (block.image) {
let image = ((block.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + block.image;
item.querySelector("img").setAttribute("src", image);
}
blocksSubList.append(item);
}
}
}
});
},
loadStylesGroups : function() {
let stylesList = document.querySelectorAll(".styles-list");
let item = {};
stylesList.forEach(function (list, i) {
let type = list.dataset.type;
list.replaceChildren();
for (group in Vvveb.StylesGroup) {
list.append(generateElements(
`<li class="header" data-section="${group}" data-search="">
<label class="header" for="${type}_stylehead_${group}">
${group}<div class="header-arrow"></div>
</label>
<input class="header_check" type="checkbox" checked="true" id="${type}_stylehead_${group}">
<ol></ol>
</li>`)[0]);
let stylesSubList = list.querySelector('li[data-section="' + group + '"] ol');
styles = Vvveb.StylesGroup[ group ];
for (i in styles) {
const styleType = styles[i];
const style = Vvveb.Blocks.get(styleType);
if (style) {
item = generateElements(`<li data-section="${group}" data-drag-type="style" data-type="${styleType}" data-search="${style.name.toLowerCase()}">
<span class="name">${style.name}</span>
<img class="preview" src="" loading="lazy">
</li>`)[0];
if (style.image) {
let image = ((style.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + style.image;
item.querySelector("img").setAttribute("src", image);
}
stylesSubList.append(item);
}
}
}
});
},
loadUrl : function(url, callback) {
let self = this;
document.getElementById("select-box").style.display = "none";
self.initCallback = callback;
if (Vvveb.Builder.iframe.src != url) Vvveb.Builder.iframe.src = url;
},
/* iframe */
_loadIframe : function(url) {
let self = this;
self.iframe = this.documentFrame;
self.iframe.src = url;
return this.documentFrame.addEventListener("load", function() {
window.FrameWindow = self.iframe.contentWindow;
window.FrameDocument = self.iframe.contentWindow.document;
let addSectionBox = document.getElementById("add-section-box");
let highlightBox = document.getElementById("highlight-box");
let SelectBox = document.getElementById("select-box");
highlightBox.style.display = "none";
window.FrameWindow.addEventListener("beforeunload", function(event) {
if (Vvveb.Undo.undoIndex >= 0) {
let dialogText = "You have unsaved changes";
event.returnValue = dialogText;
return dialogText;
}
});
window.FrameWindow.addEventListener("unload", function(event) {
document.querySelector(".loading-message").classList.add("active");
Vvveb.Undo.reset();
});
//prevent accidental clicks on links when editing text
window.FrameDocument.addEventListener("click", function(event) {
if (Vvveb.WysiwygEditor.isActive && event.target.closest("a")) {
event.preventDefault();
return false;
}
});
selectBoxPosition = function(event) {
let pos;
let target = self.selectedEl;// ?? self.highlightEl;
highlightBox.style.display = "none";
if (target) {
pos = offset(target);
SelectBox.style.top = (pos.top - (self.frameDoc.scrollTop ?? 0) - self.selectPadding) + "px";
SelectBox.style.left = (pos.left - (self.frameDoc.scrollLeft ?? 0) - self.selectPadding) + "px";
SelectBox.style.width = ((target.offsetWidth ?? target.clientWidth) + self.selectPadding * 2) + "px";
SelectBox.style.height = ((target.offsetHeight ?? target.clientHeight) + self.selectPadding * 2) + "px";
}
}
window.FrameWindow.addEventListener("scroll", selectBoxPosition);
window.FrameWindow.addEventListener("resize", selectBoxPosition);
Vvveb.WysiwygEditor.init(window.FrameDocument);
Vvveb.StyleManager.init(window.FrameDocument);
Vvveb.ColorPaletteManager.init(window.FrameDocument);
if (self.initCallback) self.initCallback();
return self._frameLoaded();
});
},
_frameLoaded : function() {
let self = Vvveb.Builder;
self.frameDoc = window.FrameDocument;
self.frameHtml = window.FrameDocument.querySelector("html");
self.frameBody = window.FrameDocument.querySelector("body");
self.frameHead = window.FrameDocument.querySelector("head");
//insert editor helpers like non editable areas
self.frameHead.append(generateElements('<link data-vvveb-helpers href="' + Vvveb.baseUrl + '../../css/vvvebjs-editor-helpers.css" rel="stylesheet">')[0]);
self.frameHtml.setAttribute("data-vvvebjs-editor","");
self._initHighlight();
window.dispatchEvent(new CustomEvent("vvveb.iframe.loaded", {detail: self.frameDoc}));
document.querySelector(".loading-message").classList.remove("active");
//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 =
"<!DOCTYPE "
+ doc.doctype.name
+ (doc.doctype.publicId ? ' PUBLIC "' + doc.doctype.publicId + '"' : '')
+ (!doc.doctype.publicId && doc.doctype.systemId ? ' SYSTEM' : '')
+ (doc.doctype.systemId ? ' "' + doc.doctype.systemId + '"' : '')
+ ">\n";
Vvveb.FontsManager.cleanUnusedFonts();
html += doc.documentElement.outerHTML;
html = this.removeHelpers(html, keepHelperAttributes);
if (filter) {
window.dispatchEvent(new CustomEvent("vvveb.getHtml.after", {detail: doc}));
window.dispatchEvent(new CustomEvent("vvveb.getHtml.filter", {detail: html}));
}
return html;
},
setHtml: function(html) {
//documentElement.innerHTML resets <head> each time and the page flickers
//return window.FrameDocument.documentElement.innerHTML = html;
function getTag(html, tag, outerHtml = false) {
const start = html.indexOf("<" + tag);
const end = html.indexOf("</" + tag);
if (start >= 0 && end >= 0) {
if (outerHtml)
return html.slice(start, end + 3 + tag.length);
else
return html.slice(html.indexOf(">", start) + 1, end);
} else {
return html;
}
}
if (this.runJsOnSetHtml) {
this.frameBody.innerHTML = getTag(html, "body");
}
else {
window.FrameDocument.body.innerHTML = getTag(html, "body");
}
//use outerHTML if you want to set body tag attributes
//window.FrameDocument.body.outerHTML = getTag(html, "body", true);
//set head html only if changed to avoid page flicker
let headHtml = getTag(html, "head");
if (window.FrameDocument.head.innerHTML != headHtml) {
window.FrameDocument.head.innerHTML = headHtml;
}
},
saveElement: function(element, type, name, callback) {
if (type == 'section') {
Vvveb.Sections.add('reusable/'+ name, {
name,
image: "img/logo-small.png",
html: element.outerHTML});
if (Vvveb.SectionsGroup["Reusable"] === undefined) {
Vvveb.SectionsGroup["Reusable"] = [];
}
Vvveb.SectionsGroup["Reusable"].push('reusable/'+ name);
Vvveb.Builder.loadSectionGroups();
} else {
Vvveb.Blocks.add('reusable/'+ name, {
name,
image: "img/logo-small.png",
html: element.outerHTML});
if (Vvveb.BlocksGroup["Reusable"] === undefined) {
Vvveb.BlocksGroup["Reusable"] = [];
}
Vvveb.BlocksGroup["Reusable"].push('reusable/'+ name);
Vvveb.Builder.loadBlockGroups();
}
let data = {type, name, html:element.outerHTML, csrf: document.getElementById('csrf')?.value};
fetch(saveReusableUrl, {method: "POST", body: new URLSearchParams(data)})
.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('<style id="vvvebjs-styles"></style>')[0];
doc.head.append(style);
return this.cssContainer = style;
}
//if it exists
this.cssContainer = style;
this.loadCss();
return this.cssContainer;
}
},
loadCss: function() {
let style = this.cssContainer.sheet;
//if style exist then load all css styles for editor
for (let j = 0; j < style.cssRules.length; j++) {
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 = ` - <span class="text-secondary">${title}</span>`;
}
if (tree[i].children.length > 0) {
li = generateElements('<li data-component="' + node.name + '">\
<label for="id' + id + '" style="background-image:url(' + Vvveb.imgBaseUrl + node.image + ')">\
<span>' + node.name + '</span>' + title + '\
</label>\
<input type="checkbox" id="id' + id + '">\
</li>')[0];
li.append(drawComponentsTreeTraverse(node.children));
}
else {
li = generateElements('<li data-component="' + node.name + '" class="file">\
<label for="id' + id + '" style="background-image:url(' + Vvveb.imgBaseUrl + node.image + ')">\
<span>' + node.name + '</span>' + title + '\
</label>\
<input type="checkbox" id="id' + id + '">\
</li>')[0];
}
li._treeNode = node.node;
list.append(li);
}
return list;
}
return drawComponentsTreeTraverse(tree);
}
let selected = null;
let dragover = null;
Vvveb.SectionList = {
selector: '.sections-container',
allowedComponents: {},
init: function(allowedComponents = {}) {
this.allowedComponents = allowedComponents;
document.querySelector(this.selector).addEventListener("click", function (e) {
let element = e.target.closest(":scope > div .controls");
if (element) {
let node = element.parentNode._node;
if (node) {
node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
//node.click();
Vvveb.Builder.selectNode(node);
Vvveb.Builder.loadNodeComponent(node);
}
}
});
document.querySelector(this.selector).addEventListener("dblclick", function (e) {
let element = e.target.closest(":scope > div");
if (element) {
const node = element._node;
node.click();
}
});
document.querySelector(this.selector).addEventListener("click", function (e) {
let element = e.target.closest("li[data-component] label");
if (element) {
let node = element.parentNode._node;
if (node) {
node.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
node.click();
}
}
});
document.querySelector(this.selector).addEventListener("mouseenter", function (e) {
let element = e.target.closest("li[data-component] label");
if (element) {
const node = document.querySelector(element.parentNode._node);
node.css("outline","1px dashed blue");
}
});
document.querySelector(this.selector).addEventListener("mouseleave", function (e){
let element = e.target.closest("li[data-component] label");
if (element) {
const node = document.querySelector(element.parentNode._node);
node.css("outline","");
if (node.getAttribute("style") == "") node.removeAttribute("style");
}
});
document.querySelector(this.selector).addEventListener("dragstart", this.dragStart);
document.querySelector(this.selector).addEventListener("dragover", this.dragOver);
document.querySelector(this.selector).addEventListener("dragend", this.dragEnd);
document.querySelector(this.selector).addEventListener("click", function (e) {
let element = e.target.closest(".delete-btn");
if (element) {
let section = element.closest(".section-item");
let node = section._node;
node.remove();
section.remove();
Vvveb.TreeList.loadComponents();
e.stopPropagation();
e.preventDefault();
}
});
let sectionIn;
let img = document.querySelector(".block-preview img");
document.querySelector(".sections-list").addEventListener("mouseover", function (e) {
let element = e.target.closest("li[data-type]");
if (element) {
if (sectionIn != element) {
let src = element.querySelector("img").getAttribute("src");
sectionIn = element;
img.setAttribute("src", src);
img.style.display = "";
}
} else {
sectionIn = element;
img.setAttribute("src", "");
img.style.display = "none";
}
})
/*
document.querySelector(this.selector).addEventListener("click", ".up-btn", function (e) {
let section = e.target.closest(".section-item");
let node = section._node;
Vvveb.Builder.moveNodeUp(node);
Vvveb.Builder.moveNodeUp(section);
e.preventDefault();
});
document.querySelector(this.selector).addEventListener("click", ".down-btn", function (e) {
let section = e.target.closest(".section-item");
let node = section._node;
Vvveb.Builder.moveNodeDown(node);
Vvveb.Builder.moveNodeDown(section);
e.preventDefault();
});
*/
let self = this;
document.querySelector(".sections-list").addEventListener("click", function (e) {
let element = e.target.closest(".add-section-btn");
if (element) {
let item = element.closest("li");
let section = Vvveb.Sections.get(item.dataset.type);
let node = generateElements(section.html)[0];
let sectionType = node.tagName.toLowerCase();
let afterSection = Vvveb.Builder.frameBody.querySelector(":scope > " + sectionType + ":last-of-type");
if (afterSection) {
afterSection.after(node);
} else {
if (sectionType == "nav") {
afterSection = Vvveb.Builder.frameBody.querySelector(":scope > nav:first,> header:last-of-type");
if (afterSection) {
afterSection.before(node);
} else {
Vvveb.Builder.frameBody.append(node);
}
} else if (sectionType != "footer") {
afterSection = Vvveb.Builder.frameBody.querySelector("body > footer:last-of-type");
if (afterSection) {
afterSection.before(node);
} else {
Vvveb.Builder.frameBody.append(node);
}
} else {
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("<optgroup label='" + groupName + "'></optgroup>")[0];
for (const font in fontList) {
fontNames.push({"text":font, "value":font, "data-provider": provider});
let option = new Option(font, font);
option.dataset.provider = provider;
//option.style.setProperty("font-family", font);//font preview if the fonts are loaded in editor
fontSelect.append(option);
}
document.getElementById("font-family").append(fontSelect);
let list = Vvveb.Components.getProperty("_base", "font-family");
if (list) {
list.onChange = function (node,value, input, component) {
let option = input.options[input.selectedIndex];
Vvveb.FontsManager.addFont(option.dataset.provider, value, node);
return node;
};
list.data.options.push({optgroup:groupName});
list.data.options = list.data.options.concat(fontNames);
Vvveb.Components.updateProperty("_base", "font-family", {data:list.data});
//update default font list
fontList = list.data.options;
}
},
addProvider: function(provider, Obj) {
this.providers[provider] = Obj;
},
//add also element so we can keep track of the used fonts to remove unused ones
addFont: function(provider, fontFamily, element = false) {
if (!provider) return;
let providerObj = this.providers[provider];
if (providerObj) {
providerObj.addFont(fontFamily);
this.activeFonts.push({provider, fontFamily, element});
}
},
removeFont: function(provider, fontFamily) {
if (!provider) return;
let providerObj = this.providers[provider];
if (provider!= "default" && providerObj) {
providerObj.removeFont(fontFamily);
}
},
//check if the added fonts are still used for the elements they were set and remove unused ones
cleanUnusedFonts: function (){
for (i in this.activeFonts) {
let elementFont = this.activeFonts[i];
if (elementFont.element) {
if (Vvveb.StyleManager.getStyle(elementFont.element,'font-family').replaceAll('"','') != elementFont.fontFamily) {
this.removeFont(elementFont.provider, elementFont.fontFamily);
}
}
}
}
};
Vvveb.ColorPalette = {
colors: {},
getAll: function() {
return this.colors;
},
add: function(name, color) {
this.colors[name] = color;
},
remove: function(color) {
delete this.colors[name];
},
}
function friendlyName(name) {
name = name.replaceAll("--bs-","").replace(/[-_]/g, " ").trim();
return name = name[0].toUpperCase() + name.slice(1);
}
Vvveb.ColorPaletteManager = {
cssVars: {"font": {}, "color" : {}, "dimensions": {}},
getType: function (type) {
return this.cssVars[type];
},
getAllCSSVariableNames: function (styleSheets = document.styleSheets, selector){
for(let i = 0; i < styleSheets.length; i++){
try{
let cssRules = styleSheets[i].cssRules;
for( let j = 0; j < cssRules.length; j++){
try{
let style = cssRules[j].style;
if (selector && cssRules[j].selectorText && cssRules[j].selectorText != selector) continue;
for(let k = 0; k < style.length; k++){
let name = style[k];
let value = style.getPropertyValue(name).trim();
let type = "";
if(name.startsWith("--")){
//ignore bootstrap rgb variables
if (name.endsWith("-rgb")) continue;
//ignore variables depending on other variables
if (value.startsWith("var(")) continue;
let friendlyName = name.replace("--bs-","").replaceAll("-", " ");
if (value.startsWith("#")) {
type = "color";
} else if (value.indexOf('"') >= 0 || value.indexOf("'") >= 0) {
type = "font";
} else if (value.endsWith('em') > 0 || value.endsWith('px') > 0) {
type = "dimensions";
} else if (!isNaN(parseFloat(value))) {
type = "dimensions";
}
if (type) {
if (!this.cssVars[type]) this.cssVars[type] = {};
this.cssVars[type][name] = {value, type, friendlyName};
}
}
}
} catch (error) {}
}
} catch (error) {}
}
return this.cssVars;
},
getCssWithVars: function (styleSheets = document.styleSheets, vars){
let cssVars = {};
let css = "";
let cssStyles = "";
for(let i = 0; i < styleSheets.length; i++){
try{
let cssRules = styleSheets[i].cssRules;
for( let j = 0; j < cssRules.length; j++){
try{
let style = cssRules[j].style;
//if (selector && cssRules[j].selectorText && cssRules[j].selectorText != selector) continue;
cssStyles = "";
for(let k = 0; k < style.length; k++){
let name = style[k];
let value = style.getPropertyValue(name);
if(name.startsWith('--bs-btn-')) {
for (v in vars) {
if (value == vars[v]) {
cssVars[name] = v;
cssStyles += name + ":var(" + v + ");\n";
}
}
}
}
if (cssStyles) {
css += cssRules[j].selectorText + "{\n"
css += cssStyles;
css += "}\n";
}
} catch (error) {}
}
} catch (error) {}
}
return cssVars;
},
init: function(document) {
Vvveb.Components.render("config/bootstrap", "#configuration .component-properties");
//apply current theme color palette
//let colors = Vvveb.ColorPaletteManager.getType("color");
let colors = this.cssVars.color;
for (const name in colors) {
let color = colors[name].value;
if (color[0] == "#" && color.length == 7) {//add only valid hex color values 7 char long
//add color as name to keep values unique
Vvveb.ColorPalette.add(color, color);
}
}
},
};
Vvveb.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'
}];