grapesjs_codeapps
Version:
Free and Open Source Web Builder Framework/SC Modification
692 lines (614 loc) • 16.9 kB
JavaScript
import {
isUndefined,
defaults,
isArray,
contains,
toArray,
keys
} from 'underscore';
import { getModel } from 'utils/mixins';
const deps = [
require('utils'),
require('keymaps'),
require('undo_manager'),
require('storage_manager'),
require('device_manager'),
require('parser'),
require('style_manager'),
require('style_manager_bg'),
require('selector_manager'),
require('modal_dialog'),
require('code_manager'),
require('panels'),
require('rich_text_editor'),
require('asset_manager'),
require('css_composer'),
require('trait_manager'),
require('dom_components'),
require('navigator'),
require('canvas'),
require('commands'),
require('block_manager'),
require('template_list'),
require('location_manager'),
require('bg_manager')
];
const Backbone = require('backbone');
const { Collection } = Backbone;
let timedInterval;
require('utils/extender')({
Backbone: Backbone,
$: Backbone.$
});
const $ = Backbone.$;
const logs = {
debug: console.log,
info: console.info,
warning: console.warn,
error: console.error
};
module.exports = Backbone.Model.extend({
defaults() {
return {
editing: 0,
selected: new Collection(),
clipboard: null,
designerMode: false,
componentHovered: null,
previousModel: null,
changesCount: 0,
storables: [],
modules: [],
toLoad: [],
opened: {},
device: ''
};
},
initialize(c = {}) {
this.config = c;
this.set('Config', c);
this.set('modules', []);
this.set('toLoad', []);
this.set('storables', []);
const el = c.el;
const log = c.log;
const toLog = log === true ? keys(logs) : isArray(log) ? log : [];
if (el && c.fromElement) this.config.components = el.innerHTML;
this.attrsOrig = el
? toArray(el.attributes).reduce((res, next) => {
res[next.nodeName] = next.nodeValue;
return res;
}, {})
: '';
// Load modules
deps.forEach(name => this.loadModule(name));
this.on('change:componentHovered', this.componentHovered, this);
this.on('change:changesCount', this.updateChanges, this);
toLog.forEach(e => this.listenLog(e));
// Deprecations
[{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(
event => {
const eventFrom = event.from;
const eventTo = event.to;
this.listenTo(this, eventFrom, (...args) => {
this.trigger(eventTo, ...args);
this.logWarning(
`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`
);
});
}
);
},
listenLog(event) {
this.listenTo(this, `log:${event}`, logs[event]);
},
/**
* Get configurations
* @param {string} [prop] Property name
* @return {any} Returns the configuration object or
* the value of the specified property
*/
getConfig(prop) {
const config = this.config;
return isUndefined(prop) ? config : config[prop];
},
/**
* Should be called after all modules and plugins are loaded
* @param {Function} clb
* @private
*/
loadOnStart(clb = null) {
const sm = this.get('StorageManager');
// Generally, with `onLoad`, the module will try to load the data from
// its configurations
this.get('toLoad').forEach(module => {
module.onLoad();
});
// Stuff to do post load
const postLoad = () => {
const modules = this.get('modules');
modules.forEach(module => module.postLoad && module.postLoad(this));
clb && clb();
};
if (sm && sm.canAutoload()) {
this.load(postLoad);
} else {
postLoad();
}
},
/**
* Set the alert before unload in case it's requested
* and there are unsaved changes
* @private
*/
updateChanges() {
const stm = this.get('StorageManager');
const changes = this.get('changesCount');
if (this.config.noticeOnUnload) {
window.onbeforeunload = changes ? e => 1 : null;
}
if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) {
this.store();
}
},
/**
* Load generic module
* @param {String} moduleName Module name
* @return {this}
* @private
*/
loadModule(moduleName) {
var c = this.config;
var Mod = new moduleName();
var name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1);
var cfg = c[name] || c[Mod.name] || {};
cfg.pStylePrefix = c.pStylePrefix || '';
// Check if module is storable
var sm = this.get('StorageManager');
if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm;
var storables = this.get('storables');
storables.push(Mod);
this.set('storables', storables);
}
cfg.em = this;
Mod.init({ ...cfg });
// Bind the module to the editor model if public
!Mod.private && this.set(Mod.name, Mod);
Mod.onLoad && this.get('toLoad').push(Mod);
this.get('modules').push(Mod);
return this;
},
/**
* Initialize editor model and set editor instance
* @param {Editor} editor Editor instance
* @return {this}
* @private
*/
init(editor) {
this.set('Editor', editor);
},
getEditor() {
return this.get('Editor');
},
/**
* This method handles updates on the editor and tries to store them
* if requested and if the changesCount is exceeded
* @param {Object} model
* @param {any} val Value
* @param {Object} opt Options
* @private
* */
handleUpdates(model, val, opt = {}) {
// Component has been added temporarily - do not update storage or record changes
if (opt.temporary) {
return;
}
timedInterval && clearInterval(timedInterval);
timedInterval = setTimeout(() => {
if (!opt.avoidStore) {
this.set('changesCount', this.get('changesCount') + 1, opt);
}
}, 0);
},
/**
* Callback on component hover
* @param {Object} Model
* @param {Mixed} New value
* @param {Object} Options
* @private
* */
componentHovered(editor, component, options) {
const prev = this.previous('componentHovered');
prev && this.trigger('component:unhovered', prev, options);
component && this.trigger('component:hovered', component, options);
},
/**
* Returns model of the selected component
* @return {Component|null}
* @private
*/
getSelected() {
return this.get('selected').last();
},
/**
* Returns an array of all selected components
* @return {Array}
* @private
*/
getSelectedAll() {
return this.get('selected').models;
},
/**
* Select a component
* @param {Component|HTMLElement} el Component to select
* @param {Object} [opts={}] Options, optional
* @private
*/
setSelected(el, opts = {}) {
const multiple = isArray(el);
const els = multiple ? el : [el];
const selected = this.get('selected');
// If an array is passed remove all selected
// expect those yet to be selected
multiple && this.removeSelected(selected.filter(s => !contains(els, s)));
els.forEach(el => {
const model = getModel(el, $);
if (model && !model.get('selectable')) return;
!multiple && this.removeSelected(selected.filter(s => s !== model));
this.addSelected(model, opts);
});
},
/**
* Add component to selection
* @param {Component|HTMLElement} el Component to select
* @param {Object} [opts={}] Options, optional
* @private
*/
addSelected(el, opts = {}) {
const model = getModel(el, $);
const models = isArray(model) ? model : [model];
models.forEach(model => {
if (model && !model.get('selectable')) return;
const selected = this.get('selected');
opts.forceChange && selected.remove(model, opts);
selected.push(model, opts);
});
},
/**
* Remove component from selection
* @param {Component|HTMLElement} el Component to select
* @param {Object} [opts={}] Options, optional
* @private
*/
removeSelected(el, opts = {}) {
this.get('selected').remove(getModel(el, $), opts);
},
/**
* Toggle component selection
* @param {Component|HTMLElement} el Component to select
* @param {Object} [opts={}] Options, optional
* @private
*/
toggleSelected(el, opts = {}) {
const model = getModel(el, $);
const models = isArray(model) ? model : [model];
models.forEach(model => {
if (this.get('selected').contains(model)) {
this.removeSelected(model, opts);
} else {
this.addSelected(model, opts);
}
});
},
/**
* Hover a component
* @param {Component|HTMLElement} el Component to select
* @param {Object} [opts={}] Options, optional
* @private
*/
setHovered(el, opts = {}) {
const model = getModel(el, $);
if (model && !model.get('hoverable')) return;
opts.forceChange && this.set('componentHovered', '');
this.set('componentHovered', model, opts);
},
/**
* Set components inside editor's canvas. This method overrides actual components
* @param {Object|string} components HTML string or components model
* @return {this}
* @private
*/
setComponents(components) {
return this.get('DomComponents').setComponents(components);
},
/**
* Returns components model from the editor's canvas
* @return {Components}
* @private
*/
getComponents() {
var cmp = this.get('DomComponents');
var cm = this.get('CodeManager');
if (!cmp || !cm) return;
var wrp = cmp.getComponents();
return cm.getCode(wrp, 'json');
},
/**
* Set style inside editor's canvas. This method overrides actual style
* @param {Object|string} style CSS string or style model
* @return {this}
* @private
*/
setStyle(style) {
var rules = this.get('CssComposer').getAll();
for (var i = 0, len = rules.length; i < len; i++) rules.pop();
rules.add(style);
return this;
},
/**
* Returns rules/style model from the editor's canvas
* @return {Rules}
* @private
*/
getStyle() {
return this.get('CssComposer').getAll();
},
/**
* Returns HTML built inside canvas
* @return {string} HTML string
* @private
*/
getHtml() {
const config = this.config;
const exportWrapper = config.exportWrapper;
const wrappesIsBody = config.wrappesIsBody;
const js = config.jsInHtml ? this.getJs() : '';
var wrp = this.get('DomComponents').getComponent();
var html = this.get('CodeManager').getCode(wrp, 'html', {
exportWrapper,
wrappesIsBody
});
html += js ? `<script>${js}</script>` : '';
return html;
},
/**
* Returns CSS built inside canvas
* @param {Object} [opts={}] Options
* @return {string} CSS string
* @private
*/
getCss(opts = {}) {
const config = this.config;
const wrappesIsBody = config.wrappesIsBody;
const avoidProt = opts.avoidProtected;
const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles)
? opts.keepUnusedStyles
: config.keepUnusedStyles;
const cssc = this.get('CssComposer');
const wrp = this.get('DomComponents').getComponent();
const protCss = !avoidProt ? config.protectedCss : '';
return (
protCss +
this.get('CodeManager').getCode(wrp, 'css', {
cssc,
wrappesIsBody,
keepUnusedStyles
})
);
},
/**
* Returns JS of all components
* @return {string} JS string
* @private
*/
getJs() {
var wrp = this.get('DomComponents').getWrapper();
return this.get('CodeManager')
.getCode(wrp, 'js')
.trim();
},
/**
* Store data to the current storage
* @param {Function} clb Callback function
* @return {Object} Stored data
* @private
*/
store(clb) {
var sm = this.get('StorageManager');
var store = {};
if (!sm) return;
// Fetch what to store
this.get('storables').forEach(m => {
var obj = m.store(1);
for (var el in obj) store[el] = obj[el];
});
sm.store(store, res => {
clb && clb(res);
this.set('changesCount', 0);
this.trigger('storage:store', store);
});
return store;
},
/**
* Load data from the current storage
* @param {Function} clb Callback function
* @private
*/
load(clb = null) {
this.getCacheLoad(1, res => {
this.get('storables').forEach(module => module.load(res));
clb && clb(res);
});
},
/**
* Returns cached load
* @param {Boolean} force Force to reload
* @param {Function} clb Callback function
* @return {Object}
* @private
*/
getCacheLoad(force, clb) {
var f = force ? 1 : 0;
if (this.cacheLoad && !f) return this.cacheLoad;
var sm = this.get('StorageManager');
var load = [];
if (!sm) return {};
this.get('storables').forEach(m => {
var key = m.storageKey;
key = typeof key === 'function' ? key() : key;
var keys = key instanceof Array ? key : [key];
keys.forEach(k => {
load.push(k);
});
});
sm.load(load, res => {
this.cacheLoad = res;
clb && clb(res);
setTimeout(() => this.trigger('storage:load', res), 0);
});
},
/**
* Returns device model by name
* @return {Device|null}
* @private
*/
getDeviceModel() {
var name = this.get('device');
return this.get('DeviceManager').get(name);
},
/**
* Run default command if setted
* @param {Object} [opts={}] Options
* @private
*/
runDefault(opts = {}) {
var command = this.get('Commands').get(this.config.defaultCommand);
if (!command || this.defaultRunning) return;
command.stop(this, this, opts);
command.run(this, this, opts);
this.defaultRunning = 1;
},
/**
* Stop default command
* @param {Object} [opts={}] Options
* @private
*/
stopDefault(opts = {}) {
var command = this.get('Commands').get(this.config.defaultCommand);
if (!command) return;
command.stop(this, this, opts);
this.defaultRunning = 0;
},
/**
* Update canvas dimensions and refresh data useful for tools positioning
* @private
*/
refreshCanvas() {
this.set('canvasOffset', this.get('Canvas').getOffset());
},
/**
* Clear all selected stuf inside the window, sometimes is useful to call before
* doing some dragging opearation
* @param {Window} win If not passed the current one will be used
* @private
*/
clearSelection(win) {
var w = win || window;
w.getSelection().removeAllRanges();
},
/**
* Get the current media text
* @return {string}
*/
getCurrentMedia() {
const config = this.config;
const device = this.getDeviceModel();
const condition = config.mediaCondition;
const preview = config.devicePreviewMode;
const width = device && device.get('widthMedia');
return device && width && !preview ? `(${condition}: ${width})` : '';
},
/**
* Return the component wrapper
* @return {Component}
*/
getWrapper() {
return this.get('DomComponents').getWrapper();
},
/**
* Return the count of changes made to the content and not yet stored.
* This count resets at any `store()`
* @return {number}
*/
getDirtyCount() {
return this.get('changesCount');
},
/**
* Destroy editor
*/
destroyAll() {
const {
DomComponents,
CssComposer,
UndoManager,
Panels,
Canvas
} = this.attributes;
DomComponents.clear();
CssComposer.clear();
UndoManager.clear().removeAll();
Panels.getPanels().reset();
Canvas.getCanvasView().remove();
this.view.remove();
this.stopListening();
$(this.config.el)
.empty()
.attr(this.attrsOrig);
},
setEditing(value) {
this.set('editing', value);
return this;
},
isEditing() {
return !!this.get('editing');
},
log(msg, opts = {}) {
const { ns, level = 'debug' } = opts;
this.trigger('log', msg, opts);
level && this.trigger(`log:${level}`, msg, opts);
if (ns) {
const logNs = `log-${ns}`;
this.trigger(logNs, msg, opts);
level && this.trigger(`${logNs}:${level}`, msg, opts);
}
},
logInfo(msg, opts) {
this.log(msg, { ...opts, level: 'info' });
},
logWarning(msg, opts) {
this.log(msg, { ...opts, level: 'warning' });
},
logError(msg, opts) {
this.log(msg, { ...opts, level: 'error' });
},
/**
* Set/get data from the HTMLElement
* @param {HTMLElement} el
* @param {string} name Data name
* @param {any} value Date value
* @return {any}
* @private
*/
data(el, name, value) {
const varName = '_gjs-data';
if (!el[varName]) {
el[varName] = {};
}
if (isUndefined(value)) {
return el[varName][name];
} else {
el[varName][name] = value;
}
}
});