@silexlabs/grapesjs-symbols
Version:
Symbols for GrapesJS
250 lines • 9.45 kB
JavaScript
import Backbone from 'backbone';
import { allowDrop, closestInstance, wait } from '../utils';
import Symbol, { getSymbolId } from './Symbol';
export class Symbols extends Backbone.Collection {
constructor(models, { editor, options, ...opts }) {
super(models, opts);
this.updating = false;
this.editor = editor;
this.options = options;
if (!options.headless) {
this.initEvents();
}
// Make sure the symbol CRUD operations are undoable
this.editor.UndoManager.add(this);
}
disableUndo(disable) {
if (disable)
this.editor.UndoManager.stop();
else
this.editor.UndoManager.start();
}
async preventUndo(cbk) {
this.editor.UndoManager.stop();
await cbk();
this.editor.UndoManager.start();
}
initEvents() {
this.editor.on('component:create', c => this.onAdd(c));
this.editor.on('component:update:components', (parent, comp) => this.onUpdateChildren(parent, comp));
this.editor.on('component:update:attributes', c => this.onUpdateAttributes(c));
this.editor.on('component:update:classes', c => this.onUpdateClasses(c));
this.editor.on('component:input', c => this.onUpdateContent(c));
this.editor.on('styleable:change', cssRule => {
// Sometimes the event is fired with a Component instead of a CssRule
if (!cssRule.getComponent)
return;
if (!cssRule.changed)
return;
this.onStyleChanged(cssRule);
});
this.editor.on('component:drag', ({ target, parent }) => this.onDrag({ target, parent }));
//this.editor.on('undo', () => {
// this.updating = true
// setTimeout(() => {
// this.updating = false
// }, 1000)
//})
}
/**
* Update sybols with existing components
* This is used on load only
* TODO: Use `storage:end:load`? But this event is fired after the components are loaded
* TODO: Needs review
* @private
*/
updateComponents(components) {
components.forEach(c => this.onAdd(c));
}
/**
* Prevent drop on a symbol into itself or things similar
*/
onDrag({ target, parent }) {
if ((parent === null || parent === void 0 ? void 0 : parent.get('droppable')) && !allowDrop({ target, parent })) {
// Prevent drop
parent.set('droppable', false);
// Reset after drop
this.editor.once('component:drag:end', () => {
parent.set('droppable', true);
});
}
}
/**
* Add a component to a symbol
* This is useful only when loading new HTML content
* When loading a new component which is a symbol,
* add a ref to the component in its symbol.get('instances')
* Export this method for unit tests
* TODO: Needs review
* @private
*/
onAdd(c) {
const symbolId = getSymbolId(c);
if (symbolId) {
const symbol = this.get(symbolId);
if (symbol) {
if (symbol.isInstance(c)) {
console.warn('Could not add instance', c, `It is already an instance of symbol with id ${symbolId}`);
}
else {
// This is probably a duplication of a symbol
// Or we have just loaded the website
// Make it an instance
symbol.addInstance(c);
}
}
else {
// The symbol is not yet loaded
setTimeout(() => {
// Check again
if (this.get(symbolId)) {
// Allright in the end, it was just during loading
}
else {
console.error(`Could not make component with id \`${c.getId()}\` an instance of symbol with id \`${symbolId}\`: symbol not found`);
this.editor.runCommand('notifications:add', {
type: 'error',
group: 'Symbols errors',
message: `There is a problem with this component: it is supposed to be an instance of a symbol, but the symbol is not found. Symbol id: ${symbolId}`,
componentId: c.getId(),
});
}
});
}
}
}
/**
* A component's components() has changed
*/
async onUpdateChildren(parent, component) {
if (this.updating)
return;
const inst = closestInstance(parent);
if (inst) {
const symbolId = getSymbolId(inst);
const s = this.get(symbolId);
if (s) {
// wait for the component's children to be changed
// I couldn't find an event like `component:update:components:after`
// TODO: need review
await wait();
this.updating = true;
await this.preventUndo(async () => {
s.applyChildren(inst, parent, component);
});
this.updating = false;
}
else {
console.warn('Could not update instance', component, ': could not find the symbol with id', symbolId);
}
}
}
/**
* A component's attributes has changed
*/
onUpdateAttributes(c) {
if (this.updating)
return;
const inst = closestInstance(c);
if (inst) {
const symbolId = getSymbolId(inst);
const s = this.get(symbolId);
if (s) {
this.updating = true;
s.applyAttributes(inst, c);
this.updating = false;
}
else {
console.warn('Could not update instance', c, ': could not find the symbol with id', symbolId);
}
}
}
/**
* A component's css classes have changed
*/
async onUpdateClasses(c) {
if (this.updating)
return;
const inst = closestInstance(c);
if (inst) {
const symbolId = getSymbolId(inst);
const s = this.get(symbolId);
if (s) {
await wait(); // Needed for undo to work
this.updating = true;
this.preventUndo(() => {
s.applyClasses(inst, c);
});
this.updating = false;
}
else {
console.warn('Could not update instance', c, ': could not find the symbol with id', symbolId);
}
}
}
/**
* A component's text content has changed
*/
onUpdateContent(c) {
if (this.updating)
return;
const inst = closestInstance(c);
if (inst) {
const symbolId = getSymbolId(inst);
const s = this.get(symbolId);
if (s) {
this.updating = true;
this.preventUndo(() => {
s.applyContent(inst, c);
});
this.updating = false;
}
else {
console.warn('Could not update instance', c, ': could not find the symbol with id', symbolId);
}
}
}
/**
* A component's style has changed
* TODO: Needs review: isn't the style supposed to be just an attribute => we should not need to sync it, just attributes?
*/
onStyleChanged(cssRule) {
if (this.updating)
return;
const c = cssRule.getComponent();
const { style } = cssRule.changed;
if (c && style) {
const inst = closestInstance(c);
if (inst) {
const symbolId = getSymbolId(inst);
const s = this.get(symbolId);
if (s) {
// Keep only changed values
// TODO: Needs review - isn't cssRule.changed supposed to be only what changed?
const changed = Object.entries(style)
.filter(([key, value]) => cssRule.previousAttributes().style[key] !== value || !cssRule.previousAttributes().style[key])
.reduce((result, [key, value]) => {
return {
...result,
[key]: value,
};
}, {});
// Removed keys
const removed = Object.keys(cssRule.previousAttributes().style)
.filter(key => !style[key]);
if (Object.values(changed).length > 0 || removed.length > 0) {
this.updating = true;
s.applyStyle(inst, c, changed, removed);
this.updating = false;
}
}
else {
console.warn('Could not update instance', c, ': could not find the symbol with id', symbolId);
}
}
}
}
}
// From https://stackoverflow.com/questions/19673089/how-to-properly-define-backbone-collection-using-typescript
Symbols.prototype.model = Symbol;
//# sourceMappingURL=Symbols.js.map