@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
445 lines (393 loc) • 13.2 kB
text/typescript
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/GrapesJS/grapesjs/blob/master/src/commands/config/config.ts)
* ```js
* const editor = grapesjs.init({
* commands: {
* // 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('run', () => { ... });
*
* // Use the API
* const commands = editor.Commands;
* commands.add(...);
* ```
*
** ## Available Events
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* * `run:{commandName}:before` - Triggered before the command is called
* * `stop:{commandName}:before` - Triggered before the command is called to stop
* * `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* * `run` - Triggered on run of any command. The id and the result are passed as arguments to the callback
* * `stop` - Triggered on stop of any command. The id and the result are passed as arguments to the callback
*
* ## Methods
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [extend](#extend)
* * [has](#has)
* * [run](#run)
* * [stop](#stop)
* * [isActive](#isactive)
* * [getActive](#getactive)
*
* @module Commands
*/
import { isFunction, includes } from 'underscore';
import CommandAbstract, { Command, CommandOptions, CommandObject, CommandFunction } from './view/CommandAbstract';
import defaults, { CommandsConfig } from './config/config';
import { Module } from '../abstract';
import { eventDrag } from '../dom_components/model/Component';
import Editor from '../editor/model/Editor';
import { ObjectAny } from '../common';
export type CommandEvent = 'run' | 'stop' | `run:${string}` | `stop:${string}` | `abort:${string}`;
const commandsDef = [
['preview', 'Preview', 'preview'],
['resize', 'Resize', 'resize'],
['rotate', 'Rotate', 'rotate'],
['fullscreen', 'Fullscreen', 'fullscreen'],
['copy', 'CopyComponent'],
['paste', 'PasteComponent'],
['canvas-move', 'CanvasMove'],
['canvas-clear', 'CanvasClear'],
['open-code', 'ExportTemplate', 'export-template'],
['open-layers', 'OpenLayers', 'open-layers'],
['open-styles', 'OpenStyleManager', 'open-sm'],
['open-traits', 'OpenTraitManager', 'open-tm'],
['open-blocks', 'OpenBlocks', 'open-blocks'],
['open-assets', 'OpenAssets', 'open-assets'],
['component-select', 'SelectComponent', 'select-comp'],
['component-outline', 'SwitchVisibility', 'sw-visibility'],
['component-offset', 'ShowOffset', 'show-offset'],
['component-move', 'MoveComponent', 'move-comp'],
['component-next', 'ComponentNext'],
['component-prev', 'ComponentPrev'],
['component-enter', 'ComponentEnter'],
['component-exit', 'ComponentExit', 'select-parent'],
['component-delete', 'ComponentDelete'],
['component-style-clear', 'ComponentStyleClear'],
['component-drag', 'ComponentDrag'],
];
export default class CommandsModule extends Module<CommandsConfig & { pStylePrefix?: string }> {
CommandAbstract = CommandAbstract;
defaultCommands: Record<string, Command> = {};
commands: Record<string, CommandObject> = {};
active: Record<string, any> = {};
/**
* @private
*/
constructor(em: Editor) {
super(em, 'Commands', defaults);
const { config } = this;
const ppfx = config.pStylePrefix;
const { defaultCommands } = this;
if (ppfx) {
config.stylePrefix = ppfx + config.stylePrefix;
}
// Load commands passed via configuration
Object.keys(config.defaults!).forEach(k => {
const obj = config.defaults![k];
if (obj.id) this.add(obj.id, obj);
});
defaultCommands['tlb-delete'] = {
run(ed) {
return ed.runCommand('core:component-delete');
},
};
defaultCommands['tlb-clone'] = {
run(ed) {
ed.runCommand('core:copy');
ed.runCommand('core:paste', { action: 'clone-component' });
},
};
defaultCommands['tlb-move'] = {
run(ed, sender, opts = {}) {
let dragger;
const em = ed.getModel();
const event = opts && opts.event;
const { target } = opts;
const sel = target || ed.getSelected();
const selAll = target ? [target] : [...ed.getSelectedAll()];
const nativeDrag = event && event.type == 'dragstart';
const defComOptions = { preserveSelected: 1 };
const modes = ['absolute', 'translate'];
if (!sel || !sel.get('draggable')) {
return em.logWarning('The element is not draggable');
}
const mode = sel.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
const altMode = includes(modes, mode);
selAll.forEach(sel => sel.trigger('disable'));
// Without setTimeout the ghost image disappears
nativeDrag ? setTimeout(hideTlb, 0) : hideTlb();
const onStart = (data: any) => {
em.trigger(`${eventDrag}:start`, data);
};
const onDrag = (data: any) => {
em.trigger(eventDrag, data);
};
const onEnd = (e: any, opts: any, data: any) => {
selAll.forEach(sel => sel.set('status', 'selected'));
ed.select(selAll);
sel.emitUpdate();
em.trigger(`${eventDrag}:end`, data);
// Defer selectComponent in order to prevent canvas "freeze" #2692
setTimeout(() => em.runDefault(defComOptions));
// Dirty patch to prevent parent selection on drop
(altMode || data.cancelled) && em.set('_cmpDrag', 1);
};
if (altMode) {
// TODO move grabbing func in editor/canvas from the Sorter
dragger = ed.runCommand('core:component-drag', {
guidesInfo: 1,
mode,
target: sel,
onStart,
onDrag,
onEnd,
event,
});
} else {
if (nativeDrag) {
event.dataTransfer.setDragImage(sel.view.el, 0, 0);
//sel.set('status', 'freezed');
}
const cmdMove = ed.Commands.get('move-comp')!;
cmdMove.onStart = onStart;
cmdMove.onDrag = onDrag;
cmdMove.onEndMoveFromModel = onEnd;
// @ts-ignore
cmdMove.initSorterFromModels(selAll);
}
selAll.forEach(sel => sel.set('status', 'freezed-selected'));
},
};
// Core commands
defaultCommands['core:undo'] = e => e.UndoManager.undo();
defaultCommands['core:redo'] = e => e.UndoManager.redo();
commandsDef.forEach(item => {
const oldCmd = item[2];
const cmd = require(`./view/${item[1]}`).default;
const cmdName = `core:${item[0]}`;
defaultCommands[cmdName] = cmd;
if (oldCmd) {
defaultCommands[oldCmd] = cmd;
// Propogate old commands (can be removed once we stop to call old commands)
['run', 'stop'].forEach(name => {
em.on(`${name}:${oldCmd}`, (...args) => em.trigger(`${name}:${cmdName}`, ...args));
});
}
});
// @ts-ignore TODO check where it's used
config.model = em.Canvas;
for (const id in defaultCommands) {
this.add(id, defaultCommands[id]);
}
return this;
}
/**
* Add new command to the collection
* @param {string} id Command's ID
* @param {Object|Function} command Object representing your command,
* By passing just a function it's intended as a stateless command
* (just like passing an object with only `run` method).
* @return {this}
* @example
* commands.add('myCommand', {
* run(editor, sender) {
* alert('Hello world!');
* },
* stop(editor, sender) {
* },
* });
* // As a function
* commands.add('myCommand2', editor => { ... });
* */
add<T extends ObjectAny = {}>(id: string, command: CommandFunction | CommandObject<any, T>) {
let result: CommandObject = isFunction(command) ? { run: command } : command;
if (!result.stop) {
result.noStop = true;
}
delete result.initialize;
result.id = id;
this.commands[id] = CommandAbstract.extend(result);
return this;
}
/**
* Get command by ID
* @param {string} id Command's ID
* @return {Object} Object representing the command
* @example
* var myCommand = commands.get('myCommand');
* myCommand.run();
* */
get(id: string): CommandObject | undefined {
let command: any = this.commands[id];
if (isFunction(command)) {
command = new command(this.config);
this.commands[id] = command;
} else if (!command) {
this.em.logWarning(`'${id}' command not found`);
}
return command;
}
/**
* Extend the command. The command to extend should be defined as an object
* @param {string} id Command's ID
* @param {Object} Object with the new command functions
* @returns {this}
* @example
* commands.extend('old-command', {
* someInnerFunction() {
* // ...
* }
* });
* */
extend(id: string, cmd: CommandObject = {}) {
const command = this.get(id);
if (command) {
const cmdObj = {
...command.constructor.prototype,
...cmd,
};
this.add(id, cmdObj);
// Extend also old name commands if exist
const oldCmd = commandsDef.filter(cmd => `core:${cmd[0]}` === id && cmd[2])[0];
oldCmd && this.add(oldCmd[2], cmdObj);
}
return this;
}
/**
* Check if command exists
* @param {string} id Command's ID
* @return {Boolean}
* */
has(id: string) {
return !!this.commands[id];
}
/**
* Get an object containing all the commands
* @return {Object}
*/
getAll() {
return this.commands;
}
/**
* Execute the command
* @param {String} id Command ID
* @param {Object} [options={}] Options
* @return {*} The return is defined by the command
* @example
* commands.run('myCommand', { someOption: 1 });
*/
run(id: string, options: CommandOptions = {}) {
return this.runCommand(this.get(id), options);
}
/**
* Stop the command
* @param {String} id Command ID
* @param {Object} [options={}] Options
* @return {*} The return is defined by the command
* @example
* commands.stop('myCommand', { someOption: 1 });
*/
stop(id: string, options: CommandOptions = {}) {
return this.stopCommand(this.get(id), options);
}
/**
* Check if the command is active. You activate commands with `run`
* and disable them with `stop`. If the command was created without `stop`
* method it can't be registered as active
* @param {String} id Command id
* @return {Boolean}
* @example
* const cId = 'some-command';
* commands.run(cId);
* commands.isActive(cId);
* // -> true
* commands.stop(cId);
* commands.isActive(cId);
* // -> false
*/
isActive(id: string) {
return this.getActive().hasOwnProperty(id);
}
/**
* Get all active commands
* @return {Object}
* @example
* console.log(commands.getActive());
* // -> { someCommand: itsLastReturn, anotherOne: ... };
*/
getActive() {
return this.active;
}
/**
* Run command via its object
* @param {Object} command
* @param {Object} options
* @return {*} Result of the command
* @private
*/
runCommand(command?: CommandObject, options: CommandOptions = {}) {
let result;
if (command && command.run) {
const { em, config } = this;
const id = command.id as string;
const editor = em.Editor;
if (!this.isActive(id) || options.force || !config.strict) {
// @ts-ignore
result = editor && command.callRun(editor, options);
if (id && command.stop && !command.noStop && !options.abort) {
this.active[id] = result;
}
}
}
return result;
}
/**
* Stop the command
* @param {Object} command
* @param {Object} options
* @return {*} Result of the command
* @private
*/
stopCommand(command?: CommandObject, options: CommandOptions = {}) {
let result;
if (command && command.run) {
const { em, config } = this;
const id = command.id as string;
const editor = em.Editor;
if (this.isActive(id) || options.force || !config.strict) {
if (id) delete this.active[id];
// @ts-ignore
result = command.callStop(editor, options);
}
}
return result;
}
/**
* Create anonymous Command instance
* @param {Object} command Command object
* @return {Command}
* @private
* */
create(command: CommandObject) {
if (!command.stop) command.noStop = true;
const cmd = CommandAbstract.extend(command);
return new cmd(this.config);
}
destroy() {
this.defaultCommands = {};
this.commands = {};
this.active = {};
}
}