grapesjs-clot
Version:
Free and Open Source Web Builder Framework
376 lines (330 loc) • 11.1 kB
JavaScript
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/block_manager/config/config.js)
* ```js
* const editor = grapesjs.init({
* blockManager: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('block:add', (block) => { ... });
*
* // Use the API
* const blockManager = editor.BlockManager;
* blockManager.add(...);
* ```
*
* ## Available Events
* * `block:add` - Block added. The [Block] is passed as an argument to the callback.
* * `block:remove` - Block removed. The [Block] is passed as an argument to the callback.
* * `block:update` - Block updated. The [Block] and the object containing changes are passed as arguments to the callback.
* * `block:drag:start` - Started dragging block, the [Block] is passed as an argument.
* * `block:drag` - Dragging block, the [Block] is passed as an argument.
* * `block:drag:stop` - Dragging of the block is stopped. The dropped [Component] (if dropped successfully) and the [Block] are passed as arguments.
* * `block` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [getAllVisible](#getallvisible)
* * [remove](#remove)
* * [getConfig](#getconfig)
* * [getCategories](#getcategories)
* * [getContainer](#getcontainer)
* * [render](#render)
*
* [Block]: block.html
* [Component]: component.html
*
* @module BlockManager
*/
import { isElement, isArray } from 'underscore';
import Module from 'common/module';
import defaults from './config/config';
import Block from './model/Block';
import Blocks from './model/Blocks';
import Category from './model/Category';
import Categories from './model/Categories';
import BlocksView from './view/BlocksView';
export const evAll = 'block';
export const evPfx = `${evAll}:`;
export const evAdd = `${evPfx}add`;
export const evUpdate = `${evPfx}update`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evDrag = `${evPfx}drag`;
export const evDragStart = `${evDrag}:start`;
export const evDragStop = `${evDrag}:stop`;
export const evCustom = `${evPfx}custom`;
export default () => {
var c = {};
var blocks, blocksVisible, blocksView;
var categories = [];
return {
...Module,
name: 'BlockManager',
Block,
Blocks,
Category,
Categories,
events: {
all: evAll,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
drag: evDrag,
dragStart: evDragStart,
dragEnd: evDragStop,
custom: evCustom,
},
init(config = {}) {
c = { ...defaults, ...config };
const { em } = c;
this.em = em;
// Global blocks collection
blocks = new Blocks(c.blocks);
blocksVisible = new Blocks(blocks.models);
categories = new Categories();
this.all = blocks;
this.__initListen();
// Setup the sync between the global and public collections
blocks.on('add', model => blocksVisible.add(model));
blocks.on('remove', model => blocksVisible.remove(model));
blocks.on('reset', coll => blocksVisible.reset(coll.models));
return this;
},
__trgCustom() {
this.em.trigger(this.events.custom, this.__customData());
},
__customData() {
const bhv = this.__getBehaviour();
return {
bm: this,
blocks: this.getAll().models,
container: bhv.container,
dragStart: (block, ev) => this.startDrag(block, ev),
drag: ev => this.__drag(ev),
dragStop: cancel => this.endDrag(cancel),
};
},
__startDrag(block, ev) {
const { em, events } = this;
const content = block.getContent ? block.getContent() : block;
this._dragBlock = block;
em.set({ dragResult: null, dragContent: content });
[em, blocks].map(i => i.trigger(events.dragStart, block, ev));
},
__drag(ev) {
const { em, events } = this;
const block = this._dragBlock;
[em, blocks].map(i => i.trigger(events.drag, block, ev));
},
__endDrag() {
const { em, events } = this;
const block = this._dragBlock;
const cmp = em.get('dragResult');
this._dragBlock = null;
if (cmp) {
const oldKey = 'activeOnRender';
const oldActive = cmp.get && cmp.get(oldKey);
const toActive = block.get('activate') || oldActive;
const toSelect = block.get('select');
const first = isArray(cmp) ? cmp[0] : cmp;
if (toSelect || (toActive && toSelect !== false)) {
em.setSelected(first);
}
if (toActive) {
first.trigger('active');
oldActive && first.unset(oldKey);
}
if (block.get('resetId')) {
first.onAll(block => block.resetId());
}
}
em.set({ dragResult: null, dragContent: null });
[em, blocks].map(i => i.trigger(events.dragEnd, cmp, block));
},
__getFrameViews() {
return this.em
.get('Canvas')
.getFrames()
.map(frame => frame.view);
},
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),
...opts,
});
},
__getBehaviour() {
return this._bhv || {};
},
startDrag(block, ev) {
this.__startDrag(block, ev);
this.__getFrameViews().forEach(fv => fv.droppable.startCustom());
},
endDrag(cancel) {
this.__getFrameViews().forEach(fv => fv.droppable.endCustom(cancel));
this.__endDrag();
},
/**
* Get configuration object
* @return {Object}
*/
getConfig() {
return c;
},
postRender() {
const collection = blocksVisible;
blocksView = new BlocksView({ collection, categories }, c);
const elTo = this.getConfig().appendTo;
if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo);
if (!el) return this.__logWarn('"appendTo" element not found');
el.appendChild(this.render(blocksVisible.models));
}
this.__trgCustom();
},
/**
* Add new block.
* @param {String} id Block ID
* @param {[Block]} props Block properties
* @returns {[Block]} Added block
* @example
* blockManager.add('h1-block', {
* label: 'Heading',
* content: '<h1>Put your title here</h1>',
* category: 'Basic',
* attributes: {
* title: 'Insert h1 block'
* }
* });
*/
add(id, props, opts = {}) {
const prp = props || {};
prp.id = id;
return blocks.add(prp, opts);
},
/**
* Get the block by id.
* @param {String} id Block id
* @returns {[Block]}
* @example
* const block = blockManager.get('h1-block');
* console.log(JSON.stringify(block));
* // {label: 'Heading', content: '<h1>Put your ...', ...}
*/
get(id) {
return blocks.get(id);
},
/**
* Return all blocks.
* @returns {Collection<[Block]>}
* @example
* const blocks = blockManager.getAll();
* console.log(JSON.stringify(blocks));
* // [{label: 'Heading', content: '<h1>Put your ...'}, ...]
*/
getAll() {
return blocks;
},
/**
* Return the visible collection, which containes blocks actually rendered
* @returns {Collection<[Block]>}
*/
getAllVisible() {
return blocksVisible;
},
/**
* Remove block.
* @param {String|[Block]} block Block or block ID
* @returns {[Block]} Removed block
* @example
* const removed = blockManager.remove('BLOCK_ID');
* // or by passing the Block
* const block = blockManager.get('BLOCK_ID');
* blockManager.remove(block);
*/
remove(block, opts = {}) {
return this.__remove(block, opts);
},
/**
* Get all available categories.
* It's possible to add categories only within blocks via 'add()' method
* @return {Array|Collection}
*/
getCategories() {
return categories;
},
/**
* Return the Blocks container element
* @return {HTMLElement}
*/
getContainer() {
return blocksView.el;
},
/**
* Render blocks
* @param {Array} blocks Blocks to render, without the argument will render all global blocks
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.external] Render blocks in a new container (HTMLElement will be returned)
* @param {Boolean} [opts.ignoreCategories] Render blocks without categories
* @return {HTMLElement} Rendered element
* @example
* // Render all blocks (inside the global collection)
* blockManager.render();
*
* // Render new set of blocks
* const blocks = blockManager.getAll();
* const filtered = blocks.filter(block => block.get('category') == 'sections')
*
* blockManager.render(filtered);
* // Or a new set from an array
* blockManager.render([
* {label: 'Label text', content: '<div>Content</div>'}
* ]);
*
* // Back to blocks from the global collection
* blockManager.render();
*
* // You can also render your blocks outside of the main block container
* const newBlocksEl = blockManager.render(filtered, { external: true });
* document.getElementById('some-id').appendChild(newBlocksEl);
*/
render(blocks, opts = {}) {
const toRender = blocks || this.getAll().models;
if (opts.external) {
const collection = new Blocks(toRender);
return new BlocksView({ collection, categories }, { ...c, ...opts }).render().el;
}
if (blocksView) {
blocksView.updateConfig(opts);
blocksView.collection.reset(toRender);
if (!blocksView.rendered) {
blocksView.render();
blocksView.rendered = 1;
}
}
return this.getContainer();
},
destroy() {
const colls = [blocks, blocksVisible, categories];
colls.map(c => c.stopListening());
colls.map(c => c.reset());
blocksView && blocksView.remove();
c = {};
blocks = {};
blocksVisible = {};
blocksView = {};
categories = [];
this.all = {};
},
};
};