Free and Open source Web Builder Framework. Next generation tool for building templates without coding
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

459 lines
11 KiB

import Styleable from 'domain_abstract/model/Styleable';
var Backbone = require('backbone');
var Components = require('./Components');
var Selectors = require('selector_manager/model/Selectors');
var Traits = require('trait_manager/model/Traits');
const escapeRegExp = (str) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
}
module.exports = Backbone.Model.extend(Styleable).extend({
defaults: {
// HTML tag of the component
tagName: 'div',
// Component type, eg. 'text', 'image', 'video', etc.
type: '',
// True if the component is removable from the canvas
removable: true,
// Indicates if it's possible to drag the component inside other
// Tip: Indicate an array of selectors where it could be dropped inside
draggable: true,
// Indicates if it's possible to drop other components inside
// Tip: Indicate an array of selectors which could be dropped inside
droppable: true,
// Set false if don't want to see the badge (with the name) over the component
badgable: true,
// True if it's possible to style it
// Tip: Indicate an array of CSS properties which is possible to style
stylable: true,
// Highlightable with 'dotted' style if true
highlightable: true,
// True if it's possible to clone the component
copyable: true,
// Indicates if it's possible to resize the component (at the moment implemented only on Image Components)
// It's also possible to pass an object as options for the Resizer
resizable: false,
// Allow to edit the content of the component (used on Text components)
editable: false,
// Hide the component inside Layers
layerable: true,
// This property is used by the HTML exporter as void elements do not
// have closing tag, eg. <br/>, <hr/>, etc.
void: false,
// Indicates if the component is in some CSS state like ':hover', ':active', etc.
state: '',
// State, eg. 'selected'
status: '',
// Content of the component (not escaped) which will be appended before children rendering
content: '',
// Component icon, this string will be inserted before the name, eg. '<i class="fa fa-square-o"></i>'
icon: '',
// Component related style
style: {},
// Key-value object of the component's attributes
attributes: '',
// Array of classes
classes: '',
// Component's javascript
script: '',
// Traits
traits: ['id', 'title'],
/**
* Set an array of items to show up inside the toolbar (eg. move, clone, delete)
* when the component is selected
* toolbar: [{
* attributes: {class: 'fa fa-arrows'},
* command: 'tlb-move',
* },{
* attributes: {class: 'fa fa-clone'},
* command: 'tlb-clone',
* }]
*/
toolbar: null,
},
initialize(props = {}, opt = {}) {
const em = opt.sm || {};
// Check void elements
if(opt && opt.config &&
opt.config.voidElements.indexOf(this.get('tagName')) >= 0) {
this.set('void', true);
}
this.opt = opt;
this.sm = em;
this.config = props;
this.set('attributes', this.get('attributes') || {});
this.listenTo(this, 'change:script', this.scriptUpdated);
this.listenTo(this, 'change:traits', this.traitsUpdated);
this.loadTraits();
this.initClasses();
this.initComponents();
this.initToolbar();
// Normalize few properties from strings to arrays
var toNormalize = ['stylable'];
toNormalize.forEach(function(name) {
var value = this.get(name);
if (typeof value == 'string') {
var newValue = value.split(',').map(prop => prop.trim());
this.set(name, newValue);
}
}, this);
this.set('status', '');
this.init();
},
initClasses() {
const classes = this.normalizeClasses(this.get('classes') || this.config.classes || []);
this.set('classes', new Selectors(classes));
return this;
},
initComponents() {
let comps = new Components(this.get('components'), this.opt);
comps.parent = this;
this.set('components', comps);
return this;
},
/**
* Initialize callback
*/
init() {},
/**
* Script updated
*/
scriptUpdated() {
this.set('scriptUpdated', 1);
},
/**
* Once traits are updated I have to populates model's attributes
*/
traitsUpdated() {
let found = 0;
const attrs = Object.assign({}, this.get('attributes'));
const traits = this.get('traits');
if (!(traits instanceof Traits)) {
this.loadTraits();
return;
}
traits.each((trait) => {
found = 1;
if (!trait.get('changeProp')) {
const value = trait.getInitValue();
if (value) {
attrs[trait.get('name')] = value;
}
}
});
found && this.set('attributes', attrs);
},
/**
* Init toolbar
*/
initToolbar() {
var model = this;
if(!model.get('toolbar')) {
var tb = [];
if(model.collection) {
tb.push({
attributes: {class: 'fa fa-arrow-up'},
command: 'select-parent',
});
}
if(model.get('draggable')) {
tb.push({
attributes: {class: 'fa fa-arrows'},
command: 'tlb-move',
});
}
if(model.get('copyable')) {
tb.push({
attributes: {class: 'fa fa-clone'},
command: 'tlb-clone',
});
}
if(model.get('removable')) {
tb.push({
attributes: {class: 'fa fa-trash-o'},
command: 'tlb-delete',
});
}
model.set('toolbar', tb);
}
},
/**
* Load traits
* @param {Array} traits
* @private
*/
loadTraits(traits, opts = {}) {
var trt = new Traits();
trt.setTarget(this);
traits = traits || this.get('traits');
if (traits.length) {
trt.add(traits);
}
this.set('traits', trt, opts);
return this;
},
/**
* Normalize input classes from array to array of objects
* @param {Array} arr
* @return {Array}
* @private
*/
normalizeClasses(arr) {
var res = [];
if(!this.sm.get)
return;
var clm = this.sm.get('SelectorManager');
if(!clm)
return;
arr.forEach(val => {
var name = '';
if(typeof val === 'string')
name = val;
else
name = val.name;
var model = clm.add(name);
res.push(model);
});
return res;
},
/**
* Override original clone method
* @private
*/
clone(reset) {
var attr = _.clone(this.attributes),
comp = this.get('components'),
traits = this.get('traits'),
cls = this.get('classes');
attr.components = [];
attr.classes = [];
attr.traits = [];
comp.each((md, i) => {
attr.components[i] = md.clone(1);
});
traits.each((md, i) => {
attr.traits[i] = md.clone();
});
cls.each((md, i) => {
attr.classes[i] = md.get('name');
});
attr.status = '';
attr.view = '';
if(reset){
this.opt.collection = null;
}
return new this.constructor(attr, this.opt);
},
/**
* Get the name of the component
* @return {string}
* */
getName() {
let customName = this.get('custom-name');
let tag = this.get('tagName');
tag = tag == 'div' ? 'box' : tag;
let name = this.get('type') || tag;
name = name.charAt(0).toUpperCase() + name.slice(1);
return customName || name;
},
/**
* Get the icon string
* @return {string}
*/
getIcon() {
let icon = this.get('icon');
return icon ? icon + ' ' : '';
},
/**
* Return HTML string of the component
* @param {Object} opts Options
* @return {string} HTML string
* @private
*/
toHTML(opts) {
var code = '';
var m = this;
var tag = m.get('tagName');
var idFound = 0;
var sTag = m.get('void');
var attrId = '';
var strAttr = '';
var attr = this.getAttrToHTML();
for (var prop in attr) {
if (prop == 'id') {
idFound = 1;
}
var val = attr[prop];
strAttr += typeof val !== undefined && val !== '' ?
' ' + prop + '="' + val + '"' : '';
}
// Build the string of classes
var strCls = '';
m.get('classes').each(m => {
strCls += ' ' + m.get('name');
});
strCls = strCls !== '' ? ' class="' + strCls.trim() + '"' : '';
// If style is not empty I need an ID attached to the component
if(!_.isEmpty(m.get('style')) && !idFound)
attrId = ' id="' + m.getId() + '" ';
code += '<' + tag + strCls + attrId + strAttr + (sTag ? '/' : '') + '>' + m.get('content');
m.get('components').each(m => {
code += m.toHTML();
});
if(!sTag)
code += '</'+tag+'>';
return code;
},
/**
* Returns object of attributes for HTML
* @return {Object}
* @private
*/
getAttrToHTML() {
var attr = this.get('attributes') || {};
delete attr.style;
return attr;
},
/**
* Return a shallow copy of the model's attributes for JSON
* stringification.
* @return {Object}
* @private
*/
toJSON(...args) {
var obj = Backbone.Model.prototype.toJSON.apply(this, args);
var scriptStr = this.getScriptString();
delete obj.toolbar;
if (scriptStr) {
obj.script = scriptStr;
}
return obj;
},
/**
* Return model id
* @return {string}
*/
getId() {
let attrs = this.get('attributes') || {};
return attrs.id || this.cid;
},
/**
* Return script in string format, cleans 'function() {..' from scripts
* if it's a function
* @param {string|Function} script
* @return {string}
* @private
*/
getScriptString(script) {
var scr = script || this.get('script');
if (!scr) {
return scr;
}
// Need to convert script functions to strings
if (typeof scr == 'function') {
var scrStr = scr.toString().trim();
scrStr = scrStr.replace(/^function[\s\w]*\(\)\s?\{/, '').replace(/\}$/, '');
scr = scrStr.trim();
}
var config = this.sm.config || {};
var tagVarStart = escapeRegExp(config.tagVarStart || '{[ ');
var tagVarEnd = escapeRegExp(config.tagVarEnd || ' ]}');
var reg = new RegExp(`${tagVarStart}(\\w+)${tagVarEnd}`, 'g');
scr = scr.replace(reg, (match, v) => {
// If at least one match is found I have to track this change for a
// better optimization inside JS generator
this.scriptUpdated();
return this.attributes[v];
})
return scr;
}
},{
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) {
return {tagName: el.tagName ? el.tagName.toLowerCase() : ''};
},
});