/*
Copyright 2017 Ziadin Givan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
https://github.com/givanz/VvvebJs
*/
// Simple JavaScript Templating
// John Resig - https://johnresig.com/ - MIT Licensed
(function(){
var cache = {};
var startTag = "{%";
var endTag = "%}";
var re1 = new RegExp(`((^|${endTag})[^\t]*)'`,"g");
var 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.
var 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",
"var 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;
};
})();
var delay = (function(){
var 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"*/;
}
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.dragHtml = '
';
Vvveb.baseUrl = document.currentScript?document.currentScript.src.replace(/[^\/]*?\.js$/,''):'';
Vvveb.imgBaseUrl = Vvveb.baseUrl;
Vvveb.ComponentsGroup = {};
Vvveb.SectionsGroup = {};
Vvveb.BlocksGroup = {};
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]["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 (var i in data.nodes)
{
this._nodesLookup[ data.nodes[i] ] = data;
}
}
if (data.attributes)
{
if (data.attributes.constructor === Array)
{
for (var i in data.attributes)
{
this._attributesLookup[ data.attributes[i] ] = data;
}
} else
{
for (var 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 (var i in data.classes)
{
this._classesLookup[ data.classes[i] ] = data;
}
}
if (data.classesRegex)
{
for (var i in data.classesRegex)
{
this._classesRegexLookup[ data.classesRegex[i] ] = data;
}
}
},
extend: function(inheritType, type, data) {
var newData = data;
if (inheritData = this._components[inheritType])
{
newData = $.extend(true,{}, inheritData, data);
newData.properties = $.merge( $.merge([], inheritData.properties?inheritData.properties:[]), data.properties?data.properties:[]);
}
//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;
});
/*
var output = array.reduce(function(o, cur) {
// Get the index of the key-value pair.
var occurs = o.reduce(function(n, item, i) {
return (item.key === cur.key) ? i : n;
}, -1);
// If the name is found,
if (occurs >= 0) {
// append the current value to its list of values.
o[occurs].value = o[occurs].value.concat(cur.value);
// Otherwise,
} else {
// add the current item to o (but make sure the value is an array).
var obj = {name: cur.key, value: [cur.value]};
o = o.concat([obj]);
}
return o;
}, newData.properties);
*/
this.add(type, newData);
},
matchNode: function(node) {
var component = {};
if (!node || !node.tagName) return false;
if (node.attributes && node.attributes.length)
{
//search for attributes
for (var 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 (var 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;
},
render: function(type, panel = false) {
var component = this._components[type];
if (panel) {
componentsPanel = panel;
} else {
panel = this.componentPropertiesElement;
}
var componentsPanel = $(panel);
var defaultSection = this.componentPropertiesDefaultSection;
var componentsPanelSections = {};
$(panel + " .tab-pane").each(function ()
{
var sectionName = this.dataset.section;
componentsPanelSections[sectionName] = $(this);
$('.section[data-section!="default"]', this).remove();
$('label[for!="header_default"]', this).remove();
$('input[id!="header_default"]', this).remove();
});
var section = componentsPanelSections[defaultSection].find('.section[data-section="default"]');
if (!(Vvveb.preservePropertySections && section.length))
{
componentsPanelSections[defaultSection].html('').append(tmpl("vvveb-input-sectioninput", {key:"default", header:component.name}));
section = componentsPanelSections[defaultSection].find(".section");
}
componentsPanelSections[defaultSection].find('[data-header="default"] span').html(component.name);
section.html("")
if (component.beforeInit) component.beforeInit(Vvveb.Builder.selectedEl.get(0));
var element;
var fn = function(component, property) {
return property.input.on('propertyChange', function (event, value, input) {
var element = Vvveb.Builder.selectedEl;
if (property.child) element = element.find(property.child);
if (property.parent) element = element.parent(property.parent);
if (property.onChange)
{
element = property.onChange(element, value, input, component);
}/* else */
if (property.htmlAttr)
{
oldValue = element.attr(property.htmlAttr);
if (property.htmlAttr == "class" && property.validValues)
{
element.removeClass(property.validValues.join(" "));
element = element.addClass(value);
}
else if (property.htmlAttr == "style")
{
oldStyle = $("#vvvebjs-styles", window.FrameDocument).html();
element = Vvveb.StyleManager.setStyle(element, property.key, value);
}
else if (property.htmlAttr == "innerHTML")
{
element = Vvveb.ContentManager.setHtml(element, value);
}
else
{
//if value is empty then remove attribute useful for attributes without values like disabled
if (value)
{
element = element.attr(property.htmlAttr, value);
} else
{
element = element.removeAttr(property.htmlAttr);
}
}
if (property.htmlAttr == "style") {
mutation = {
type: 'style',
target: element.get(0),
attributeName: property.htmlAttr,
oldValue: oldStyle,
newValue: $("#vvvebjs-styles", window.FrameDocument).html()};
Vvveb.Undo.addMutation(mutation);
} else {
Vvveb.Undo.addMutation(
{type: 'attributes',
target: element.get(0),
attributeName: property.htmlAttr,
oldValue: oldValue,
newValue: element.attr(property.htmlAttr)});
}
}
if (component.onChange)
{
element = component.onChange(element, property, value, input);
}
if (!property.child || !property.parent) Vvveb.Builder.selectNode(element);
return element;
});
};
var nodeElement = Vvveb.Builder.selectedEl;
for (var i in component.properties)
{
var property = component.properties[i];
var element = nodeElement;
if (property.beforeInit) property.beforeInit(element.get(0))
if (property.child) element = element.find(property.child);
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);
let value;
if (property.init)
{
property.inputtype.setValue(property.init(element.get(0)));
} 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
{
value = element.prop(property.htmlAttr);
if (!value && property.key=="src") {
value = element.prevObject.prop(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)
{
value = value.split(" ").filter(function(el) {
return property.validValues.indexOf(el) != -1
});
}
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);
var propertySection = defaultSection;
if (property.section)
{
propertySection = property.section;
}
if (property.inputtype == SectionInput)
{
section = componentsPanelSections[propertySection].find('.section[data-section="' + property.key + '"]');
if (Vvveb.preservePropertySections && section.length)
{
section.html("");
} else
{
componentsPanelSections[propertySection].append(property.input);
section = componentsPanelSections[propertySection].find('.section[data-section="' + property.key + '"]');
}
}
else
{
var row = $(tmpl('vvveb-property', property));
row.find('.input').append(property.input);
section.append(row);
}
if (property.inputtype.afterInit)
{
property.inputtype.afterInit(property.input);
}
if (property.afterInit)
{
property.afterInit(element.get(0), property.input);
}
}
if (component.init) component.init(Vvveb.Builder.selectedEl.get(0));
}
};
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.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);
range.surroundContents(element);
range.selectNodeContents(element.childNodes[0], 0);
}
}
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);
}
return element;
},
init: function(doc) {
this.doc = doc;
let self = this;
$("#bold-btn").on("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;
});
$("#italic-btn").on("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;
});
$("#underline-btn").on("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;
});
$("#strike-btn").on("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;
});
$("#link-btn").on("click", function (e) {
//doc.execCommand('createLink',false,"#");
self.editorSetStyle("a");
e.preventDefault();
return false;
});
$("#fore-color").on("change", function (e) {
//doc.execCommand('foreColor',false,this.value);
self.editorSetStyle(false, {"color" : this.value});
e.preventDefault();
return false;
});
$("#back-color").on("change", function (e) {
//doc.execCommand('hiliteColor',false,this.value);
self.editorSetStyle(false, {"background-color" : this.value});
e.preventDefault();
return false;
});
$("#font-size").on("change", function (e) {
//doc.execCommand('fontSize',false,this.value);
self.editorSetStyle(false, {"font-size" : this.value});
e.preventDefault();
return false;
});
let sizes = "";
for (i = 1;i <= 128; i++) {
sizes += "";
}
$("#font-size").html(sizes);
$("#font-family").on("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;
});
$("#justify-btn a").on("click", function (e) {
//var command = "justify" + this.dataset.value;
//doc.execCommand(command,false,"#");
self.editorSetStyle(false, {"text-align" : this.dataset.value});
e.preventDefault();
return false;
});
},
undo: function(element) {
this.doc.execCommand('undo',false,null);
},
redo: function(element) {
this.doc.execCommand('redo',false,null);
},
edit: function(element) {
element.attr({'contenteditable':true, 'spellcheckker':false});
$("#wysiwyg-editor").show();
this.element = element;
this.isActive = true;
this.oldValue = element.html();
$("#font-familly").val(getComputedStyle(element[0])['font-family']);
},
destroy: function(element) {
element.removeAttr('contenteditable spellcheckker');
$("#wysiwyg-editor").hide();
this.isActive = false;
node = this.element.get(0);
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"],
init: function(url, callback) {
var self = this;
self.loadControlGroups();
self.loadBlockGroups();
self.loadSectionGroups();
self.selectedEl = null;
self.highlightEl = null;
self.initCallback = callback;
self.documentFrame = $("#iframe-wrapper > iframe");
self.canvas = $("#canvas");
self._loadIframe(url);
self._initDragdrop();
self._initBox();
self.dragElement = null;
self.highlightEnabled = true;
self.leftPanelWidth = $("#left-panel").width();
self.adjustListsHeight();
},
/* controls */
loadControlGroups : function() {
var componentsList = $(".components-list");
componentsList.empty();
var item = {}, component = {};
var count = 0;
componentsList.each(function ()
{
var list = $(this);
var type = this.dataset.type;
count ++;
for (group in Vvveb.ComponentsGroup)
{
list.append(
`
`);
//list.append('
');
var componentsSubList = list.find('li[data-section="' + group + '"] ol');
components = Vvveb.ComponentsGroup[ group ];
for (i in components)
{
componentType = components[i];
component = Vvveb.Components.get(componentType);
if (component)
{
item = $(`
");
if (component.image) {
item.css({
backgroundImage: "url(" + Vvveb.imgBaseUrl + component.image + ")",
backgroundRepeat: "no-repeat"
})
}
componentsSubList.append(item)
}
}
}
});
},
loadSectionGroups : function() {
var sectionsList = $(".sections-list");
sectionsList.empty();
var item = {};
sectionsList.each(function ()
{
var list = $(this);
var type = this.dataset.type;
for (group in Vvveb.SectionsGroup)
{
list.append(
`
`);
var sectionsSubList = list.find('li[data-section="' + group + '"] ol');
sections = Vvveb.SectionsGroup[ group ];
for (i in sections)
{
sectionType = sections[i];
section = Vvveb.Sections.get(sectionType);
if (section)
{
item = $(`
`);
if (section.image) {
var image = ((section.image.indexOf('/') == -1) ? Vvveb.imgBaseUrl:'') + section.image;
item.css({
//backgroundImage: "url(" + image + ")",
backgroundRepeat: "no-repeat"
}).find("img").attr("src", image);
}
sectionsSubList.append(item)
}
}
}
});
},
loadBlockGroups : function() {
var blocksList = $(".blocks-list");
blocksList.empty();
var item = {};
blocksList.each(function ()
{
var list = $(this);
var type = this.dataset.type;
for (group in Vvveb.BlocksGroup)
{
list.append(
`
`);
var blocksSubList = list.find('li[data-section="' + group + '"] ol');
blocks = Vvveb.BlocksGroup[ group ];
for (i in blocks)
{
blockType = blocks[i];
block = Vvveb.Blocks.get(blockType);
if (block)
{
item = $(`