Browse Source

Add types to command (#6765)

* Initial setup for typed commands

* Remove usage of require

* Update tests

* Update tests

* Move TS checks down

* Refactor Preview

* Refactor resize command

* Refactor CommandCopyComponent

* Refactor PasteComponent

* Refactor CommandCanvasMove

* Refactor CommandCanvasClear

* Refactor ExportTemplate

* Refactor OpenLayers

* Refactor CommandOpenStyleManager

* Refactor OpenTraitManager

* Refactor OpenBlocks

* Refactor OpenAssets

* Refactor SwitchVisibility

* Refactor ShowOffset

* Refactor MoveComponent

* Refactor SelectComponent

* Refactor ComponentNext

* Refactor ComponentPrev

* Refactor ComponentEnter

* Refactor ComponentExit

* Refactor ComponentDelete

* Refactor ComponentStyleClear

* Refactor ComponentDrag

* Refactor SelectPosition

* Add remove command method to the CommandManager class

* Format
release-v0.23.1-rc.0
Artur Arseniev 4 months ago
committed by GitHub
parent
commit
6e3c668a06
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 15
      packages/core/src/commands/config/config.ts
  2. 157
      packages/core/src/commands/index.ts
  3. 130
      packages/core/src/commands/registry.ts
  4. 8
      packages/core/src/commands/registryHelpers.ts
  5. 15
      packages/core/src/commands/view/CanvasClear.ts
  6. 64
      packages/core/src/commands/view/CanvasMove.ts
  7. 73
      packages/core/src/commands/view/CommandAbstract.ts
  8. 24
      packages/core/src/commands/view/ComponentDelete.ts
  9. 372
      packages/core/src/commands/view/ComponentDrag.ts
  10. 20
      packages/core/src/commands/view/ComponentEnter.ts
  11. 19
      packages/core/src/commands/view/ComponentExit.ts
  12. 20
      packages/core/src/commands/view/ComponentNext.ts
  13. 20
      packages/core/src/commands/view/ComponentPrev.ts
  14. 25
      packages/core/src/commands/view/ComponentStyleClear.ts
  15. 16
      packages/core/src/commands/view/CopyComponent.ts
  16. 45
      packages/core/src/commands/view/ExportTemplate.ts
  17. 123
      packages/core/src/commands/view/Fullscreen.ts
  18. 109
      packages/core/src/commands/view/MoveComponent.ts
  19. 62
      packages/core/src/commands/view/OpenAssets.ts
  20. 41
      packages/core/src/commands/view/OpenBlocks.ts
  21. 27
      packages/core/src/commands/view/OpenLayers.ts
  22. 39
      packages/core/src/commands/view/OpenStyleManager.ts
  23. 83
      packages/core/src/commands/view/OpenTraitManager.ts
  24. 23
      packages/core/src/commands/view/PasteComponent.ts
  25. 47
      packages/core/src/commands/view/Preview.ts
  26. 46
      packages/core/src/commands/view/Resize.ts
  27. 314
      packages/core/src/commands/view/SelectComponent.ts
  28. 44
      packages/core/src/commands/view/SelectPosition.ts
  29. 168
      packages/core/src/commands/view/ShowOffset.ts
  30. 38
      packages/core/src/commands/view/SwitchVisibility.ts
  31. 9
      packages/core/src/editor/index.ts
  32. 1
      packages/core/src/index.ts
  33. 118
      packages/core/test/specs/commands/index.ts
  34. 16
      packages/core/test/specs/commands/view/CanvasClear.ts
  35. 24
      packages/core/test/specs/commands/view/CanvasMove.ts
  36. 58
      packages/core/test/specs/commands/view/ComponentDelete.ts
  37. 23
      packages/core/test/specs/commands/view/ComponentDrag.ts
  38. 38
      packages/core/test/specs/commands/view/ComponentEnter.ts
  39. 61
      packages/core/test/specs/commands/view/ComponentExit.ts
  40. 44
      packages/core/test/specs/commands/view/ComponentNext.ts
  41. 43
      packages/core/test/specs/commands/view/ComponentPrev.ts
  42. 76
      packages/core/test/specs/commands/view/ComponentStyleClear.ts
  43. 37
      packages/core/test/specs/commands/view/CopyComponent.ts
  44. 59
      packages/core/test/specs/commands/view/ExportTemplate.ts
  45. 63
      packages/core/test/specs/commands/view/Fullscreen.ts
  46. 42
      packages/core/test/specs/commands/view/MoveComponent.ts
  47. 39
      packages/core/test/specs/commands/view/OpenAssets.ts
  48. 33
      packages/core/test/specs/commands/view/OpenBlocks.ts
  49. 35
      packages/core/test/specs/commands/view/OpenLayers.ts
  50. 27
      packages/core/test/specs/commands/view/OpenStyleManager.ts
  51. 27
      packages/core/test/specs/commands/view/OpenTraitManager.ts
  52. 53
      packages/core/test/specs/commands/view/PasteComponent.ts
  53. 53
      packages/core/test/specs/commands/view/Preview.ts
  54. 30
      packages/core/test/specs/commands/view/Resize.ts
  55. 30
      packages/core/test/specs/commands/view/SelectComponent.ts
  56. 31
      packages/core/test/specs/commands/view/SelectPosition.ts
  57. 34
      packages/core/test/specs/commands/view/ShowOffset.ts
  58. 22
      packages/core/test/specs/commands/view/SwitchVisibility.ts
  59. 2
      packages/core/test/specs/grapesjs/index.ts

15
packages/core/src/commands/config/config.ts

@ -1,10 +1,15 @@
import type { CommandObject, CommandOptions } from '../view/CommandAbstract';
import type { CommandObject } from '../view/CommandAbstract';
import type { CommandKnownId, CommandRunOptions, CommandStopOptions } from '../registry';
interface CommandConfigDefaultOptions {
run?: (options: CommandOptions) => CommandOptions;
stop?: (options: CommandOptions) => CommandOptions;
export interface CommandConfigDefaultOptions<TId extends string = string> {
run?: (options: CommandRunOptions<TId>) => CommandRunOptions<TId>;
stop?: (options: CommandStopOptions<TId>) => CommandStopOptions<TId>;
}
export type CommandsDefaultOptions = {
[TId in CommandKnownId]?: CommandConfigDefaultOptions<TId>;
} & Record<string, CommandConfigDefaultOptions>;
export interface CommandsConfig {
/**
* Style prefix
@ -50,7 +55,7 @@ export interface CommandsConfig {
* }
* }
*/
defaultOptions?: Record<string, CommandConfigDefaultOptions>;
defaultOptions?: CommandsDefaultOptions;
}
const config: () => CommandsConfig = () => ({

157
packages/core/src/commands/index.ts

@ -23,6 +23,7 @@
*
* ## Methods
* * [add](#add)
* * [remove](#remove)
* * [get](#get)
* * [getAll](#getall)
* * [extend](#extend)
@ -36,42 +37,77 @@
*/
import { isFunction, includes } from 'underscore';
import CommandAbstract, { Command, CommandOptions, CommandObject, CommandFunction } from './view/CommandAbstract';
import CommandAbstract, { Command, CommandConstructor, CommandOptions, CommandStored } from './view/CommandAbstract';
import CanvasClear from './view/CanvasClear';
import CanvasMove from './view/CanvasMove';
import ComponentDelete from './view/ComponentDelete';
import ComponentDrag from './view/ComponentDrag';
import ComponentEnter from './view/ComponentEnter';
import ComponentExit from './view/ComponentExit';
import ComponentNext from './view/ComponentNext';
import ComponentPrev from './view/ComponentPrev';
import ComponentStyleClear from './view/ComponentStyleClear';
import CopyComponent from './view/CopyComponent';
import ExportTemplate from './view/ExportTemplate';
import CommandFullscreen from './view/Fullscreen';
import MoveComponent from './view/MoveComponent';
import OpenAssets from './view/OpenAssets';
import OpenBlocks from './view/OpenBlocks';
import OpenLayers from './view/OpenLayers';
import OpenStyleManager from './view/OpenStyleManager';
import OpenTraitManager from './view/OpenTraitManager';
import PasteComponent from './view/PasteComponent';
import Preview from './view/Preview';
import Resize from './view/Resize';
import SelectComponent from './view/SelectComponent';
import ShowOffset from './view/ShowOffset';
import SwitchVisibility from './view/SwitchVisibility';
import defConfig, { CommandsConfig } from './config/config';
import { Module } from '../abstract';
import Component from '../dom_components/model/Component';
import { ComponentsEvents } from '../dom_components/types';
import type Editor from '../editor/model/Editor';
import type { ObjectAny } from '../common';
import type {
CommandDefinitionById,
CommandObjectById,
CommandRunArgs,
CommandRunResult,
CommandStopArgs,
CommandStopResult,
} from './registry';
import CommandsEvents from './types';
export type { CommandEvent } from './types';
const isCommandConstructor = (command: Command): command is CommandConstructor =>
isFunction(command) && (command === CommandAbstract || command.prototype instanceof CommandAbstract);
const commandsDef = [
['preview', 'Preview', 'preview'],
['resize', 'Resize', 'resize'],
['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'],
];
['preview', Preview, 'preview'],
['resize', Resize, 'resize'],
['fullscreen', CommandFullscreen, '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],
] as const;
const defComOptions = { preserveSelected: 1 };
@ -99,7 +135,7 @@ export const getOnComponentDragEnd =
export default class CommandsModule extends Module<CommandsConfig & { pStylePrefix?: string }> {
CommandAbstract = CommandAbstract;
defaultCommands: Record<string, Command> = {};
commands: Record<string, CommandObject> = {};
commands: Record<string, CommandStored> = {};
active: Record<string, any> = {};
events = CommandsEvents;
@ -119,7 +155,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
// Load commands passed via configuration
Object.keys(config.defaults!).forEach((k) => {
const obj = config.defaults![k];
if (obj.id) this.add(obj.id, obj);
if (obj.id) this.add(obj.id, obj as CommandDefinitionById<typeof obj.id>);
});
defaultCommands['tlb-delete'] = {
@ -180,7 +216,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
//sel.set('status', 'freezed');
}
const cmdMove = ed.Commands.get('move-comp')!;
const cmdMove = ed.Commands.get('move-comp') as any;
cmdMove.onStart = onStart;
cmdMove.onDrag = onDrag;
cmdMove.onEndMoveFromModel = onEnd;
@ -197,7 +233,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
defaultCommands['core:redo'] = (e) => e.UndoManager.redo();
commandsDef.forEach((item) => {
const oldCmd = item[2];
const cmd = require(`./view/${item[1]}`).default;
const cmd = item[1];
const cmdName = `core:${item[0]}`;
defaultCommands[cmdName] = cmd;
if (oldCmd) {
@ -237,8 +273,19 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* // 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;
add<const TId extends string, T extends ObjectAny = {}>(id: TId, command: CommandDefinitionById<TId, T>) {
if (isCommandConstructor(command)) {
const { prototype } = command;
const noStop = prototype.stop === CommandAbstract.prototype.stop;
prototype.noStop = noStop;
prototype.id = id;
this.commands[id] = command;
return this;
}
let result = (isFunction(command) ? { run: command } : command) as CommandObjectById<string, T>;
if (!result.stop) {
result.noStop = true;
@ -247,7 +294,23 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
delete result.initialize;
result.id = id;
this.commands[id] = CommandAbstract.extend(result);
this.commands[id] = CommandAbstract.extend(result) as CommandConstructor;
return this;
}
/**
* Remove command from the collection
* @param {string} id Command's ID
* @return {this}
*/
remove(id: string) {
if (this.isActive(id)) {
this.stopCommand(this.get(id), { force: true });
}
delete this.active[id];
delete this.commands[id];
return this;
}
@ -260,8 +323,8 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* var myCommand = commands.get('myCommand');
* myCommand.run();
* */
get(id: string): CommandObject | undefined {
let command: any = this.commands[id];
get(id: string): CommandAbstract | undefined {
let command = this.commands[id];
if (isFunction(command)) {
command = new command(this.config);
@ -285,7 +348,10 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* }
* });
* */
extend(id: string, cmd: CommandObject = {}) {
extend<const TId extends string>(
id: TId,
cmd: CommandObjectById<TId, ObjectAny> = {} as CommandObjectById<TId, ObjectAny>,
) {
const command = this.get(id);
if (command) {
@ -296,7 +362,8 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
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);
const oldCmdId = oldCmd?.[2];
oldCmdId && this.add(oldCmdId, cmdObj);
}
return this;
@ -327,8 +394,8 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* @example
* commands.run('myCommand', { someOption: 1 });
*/
run(id: string, options: CommandOptions = {}) {
return this.runCommand(this.get(id), options);
run<const TId extends string>(id: TId, ...args: CommandRunArgs<TId>): CommandRunResult<TId> {
return this.runCommand(this.get(id), args[0] as CommandOptions) as CommandRunResult<TId>;
}
/**
@ -339,8 +406,8 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* @example
* commands.stop('myCommand', { someOption: 1 });
*/
stop(id: string, options: CommandOptions = {}) {
return this.stopCommand(this.get(id), options);
stop<const TId extends string>(id: TId, ...args: CommandStopArgs<TId>): CommandStopResult<TId> {
return this.stopCommand(this.get(id), args[0] as CommandOptions) as CommandStopResult<TId>;
}
/**
@ -380,7 +447,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* @return {*} Result of the command
* @private
*/
runCommand(command?: CommandObject, options: CommandOptions = {}) {
runCommand(command?: CommandAbstract, options: CommandOptions = {}) {
let result;
if (command?.run) {
@ -405,7 +472,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* @return {*} Result of the command
* @private
*/
stopCommand(command?: CommandObject, options: CommandOptions = {}) {
stopCommand(command?: CommandAbstract, options: CommandOptions = {}) {
let result;
if (command?.run) {
@ -429,9 +496,9 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
* @return {Command}
* @private
* */
create(command: CommandObject) {
create(command: CommandObjectById<string, ObjectAny>) {
if (!command.stop) command.noStop = true;
const cmd = CommandAbstract.extend(command);
const cmd = CommandAbstract.extend(command) as CommandConstructor;
return new cmd(this.config);
}

130
packages/core/src/commands/registry.ts

@ -0,0 +1,130 @@
import type { ObjectAny } from '../common';
import type { CommandPublicOptions } from './registryHelpers';
import type CommandAbstract from './view/CommandAbstract';
import type { CommandConstructor, CommandFunction, CommandObject } from './view/CommandAbstract';
import type { FullscreenCommandRegistryRun, FullscreenCommandRegistryStop } from './view/Fullscreen';
import type { PreviewCommandRegistryRun, PreviewCommandRegistryStop } from './view/Preview';
import type { ResizeCommandRegistryRun, ResizeCommandRegistryStop } from './view/Resize';
import type { ComponentNextCommandRegistryRun } from './view/ComponentNext';
import type { ComponentPrevCommandRegistryRun } from './view/ComponentPrev';
import type { ComponentEnterCommandRegistryRun } from './view/ComponentEnter';
import type { ComponentExitCommandRegistryRun } from './view/ComponentExit';
import type { ComponentDeleteCommandRegistryRun } from './view/ComponentDelete';
import type { ComponentStyleClearCommandRegistryRun } from './view/ComponentStyleClear';
import type { ComponentDragCommandRegistryRun } from './view/ComponentDrag';
import type { CopyComponentCommandRegistryRun } from './view/CopyComponent';
import type { PasteComponentCommandRegistryRun } from './view/PasteComponent';
import type { CanvasMoveCommandRegistryRun, CanvasMoveCommandRegistryStop } from './view/CanvasMove';
import type { CanvasClearCommandRegistryRun } from './view/CanvasClear';
import type { ExportTemplateCommandRegistryRun, ExportTemplateCommandRegistryStop } from './view/ExportTemplate';
import type { OpenAssetsCommandRegistryRun, OpenAssetsCommandRegistryStop } from './view/OpenAssets';
import type { OpenLayersCommandRegistryRun, OpenLayersCommandRegistryStop } from './view/OpenLayers';
import type { OpenBlocksCommandRegistryRun, OpenBlocksCommandRegistryStop } from './view/OpenBlocks';
import type { MoveComponentCommandRegistryRun, MoveComponentCommandRegistryStop } from './view/MoveComponent';
import type { SelectComponentCommandRegistryRun, SelectComponentCommandRegistryStop } from './view/SelectComponent';
import type { ShowOffsetCommandRegistryRun, ShowOffsetCommandRegistryStop } from './view/ShowOffset';
import type { SwitchVisibilityCommandRegistryRun, SwitchVisibilityCommandRegistryStop } from './view/SwitchVisibility';
import type { OpenStyleManagerCommandRegistryRun, OpenStyleManagerCommandRegistryStop } from './view/OpenStyleManager';
import type { OpenTraitManagerCommandRegistryRun, OpenTraitManagerCommandRegistryStop } from './view/OpenTraitManager';
type CommandRegistryHandler = (...args: any[]) => any;
type CommandRegistryEntry<TRegistry, TId extends string> = TId extends keyof TRegistry
? TRegistry[TId] extends CommandRegistryHandler
? TRegistry[TId]
: CommandRegistryHandler
: CommandRegistryHandler;
export interface CommandRegistryRun
extends FullscreenCommandRegistryRun,
PreviewCommandRegistryRun,
ResizeCommandRegistryRun,
ComponentNextCommandRegistryRun,
ComponentPrevCommandRegistryRun,
ComponentEnterCommandRegistryRun,
ComponentExitCommandRegistryRun,
ComponentDeleteCommandRegistryRun,
ComponentStyleClearCommandRegistryRun,
ComponentDragCommandRegistryRun,
CopyComponentCommandRegistryRun,
PasteComponentCommandRegistryRun,
CanvasMoveCommandRegistryRun,
CanvasClearCommandRegistryRun,
ExportTemplateCommandRegistryRun,
MoveComponentCommandRegistryRun,
OpenAssetsCommandRegistryRun,
OpenBlocksCommandRegistryRun,
OpenLayersCommandRegistryRun,
OpenStyleManagerCommandRegistryRun,
OpenTraitManagerCommandRegistryRun,
SelectComponentCommandRegistryRun,
ShowOffsetCommandRegistryRun,
SwitchVisibilityCommandRegistryRun {}
export interface CommandRegistryStop
extends FullscreenCommandRegistryStop,
PreviewCommandRegistryStop,
ResizeCommandRegistryStop,
CanvasMoveCommandRegistryStop,
ExportTemplateCommandRegistryStop,
MoveComponentCommandRegistryStop,
OpenAssetsCommandRegistryStop,
OpenBlocksCommandRegistryStop,
OpenLayersCommandRegistryStop,
OpenStyleManagerCommandRegistryStop,
OpenTraitManagerCommandRegistryStop,
SelectComponentCommandRegistryStop,
ShowOffsetCommandRegistryStop,
SwitchVisibilityCommandRegistryStop {}
export type CommandRunKnownId = Extract<keyof CommandRegistryRun, string>;
export type CommandStopKnownId = Extract<keyof CommandRegistryStop, string>;
export type CommandKnownId = Extract<keyof CommandRegistryRun | keyof CommandRegistryStop, string>;
export type CommandRunPublicFn<TId extends string> = CommandRegistryEntry<CommandRegistryRun, TId>;
export type CommandStopPublicFn<TId extends string> = CommandRegistryEntry<CommandRegistryStop, TId>;
export type CommandRunArgs<TId extends string> = TId extends keyof CommandRegistryRun
? Parameters<CommandRunPublicFn<TId>>
: [options?: any];
export type CommandStopArgs<TId extends string> = TId extends keyof CommandRegistryStop
? Parameters<CommandStopPublicFn<TId>>
: [options?: any];
export type CommandRunOptions<TId extends string> = TId extends keyof CommandRegistryRun
? CommandPublicOptions<CommandRunPublicFn<TId>>
: any;
export type CommandStopOptions<TId extends string> = TId extends keyof CommandRegistryStop
? CommandPublicOptions<CommandStopPublicFn<TId>>
: any;
export type CommandRunResult<TId extends string> = TId extends keyof CommandRegistryRun
? ReturnType<CommandRunPublicFn<TId>>
: any;
export type CommandStopResult<TId extends string> = TId extends keyof CommandRegistryStop
? ReturnType<CommandStopPublicFn<TId>>
: any;
export type CommandFunctionById<TId extends string> = CommandFunction<CommandRunOptions<TId>, CommandRunResult<TId>>;
export type CommandObjectById<TId extends string, T extends ObjectAny = {}> = CommandObject<
CommandRunOptions<TId>,
T,
CommandStopOptions<TId>,
CommandRunResult<TId>,
CommandStopResult<TId>
>;
export type CommandConstructorById<TId extends string> = CommandConstructor<
CommandRunOptions<TId>,
CommandStopOptions<TId>,
CommandRunResult<TId>,
CommandStopResult<TId>
>;
export type CommandDefinitionById<TId extends string, T extends ObjectAny = {}> =
| CommandFunctionById<TId>
| CommandObjectById<TId, T>
| CommandConstructorById<TId>;
export type CommandInstanceById<TId extends string> = CommandAbstract<
CommandRunOptions<TId>,
CommandStopOptions<TId>,
CommandRunResult<TId>,
CommandStopResult<TId>
>;

8
packages/core/src/commands/registryHelpers.ts

@ -0,0 +1,8 @@
type CommandPublicHandler = (...args: any[]) => any;
export type CommandPublicFnFromHandler<T> = T extends (editor: any, sender: any, ...args: infer P) => infer R
? (...args: P) => R
: never;
export type CommandPublicOptions<T extends CommandPublicHandler> =
Parameters<T> extends [] ? undefined : Parameters<T>[0];

15
packages/core/src/commands/view/CanvasClear.ts

@ -1,8 +1,13 @@
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed) {
export interface CanvasClearCommandRegistryRun {
'core:canvas-clear': CommandPublicFnFromHandler<CommandCanvasClear['run']>;
}
export default class CommandCanvasClear extends CommandAbstract {
run(ed: any) {
ed.Components.clear();
ed.Css.clear();
},
} as CommandObject;
}
}

64
packages/core/src/commands/view/CanvasMove.ts

@ -1,34 +1,49 @@
import { bindAll } from 'underscore';
import { CanvasEvents } from '../../canvas/types';
import Editor from '../../editor';
import Dragger from '../../utils/Dragger';
import { getKeyChar, off, on } from '../../utils/dom';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed) {
export interface CanvasMoveCommandRegistryRun {
'core:canvas-move': CommandPublicFnFromHandler<CommandCanvasMove['run']>;
}
export interface CanvasMoveCommandRegistryStop {
'core:canvas-move': CommandPublicFnFromHandler<CommandCanvasMove['stop']>;
}
export default class CommandCanvasMove extends CommandAbstract {
editor!: Editor;
canvasModel: any;
dragger?: Dragger;
run(ed: Editor) {
bindAll(this, 'onKeyUp', 'enableDragger', 'disableDragger');
this.editor = ed;
this.canvasModel = this.canvas.getCanvasView().model;
this.toggleMove(1);
},
stop(ed) {
this.toggleMove(true);
}
stop() {
this.toggleMove();
this.disableDragger();
},
this.disableDragger(new MouseEvent('mouseup'));
}
onKeyUp(ev: KeyboardEvent) {
if (getKeyChar(ev) === ' ') {
this.editor.stopCommand(this.id);
this.editor.stopCommand(this.id as string);
}
},
}
enableDragger(ev: Event) {
this.toggleDragger(1, ev);
},
this.toggleDragger(true, ev);
}
disableDragger(ev: Event) {
this.toggleDragger(0, ev);
},
disableDragger(ev?: Event) {
this.toggleDragger(false, ev as Event);
}
toggleDragger(enable: boolean, ev: Event) {
const { canvasModel, em } = this;
@ -47,23 +62,23 @@ export default {
setPosition({ x, y }) {
canvasModel.set({ x, y });
},
onStart(ev, dragger) {
onStart(_ev, dragger) {
em.trigger(CanvasEvents.moveStart, dragger);
},
onDrag(ev, dragger) {
onDrag(_ev, dragger) {
em.trigger(CanvasEvents.move, dragger);
},
onEnd(ev, dragger) {
onEnd(_ev, dragger) {
em.trigger(CanvasEvents.moveEnd, dragger);
},
});
this.dragger = dragger;
}
enable ? dragger.start(ev) : dragger.stop();
},
enable ? dragger!.start(ev) : dragger!.stop(ev);
}
toggleMove(enable: boolean) {
toggleMove(enable = false) {
const { ppfx } = this;
const methodCls = enable ? 'add' : 'remove';
const methodEv = enable ? 'on' : 'off';
@ -75,10 +90,5 @@ export default {
methodsEv[methodEv](document, 'keyup', this.onKeyUp);
methodsEv[methodEv](canvas, 'mousedown', this.enableDragger);
methodsEv[methodEv](document, 'mouseup', this.disableDragger);
},
} as CommandObject<
any,
{
[key: string]: any;
}
>;
}

73
packages/core/src/commands/view/CommandAbstract.ts

@ -4,28 +4,63 @@ import Editor from '../../editor';
import EditorModel from '../../editor/model/Editor';
import CommandsEvents, { type CommandCallEventData, type CommandEventData } from '../types';
interface ICommand<O extends ObjectAny = any> {
run?: CommandAbstract<O>['run'];
stop?: CommandAbstract<O>['stop'];
interface ICommand<TRunOptions = any, TStopOptions = TRunOptions, TRunResult = any, TStopResult = any> {
run?: CommandAbstract<TRunOptions, TStopOptions, TRunResult, TStopResult>['run'];
stop?: CommandAbstract<TRunOptions, TStopOptions, TRunResult, TStopResult>['stop'];
id?: string;
noStop?: boolean;
initialize?: unknown;
[key: string]: unknown;
}
export type CommandFunction<O extends ObjectAny = any> = CommandAbstract<O>['run'];
export type CommandFunction<TRunOptions = any, TRunResult = any> = CommandAbstract<
TRunOptions,
any,
TRunResult,
any
>['run'];
export interface CommandConstructor<
TRunOptions = any,
TStopOptions = TRunOptions,
TRunResult = any,
TStopResult = any,
> {
new (o: any): CommandAbstract<TRunOptions, TStopOptions, TRunResult, TStopResult>;
prototype: CommandAbstract<TRunOptions, TStopOptions, TRunResult, TStopResult>;
}
export type Command = CommandObject | CommandFunction;
export type Command = CommandObject<any, ObjectAny, any, any, any> | CommandFunction | CommandConstructor;
export type CommandStored = CommandConstructor | CommandAbstract;
export type CommandOptions = Record<string, any>;
export type CommandObject<O extends ObjectAny = any, T extends ObjectAny = {}> = ICommand<O> &
export type CommandObject<
TRunOptions = any,
T extends ObjectAny = {},
TStopOptions = TRunOptions,
TRunResult = any,
TStopResult = any,
> = ICommand<TRunOptions, TStopOptions, TRunResult, TStopResult> &
T &
ThisType<T & CommandAbstract<O>>;
export function defineCommand<O extends ObjectAny = any, T extends ObjectAny = {}>(def: CommandObject<O, T>) {
ThisType<T & CommandAbstract<TRunOptions, TStopOptions, TRunResult, TStopResult>>;
export function defineCommand<
TRunOptions = any,
T extends ObjectAny = {},
TStopOptions = TRunOptions,
TRunResult = any,
TStopResult = any,
>(def: CommandObject<TRunOptions, T, TStopOptions, TRunResult, TStopResult>) {
return def;
}
export default class CommandAbstract<O extends ObjectAny = any> extends Model {
export default class CommandAbstract<
TRunOptions = any,
TStopOptions = TRunOptions,
TRunResult = any,
TStopResult = any,
> extends Model {
config: any;
em: EditorModel;
pfx: string;
@ -109,8 +144,9 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* @param {Object} [options={}] Options
* @private
* */
callRun(editor: Editor, options: any = {}) {
callRun(editor: Editor, opts: TRunOptions = {} as TRunOptions) {
const { id } = this;
const options = opts as CommandOptions;
editor.trigger(`${CommandsEvents.runBeforeCommand}${id}`, { options });
if (options.abort) {
@ -119,7 +155,7 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
}
const sender = options.sender || editor;
const result = this.run(editor, sender, options);
const result = this.run(editor, sender, options as TRunOptions);
const data: CommandEventData = { id, result, options };
const dataCall: CommandCallEventData = { ...data, type: 'run' };
@ -141,11 +177,12 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* @param {Object} [options={}] Options
* @private
* */
callStop(editor: Editor, options: any = {}) {
callStop(editor: Editor, opts: TStopOptions = {} as TStopOptions) {
const { id } = this;
const options = opts as CommandOptions;
const sender = options.sender || editor;
editor.trigger(`${CommandsEvents.stopBeforeCommand}${id}`, { options });
const result = this.stop(editor, sender, options);
const result = this.stop(editor, sender, options as TStopOptions);
const data: CommandEventData = { id, result, options };
const dataCall: CommandCallEventData = { ...data, type: 'stop' };
delete editor.Commands.active[id];
@ -169,7 +206,9 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* @param {Object} sender Button sender
* @private
* */
run(em: Editor, sender: any, options: O) {}
run(em: Editor, sender: any, options: TRunOptions): TRunResult {
return undefined as TRunResult;
}
/**
* Method that stop command
@ -177,5 +216,7 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* @param {Object} sender Button sender
* @private
* */
stop(em: Editor, sender: any, options: O) {}
stop(em: Editor, sender: any, options: TStopOptions): TStopResult {
return undefined as TStopResult;
}
}

24
packages/core/src/commands/view/ComponentDelete.ts

@ -1,9 +1,19 @@
import { isArray } from 'underscore';
import Component from '../../dom_components/model/Component';
import { CommandObject } from './CommandAbstract';
import type Component from '../../dom_components/model/Component';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
const command: CommandObject<{ component?: Component }> = {
run(ed, s, opts = {}) {
export interface ComponentDeleteRunOptions {
component?: Component | Component[];
}
export interface ComponentDeleteCommandRegistryRun {
'core:component-delete': CommandPublicFnFromHandler<CommandComponentDelete['run']>;
}
export default class CommandComponentDelete extends CommandAbstract<ComponentDeleteRunOptions> {
run(ed: Editor, s: any, opts: ComponentDeleteRunOptions = {}) {
const removed: Component[] = [];
let components = opts.component || ed.getSelectedAll();
components = isArray(components) ? [...components] : [components];
@ -23,7 +33,5 @@ const command: CommandObject<{ component?: Component }> = {
ed.selectRemove(removed);
return removed;
},
};
export default command;
}
}

372
packages/core/src/commands/view/ComponentDrag.ts

@ -1,18 +1,38 @@
import { keys, bindAll, each, isUndefined, debounce } from 'underscore';
import { CanvasEvents } from '../../canvas/types';
import Dragger, { DraggerOptions } from '../../utils/Dragger';
import type { CommandObject } from './CommandAbstract';
import type Editor from '../../editor';
import type Component from '../../dom_components/model/Component';
import type EditorModel from '../../editor/model/Editor';
import { getComponentModel, getComponentView } from '../../utils/mixins';
import type ComponentView from '../../dom_components/view/ComponentView';
import type CommandAbstract from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
const evName = 'dmode';
export default {
run(editor, _sender, opts = {} as ComponentDragOpts) {
export interface ComponentDragCommandRegistryRun {
'core:component-drag': CommandPublicFnFromHandler<CommandComponentDrag['run']>;
}
export default class CommandComponentDrag extends CommandAbstract<ComponentDragOpts> {
editor!: Editor;
em!: EditorModel;
opts!: ComponentDragOpts;
target!: Component;
guides?: ComponentDragGuide[];
guidesContainer?: HTMLElement;
guidesEl?: HTMLElement;
guidesStatic?: ComponentDragGuide[];
guidesTarget?: ComponentDragGuide[];
isTran?: boolean;
elGuideInfoX?: HTMLElement;
elGuideInfoY?: HTMLElement;
elGuideInfoContentX?: HTMLElement;
elGuideInfoContentY?: HTMLElement;
dragger?: Dragger;
run(editor: Editor, _sender: any, opts = {} as ComponentDragOpts) {
bindAll(
this,
'setPosition',
@ -62,24 +82,24 @@ export default {
this.em.trigger(`${evName}:start`, this.getEventOpts());
return drg;
},
}
getEventOpts() {
getEventOpts(): ComponentDragEventProps {
const guidesActive = this.guidesTarget?.filter((item) => item.active) ?? [];
return {
mode: this.opts.mode,
component: this.target,
target: this.target,
guidesTarget: this.guidesTarget,
guidesStatic: this.guidesStatic,
guidesTarget: this.guidesTarget ?? [],
guidesStatic: this.guidesStatic ?? [],
guidesMatched: this.getGuidesMatched(guidesActive),
command: this,
};
},
}
stop() {
this.toggleDrag();
},
}
setupGuides() {
(this.guides ?? []).forEach((item) => {
@ -87,7 +107,7 @@ export default {
guide?.parentNode?.removeChild(guide);
});
this.guides = [];
},
}
getGuidesContainer() {
let { guidesEl } = this;
@ -125,7 +145,7 @@ export default {
}
return guidesEl;
},
}
getGuidesStatic() {
let result: ComponentDragGuide[] = [];
@ -138,19 +158,19 @@ export default {
);
return result.concat(this.getElementGuides(parentNode));
},
}
getGuidesTarget() {
return this.getElementGuides(this.target.getEl()!);
},
}
updateGuides(guides) {
let lastEl: HTMLElement;
let lastPos: ComponentOrigRect;
updateGuides(guides?: ComponentDragGuide[]) {
let lastEl: HTMLElement | undefined;
let lastPos: ComponentOrigRect | undefined;
const guidesToUpdate = guides ?? this.guides ?? [];
guidesToUpdate.forEach((item) => {
const { origin } = item;
const pos = lastEl === origin ? lastPos : this.getElementPos(origin);
const pos = lastEl === origin ? lastPos! : this.getElementPos(origin);
lastEl = origin;
lastPos = pos;
each(this.getGuidePosUpdate(item, pos), (val, key) => {
@ -158,9 +178,9 @@ export default {
});
item.originRect = pos;
});
},
}
getGuidePosUpdate(item, rect) {
getGuidePosUpdate(item: ComponentDragGuide, rect: ComponentOrigRect) {
const result: { x?: number; y?: number } = {};
const { top, height, left, width } = rect;
@ -186,9 +206,9 @@ export default {
}
return result;
},
}
renderGuide(item) {
renderGuide(item: { active?: boolean; guide?: HTMLElement; x?: number; y?: number }) {
if (this.opts.skipGuidesRender) return;
const el = item.guide ?? document.createElement('div');
const un = 'px';
@ -216,13 +236,13 @@ export default {
!item.guide && this.guidesContainer?.appendChild(el);
return el;
},
}
getElementPos(el) {
getElementPos(el: HTMLElement) {
return this.editor.Canvas.getElementPos(el, { noScroll: 1 });
},
}
getElementGuides(el) {
getElementGuides(el: HTMLElement) {
const { opts } = this;
const origin = el;
const originRect = this.getElementPos(el);
@ -231,12 +251,12 @@ export default {
const { top, height, left, width } = originRect;
const guidePoints: { type: string; x?: number; y?: number }[] = [
{ type: 't', y: top }, // Top
{ type: 'b', y: top + height }, // Bottom
{ type: 'l', x: left }, // Left
{ type: 'r', x: left + width }, // Right
{ type: 'x', x: left + width / 2 }, // Mid x
{ type: 'y', y: top + height / 2 }, // Mid y
{ type: 't', y: top },
{ type: 'b', y: top + height },
{ type: 'l', x: left },
{ type: 'r', x: left + width },
{ type: 'x', x: left + width / 2 },
{ type: 'y', y: top + height / 2 },
];
const guides = guidePoints.map((guidePoint) => {
@ -252,14 +272,14 @@ export default {
guideEl: guide,
guide,
};
}) as ComponentDragGuide[];
}) as unknown as ComponentDragGuide[];
guides.forEach((guidePoint) => this.guides?.push(guidePoint));
return guides;
},
}
getTranslate(transform, axis = 'x') {
getTranslate(transform: string, axis = 'x') {
let result = 0;
(transform || '').split(' ').forEach((item) => {
const itemStr = item.trim();
@ -267,9 +287,9 @@ export default {
if (itemStr.indexOf(fn) === 0) result = parseFloat(itemStr.replace(fn, ''));
});
return result;
},
}
setTranslate(transform, axis, value) {
setTranslate(transform: string, axis: string, value: string) {
const fn = `translate${axis.toUpperCase()}(`;
const val = `${fn}${value})`;
let result = (transform || '')
@ -283,7 +303,7 @@ export default {
if (result.indexOf(fn) < 0) result += ` ${val}`;
return result;
},
}
getPosition() {
const { target, isTran } = this;
@ -305,12 +325,12 @@ export default {
}
return { x, y };
},
}
setPosition({ x, y, end, position, width, height }) {
setPosition({ x, y, end, position, width, height }: any) {
const { target, isTran, em, opts } = this;
const unit = 'px';
const __p = !end; // Indicate if partial change
const __p = !end;
const left = `${parseInt(`${x}`, 10)}${unit}`;
const top = `${parseInt(`${y}`, 10)}${unit}`;
let styleUp = {};
@ -337,7 +357,7 @@ export default {
}
em.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() });
},
}
_getDragData() {
const { target } = this;
@ -346,9 +366,9 @@ export default {
parent: target.parent(),
index: target.index(),
};
},
}
onStart(event) {
onStart(event: Event) {
const { target, editor, isTran, opts } = this;
const { Canvas } = editor;
const style = target.getStyle();
@ -362,17 +382,15 @@ export default {
let parent = target.parent();
let parentRel = null;
// Check for the relative parent
do {
const pStyle = parent?.getStyle();
const position = pStyle?.position as string | undefined;
if (position) {
parentRel = relPos.indexOf(position) >= 0 ? parent : null;
const parentPosition = pStyle?.position as string | undefined;
if (parentPosition) {
parentRel = relPos.indexOf(parentPosition) >= 0 ? parent : null;
}
parent = parent?.parent();
} while (parent && !parentRel);
// Center the target to the pointer position (used in Droppable for Blocks)
if (opts.center) {
const { x, y } = Canvas.getMouseRelativeCanvas(event as MouseEvent);
left = x;
@ -392,11 +410,10 @@ export default {
});
}
// Recalculate guides to avoid issues with the new position durin the first drag
this.guidesStatic = this.getGuidesStatic();
},
}
onDrag(event) {
onDrag(event: Event) {
const { guidesTarget, opts } = this;
this.updateGuides(guidesTarget);
@ -406,25 +423,25 @@ export default {
this.opts.event = event;
this.em.trigger(`${evName}:move`, this.getEventOpts());
},
}
onEnd(ev, _dragger, opt) {
onEnd(ev: Event, _dragger: any, opt: any) {
const { editor, opts, id } = this;
opts.onEnd?.(ev, opt, { event: ev, ...opt, ...this._getDragData() });
editor.stopCommand(`${id}`);
this.hideGuidesInfo();
this.em.trigger(`${evName}:end`, this.getEventOpts());
},
}
hideGuidesInfo() {
['X', 'Y'].forEach((item) => {
const guide = this[`elGuideInfo${item}` as ElGuideInfoKey];
if (guide) guide.style.display = 'none';
});
},
}
renderGuideInfo(guides = []) {
renderGuideInfo(guides: ComponentDragGuide[] = []) {
this.hideGuidesInfo();
const guidesMatched = this.getGuidesMatched(guides);
@ -439,9 +456,9 @@ export default {
...guideMatched,
});
});
},
}
renderSingleGuideInfo(guideMatched) {
renderSingleGuideInfo(guideMatched: ComponentDragGuideMatched) {
const { posFirst, posSecond, size, sizeRaw, guide, elGuideInfo, elGuideInfoCnt } = guideMatched;
const axis = isUndefined(guide.x) ? 'y' : 'x';
@ -455,9 +472,9 @@ export default {
guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`;
elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`;
},
}
getGuidesMatched(guides = []) {
getGuidesMatched(guides: ComponentDragGuide[] = []) {
const { guidesStatic = [] } = this;
return guides
.map((guide) => {
@ -466,26 +483,22 @@ export default {
const axis = isUndefined(x) ? 'y' : 'x';
const isY = axis === 'y';
// Calculate the edges of the element
const origEdge1 = rectOrigin[isY ? 'left' : 'top'];
const origEdge1Raw = rectOrigin.rect[isY ? 'left' : 'top'];
const origEdge2 = isY ? origEdge1 + rectOrigin.width : origEdge1 + rectOrigin.height;
const origEdge2Raw = isY ? origEdge1Raw + rectOrigin.rect.width : origEdge1Raw + rectOrigin.rect.height;
// Find the nearest element
const guidesMatched = guidesStatic
.filter((guideStatic) => {
// Define complementary guide types
const complementaryTypes: Record<string, string[]> = {
l: ['r', 'x'], // Left can match with Right or Middle (horizontal)
r: ['l', 'x'], // Right can match with Left or Middle (horizontal)
x: ['l', 'r'], // Middle (horizontal) can match with Left or Right
t: ['b', 'y'], // Top can match with Bottom or Middle (vertical)
b: ['t', 'y'], // Bottom can match with Top or Middle (vertical)
y: ['t', 'b'], // Middle (vertical) can match with Top or Bottom
l: ['r', 'x'],
r: ['l', 'x'],
x: ['l', 'r'],
t: ['b', 'y'],
b: ['t', 'y'],
y: ['t', 'b'],
};
// Check if the guide type matches or is complementary
return guideStatic.type === guide.type || complementaryTypes[guide.type]?.includes(guideStatic.type);
})
.map((guideStatic) => {
@ -500,7 +513,6 @@ export default {
.filter((item) => item.gap > 0)
.sort((a, b) => a.gap - b.gap)
.map((item) => item.guide)
// Filter the guides that don't match the position of the dragged element
.filter((item) => {
switch (guide.type) {
case 'l':
@ -516,7 +528,6 @@ export default {
}
});
// TODO: consider supporting multiple guides
const firstGuideMatched = guidesMatched[0];
if (firstGuideMatched) {
@ -550,9 +561,9 @@ export default {
}
})
.filter(Boolean) as ComponentDragGuideMatched[];
},
}
toggleDrag(enable) {
toggleDrag(enable?: boolean) {
const { ppfx, editor } = this;
const methodCls = enable ? 'add' : 'remove';
const classes = [`${ppfx}is__grabbing`];
@ -560,56 +571,7 @@ export default {
const body = Canvas.getBody();
classes.forEach((cls) => body.classList[methodCls](cls));
Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll']();
},
// These properties values are set in the run method, they need to be initialized here to avoid TS errors
editor: undefined as unknown as Editor,
em: undefined as unknown as EditorModel,
opts: undefined as unknown as ComponentDragOpts,
target: undefined as unknown as Component,
} as CommandObject<ComponentDragOpts, ComponentDragProps>;
interface ComponentDragProps {
editor: Editor;
em?: EditorModel;
guides?: ComponentDragGuide[];
guidesContainer?: HTMLElement;
guidesEl?: HTMLElement;
guidesStatic?: ComponentDragGuide[];
guidesTarget?: ComponentDragGuide[];
isTran?: boolean;
opts: ComponentDragOpts;
target: Component;
elGuideInfoX?: HTMLElement;
elGuideInfoY?: HTMLElement;
elGuideInfoContentX?: HTMLElement;
elGuideInfoContentY?: HTMLElement;
dragger?: Dragger;
getEventOpts: () => ComponentDragEventProps;
stop: () => void;
setupGuides: () => void;
getGuidesContainer: () => HTMLElement;
getGuidesStatic: () => ComponentDragGuide[];
getGuidesTarget: () => ComponentDragGuide[];
updateGuides: (guides?: ComponentDragGuide[]) => void;
getGuidePosUpdate: (item: ComponentDragGuide, rect: ComponentOrigRect) => { x?: number; y?: number };
renderGuide: (item: { active?: boolean; guide?: HTMLElement; x?: number; y?: number }) => HTMLElement;
getElementPos: (el: HTMLElement) => ComponentOrigRect;
getElementGuides: (el: HTMLElement) => ComponentDragGuide[];
getTranslate: (transform: string, axis?: string) => number;
setTranslate: (transform: string, axis: string, value: string) => string;
getPosition: DraggerOptions['getPosition'];
setPosition: (data: any) => void; // TODO: fix any
_getDragData: () => { target: Component; parent?: Component; index?: number };
onStart: DraggerOptions['onStart'];
onDrag: DraggerOptions['onDrag'];
onEnd: DraggerOptions['onEnd'];
hideGuidesInfo: () => void;
renderGuideInfo: (guides?: ComponentDragGuide[]) => void;
renderSingleGuideInfo: (guideMatched: ComponentDragGuideMatched) => void;
getGuidesMatched: (guides?: ComponentDragGuide[]) => ComponentDragGuideMatched[];
toggleDrag: (enable?: boolean) => void;
}
}
interface ComponentDragOpts {
@ -622,8 +584,8 @@ interface ComponentDragOpts {
mode?: 'absolute' | 'translate';
skipGuidesRender?: boolean;
addStyle?: (data: { component?: Component; styles?: Record<string, unknown>; partial?: boolean }) => void;
onStart?: (data: any) => Editor;
onDrag?: (data: any) => Editor;
onStart?: (data: any) => any;
onDrag?: (data: any) => any;
onEnd?: (ev: Event, opt: any, data: any) => void;
}
@ -631,141 +593,97 @@ interface ComponentDragOpts {
* Represents the properties of the drag events.
*/
export interface ComponentDragEventProps {
/**
* The mode of the drag (absolute or translate).
*/
mode: ComponentDragOpts['mode'];
/**
* The component being dragged.
* @deprecated Use `component` instead.
*/
target: Component;
/**
* The component being dragged.
*/
component: Component;
/**
* The guides of the component being dragged.
* @deprecated Use `guidesMatched` instead.
*/
guidesTarget: ComponentDragGuide[];
/**
* All the guides except the ones of the component being dragged.
* @deprecated Use `guidesMatched` instead.
*/
guidesStatic: ComponentDragGuide[];
/**
* The guides that are being matched.
*/
guidesMatched: ComponentDragGuideMatched[];
/**
* The options used for the drag event.
*/
command: ComponentDragProps & CommandAbstract<ComponentDragOpts>;
}
/**
* Represents a guide used during component dragging.
*/
interface ComponentDragGuide {
/**
* The type of the guide (e.g., 't', 'b', 'l', 'r', 'x', 'y').
*/
type: string;
/**
* The vertical position of the guide.
*/
y: number;
/**
* The horizontal position of the guide.
*/
x: number;
/**
* The component associated with the guide.
*/
component: Component;
/**
* The view of the component associated with the guide.
*/
componentView: ComponentView;
/**
* The HTML element associated with the guide.
* @deprecated Use `componentEl` instead.
*/
origin: HTMLElement;
/**
* The HTML element associated with the guide.
*/
componentEl: HTMLElement;
/**
* The rectangle (position and dimensions) of the guide's element.
* @deprecated Use `componentElRect` instead.
*/
originRect: ComponentOrigRect;
/**
* The rectangle (position and dimensions) of the guide's element.
*/
componentElRect: ComponentOrigRect;
/**
* The HTML element representing the guide.
* @deprecated Use `guideEl` instead.
*/
guide?: HTMLElement;
/**
* The HTML element representing the guide.
*/
guideEl?: HTMLElement;
/**
* Indicates whether the guide is active.
* @todo The `active` property is not set in the code, but the value is changing.
*/
active?: boolean;
}
/**
* Represents a matched guide during component dragging.
*/
interface ComponentDragGuideMatched {
/**
* The static guides used for matching.
*/
guidesStatic: ComponentDragGuide[];
/**
* The origin component guide.
*/
guide: ComponentDragGuide;
/**
* The matched component guide.
*/
matched: ComponentDragGuide;
/**
* The primary position of the guide (either x or y depending on the axis).
*/
posFirst: number;
/**
* The secondary position of the guide (the opposite axis of posFirst).
*/
posSecond: number;
/**
* The distance between the two matched guides in pixels.
*/
size: number;
/**
* The raw distance between the two matched guides in pixels.
*/
sizeRaw: number;
/**
* The HTML element representing the guide info (line between the guides).
*/
elGuideInfo: HTMLElement;
/**
* The container element for the guide info (text content of the line).
*/
elGuideInfoCnt: HTMLElement;
}
type ComponentRect = { left: number; width: number; top: number; height: number };
type ComponentOrigRect = ComponentRect & { rect: ComponentRect };
interface ComponentOrigRect {
top: number;
left: number;
width: number;
height: number;
rect: {
top: number;
left: number;
width: number;
height: number;
};
}
type ElGuideInfoKey = 'elGuideInfoX' | 'elGuideInfoY';
type ElGuideInfoContentKey = 'elGuideInfoContentX' | 'elGuideInfoContentY';
interface ComponentDragProps {
editor: Editor;
em?: EditorModel;
guides?: ComponentDragGuide[];
guidesContainer?: HTMLElement;
guidesEl?: HTMLElement;
guidesStatic?: ComponentDragGuide[];
guidesTarget?: ComponentDragGuide[];
isTran?: boolean;
opts: ComponentDragOpts;
target: Component;
elGuideInfoX?: HTMLElement;
elGuideInfoY?: HTMLElement;
elGuideInfoContentX?: HTMLElement;
elGuideInfoContentY?: HTMLElement;
dragger?: Dragger;
getEventOpts: () => ComponentDragEventProps;
stop: () => void;
setupGuides: () => void;
getGuidesContainer: () => HTMLElement;
getGuidesStatic: () => ComponentDragGuide[];
getGuidesTarget: () => ComponentDragGuide[];
updateGuides: (guides?: ComponentDragGuide[]) => void;
getGuidePosUpdate: (item: ComponentDragGuide, rect: ComponentOrigRect) => { x?: number; y?: number };
renderGuide: (item: { active?: boolean; guide?: HTMLElement; x?: number; y?: number }) => HTMLElement | undefined;
getElementPos: (el: HTMLElement) => ComponentOrigRect;
getElementGuides: (el: HTMLElement) => ComponentDragGuide[];
getTranslate: (transform: string, axis?: string) => number;
setTranslate: (transform: string, axis: string, value: string) => string;
getPosition: DraggerOptions['getPosition'];
setPosition: (data: any) => void;
_getDragData: () => { target: Component; parent?: Component; index?: number };
onStart: DraggerOptions['onStart'];
onDrag: DraggerOptions['onDrag'];
onEnd: DraggerOptions['onEnd'];
hideGuidesInfo: () => void;
renderGuideInfo: (guides?: ComponentDragGuide[]) => void;
renderSingleGuideInfo: (guideMatched: ComponentDragGuideMatched) => void;
getGuidesMatched: (guides?: ComponentDragGuide[]) => ComponentDragGuideMatched[];
toggleDrag: (enable?: boolean) => void;
}

20
packages/core/src/commands/view/ComponentEnter.ts

@ -1,17 +1,23 @@
import Component from '../../dom_components/model/Component';
import { CommandObject } from './CommandAbstract';
import type Component from '../../dom_components/model/Component';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed) {
export interface ComponentEnterCommandRegistryRun {
'core:component-enter': CommandPublicFnFromHandler<CommandComponentEnter['run']>;
}
export default class CommandComponentEnter extends CommandAbstract {
run(ed: Editor) {
if (!ed.Canvas.hasFocus()) return;
const toSelect: Component[] = [];
ed.getSelectedAll().forEach((component) => {
const coll = component.components();
const next = coll && coll.filter((c: any) => c.get('selectable'))[0];
const next = coll && coll.filter((c) => !!c.get('selectable'))[0];
next && toSelect.push(next);
});
toSelect.length && ed.select(toSelect);
},
} as CommandObject;
}
}

19
packages/core/src/commands/view/ComponentExit.ts

@ -1,8 +1,15 @@
import Component from '../../dom_components/model/Component';
import { CommandObject } from './CommandAbstract';
import type Component from '../../dom_components/model/Component';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed, snd, opts = {}) {
export interface ComponentExitCommandRegistryRun {
'core:component-exit': CommandPublicFnFromHandler<CommandComponentExit['run']>;
'select-parent': CommandPublicFnFromHandler<CommandComponentExit['run']>;
}
export default class CommandComponentExit extends CommandAbstract {
run(ed: Editor, _: any, opts: any = {}) {
if (!ed.Canvas.hasFocus() && !opts.force) return;
const toSelect: Component[] = [];
@ -18,5 +25,5 @@ export default {
});
toSelect.length && ed.select(toSelect);
},
} as CommandObject;
}
}

20
packages/core/src/commands/view/ComponentNext.ts

@ -1,8 +1,14 @@
import Component from '../../dom_components/model/Component';
import { CommandObject } from './CommandAbstract';
import type Component from '../../dom_components/model/Component';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed) {
export interface ComponentNextCommandRegistryRun {
'core:component-next': CommandPublicFnFromHandler<CommandComponentNext['run']>;
}
export default class CommandComponentNext extends CommandAbstract {
run(ed: Editor) {
if (!ed.Canvas.hasFocus()) return;
const toSelect: Component[] = [];
@ -13,7 +19,7 @@ export default {
const len = parent.components().length;
let incr = 0;
let at = 0;
let next: any;
let next: Component | null = null;
// Get the next selectable component
do {
@ -26,5 +32,5 @@ export default {
});
toSelect.length && ed.select(toSelect);
},
} as CommandObject;
}
}

20
packages/core/src/commands/view/ComponentPrev.ts

@ -1,8 +1,14 @@
import Component from '../../dom_components/model/Component';
import { CommandObject } from './CommandAbstract';
import type Component from '../../dom_components/model/Component';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed) {
export interface ComponentPrevCommandRegistryRun {
'core:component-prev': CommandPublicFnFromHandler<CommandComponentPrev['run']>;
}
export default class CommandComponentPrev extends CommandAbstract {
run(ed: Editor) {
if (!ed.Canvas.hasFocus()) return;
const toSelect: Component[] = [];
@ -12,7 +18,7 @@ export default {
let incr = 0;
let at = 0;
let next: any;
let next: Component | null = null;
// Get the first selectable component
do {
@ -25,5 +31,5 @@ export default {
});
toSelect.length && ed.select(toSelect);
},
} as CommandObject;
}
}

25
packages/core/src/commands/view/ComponentStyleClear.ts

@ -1,16 +1,27 @@
import { flatten } from 'underscore';
import CssRule from '../../css_composer/model/CssRule';
import { CommandObject } from './CommandAbstract';
import type CssRule from '../../css_composer/model/CssRule';
import type Component from '../../dom_components/model/Component';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed, s, opts = {}) {
export interface ComponentStyleClearRunOptions {
target: Component;
}
export interface ComponentStyleClearCommandRegistryRun {
'core:component-style-clear': CommandPublicFnFromHandler<CommandComponentStyleClear['run']>;
}
export default class CommandComponentStyleClear extends CommandAbstract<ComponentStyleClearRunOptions> {
run(ed: Editor, s: any, opts: ComponentStyleClearRunOptions) {
const { target } = opts;
let toRemove: CssRule[] = [];
if (!target.get('styles')) return toRemove;
// Find all components in the project, of the target component type
const type = target.get('type');
const type = target.get('type')!;
const wrappers = ed.Pages.getAllWrappers();
const len = flatten(wrappers.map((wrp) => wrp.findType(type))).length;
@ -23,5 +34,5 @@ export default {
}
return toRemove;
},
} as CommandObject;
}
}

16
packages/core/src/commands/view/CopyComponent.ts

@ -1,9 +1,15 @@
import { CommandObject } from './CommandAbstract';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed) {
export interface CopyComponentCommandRegistryRun {
'core:copy': CommandPublicFnFromHandler<CommandCopyComponent['run']>;
}
export default class CommandCopyComponent extends CommandAbstract {
run(ed: Editor) {
const em = ed.getModel();
const models = [...ed.getSelectedAll()].map((md) => md.delegate?.copy?.(md) || md).filter(Boolean);
models.length && em.set('clipboard', models);
},
} as CommandObject;
}
}

45
packages/core/src/commands/view/ExportTemplate.ts

@ -1,14 +1,35 @@
import { CommandObject } from './CommandAbstract';
import { EditorParam } from '../../editor';
import Editor, { EditorParam } from '../../editor';
import { createEl } from '../../utils/dom';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
interface ExportTemplateRunOptions {
export interface ExportTemplateRunOptions {
optsHtml?: EditorParam<'getHtml', 0>;
optsCss?: EditorParam<'getCss', 0>;
}
export default {
run(editor, sender, opts: ExportTemplateRunOptions = {}) {
export interface ExportTemplateCommandRegistryRun {
'core:open-code': CommandPublicFnFromHandler<CommandExportTemplate['run']>;
'export-template': CommandPublicFnFromHandler<CommandExportTemplate['run']>;
}
export interface ExportTemplateCommandRegistryStop {
'core:open-code': CommandPublicFnFromHandler<CommandExportTemplate['stop']>;
'export-template': CommandPublicFnFromHandler<CommandExportTemplate['stop']>;
}
export default class CommandExportTemplate extends CommandAbstract<
ExportTemplateRunOptions,
ExportTemplateRunOptions,
void,
void
> {
cm: Editor['CodeManager'] | null = null;
editors?: HTMLElement;
htmlEditor?: { setContent: (content: string) => void };
cssEditor?: { setContent: (content?: string) => void };
run(editor: Editor, sender: any, opts: ExportTemplateRunOptions = {}) {
sender && sender.set && sender.set('active', 0);
const config = editor.getConfig();
const modal = editor.Modal;
@ -33,14 +54,14 @@ export default {
})
.getModel()
.once('change:open', () => editor.stopCommand(`${this.id}`));
this.htmlEditor.setContent(editor.getHtml(opts.optsHtml));
this.cssEditor.setContent(editor.getCss(opts.optsCss));
},
this.htmlEditor?.setContent(editor.getHtml(opts.optsHtml));
this.cssEditor?.setContent(editor.getCss(opts.optsCss));
}
stop(editor) {
stop(editor: Editor) {
const modal = editor.Modal;
modal && modal.close();
},
}
buildEditor(codeName: string, theme: string, label: string) {
const cm = this.em.CodeManager;
@ -56,5 +77,5 @@ export default {
} as any).render().el;
return { model, el };
},
} as CommandObject<{}, { [k: string]: any }>;
}
}

123
packages/core/src/commands/view/Fullscreen.ts

@ -1,48 +1,86 @@
import { isElement } from 'underscore';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export interface CommandFullscreenOptions {
target?: HTMLElement | string;
}
export interface FullscreenCommandRegistryRun {
'core:fullscreen': CommandPublicFnFromHandler<CommandFullscreen['run']>;
fullscreen: CommandPublicFnFromHandler<CommandFullscreen['run']>;
}
export interface FullscreenCommandRegistryStop {
'core:fullscreen': CommandPublicFnFromHandler<CommandFullscreen['stop']>;
fullscreen: CommandPublicFnFromHandler<CommandFullscreen['stop']>;
}
interface DocumentWithFullscreen extends Document {
webkitFullscreenElement?: Element | null;
mozFullScreenElement?: Element | null;
webkitExitFullscreen?: () => void;
mozCancelFullScreen?: () => void;
msExitFullscreen?: () => void;
}
export default class CommandFullscreen extends CommandAbstract<
CommandFullscreenOptions,
CommandFullscreenOptions,
void,
void
> {
sender: any;
fullscreenChangeEvent?: string;
fullscreenChangeHandler?: EventListener;
export default {
/**
* Check if fullscreen mode is enabled
* @return {Boolean}
*/
isEnabled() {
const d = document;
// @ts-ignore
if (d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement) {
return true;
}
return false;
},
const d = document as DocumentWithFullscreen;
return !!(d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement);
}
/**
* Enable fullscreen mode and return browser prefix
* @param {HTMLElement} el
* @return {string}
*/
enable(el: any) {
enable(el?: HTMLElement | null) {
let pfx = '';
if (el.requestFullscreen) {
el.requestFullscreen();
} else if (el.webkitRequestFullscreen) {
if (!el) {
return pfx;
}
const target = el as HTMLElement & {
webkitRequestFullscreen?: () => void;
mozRequestFullScreen?: () => void;
msRequestFullscreen?: () => void;
};
if (target.requestFullscreen) {
target.requestFullscreen();
} else if (target.webkitRequestFullscreen) {
pfx = 'webkit';
el.webkitRequestFullscreen();
} else if (el.mozRequestFullScreen) {
target.webkitRequestFullscreen();
} else if (target.mozRequestFullScreen) {
pfx = 'moz';
el.mozRequestFullScreen();
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen();
target.mozRequestFullScreen();
} else if (target.msRequestFullscreen) {
target.msRequestFullscreen();
}
return pfx;
},
}
/**
* Disable fullscreen mode
*/
disable() {
const d: any = document;
const d = document as DocumentWithFullscreen;
if (this.isEnabled()) {
if (d.exitFullscreen) d.exitFullscreen();
@ -50,32 +88,39 @@ export default {
else if (d.mozCancelFullScreen) d.mozCancelFullScreen();
else if (d.msExitFullscreen) d.msExitFullscreen();
}
},
}
/**
* Triggered when the state of the fullscreen is changed. Inside detects if
* it's enabled
* @param {strinf} pfx Browser prefix
* @param {Event} e
*/
fsChanged(pfx: string) {
onFullscreenChange() {
if (!this.isEnabled()) {
this.stopCommand({ sender: this.sender });
document.removeEventListener(`${pfx || ''}fullscreenchange`, this.fsChanged);
}
},
}
run(editor, sender, opts = {}) {
run(editor: any, sender: any, opts: CommandFullscreenOptions = {}) {
this.sender = sender;
const { target } = opts;
const targetEl = isElement(target) ? target : document.querySelector(target!);
const targetEl = isElement(target)
? target
: typeof target === 'string'
? document.querySelector<HTMLElement>(target)
: undefined;
const pfx = this.enable(targetEl || editor.getContainer());
this.fsChanged = this.fsChanged.bind(this, pfx);
document.addEventListener(pfx + 'fullscreenchange', this.fsChanged);
},
this.fullscreenChangeEvent = `${pfx}fullscreenchange`;
this.fullscreenChangeHandler = this.onFullscreenChange.bind(this);
document.addEventListener(this.fullscreenChangeEvent, this.fullscreenChangeHandler);
}
stop(_editor: any, sender: any, _opts: CommandFullscreenOptions = {}) {
if (sender && sender.set) {
sender.set('active', false);
}
if (this.fullscreenChangeEvent && this.fullscreenChangeHandler) {
document.removeEventListener(this.fullscreenChangeEvent, this.fullscreenChangeHandler);
this.fullscreenChangeEvent = undefined;
this.fullscreenChangeHandler = undefined;
}
stop(editor, sender) {
if (sender && sender.set) sender.set('active', false);
this.disable();
},
} as CommandObject<{ target?: HTMLElement | string }, { [k: string]: any }>;
}
}

109
packages/core/src/commands/view/MoveComponent.ts

@ -1,38 +1,58 @@
import { bindAll, extend } from 'underscore';
import { bindAll } from 'underscore';
import { $ } from '../../common';
import Component from '../../dom_components/model/Component';
import type Component from '../../dom_components/model/Component';
import { off, on } from '../../utils/dom';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
import SelectComponent from './SelectComponent';
import SelectPosition from './SelectPosition';
export default extend({}, SelectPosition, SelectComponent, {
const SelectComponentProto = SelectComponent.prototype as any;
const SelectPositionProto = SelectPosition.prototype as any;
export interface MoveComponentCommandRegistryRun {
'core:component-move': CommandPublicFnFromHandler<CommandMoveComponent['run']>;
'move-comp': CommandPublicFnFromHandler<CommandMoveComponent['run']>;
}
export interface MoveComponentCommandRegistryStop {
'core:component-move': CommandPublicFnFromHandler<CommandMoveComponent['stop']>;
'move-comp': CommandPublicFnFromHandler<CommandMoveComponent['stop']>;
}
export default class CommandMoveComponent extends CommandAbstract {
[key: string]: any;
init(o: any) {
SelectComponent.init.apply(this, arguments);
SelectComponentProto.init.apply(this, arguments as any);
bindAll(this, 'initSorter', 'rollback', 'onEndMove');
this.opt = o;
this.hoverClass = this.ppfx + 'highlighter-warning';
this.badgeClass = this.ppfx + 'badge-warning';
this.noSelClass = this.ppfx + 'no-select';
},
this.hoverClass = `${this.ppfx}highlighter-warning`;
this.badgeClass = `${this.ppfx}badge-warning`;
this.noSelClass = `${this.ppfx}no-select`;
}
enable(...args: any) {
SelectComponent.enable.apply(this, args);
run(...args: any[]) {
return SelectPositionProto.run.apply(this, args);
}
enable(...args: any[]) {
SelectComponentProto.enable.apply(this, args);
this.getBadgeEl().addClass(this.badgeClass);
this.getHighlighterEl().addClass(this.hoverClass);
var wp = this.$wrapper;
const wp = this.$wrapper;
wp.css('cursor', 'move');
wp.on('mousedown', this.initSorter);
// Avoid strange moving behavior
wp.addClass(this.noSelClass);
},
}
/**
* Overwrite for doing nothing
* @private
*/
toggleClipboard() {},
toggleClipboard() {}
/**
* Delegate sorting
@ -40,8 +60,8 @@ export default extend({}, SelectPosition, SelectComponent, {
* @private
* */
initSorter(e: any) {
var el = $(e.target).data('model');
var drag = el.get('draggable');
const el = $(e.target).data('model');
const drag = el.get('draggable');
if (!drag) return;
// Avoid badge showing on move
@ -52,7 +72,7 @@ export default extend({}, SelectPosition, SelectComponent, {
this.stopSelectComponent();
this.$wrapper.off('mousedown', this.initSorter);
on(this.getContentWindow(), 'keydown', this.rollback);
},
}
/**
* Init sorter from model
@ -60,27 +80,18 @@ export default extend({}, SelectPosition, SelectComponent, {
* @private
*/
initSorterFromModel(model: Component) {
var drag = model.get('draggable');
const drag = model.get('draggable');
if (!drag) return;
// Avoid badge showing on move
this.cacheEl = null;
// @ts-ignore
var el = model.view.el;
const el = model.view?.el;
if (!el) return;
this.startSelectPosition(el, this.frameEl.contentDocument);
this.sorter.draggable = drag;
this.sorter.eventHandlers.legacyOnEndMove = this.onEndMoveFromModel.bind(this);
/*
this.sorter.setDragHelper(el);
var dragHelper = this.sorter.dragHelper;
dragHelper.className = this.ppfx + 'drag-helper';
dragHelper.innerHTML = '';
dragHelper.backgroundColor = 'white';
*/
this.stopSelectComponent();
on(this.getContentWindow(), 'keydown', this.rollback);
},
}
/**
* Init sorter from models
@ -101,11 +112,11 @@ export default extend({}, SelectPosition, SelectComponent, {
this.sorter.eventHandlers.legacyOnEndMove = this.onEndMoveFromModel.bind(this);
this.stopSelectComponent();
on(this.getContentWindow(), 'keydown', this.rollback);
},
}
onEndMoveFromModel() {
off(this.getContentWindow(), 'keydown', this.rollback);
},
}
/**
* Callback after sorting
@ -114,7 +125,7 @@ export default extend({}, SelectPosition, SelectComponent, {
onEndMove() {
this.enable();
off(this.getContentWindow(), 'keydown', this.rollback);
},
}
/**
* Say what to do after the component was selected (selectComponent)
@ -122,7 +133,7 @@ export default extend({}, SelectPosition, SelectComponent, {
* @param {Object} Selected element
* @private
* */
onSelect(e: any, el: any) {},
onSelect(e: any, el: any) {}
/**
* Used to bring the previous situation before start moving the component
@ -130,13 +141,13 @@ export default extend({}, SelectPosition, SelectComponent, {
* @param {Boolean} Indicates if rollback in anycase
* @private
* */
rollback(e: any, force: boolean) {
var key = e.which || e.keyCode;
rollback(e: any, force?: boolean) {
const key = e.which || e.keyCode;
if (key == 27 || force) {
this.sorter.cancelDrag();
}
return;
},
}
/**
* Returns badge element
@ -146,7 +157,7 @@ export default extend({}, SelectPosition, SelectComponent, {
getBadgeEl() {
if (!this.$badge) this.$badge = $(this.getBadge());
return this.$badge;
},
}
/**
* Returns highlighter element
@ -156,14 +167,22 @@ export default extend({}, SelectPosition, SelectComponent, {
getHighlighterEl() {
if (!this.$hl) this.$hl = $(this.canvas.getHighlighter());
return this.$hl;
},
}
stop(...args) {
// @ts-ignore
SelectComponent.stop.apply(this, args);
stop(...args: any[]) {
SelectComponentProto.stop.apply(this, args);
this.getBadgeEl().removeClass(this.badgeClass);
this.getHighlighterEl().removeClass(this.hoverClass);
var wp = this.$wrapper;
const wp = this.$wrapper;
wp.css('cursor', '').unbind().removeClass(this.noSelClass);
},
} as CommandObject<{}, { [k: string]: any }>);
}
}
[SelectPositionProto as Record<string, unknown>, SelectComponentProto as Record<string, unknown>].forEach((source) => {
Object.getOwnPropertyNames(source).forEach((key) => {
if (key === 'constructor') return;
if (!(key in CommandMoveComponent.prototype)) {
(CommandMoveComponent.prototype as Record<string, unknown>)[key] = source[key];
}
});
});

62
packages/core/src/commands/view/OpenAssets.ts

@ -1,29 +1,53 @@
import { isFunction } from 'underscore';
import Asset from '../../asset_manager/model/Asset';
import Editor from '../../editor';
import { createEl } from '../../utils/dom';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
open(content: string) {
export interface OpenAssetsCommandRegistryRun {
'core:open-assets': CommandPublicFnFromHandler<CommandOpenAssets['run']>;
'open-assets': CommandPublicFnFromHandler<CommandOpenAssets['run']>;
}
export interface OpenAssetsCommandRegistryStop {
'core:open-assets': CommandPublicFnFromHandler<CommandOpenAssets['stop']>;
'open-assets': CommandPublicFnFromHandler<CommandOpenAssets['stop']>;
}
export default class CommandOpenAssets extends CommandAbstract {
title = '';
editor?: Editor;
am?: any;
rendered?: HTMLElement;
open(content: string | HTMLElement) {
const { editor, title, config, am } = this;
if (!editor || !config || !am) return;
const { custom } = config;
if (isFunction(custom.open)) {
return custom.open(am.__customData());
}
const { Modal } = editor;
Modal.open({ title, content }).onceClose(() => editor.stopCommand(this.id));
},
Modal.open({ title, content }).onceClose(() => editor.stopCommand(this.id as string));
}
close() {
const { custom } = this.config;
const { config, am, editor } = this;
if (!config || !am || !editor) return;
const { custom } = config;
if (isFunction(custom.close)) {
return custom.close(this.am.__customData());
return custom.close(am.__customData());
}
const { Modal } = this.editor;
const { Modal } = editor;
Modal && Modal.close();
},
}
run(editor, sender, opts = {}) {
run(editor: Editor, sender: any, opts: any = {}) {
const am = editor.AssetManager;
const config = am.getConfig();
const { types = [], accept, select } = opts;
@ -59,18 +83,20 @@ export default {
this.rendered = am.getContainer();
}
if (accept) {
const uploadEl = this.rendered.querySelector(`input#${config.stylePrefix}uploadFile`);
const { rendered } = this;
if (accept && rendered) {
const uploadEl = rendered.querySelector(`input#${config.stylePrefix}uploadFile`);
uploadEl && uploadEl.setAttribute('accept', accept);
}
}
this.open(this.rendered);
const { rendered } = this;
rendered && this.open(rendered);
return this;
},
}
stop(editor) {
stop(editor: Editor) {
this.editor = editor;
this.close(this.rendered);
},
} as CommandObject<any, { [k: string]: any }>;
this.close();
}
}

41
packages/core/src/commands/view/OpenBlocks.ts

@ -1,10 +1,28 @@
import { isFunction } from 'underscore';
import { createEl } from '../../utils/dom';
import { CommandObject } from './CommandAbstract';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export interface OpenBlocksCommandRegistryRun {
'core:open-blocks': CommandPublicFnFromHandler<CommandOpenBlocks['run']>;
'open-blocks': CommandPublicFnFromHandler<CommandOpenBlocks['run']>;
}
export interface OpenBlocksCommandRegistryStop {
'core:open-blocks': CommandPublicFnFromHandler<CommandOpenBlocks['stop']>;
'open-blocks': CommandPublicFnFromHandler<CommandOpenBlocks['stop']>;
}
export default class CommandOpenBlocks extends CommandAbstract {
container?: HTMLElement;
editor?: Editor;
bm?: any;
firstRender?: boolean;
export default {
open() {
const { container, editor, bm, config } = this;
if (!container || !editor || !bm || !config) return;
const { custom, appendTo } = config;
if (isFunction(custom.open)) {
@ -19,21 +37,22 @@ export default {
if (!custom) container.appendChild(bm.render());
}
if (container) container.style.display = 'block';
},
container.style.display = 'block';
}
close() {
const { container, config } = this;
const { container, config, bm } = this;
if (!config || !bm) return;
const { custom } = config;
if (isFunction(custom.close)) {
return custom.close(this.bm.__customData());
return custom.close(bm.__customData());
}
if (container) container.style.display = 'none';
},
}
run(editor) {
run(editor: Editor) {
const bm = editor.Blocks;
this.config = bm.getConfig();
this.firstRender = !this.container;
@ -50,9 +69,9 @@ export default {
}
this.open();
},
}
stop() {
this.close();
},
} as CommandObject<{}, { [k: string]: any }>;
}
}

27
packages/core/src/commands/view/OpenLayers.ts

@ -1,7 +1,21 @@
import { CommandObject } from './CommandAbstract';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(editor) {
export interface OpenLayersCommandRegistryRun {
'core:open-layers': CommandPublicFnFromHandler<CommandOpenLayers['run']>;
'open-layers': CommandPublicFnFromHandler<CommandOpenLayers['run']>;
}
export interface OpenLayersCommandRegistryStop {
'core:open-layers': CommandPublicFnFromHandler<CommandOpenLayers['stop']>;
'open-layers': CommandPublicFnFromHandler<CommandOpenLayers['stop']>;
}
export default class CommandOpenLayers extends CommandAbstract {
layers?: HTMLDivElement;
run(editor: Editor) {
const lm = editor.LayerManager;
const pn = editor.Panels;
const lmConfig = lm.getConfig();
@ -11,7 +25,6 @@ export default {
if (!this.layers) {
const id = 'views-container';
const layers = document.createElement('div');
// @ts-ignore
const panels = pn.getPanel(id) || pn.addPanel({ id });
if (lmConfig.custom) {
@ -25,10 +38,10 @@ export default {
}
this.layers.style.display = 'block';
},
}
stop() {
const { layers } = this;
layers && (layers.style.display = 'none');
},
} as CommandObject<{}, { [k: string]: any }>;
}
}

39
packages/core/src/commands/view/OpenStyleManager.ts

@ -1,8 +1,26 @@
import { $ } from '../../common';
import { CommandObject } from './CommandAbstract';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(editor, sender) {
export interface OpenStyleManagerCommandRegistryRun {
'core:open-styles': CommandPublicFnFromHandler<CommandOpenStyleManager['run']>;
'open-sm': CommandPublicFnFromHandler<CommandOpenStyleManager['run']>;
}
export interface OpenStyleManagerCommandRegistryStop {
'core:open-styles': CommandPublicFnFromHandler<CommandOpenStyleManager['stop']>;
'open-sm': CommandPublicFnFromHandler<CommandOpenStyleManager['stop']>;
}
export default class CommandOpenStyleManager extends CommandAbstract {
sender?: any;
sm?: any;
$cnt?: any;
$cntInner?: any;
$header?: any;
run(editor: Editor, sender: any) {
this.sender = sender;
if (!this.$cnt) {
@ -19,14 +37,12 @@ export default {
$cntInner.append($cntSm);
$cnt.append($cntInner);
// Device Manager
if (DeviceManager && config.showDevices) {
const devicePanel = Panels.addPanel({ id: 'devices-c' });
const dvEl = DeviceManager.render();
devicePanel.set('appendContent', dvEl).trigger(trgEvCnt);
}
// Selector Manager container
const slmConfig = SelectorManager.getConfig();
if (slmConfig.custom) {
SelectorManager.__trgCustom({ container: $cntSlm.get(0) });
@ -34,7 +50,6 @@ export default {
$cntSlm.append(SelectorManager.render([]));
}
// Style Manager
this.sm = StyleManager;
const smConfig = StyleManager.getConfig();
const pfx = smConfig.stylePrefix;
@ -47,20 +62,16 @@ export default {
$cntSm.append(StyleManager.render());
}
// Create panel if not exists
const pnCnt = 'views-container';
const pnl = Panels.getPanel(pnCnt) || Panels.addPanel({ id: pnCnt });
// Add all containers to the panel
pnl.set('appendContent', $cnt).trigger(trgEvCnt);
// Toggle Style Manager on target selection
const em = editor.getModel();
this.listenTo(em, StyleManager.events.target, this.toggleSm);
}
this.toggleSm();
},
}
/**
* Toggle Style Manager visibility
@ -77,10 +88,10 @@ export default {
$cntInner?.hide();
$header?.show();
}
},
}
stop() {
this.$cntInner?.hide();
this.$header?.hide();
},
} as CommandObject<{}, { [k: string]: any }>;
}
}

83
packages/core/src/commands/view/OpenTraitManager.ts

@ -1,71 +1,82 @@
import { CommandObject } from './CommandAbstract';
import { $ } from '../../common';
import { ComponentsEvents } from '../../dom_components/types';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(editor, sender) {
export interface OpenTraitManagerCommandRegistryRun {
'core:open-traits': CommandPublicFnFromHandler<CommandOpenTraitManager['run']>;
'open-tm': CommandPublicFnFromHandler<CommandOpenTraitManager['run']>;
}
export interface OpenTraitManagerCommandRegistryStop {
'core:open-traits': CommandPublicFnFromHandler<CommandOpenTraitManager['stop']>;
'open-tm': CommandPublicFnFromHandler<CommandOpenTraitManager['stop']>;
}
export default class CommandOpenTraitManager extends CommandAbstract {
sender?: any;
target?: any;
$cn?: any;
$cn2?: any;
$header?: any;
run(editor: Editor, sender: any) {
this.sender = sender;
const em = editor.getModel();
const config = editor.Config;
const pfx = config.stylePrefix;
const tm = editor.TraitManager;
const confTm = tm.getConfig();
let panelC;
if (confTm.appendTo) return;
if (!this.$cn) {
this.$cn = $('<div></div>');
this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2);
const $cn = $('<div></div>');
const $cn2 = $('<div></div>');
this.$cn = $cn;
this.$cn2 = $cn2;
$cn.append($cn2);
this.$header = $('<div>').append(`<div class="${confTm.stylePrefix}header">${em.t('traitManager.empty')}</div>`);
this.$cn.append(this.$header);
$cn.append(this.$header);
if (confTm.custom) {
tm.__trgCustom({ container: this.$cn2.get(0) });
} else {
this.$cn2.append(`<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>`);
this.$cn2.append(tm.render());
}
var panels = editor.Panels;
if (!panels.getPanel('views-container')) {
// @ts-ignore
panelC = panels.addPanel({ id: 'views-container' });
tm.__trgCustom({ container: $cn2.get(0) });
} else {
panelC = panels.getPanel('views-container');
$cn2.append(`<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>`);
$cn2.append(tm.render());
}
panelC?.set('appendContent', this.$cn.get(0)).trigger('change:appendContent');
const panels = editor.Panels;
const panel = panels.getPanel('views-container') || panels.addPanel({ id: 'views-container' });
panel?.set('appendContent', $cn.get(0)).trigger('change:appendContent');
this.target = editor.getModel();
this.target = em;
this.listenTo(this.target, ComponentsEvents.toggled, this.toggleTm);
}
this.toggleTm();
},
}
/**
* Toggle Trait Manager visibility
* @private
*/
toggleTm() {
const sender = this.sender;
if (sender && sender.get && !sender.get('active')) return;
const { sender, target, $cn2, $header } = this;
if ((sender && sender.get && !sender.get('active')) || !target) return;
if (this.target.getSelectedAll().length === 1) {
this.$cn2.show();
this.$header.hide();
if (target.getSelectedAll().length === 1) {
$cn2?.show();
$header?.hide();
} else {
this.$cn2.hide();
this.$header.show();
$cn2?.hide();
$header?.show();
}
},
}
stop() {
this.$cn2 && this.$cn2.hide();
this.$header && this.$header.hide();
},
} as CommandObject<{}, { [k: string]: any }>;
this.$cn2?.hide();
this.$header?.hide();
}
}

23
packages/core/src/commands/view/PasteComponent.ts

@ -1,11 +1,20 @@
import { isArray, contains } from 'underscore';
import { contains, isArray } from 'underscore';
import Component from '../../dom_components/model/Component';
import { ComponentsEvents } from '../../dom_components/types';
import { CommandObject } from './CommandAbstract';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
run(ed, s, opts = {}) {
export interface PasteComponentOptions {
action?: string;
}
export interface PasteComponentCommandRegistryRun {
'core:paste': CommandPublicFnFromHandler<CommandPasteComponent['run']>;
}
export default class CommandPasteComponent extends CommandAbstract<PasteComponentOptions> {
run(ed: Editor, _sender: any, opts: PasteComponentOptions = {}) {
const em = ed.getModel();
const clp: Component[] | null = em.get('clipboard');
const lastSelected = ed.getSelected();
@ -25,8 +34,6 @@ export default {
added = doAdd(ed, clp, selected.parent()!, addOpts);
}
} else {
// Page body is selected
// Paste at the end of the body
const pageBody = em.Pages.getSelected()?.getMainComponent();
const addOpts = { at: pageBody?.components().length || 0, action: opts.action || 'paste-component' };
@ -39,8 +46,8 @@ export default {
lastSelected.emitUpdate();
}
},
} as CommandObject;
}
}
function doAdd(ed: Editor, clp: Component[], parent: Component, addOpts: any): Component[] | Component {
const copyable = clp.filter((cop) => cop.get('copyable'));

47
packages/core/src/commands/view/Preview.ts

@ -1,23 +1,40 @@
import { each } from 'underscore';
import Editor from '../../editor';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
const cmdOutline = 'core:component-outline';
export default {
export interface PreviewCommandRegistryRun {
'core:preview': CommandPublicFnFromHandler<CommandPreview['run']>;
preview: CommandPublicFnFromHandler<CommandPreview['run']>;
}
export interface PreviewCommandRegistryStop {
'core:preview': CommandPublicFnFromHandler<CommandPreview['stop']>;
preview: CommandPublicFnFromHandler<CommandPreview['stop']>;
}
export default class CommandPreview extends CommandAbstract {
helper?: HTMLSpanElement;
panels?: ReturnType<Editor['Panels']['getPanels']>;
selected?: ReturnType<Editor['getSelectedAll']>;
sender?: any;
shouldRunSwVisibility?: boolean;
getPanels(editor: Editor) {
if (!this.panels) {
this.panels = editor.Panels.getPanels();
}
return this.panels;
},
return this.panels!;
}
preventDrag(opts: any) {
opts.abort = 1;
},
}
tglEffects(on: boolean) {
tglEffects(on = false) {
const { em } = this;
const mthEv = on ? 'on' : 'off';
if (em) {
@ -29,9 +46,9 @@ export default {
each(elP, (item) => ((item as HTMLElement).style.pointerEvents = on ? 'all' : ''));
em[mthEv]('run:tlb-move:before', this.preventDrag);
}
},
}
run(editor, sender) {
run(editor: Editor, sender: any) {
this.sender = sender;
this.selected = [...editor.getSelectedAll()];
editor.select();
@ -58,7 +75,7 @@ export default {
this.helper.style.display = 'inline-block';
panels.forEach((panel: any) => panel.set('visible', false));
panels.forEach((panel) => panel.set('visible', false));
const canvasS = canvas.style;
canvasS.width = '100%';
@ -68,10 +85,10 @@ export default {
canvasS.padding = '0';
canvasS.margin = '0';
editor.refresh();
this.tglEffects(1);
},
this.tglEffects(true);
}
stop(editor) {
stop(editor: Editor) {
const { sender = {}, selected } = this;
sender.set && sender.set('active', 0);
const panels = this.getPanels(editor);
@ -82,7 +99,7 @@ export default {
}
editor.getModel().runDefault();
panels.forEach((panel: any) => panel.set('visible', true));
panels.forEach((panel) => panel.set('visible', true));
const canvas = editor.Canvas.getElement();
canvas.setAttribute('style', '');
@ -95,5 +112,5 @@ export default {
editor.refresh();
this.tglEffects();
},
} as CommandObject<{}, { [k: string]: any }>;
}
}

46
packages/core/src/commands/view/Resize.ts

@ -3,14 +3,17 @@ import Component from '../../dom_components/model/Component';
import { ComponentsEvents } from '../../dom_components/types';
import ComponentView from '../../dom_components/view/ComponentView';
import StyleableModel, { StyleProps } from '../../domain_abstract/model/StyleableModel';
import Editor from '../../editor';
import { getUnitFromValue } from '../../utils/mixins';
import Resizer, { RectDim, ResizerOptions } from '../../utils/Resizer';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export interface ComponentResizeOptions extends ResizerOptions {
component: Component;
componentView?: ComponentView;
el?: HTMLElement;
force?: boolean;
afterStart?: () => void;
afterEnd?: () => void;
/**
@ -101,8 +104,25 @@ export enum ConvertUnitsToPx {
perc = '%',
}
export default {
run(editor, _, options: ComponentResizeOptions) {
export interface ResizeCommandRegistryRun {
'core:resize': CommandPublicFnFromHandler<CommandResize['run']>;
resize: CommandPublicFnFromHandler<CommandResize['run']>;
}
export interface ResizeCommandRegistryStop {
'core:resize': CommandPublicFnFromHandler<CommandResize['stop']>;
resize: CommandPublicFnFromHandler<CommandResize['stop']>;
}
export default class CommandResize extends CommandAbstract<
ComponentResizeOptions,
ComponentResizeOptions,
Resizer,
void
> {
canvasResizer?: Resizer;
run(editor: Editor, _: any, options: ComponentResizeOptions): Resizer {
const { Canvas, Utils, em } = editor;
const canvasView = Canvas.getCanvasView();
const pfx = em.config.stylePrefix || '';
@ -225,13 +245,13 @@ export default {
options.afterEnd?.();
},
updateTarget: (_el, rect, options) => {
updateTarget(_el, rect, options);
updateTarget: (_el, rect, updateOptions) => {
updateTarget(_el, rect, updateOptions);
if (!modelToStyle) {
return;
}
const { store, selectedHandler, config, resizer, event } = options;
const { store, selectedHandler, config, resizer, event } = updateOptions;
const { keyHeight, keyWidth, autoHeight, autoWidth, unitWidth, unitHeight } = config;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler!) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler!) >= 0;
@ -298,21 +318,21 @@ export default {
let { canvasResizer } = this;
// Create the resizer for the canvas if not yet created
if (!canvasResizer) {
this.canvasResizer = new Utils.Resizer(resizeOptions);
canvasResizer = this.canvasResizer;
}
canvasResizer = canvasResizer!;
canvasResizer.setOptions(resizeOptions, true);
canvasResizer.blur();
canvasResizer.focus(el);
return canvasResizer;
},
}
stop() {
this.canvasResizer?.blur();
},
}
convertPxToUnit(props: ConvertPxToUnitProps): string {
const { el, valuePx, unit, dpi = 96, roundDecimals = 3, isHeight, elComputedStyle } = props;
@ -378,11 +398,5 @@ export default {
}
return `${+valueResult.toFixed(roundDecimals)}${untiResult}`;
},
} as CommandObject<
ComponentResizeOptions,
{
canvasResizer?: Resizer;
convertPxToUnit: (props: ConvertPxToUnitProps) => string;
}
>;
}

314
packages/core/src/commands/view/SelectComponent.ts

@ -7,9 +7,21 @@ import { ComponentResizeInitEventData, ComponentsEvents } from '../../dom_compon
import ToolbarView from '../../dom_components/view/ToolbarView';
import { isDoc, isTaggableNode, isVisible, off, on } from '../../utils/dom';
import { getComponentModel, getComponentView, hasWin, isObject } from '../../utils/mixins';
import { CommandObject } from './CommandAbstract';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
let showOffsets: boolean;
export interface SelectComponentCommandRegistryRun {
'core:component-select': CommandPublicFnFromHandler<CommandSelectComponent['run']>;
'select-comp': CommandPublicFnFromHandler<CommandSelectComponent['run']>;
}
export interface SelectComponentCommandRegistryStop {
'core:component-select': CommandPublicFnFromHandler<CommandSelectComponent['stop']>;
'select-comp': CommandPublicFnFromHandler<CommandSelectComponent['stop']>;
}
/**
* This command is responsible for show selecting components and displaying
* all the necessary tools around (component toolbar, badge, highlight box, etc.)
@ -28,12 +40,21 @@ let showOffsets: boolean;
* you can see stuff like the highlight box, badge, margins/paddings offsets, etc.
* so those elements are inside the Local Tools box
*
*
*/
export default {
activeResizer: false,
export default class CommandSelectComponent extends CommandAbstract {
[key: string]: any;
activeResizer = false;
init() {
this._upToolbar = debounce(() => {
this.updateToolsGlobal({ force: 1 });
}, 0);
this.updateAttached = debounce(() => {
this.updateGlobalPos();
}, 0);
this.onContainerChange = debounce(() => {
this.em.refreshCanvas();
}, 150);
this.onSelect = debounce(this.onSelect, 0);
bindAll(
this,
@ -46,22 +67,22 @@ export default {
'onFrameUpdated',
'onContainerChange',
);
},
}
enable() {
this.frameOff = this.canvasOff = this.adjScroll = null;
this.startSelectComponent();
showOffsets = true;
},
}
/**
* Start select component event
* @private
* */
startSelectComponent() {
this.toggleSelectComponent(1);
this.toggleSelectComponent(true);
this.em.getSelected() && this.onSelect();
},
}
/**
* Stop select component event
@ -69,17 +90,17 @@ export default {
* */
stopSelectComponent() {
this.toggleSelectComponent();
this.onContainerChange.cancel?.();
this.onSelect.cancel?.();
this.updateAttached.cancel?.();
this._upToolbar.cancel?.();
},
(this.onContainerChange as any).cancel?.();
(this.onSelect as any).cancel?.();
(this.updateAttached as any).cancel?.();
(this._upToolbar as any).cancel?.();
}
/**
* Toggle select component event
* @private
* */
toggleSelectComponent(enable: boolean) {
toggleSelectComponent(enable?: boolean) {
const { em, canvas } = this;
const canvasEl = canvas.getCanvasView().el;
const listenToEl = em.getConfig().listenToEl!;
@ -88,8 +109,8 @@ export default {
const methods = { on, off };
const eventCmpUpdate = ComponentsEvents.update;
!listenToEl.length && parentNode && listenToEl.push(parentNode as HTMLElement);
const trigger = (win: Window, body: HTMLBodyElement, canvasEl: HTMLElement) => {
methods[method](canvasEl, 'scroll', this.onCanvasScroll, true);
const trigger = (win: Window, body: HTMLBodyElement, currentCanvasEl: HTMLElement) => {
methods[method](currentCanvasEl, 'scroll', this.onCanvasScroll, true);
methods[method](body, 'mouseover', this.onHover);
methods[method](body, 'mouseleave', this.onOut);
methods[method](body, 'click', this.onClick);
@ -105,12 +126,12 @@ export default {
em[method]('frame:updated', this.onFrameUpdated, this);
em[method](CanvasEvents.updateTools, this.onFrameUpdated, this);
em[method](em.Canvas.events.refresh, this.updateAttached, this);
em.Canvas.getFrames().forEach((frame) => {
em.Canvas.getFrames().forEach((frame: any) => {
const { view } = frame;
const win = view?.getWindow();
win && trigger(win, view?.getBody()!, canvasEl);
});
},
}
/**
* Hover command
@ -125,7 +146,6 @@ export default {
const frameView = view?.frameView;
let model = view?.model;
// Get first valid model
if (!model) {
let parentEl = el.parentNode;
while (!model && parentEl && !isDoc(parentEl)) {
@ -137,14 +157,14 @@ export default {
this.currentDoc = el.ownerDocument;
em.setHovered(model, { useValid: true });
frameView && em.setCurrentFrame(frameView);
},
}
onFrameUpdated() {
this.updateLocalPos();
this.updateGlobalPos();
},
}
onHovered(em: any, component: Component) {
onHovered(em?: any, component?: Component) {
let result = {};
if (component) {
@ -163,18 +183,16 @@ export default {
this.currentDoc = null;
this.elHovered = 0;
this.updateToolsLocal();
this.canvas.getFrames().forEach((frame) => {
this.canvas.getFrames().forEach((frame: any) => {
const { view } = frame;
const el = view && view.getToolsEl();
el && this.toggleToolsEl(0, 0, { el });
el && this.toggleToolsEl(false, 0, { el });
});
}
},
}
/**
* Say what to do after the component was selected
* @param {Object} e
* @param {Object} el
* @private
* */
onSelect() {
@ -182,7 +200,7 @@ export default {
const component = em.getSelected();
const currentFrame = em.getCurrentFrame();
const view = component && component.getView(currentFrame?.model);
let el = view?.el;
const el = view?.el;
let result = {};
if (el && isVisible(el)) {
@ -192,48 +210,42 @@ export default {
this.elSelected = result;
this.updateToolsGlobal();
// This will hide some elements from the select component
this.updateLocalPos(result);
this.initResize(component);
},
}
updateGlobalPos() {
const sel = this.getElSelected();
if (!sel.el) return;
sel.pos = this.getElementPos(sel.el);
this.updateToolsGlobal();
},
}
updateLocalPos(data: any) {
updateLocalPos(data?: any) {
const sel = this.getElHovered();
if (!sel.el) return;
sel.pos = this.getElementPos(sel.el);
this.updateToolsLocal(data);
},
}
getElHovered() {
return this.elHovered || {};
},
}
getElSelected() {
return this.elSelected || {};
},
}
onOut() {
this.em.setHovered();
},
}
toggleToolsEl(on: boolean, view: any, opts: any = {}) {
toggleToolsEl(on?: boolean, view?: any, opts: any = {}) {
const el = opts.el || this.canvas.getToolsEl(view);
el && (el.style.display = on ? '' : 'none');
return el || {};
},
}
/**
* Show element offset viewer
* @param {HTMLElement} el
* @param {Object} pos
*/
showElementOffset(el: HTMLElement, pos: any, opts: any = {}) {
if (!showOffsets) return;
this.editor.runCommand('show-offset', {
@ -244,59 +256,39 @@ export default {
top: 0,
left: 0,
});
},
}
/**
* Hide element offset viewer
* @param {HTMLElement} el
* @param {Object} pos
*/
hideElementOffset(view: any) {
this.editor.stopCommand('show-offset', {
view,
});
},
}
/**
* Show fixed element offset viewer
* @param {HTMLElement} el
* @param {Object} pos
*/
showFixedElementOffset(el: HTMLElement, pos: any) {
this.editor.runCommand('show-offset', {
el,
elPos: pos,
state: 'Fixed',
});
},
}
/**
* Hide fixed element offset viewer
* @param {HTMLElement} el
* @param {Object} pos
*/
hideFixedElementOffset() {
if (this.editor) this.editor.stopCommand('show-offset', { state: 'Fixed' });
},
}
/**
* Hide Highlighter element
*/
hideHighlighter(view: any) {
this.canvas.getHighlighter(view).style.opacity = 0;
},
}
/**
* On element click
* @param {Event} e
* @private
*/
onClick(ev: Event) {
onClick(ev: Event): void {
ev.stopPropagation();
ev.preventDefault();
const { em } = this;
if (em.get('_cmpDrag')) return em.set('_cmpDrag');
if (em.get('_cmpDrag')) {
em.set('_cmpDrag');
return;
}
const el = ev.target as HTMLElement;
let cmp = getComponentModel(el);
@ -311,40 +303,21 @@ export default {
}
if (cmp) {
if (
em.isEditing() &&
// Avoid selection of inner text components during editing
((!cmp.get('textable') && cmp.isChildOf('text')) ||
// Prevents selecting another component if the pointer was pressed and
// dragged outside of the editing component
em.getEditing() !== cmp)
) {
if (em.isEditing() && ((!cmp.get('textable') && cmp.isChildOf('text')) || em.getEditing() !== cmp)) {
return;
}
this.select(cmp, ev);
this.select(cmp, ev as MouseEvent);
}
},
}
/**
* Select component
* @param {Component} model
* @param {Event} event
*/
select(model: Component, event: MouseEvent) {
if (!model) return;
const { em } = this;
em.setSelected(model, { event, useValid: true });
// Ensure we're passing the proper selected component #6096
this.initResize(em.getSelected());
},
}
/**
* Update badge for the component
* @param {Object} Component
* @param {Object} pos Position object
* @private
* */
updateBadge(el: HTMLElement, pos: any, opts: any = {}) {
const { canvas } = this;
const model = getComponentModel(el);
@ -374,29 +347,18 @@ export default {
pos: pos,
});
const top = targetToElem.top; //opts.topOff - badgeH < 0 ? -opts.topOff : posTop;
const top = targetToElem.top;
const left = opts.leftOff < 0 ? -opts.leftOff : 0;
bStyle.top = top + un;
bStyle.left = left + un;
},
}
/**
* Update highlighter element
* @param {HTMLElement} el
* @param {Object} pos Position object
* @private
*/
showHighlighter(view: any) {
this.canvas.getHighlighter(view).style.opacity = '';
},
}
/**
* Init resizer on the element if possible
* @param {HTMLElement|Component} elem
* @private
*/
initResize(elem: HTMLElement) {
initResize(elem: any) {
const { em, canvas } = this;
const editor = em.Editor;
const component = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
@ -440,13 +402,9 @@ export default {
editor.stopCommand('resize');
this.resizer = null;
}
},
}
/**
* Update toolbar if the component has one
* @param {Object} mod
*/
updateToolbar(mod: Component) {
updateToolbar(mod: any) {
const { canvas } = this;
const { em } = this.config;
const model = mod === em ? em.getSelected() : mod;
@ -461,8 +419,7 @@ export default {
if (!this.toolbar) {
toolbarEl.innerHTML = '';
this.toolbar = new Toolbar(toolbar);
// @ts-ignore
const toolbarView = new ToolbarView({ collection: this.toolbar, em });
const toolbarView = new ToolbarView({ collection: this.toolbar, em } as any);
toolbarEl.appendChild(toolbarView.render().el);
}
@ -472,75 +429,48 @@ export default {
} else {
toolbarStyle.display = 'none';
}
},
}
/**
* Update toolbar positions
* @param {HTMLElement} el
* @param {Object} pos
*/
updateToolbarPos(pos: any) {
const unit = 'px';
const { style } = this.canvas.getToolbarEl()!;
style.top = `${pos.top}${unit}`;
style.left = `${pos.left}${unit}`;
style.opacity = '';
},
}
/**
* Return canvas dimensions and positions
* @return {Object}
*/
getCanvasPosition() {
return this.canvas.getCanvasView().getPosition();
},
}
/**
* Returns badge element
* @return {HTMLElement}
* @private
*/
getBadge(opts: any = {}) {
return this.canvas.getBadgeEl(opts.view);
},
}
/**
* On canvas scroll callback
* @private
*/
onCanvasScroll(e: any) {
this.onFrameScroll(e);
this.onContainerChange();
},
}
/**
* On frame scroll callback
* @private
*/
onFrameScroll() {
onFrameScroll(_e?: any) {
this.updateTools();
this.canvas.refreshSpots();
},
}
onFrameResize() {
this.canvas.refresh({ all: true });
},
}
updateTools() {
this.updateLocalPos();
this.updateGlobalPos();
},
}
isCompSelected(comp: Component) {
return comp && comp.get('status') === 'selected';
},
}
/**
* Update tools visible on hover
* @param {HTMLElement} el
* @param {Object} pos
*/
updateToolsLocal(data: any) {
updateToolsLocal(data?: any) {
const config = this.em.getConfig();
const { el, pos, view, component } = data || this.getElHovered();
@ -566,7 +496,7 @@ export default {
}
const unit = 'px';
const toolsEl = this.toggleToolsEl(1, view);
const toolsEl = this.toggleToolsEl(true, view);
const { style } = toolsEl;
const frameOff = this.canvas.canvasRectOffset(el, pos);
const topOff = frameOff.top;
@ -593,25 +523,22 @@ export default {
width: pos.width,
height: pos.height,
});
},
}
_upToolbar: debounce(function () {
// @ts-ignore
this.updateToolsGlobal({ force: 1 });
}, 0),
_upToolbar() {}
_trgToolUp(type: string, opts = {}) {
this.em.trigger(CanvasEvents.toolsUpdate, {
type,
...opts,
});
},
}
updateToolsGlobal(opts: any = {}) {
const { el, pos, component } = this.getElSelected();
if (!el) {
this.toggleToolsEl(); // Hides toolbar
this.toggleToolsEl();
this.lastSelected = 0;
return;
}
@ -625,7 +552,7 @@ export default {
}
const unit = 'px';
const toolsEl = this.toggleToolsEl(1);
const toolsEl = this.toggleToolsEl(true);
const { style } = toolsEl;
const targetToElem = canvas.getTargetToElementFixed(el, canvas.getToolbarEl()!, { pos });
const topOff = targetToElem.canvasOffsetTop;
@ -644,74 +571,45 @@ export default {
width: pos.width,
height: pos.height,
});
},
}
/**
* Update attached elements, eg. component toolbar
*/
updateAttached: debounce(function () {
// @ts-ignore
this.updateGlobalPos();
}, 0),
updateAttached() {}
onContainerChange: debounce(function () {
// @ts-ignore
this.em.refreshCanvas();
}, 150),
onContainerChange() {}
/**
* Returns element's data info
* @param {HTMLElement} el
* @return {Object}
* @private
*/
getElementPos(el: HTMLElement) {
return this.canvas.getCanvasView().getElementPos(el, { noScroll: true });
},
}
/**
* Hide badge
* @private
* */
hideBadge() {
this.getBadge().style.display = 'none';
},
}
/**
* Clean previous model from different states
* @param {Component} model
* @private
*/
cleanPrevious(model: Component) {
model &&
model.set({
status: '',
state: '',
});
},
}
/**
* Returns content window
* @private
*/
getContentWindow() {
return this.canvas.getWindow();
},
}
run(editor) {
run(editor: any) {
if (!hasWin()) return;
// @ts-ignore
this.editor = editor && editor.get('Editor');
this.enable();
},
}
stop(ed, sender, opts = {}) {
stop(ed?: any, sender?: any, opts: any = {}) {
if (!hasWin()) return;
const { em, editor } = this;
this.onHovered(); // force to hide toolbar
this.onHovered();
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected();
this.toggleToolsEl();
editor?.stopCommand('resize');
},
} as CommandObject<any, { [k: string]: any }>;
}
}

44
packages/core/src/commands/view/SelectPosition.ts

@ -1,15 +1,19 @@
import { $ } from '../../common';
import CanvasComponentNode from '../../utils/sorter/CanvasComponentNode';
import { DragDirection } from '../../utils/sorter/types';
import { CommandObject } from './CommandAbstract';
export default {
import CommandAbstract from './CommandAbstract';
export default class CommandSelectPosition extends CommandAbstract {
[key: string]: any;
/**
* Start select position event
* @param {HTMLElement[]} sourceElements
* @private
* */
startSelectPosition(sourceElements: HTMLElement[], doc: Document, opts: any = {}) {
startSelectPosition(sourceElements: HTMLElement[] = [], doc?: Document, opts: any = {}) {
this.isPointed = false;
if (!sourceElements.length || !doc) return;
const utils = this.em.Utils;
const container = sourceElements[0].ownerDocument.body;
@ -39,7 +43,7 @@ export default {
sourceElements &&
sourceElements.length > 0 &&
this.sorter.startSort(sourceElements.map((element) => ({ element })));
},
}
/**
* Get frame position
@ -47,12 +51,12 @@ export default {
* @private
*/
getOffsetDim() {
var frameOff = this.offset(this.canvas.getFrameEl());
var canvasOff = this.offset(this.canvas.getElement());
var top = frameOff.top - canvasOff.top;
var left = frameOff.left - canvasOff.left;
const frameOff = this.offset(this.canvas.getFrameEl());
const canvasOff = this.offset(this.canvas.getElement());
const top = frameOff.top - canvasOff.top;
const left = frameOff.left - canvasOff.left;
return { top, left };
},
}
/**
* Stop select position event
@ -60,7 +64,7 @@ export default {
* */
stopSelectPosition() {
this.posTargetCollection = null;
this.posIndex = this.posMethod == 'after' && this.cDim.length !== 0 ? this.posIndex + 1 : this.posIndex; //Normalize
this.posIndex = this.posMethod == 'after' && this.cDim.length !== 0 ? this.posIndex + 1 : this.posIndex;
if (this.sorter) {
this.sorter.cancelDrag();
}
@ -75,7 +79,7 @@ export default {
this.posTargetModel = this.posTargetEl.data('model');
this.posTargetCollection = this.posTargetEl.data('model-comp');
}
},
}
/**
* Enabel select position
@ -83,7 +87,7 @@ export default {
*/
enable() {
this.startSelectPosition();
},
}
/**
* Check if the pointer is near to the float component
@ -94,22 +98,22 @@ export default {
* @private
* */
nearFloat(index: number, method: string, dims: any[]) {
var i = index || 0;
var m = method || 'before';
var len = dims.length;
var isLast = len !== 0 && m == 'after' && i == len;
const i = index || 0;
const m = method || 'before';
const len = dims.length;
const isLast = len !== 0 && m == 'after' && i == len;
if (len !== 0 && ((!isLast && !dims[i][4]) || (dims[i - 1] && !dims[i - 1][4]) || (isLast && !dims[i - 1][4])))
return 1;
return 0;
},
}
run() {
this.enable();
},
}
stop() {
this.stopSelectPosition();
this.$wrapper.css('cursor', '');
this.$wrapper.unbind();
},
} as CommandObject<{}, { [k: string]: any }>;
}
}

168
packages/core/src/commands/view/ShowOffset.ts

@ -1,15 +1,29 @@
import { isUndefined } from 'underscore';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { $ } from '../../common';
import { CommandObject } from './CommandAbstract';
import Editor from '../../editor';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export interface ShowOffsetCommandRegistryRun {
'core:component-offset': CommandPublicFnFromHandler<CommandShowOffset['run']>;
'show-offset': CommandPublicFnFromHandler<CommandShowOffset['run']>;
}
export interface ShowOffsetCommandRegistryStop {
'core:component-offset': CommandPublicFnFromHandler<CommandShowOffset['stop']>;
'show-offset': CommandPublicFnFromHandler<CommandShowOffset['stop']>;
}
export default class CommandShowOffset extends CommandAbstract {
[key: string]: any;
export default {
getOffsetMethod(state: string) {
var method = state || '';
return 'get' + method + 'OffsetViewerEl';
},
const method = state || '';
return `get${method}OffsetViewerEl`;
}
run(editor, sender, opts) {
run(editor: Editor, sender: any, opts: any) {
const { canvas } = this;
const opt = opts || {};
const state = opt.state || '';
@ -26,7 +40,7 @@ export default {
return;
}
var pos = { ...(opt.elPos || canvas.getElementPos(el)) };
const pos = { ...(opt.elPos || canvas.getElementPos(el)) };
if (!isUndefined(opt.top)) {
pos.top = opt.top;
@ -35,22 +49,21 @@ export default {
pos.left = opt.left;
}
var style = window.getComputedStyle(el);
var ppfx = this.ppfx;
var stateVar = state + 'State';
var method = this.getOffsetMethod(state);
// @ts-ignore
var offsetViewer = canvas[method](opts.view);
const style = window.getComputedStyle(el);
const ppfx = this.ppfx;
const stateVar = `${state}State`;
const method = this.getOffsetMethod(state);
const offsetViewer = (canvas as any)[method](opts.view);
offsetViewer.style.opacity = '';
let marginT = this['marginT' + state];
let marginB = this['marginB' + state];
let marginL = this['marginL' + state];
let marginR = this['marginR' + state];
let padT = this['padT' + state];
let padB = this['padB' + state];
let padL = this['padL' + state];
let padR = this['padR' + state];
let marginT = this[`marginT${state}`];
let marginB = this[`marginB${state}`];
let marginL = this[`marginL${state}`];
let marginR = this[`marginR${state}`];
let padT = this[`padT${state}`];
let padB = this[`padB${state}`];
let padL = this[`padL${state}`];
let padR = this[`padR${state}`];
if (offsetViewer.childNodes.length) {
this[stateVar] = '1';
@ -65,15 +78,15 @@ export default {
}
if (!this[stateVar]) {
var stateLow = state.toLowerCase();
var marginName = stateLow + 'margin-v';
var paddingName = stateLow + 'padding-v';
var marginV = $(`<div class="${ppfx}marginName">`).get(0) as HTMLElement;
var paddingV = $(`<div class="${ppfx}paddingName">`).get(0) as HTMLElement;
var marginEls = ppfx + marginName + '-el';
var paddingEls = ppfx + paddingName + '-el';
const fullMargName = `${marginEls} ${ppfx + marginName}`;
const fullPadName = `${paddingEls} ${ppfx + paddingName}`;
const stateLow = state.toLowerCase();
const marginName = `${stateLow}margin-v`;
const paddingName = `${stateLow}padding-v`;
const marginV = $(`<div class="${ppfx}marginName">`).get(0) as HTMLElement;
const paddingV = $(`<div class="${ppfx}paddingName">`).get(0) as HTMLElement;
const marginEls = `${ppfx}${marginName}-el`;
const paddingEls = `${ppfx}${paddingName}-el`;
const fullMargName = `${marginEls} ${ppfx}${marginName}`;
const fullPadName = `${paddingEls} ${ppfx}${paddingName}`;
marginT = $(`<div class="${fullMargName}-top"></div>`).get(0);
marginB = $(`<div class="${fullMargName}-bottom"></div>`).get(0);
marginL = $(`<div class="${fullMargName}-left"></div>`).get(0);
@ -82,14 +95,14 @@ export default {
padB = $(`<div class="${fullPadName}-bottom"></div>`).get(0);
padL = $(`<div class="${fullPadName}-left"></div>`).get(0);
padR = $(`<div class="${fullPadName}-right"></div>`).get(0);
this['marginT' + state] = marginT;
this['marginB' + state] = marginB;
this['marginL' + state] = marginL;
this['marginR' + state] = marginR;
this['padT' + state] = padT;
this['padB' + state] = padB;
this['padL' + state] = padL;
this['padR' + state] = padR;
this[`marginT${state}`] = marginT;
this[`marginB${state}`] = marginB;
this[`marginL${state}`] = marginL;
this[`marginR${state}`] = marginR;
this[`padT${state}`] = padT;
this[`padB${state}`] = padB;
this[`padL${state}`] = padL;
this[`padR${state}`] = padR;
marginV.appendChild(marginT);
marginV.appendChild(marginB);
marginV.appendChild(marginL);
@ -103,23 +116,22 @@ export default {
this[stateVar] = '1';
}
var unit = 'px';
var marginLeftSt = parseFloat(style.marginLeft.replace(unit, '')) * zoom;
var marginRightSt = parseFloat(style.marginRight.replace(unit, '')) * zoom;
var marginTopSt = parseFloat(style.marginTop.replace(unit, '')) * zoom;
var marginBottomSt = parseFloat(style.marginBottom.replace(unit, '')) * zoom;
var mtStyle = marginT.style;
var mbStyle = marginB.style;
var mlStyle = marginL.style;
var mrStyle = marginR.style;
var ptStyle = padT.style;
var pbStyle = padB.style;
var plStyle = padL.style;
var prStyle = padR.style;
var posLeft = parseFloat(pos.left);
var widthEl = parseFloat(style.width) * zoom + unit;
// Margin style
const unit = 'px';
const marginLeftSt = parseFloat(style.marginLeft.replace(unit, '')) * zoom;
const marginRightSt = parseFloat(style.marginRight.replace(unit, '')) * zoom;
const marginTopSt = parseFloat(style.marginTop.replace(unit, '')) * zoom;
const marginBottomSt = parseFloat(style.marginBottom.replace(unit, '')) * zoom;
const mtStyle = marginT.style;
const mbStyle = marginB.style;
const mlStyle = marginL.style;
const mrStyle = marginR.style;
const ptStyle = padT.style;
const pbStyle = padB.style;
const plStyle = padL.style;
const prStyle = padR.style;
const posLeft = parseFloat(pos.left);
const widthEl = parseFloat(style.width) * zoom + unit;
mtStyle.height = marginTopSt + unit;
mtStyle.width = widthEl;
mtStyle.top = pos.top - marginTopSt + unit;
@ -130,8 +142,8 @@ export default {
mbStyle.top = pos.top + pos.height + unit;
mbStyle.left = posLeft + unit;
var marginSideH = pos.height + marginTopSt + marginBottomSt + unit;
var marginSideT = pos.top - marginTopSt + unit;
const marginSideH = pos.height + marginTopSt + marginBottomSt + unit;
const marginSideT = pos.top - marginTopSt + unit;
mlStyle.height = marginSideH;
mlStyle.width = marginLeftSt + unit;
mlStyle.top = marginSideT;
@ -142,43 +154,31 @@ export default {
mrStyle.top = marginSideT;
mrStyle.left = posLeft + pos.width + unit;
// Padding style
var padTop = parseFloat(style.paddingTop) * zoom;
const padTop = parseFloat(style.paddingTop) * zoom;
ptStyle.height = padTop + unit;
// ptStyle.width = widthEl;
// ptStyle.top = pos.top + unit;
// ptStyle.left = posLeft + unit;
var padBot = parseFloat(style.paddingBottom) * zoom;
const padBot = parseFloat(style.paddingBottom) * zoom;
pbStyle.height = padBot + unit;
// pbStyle.width = widthEl;
// pbStyle.top = pos.top + pos.height - padBot + unit;
// pbStyle.left = posLeft + unit;
var padSideH = pos.height - padBot - padTop + unit;
var padSideT = pos.top + padTop + unit;
const padSideH = pos.height - padBot - padTop + unit;
const padSideT = pos.top + padTop + unit;
plStyle.height = padSideH;
plStyle.width = parseFloat(style.paddingLeft) * zoom + unit;
plStyle.top = padSideT;
// plStyle.left = pos.left + unit;
// plStyle.right = 0;
var padRight = parseFloat(style.paddingRight) * zoom;
const padRight = parseFloat(style.paddingRight) * zoom;
prStyle.height = padSideH;
prStyle.width = padRight + unit;
prStyle.top = padSideT;
// prStyle.left = pos.left + pos.width - padRight + unit;
// prStyle.left = 0;
},
stop(editor, sender, opts = {}) {
var opt = opts || {};
var state = opt.state || '';
var method = this.getOffsetMethod(state);
}
stop(editor: Editor, sender: any, opts: any = {}) {
const opt = opts || {};
const state = opt.state || '';
const method = this.getOffsetMethod(state);
const { view } = opts;
const canvas = this.canvas;
// @ts-ignore
var offsetViewer = canvas[method](view);
const { canvas } = this;
const offsetViewer = (canvas as any)[method](view);
offsetViewer.style.opacity = 0;
},
} as CommandObject<any, { [k: string]: any }>;
}
}

38
packages/core/src/commands/view/SwitchVisibility.ts

@ -1,21 +1,32 @@
import { bindAll } from 'underscore';
import Frame from '../../canvas/model/Frame';
import Editor from '../../editor';
import { CommandObject } from './CommandAbstract';
import { isDef } from '../../utils/mixins';
import type { CommandPublicFnFromHandler } from '../registryHelpers';
import CommandAbstract from './CommandAbstract';
export default {
export interface SwitchVisibilityCommandRegistryRun {
'core:component-outline': CommandPublicFnFromHandler<CommandSwitchVisibility['run']>;
'sw-visibility': CommandPublicFnFromHandler<CommandSwitchVisibility['run']>;
}
export interface SwitchVisibilityCommandRegistryStop {
'core:component-outline': CommandPublicFnFromHandler<CommandSwitchVisibility['stop']>;
'sw-visibility': CommandPublicFnFromHandler<CommandSwitchVisibility['stop']>;
}
export default class CommandSwitchVisibility extends CommandAbstract {
init() {
bindAll(this, '_onFramesChange');
},
}
run(ed) {
run(ed: Editor) {
this.toggleVis(ed, true);
},
}
stop(ed) {
stop(ed: Editor) {
this.toggleVis(ed, false);
},
}
toggleVis(ed: Editor, active = true) {
if (!ed.Commands.isActive('preview')) {
@ -25,7 +36,7 @@ export default {
canvasModel[mth]('change:frames', this._onFramesChange);
this.handleFrames(cv.getFrames(), active);
}
},
}
handleFrames(frames: Frame[], active?: boolean) {
frames.forEach((frame: Frame & { __ol?: boolean }) => {
@ -36,11 +47,11 @@ export default {
frame.__ol = true;
}
});
},
}
_onFramesChange(_: any, frames: Frame[]) {
this.handleFrames(frames);
},
}
_upFrame(frame: Frame, active?: boolean) {
const { ppfx, em, id } = this;
@ -48,10 +59,5 @@ export default {
const method = isActive ? 'add' : 'remove';
const cls = `${ppfx}dashed`;
frame.view?.getBody().classList[method](cls);
},
} as CommandObject<
{},
{
[key: string]: any;
}
>;
}

9
packages/core/src/editor/index.ts

@ -48,6 +48,7 @@ import BlockManager from '../block_manager';
import CanvasModule from '../canvas';
import CodeManagerModule from '../code_manager';
import CommandsModule from '../commands';
import type { CommandRunArgs, CommandRunResult, CommandStopArgs, CommandStopResult } from '../commands/registry';
import { AddOptions, EventHandler } from '../common';
import CssComposer from '../css_composer';
import CssRule from '../css_composer/model/CssRule';
@ -498,8 +499,8 @@ export default class Editor implements IBaseModule<EditorConfig> {
* @example
* editor.runCommand('myCommand', {someValue: 1});
*/
runCommand(id: string, options: Record<string, unknown> = {}) {
return this.Commands.run(id, options);
runCommand<const TId extends string>(id: TId, ...args: CommandRunArgs<TId>): CommandRunResult<TId> {
return this.Commands.run(id, ...(args as any)) as CommandRunResult<TId>;
}
/**
@ -510,8 +511,8 @@ export default class Editor implements IBaseModule<EditorConfig> {
* @example
* editor.stopCommand('myCommand', {someValue: 1});
*/
stopCommand(id: string, options: Record<string, unknown> = {}) {
return this.Commands.stop(id, options);
stopCommand<const TId extends string>(id: TId, ...args: CommandStopArgs<TId>): CommandStopResult<TId> {
return this.Commands.stop(id, ...(args as any)) as CommandStopResult<TId>;
}
/**

1
packages/core/src/index.ts

@ -98,6 +98,7 @@ export const grapesjs = {
*/
export type { CategoryProperties as BlockCategoryProperties } from './abstract/ModuleCategory';
export type { ComponentDragEventProps } from './commands/view/ComponentDrag';
export type { CommandRegistryRun, CommandRegistryStop } from './commands/registry';
// Exports for TS
export type { default as Asset } from './asset_manager/model/Asset';

118
packages/core/test/specs/commands/index.ts

@ -1,3 +1,4 @@
import type { Editor } from '../../../src';
import EditorModel from '../../../src/editor/model/Editor';
import type Commands from '../../../src/commands';
import type { Command, CommandFunction, CommandOptions } from '../../../src/commands/view/CommandAbstract';
@ -41,7 +42,31 @@ describe('Commands', () => {
obj.add('test', comm);
expect(obj.has('test')).toBe(true);
expect(Object.keys(obj.getAll()).length).toBe(len + 1);
expect(obj.get('test')!.test).toEqual('test');
expect((obj.get('test') as any).test).toEqual('test');
});
test('Remove command', () => {
obj.add('test', commSimple);
obj.remove('test');
expect(obj.has('test')).toBe(false);
expect(obj.getAll().test).toBeUndefined();
});
test('Remove active command and clean up active state', () => {
const stop = jest.fn(() => commResultStop);
obj.add(commName, {
run: () => commResultRun,
stop,
});
obj.run(commName);
obj.remove(commName);
expect(stop).toHaveBeenCalledTimes(1);
expect(obj.isActive(commName)).toBe(false);
expect(obj.has(commName)).toBe(false);
});
test('Default commands after loadDefaultCommands', () => {
@ -147,3 +172,94 @@ describe('Commands', () => {
});
});
});
interface MyCommandOptions {
value: number;
}
interface MyCommandResult {
done: boolean;
}
interface MyCommandStopOptions {
reason: string;
}
declare module '../../../src' {
interface CommandRegistryRun {
'my:command': (options: MyCommandOptions) => MyCommandResult;
'my:stateless': () => number;
}
interface CommandRegistryStop {
'my:command': (options: MyCommandStopOptions) => void;
}
}
const assertCommandTypes = () => {
const typedEditor = {} as Editor;
const fullscreenResult: void = typedEditor.runCommand('core:fullscreen');
typedEditor.runCommand('fullscreen', { target: document.body });
// @ts-expect-error Fullscreen target must be an element or selector string
typedEditor.runCommand('core:fullscreen', { target: 1 });
const customResult: MyCommandResult = typedEditor.runCommand('my:command', { value: 1 });
customResult.done;
typedEditor.stopCommand('my:command', { reason: 'done' });
// @ts-expect-error Missing required run options
typedEditor.runCommand('my:command');
// @ts-expect-error Stop options do not match the registry
typedEditor.stopCommand('my:command', { value: 1 });
const statelessResult: number = typedEditor.runCommand('my:stateless');
statelessResult.toFixed();
// @ts-expect-error Stateless commands should not accept options
typedEditor.runCommand('my:stateless', {});
typedEditor.Commands.add('my:command', {
run(_editor, _sender, options) {
options.value.toFixed();
return { done: true };
},
stop(_editor, _sender, options) {
options.reason.toUpperCase();
},
});
typedEditor.Commands.add('my:command', (_editor, _sender, options) => {
options.value.toFixed();
return { done: true };
});
typedEditor.Commands.add('my:stateless', () => 1);
typedEditor.Commands.add('my:command', {
run(_editor, _sender, options) {
// @ts-expect-error The command run options should come from the registry
options.reason.toUpperCase();
return { done: true };
},
});
typedEditor.Commands.config.defaultOptions = {
'my:command': {
run(options) {
return { ...options, value: options.value + 1 };
},
stop(options) {
return { ...options, reason: options.reason.toUpperCase() };
},
},
'core:fullscreen': {
run(options) {
return { ...options, target: options?.target ?? '.app' };
},
},
};
typedEditor.Commands.config.defaultOptions['my:command']?.run?.({ value: 1 });
return fullscreenResult;
};
void assertCommandTypes;

16
packages/core/test/specs/commands/view/CanvasClear.ts

@ -0,0 +1,16 @@
import CanvasClear from '../../../../src/commands/view/CanvasClear';
describe('CanvasClear command', () => {
test('should clear components and css', () => {
const command = new CanvasClear({});
const editor = {
Components: { clear: jest.fn() },
Css: { clear: jest.fn() },
};
command.run(editor);
expect(editor.Components.clear).toHaveBeenCalledTimes(1);
expect(editor.Css.clear).toHaveBeenCalledTimes(1);
});
});

24
packages/core/test/specs/commands/view/CanvasMove.ts

@ -0,0 +1,24 @@
import CanvasMove from '../../../../src/commands/view/CanvasMove';
describe('CanvasMove command', () => {
test('stop should toggle move off and disable the dragger', () => {
const command = new CanvasMove({});
command.toggleMove = jest.fn();
command.disableDragger = jest.fn();
command.stop();
expect(command.toggleMove).toHaveBeenCalledWith();
expect(command.disableDragger).toHaveBeenCalledWith(expect.any(MouseEvent));
});
test('onKeyUp should stop the command on space key', () => {
const command = new CanvasMove({});
command.editor = { stopCommand: jest.fn() } as any;
command.id = 'core:canvas-move';
command.onKeyUp({ which: 32 } as KeyboardEvent);
expect(command.editor.stopCommand).toHaveBeenCalledWith('core:canvas-move');
});
});

58
packages/core/test/specs/commands/view/ComponentDelete.ts

@ -0,0 +1,58 @@
import ComponentDelete from '../../../../src/commands/view/ComponentDelete';
describe('ComponentDelete command', () => {
test('should remove selected removable components', () => {
const command = new ComponentDelete({ em: { logWarning: jest.fn() } });
const componentA = { get: jest.fn(() => true), remove: jest.fn() };
const componentB = { get: jest.fn(() => true), remove: jest.fn() };
const editor = {
getSelectedAll: jest.fn(() => [componentA, componentB]),
selectRemove: jest.fn(),
};
const result = command.run(editor as any, null, {});
expect(componentA.remove).toHaveBeenCalledTimes(1);
expect(componentB.remove).toHaveBeenCalledTimes(1);
expect(editor.selectRemove).toHaveBeenCalledWith([componentA, componentB]);
expect(result).toEqual([componentA, componentB]);
});
test('should use delegated remove target when available', () => {
const command = new ComponentDelete({ em: { logWarning: jest.fn() } });
const delegated = { remove: jest.fn() };
const component = {
get: jest.fn(() => true),
delegate: {
remove: jest.fn(() => delegated),
},
};
const editor = {
getSelectedAll: jest.fn(() => [component]),
selectRemove: jest.fn(),
};
command.run(editor as any, null, {});
expect(component.delegate.remove).toHaveBeenCalledWith(component);
expect(delegated.remove).toHaveBeenCalledTimes(1);
expect(editor.selectRemove).toHaveBeenCalledWith([component]);
});
test('should warn and skip non-removable components', () => {
const logWarning = jest.fn();
const command = new ComponentDelete({ em: { logWarning } });
const component = { get: jest.fn(() => false), remove: jest.fn() };
const editor = {
getSelectedAll: jest.fn(() => [component]),
selectRemove: jest.fn(),
};
const result = command.run(editor as any, null, {});
expect(logWarning).toHaveBeenCalledWith('The element is not removable', { component });
expect(component.remove).not.toHaveBeenCalled();
expect(editor.selectRemove).toHaveBeenCalledWith([]);
expect(result).toEqual([]);
});
});

23
packages/core/test/specs/commands/view/ComponentDrag.ts

@ -0,0 +1,23 @@
import ComponentDrag from '../../../../src/commands/view/ComponentDrag';
describe('ComponentDrag command', () => {
test('getTranslate should extract axis values from transform', () => {
const command = new ComponentDrag({});
expect(command.getTranslate('translateX(10px) translateY(20px)')).toBe(10);
expect(command.getTranslate('translateX(10px) translateY(20px)', 'y')).toBe(20);
});
test('setTranslate should update and append translate values', () => {
const command = new ComponentDrag({});
expect(command.setTranslate('translateX(10px)', 'x', '15px')).toContain('translateX(15px)');
expect(command.setTranslate('translateX(10px)', 'y', '20px')).toContain('translateY(20px)');
});
test('run should require target option', () => {
const command = new ComponentDrag({});
expect(() => command.run({} as any, null, {} as any)).toThrow('Target option is required');
});
});

38
packages/core/test/specs/commands/view/ComponentEnter.ts

@ -0,0 +1,38 @@
import ComponentEnter from '../../../../src/commands/view/ComponentEnter';
describe('ComponentEnter command', () => {
test('should select the first selectable child', () => {
const command = new ComponentEnter({});
const firstSelectable = { id: 'child-1' };
const component = {
components: jest.fn(() => [
{ get: jest.fn(() => false) },
{ get: jest.fn(() => true), ...firstSelectable },
{ get: jest.fn(() => true) },
]),
};
const editor = {
Canvas: { hasFocus: jest.fn(() => true) },
getSelectedAll: jest.fn(() => [component]),
select: jest.fn(),
};
command.run(editor as any);
expect(editor.select).toHaveBeenCalledWith([expect.objectContaining(firstSelectable)]);
});
test('should do nothing if the canvas has no focus', () => {
const command = new ComponentEnter({});
const editor = {
Canvas: { hasFocus: jest.fn(() => false) },
getSelectedAll: jest.fn(),
select: jest.fn(),
};
command.run(editor as any);
expect(editor.getSelectedAll).not.toHaveBeenCalled();
expect(editor.select).not.toHaveBeenCalled();
});
});

61
packages/core/test/specs/commands/view/ComponentExit.ts

@ -0,0 +1,61 @@
import ComponentExit from '../../../../src/commands/view/ComponentExit';
describe('ComponentExit command', () => {
test('should select the first selectable parent', () => {
const command = new ComponentExit({});
const selectableParent = {
get: jest.fn(() => true),
parent: jest.fn(),
};
const nonSelectableParent = {
get: jest.fn(() => false),
parent: jest.fn(() => selectableParent),
};
const component = {
parent: jest.fn(() => nonSelectableParent),
};
const editor = {
Canvas: { hasFocus: jest.fn(() => true) },
getSelectedAll: jest.fn(() => [component]),
select: jest.fn(),
};
command.run(editor as any, null, {});
expect(editor.select).toHaveBeenCalledWith([selectableParent]);
});
test('should select parent when forced even without canvas focus', () => {
const command = new ComponentExit({});
const parent = {
get: jest.fn(() => true),
parent: jest.fn(),
};
const component = {
parent: jest.fn(() => parent),
};
const editor = {
Canvas: { hasFocus: jest.fn(() => false) },
getSelectedAll: jest.fn(() => [component]),
select: jest.fn(),
};
command.run(editor as any, null, { force: true });
expect(editor.select).toHaveBeenCalledWith([parent]);
});
test('should do nothing if the canvas has no focus and force is not set', () => {
const command = new ComponentExit({});
const editor = {
Canvas: { hasFocus: jest.fn(() => false) },
getSelectedAll: jest.fn(),
select: jest.fn(),
};
command.run(editor as any, null, {});
expect(editor.getSelectedAll).not.toHaveBeenCalled();
expect(editor.select).not.toHaveBeenCalled();
});
});

44
packages/core/test/specs/commands/view/ComponentNext.ts

@ -0,0 +1,44 @@
import ComponentNext from '../../../../src/commands/view/ComponentNext';
describe('ComponentNext command', () => {
test('should select the next selectable sibling', () => {
const command = new ComponentNext({});
const nextSelectable = { get: jest.fn(() => true) };
const notSelectable = { get: jest.fn(() => false) };
const parent = {
components: jest.fn(() => ({ length: 3 })),
getChildAt: jest.fn((index: number) => {
if (index === 1) return notSelectable;
if (index === 2) return nextSelectable;
return null;
}),
};
const selected = {
parent: jest.fn(() => parent),
index: jest.fn(() => 0),
};
const editor = {
Canvas: { hasFocus: jest.fn(() => true) },
getSelectedAll: jest.fn(() => [selected]),
select: jest.fn(),
};
command.run(editor as any);
expect(editor.select).toHaveBeenCalledWith([nextSelectable]);
});
test('should do nothing if the canvas has no focus', () => {
const command = new ComponentNext({});
const editor = {
Canvas: { hasFocus: jest.fn(() => false) },
getSelectedAll: jest.fn(),
select: jest.fn(),
};
command.run(editor as any);
expect(editor.getSelectedAll).not.toHaveBeenCalled();
expect(editor.select).not.toHaveBeenCalled();
});
});

43
packages/core/test/specs/commands/view/ComponentPrev.ts

@ -0,0 +1,43 @@
import ComponentPrev from '../../../../src/commands/view/ComponentPrev';
describe('ComponentPrev command', () => {
test('should select the previous selectable sibling', () => {
const command = new ComponentPrev({});
const prevSelectable = { get: jest.fn(() => true) };
const notSelectable = { get: jest.fn(() => false) };
const parent = {
getChildAt: jest.fn((index: number) => {
if (index === 1) return notSelectable;
if (index === 0) return prevSelectable;
return null;
}),
};
const selected = {
parent: jest.fn(() => parent),
index: jest.fn(() => 2),
};
const editor = {
Canvas: { hasFocus: jest.fn(() => true) },
getSelectedAll: jest.fn(() => [selected]),
select: jest.fn(),
};
command.run(editor as any);
expect(editor.select).toHaveBeenCalledWith([prevSelectable]);
});
test('should do nothing if the canvas has no focus', () => {
const command = new ComponentPrev({});
const editor = {
Canvas: { hasFocus: jest.fn(() => false) },
getSelectedAll: jest.fn(),
select: jest.fn(),
};
command.run(editor as any);
expect(editor.getSelectedAll).not.toHaveBeenCalled();
expect(editor.select).not.toHaveBeenCalled();
});
});

76
packages/core/test/specs/commands/view/ComponentStyleClear.ts

@ -0,0 +1,76 @@
import ComponentStyleClear from '../../../../src/commands/view/ComponentStyleClear';
describe('ComponentStyleClear command', () => {
test('should remove component style rules when no components of that type remain', () => {
const command = new ComponentStyleClear({});
const ruleA = { get: jest.fn(() => 'cmp:text') };
const ruleB = { get: jest.fn(() => 'cmp:image') };
const rules = {
filter: jest.fn((predicate: any) => [ruleA, ruleB].filter(predicate)),
remove: jest.fn(),
};
const target = {
get: jest.fn((key: string) => {
if (key === 'styles') return true;
if (key === 'type') return 'text';
}),
};
const editor = {
Pages: {
getAllWrappers: jest.fn(() => [{ findType: jest.fn(() => []) }]),
},
CssComposer: {
getAll: jest.fn(() => rules),
},
};
const result = command.run(editor as any, null, { target } as any);
expect(rules.remove).toHaveBeenCalledWith([ruleA]);
expect(result).toEqual([ruleA]);
});
test('should return empty array when target has no styles', () => {
const command = new ComponentStyleClear({});
const target = {
get: jest.fn((key: string) => (key === 'styles' ? false : 'text')),
};
const editor = {
Pages: { getAllWrappers: jest.fn() },
CssComposer: { getAll: jest.fn() },
};
const result = command.run(editor as any, null, { target } as any);
expect(editor.Pages.getAllWrappers).not.toHaveBeenCalled();
expect(result).toEqual([]);
});
test('should keep rules when components of that type still exist', () => {
const command = new ComponentStyleClear({});
const rules = {
filter: jest.fn(),
remove: jest.fn(),
};
const target = {
get: jest.fn((key: string) => {
if (key === 'styles') return true;
if (key === 'type') return 'text';
}),
};
const editor = {
Pages: {
getAllWrappers: jest.fn(() => [{ findType: jest.fn(() => [{}]) }]),
},
CssComposer: {
getAll: jest.fn(() => rules),
},
};
const result = command.run(editor as any, null, { target } as any);
expect(editor.CssComposer.getAll).not.toHaveBeenCalled();
expect(rules.remove).not.toHaveBeenCalled();
expect(result).toEqual([]);
});
});

37
packages/core/test/specs/commands/view/CopyComponent.ts

@ -0,0 +1,37 @@
import CopyComponent from '../../../../src/commands/view/CopyComponent';
describe('CopyComponent command', () => {
test('should set the clipboard with selected components', () => {
const command = new CopyComponent({});
const set = jest.fn();
const selected = [{ id: 'cmp-1' }, { id: 'cmp-2' }];
const editor = {
getModel: jest.fn(() => ({ set })),
getSelectedAll: jest.fn(() => selected),
};
command.run(editor as any);
expect(set).toHaveBeenCalledWith('clipboard', selected);
});
test('should use delegated copy target when available', () => {
const command = new CopyComponent({});
const set = jest.fn();
const delegated = { id: 'delegated' };
const component = {
delegate: {
copy: jest.fn(() => delegated),
},
};
const editor = {
getModel: jest.fn(() => ({ set })),
getSelectedAll: jest.fn(() => [component]),
};
command.run(editor as any);
expect(component.delegate.copy).toHaveBeenCalledWith(component);
expect(set).toHaveBeenCalledWith('clipboard', [delegated]);
});
});

59
packages/core/test/specs/commands/view/ExportTemplate.ts

@ -0,0 +1,59 @@
import ExportTemplate from '../../../../src/commands/view/ExportTemplate';
describe('ExportTemplate command', () => {
test('should open modal and update editors content', () => {
const command = new ExportTemplate({});
const once = jest.fn();
const open = jest.fn(() => ({
getModel: jest.fn(() => ({
once,
})),
}));
const htmlSetContent = jest.fn();
const cssSetContent = jest.fn();
const createViewer = jest
.fn()
.mockImplementationOnce(() => ({ setContent: htmlSetContent }))
.mockImplementationOnce(() => ({ setContent: cssSetContent }));
const render = jest.fn(function (this: any) {
return { el: document.createElement('div') };
});
const EditorView = jest.fn(() => ({ render }));
command.em = {
CodeManager: {
createViewer,
getConfig: jest.fn(() => ({})),
EditorView,
},
} as any;
command.id = 'core:open-code';
const sender = { set: jest.fn() };
const editor = {
getConfig: jest.fn(() => ({ stylePrefix: 'gjs-', textViewCode: 'Code' })),
Modal: { open },
CodeManager: {},
getHtml: jest.fn(() => '<div>HTML</div>'),
getCss: jest.fn(() => '.cls{}'),
stopCommand: jest.fn(),
} as any;
command.run(editor, sender, {});
expect(sender.set).toHaveBeenCalledWith('active', 0);
expect(open).toHaveBeenCalled();
expect(htmlSetContent).toHaveBeenCalledWith('<div>HTML</div>');
expect(cssSetContent).toHaveBeenCalledWith('.cls{}');
expect(once).toHaveBeenCalledWith('change:open', expect.any(Function));
});
test('stop should close the modal', () => {
const command = new ExportTemplate({});
const close = jest.fn();
command.stop({ Modal: { close } } as any);
expect(close).toHaveBeenCalledTimes(1);
});
});

63
packages/core/test/specs/commands/view/Fullscreen.ts

@ -0,0 +1,63 @@
import CommandFullscreen from '../../../../src/commands/view/Fullscreen';
import Editor from '../../../../src/editor';
describe('Fullscreen command', () => {
let editor: Editor;
let container: HTMLElement;
let requestFullscreen: jest.Mock<Promise<void>, []>;
let isFullscreen: boolean;
let exitFullscreen: jest.Mock;
beforeEach(() => {
isFullscreen = false;
requestFullscreen = jest.fn(async () => {
isFullscreen = true;
});
container = document.createElement('div');
Object.defineProperty(container, 'requestFullscreen', {
configurable: true,
value: requestFullscreen,
});
exitFullscreen = jest.fn(() => {
isFullscreen = false;
});
Object.defineProperty(document, 'fullscreenElement', {
configurable: true,
get: () => (isFullscreen ? container : null),
});
Object.defineProperty(document, 'exitFullscreen', {
configurable: true,
value: exitFullscreen,
});
editor = new Editor({ el: container } as any);
});
afterEach(() => {
editor.destroy();
});
test('runs from canonical and legacy ids', () => {
const addSpy = jest.spyOn(document, 'addEventListener');
const removeSpy = jest.spyOn(document, 'removeEventListener');
expect(editor.Commands.get('core:fullscreen')).toBeInstanceOf(CommandFullscreen);
expect(editor.Commands.get('fullscreen')).toBeInstanceOf(CommandFullscreen);
editor.runCommand('core:fullscreen');
expect(requestFullscreen).toHaveBeenCalledTimes(1);
expect(addSpy).toHaveBeenCalledWith('fullscreenchange', expect.any(Function));
editor.stopCommand('core:fullscreen');
expect(exitFullscreen).toHaveBeenCalledTimes(1);
expect(removeSpy).toHaveBeenCalledWith('fullscreenchange', expect.any(Function));
editor.runCommand('fullscreen', { target: container });
expect(requestFullscreen).toHaveBeenCalledTimes(2);
editor.stopCommand('fullscreen');
expect(exitFullscreen).toHaveBeenCalledTimes(2);
});
});

42
packages/core/test/specs/commands/view/MoveComponent.ts

@ -0,0 +1,42 @@
import MoveComponent from '../../../../src/commands/view/MoveComponent';
describe('MoveComponent command', () => {
test('rollback should cancel drag on escape', () => {
const command = new MoveComponent({});
const cancelDrag = jest.fn();
command.sorter = { cancelDrag };
command.rollback({ which: 27 }, false);
expect(cancelDrag).toHaveBeenCalledTimes(1);
});
test('stop should reset wrapper and remove helper classes', () => {
const command = new MoveComponent({});
const wrapper: any = {};
wrapper.css = jest.fn(() => wrapper);
wrapper.unbind = jest.fn(() => wrapper);
wrapper.removeClass = jest.fn(() => wrapper);
const removeBadgeClass = jest.fn();
const removeHighlighterClass = jest.fn();
command.$wrapper = wrapper;
command.$badge = { removeClass: removeBadgeClass };
command.$hl = { removeClass: removeHighlighterClass };
command.badgeClass = 'badge-warning';
command.hoverClass = 'highlighter-warning';
command.noSelClass = 'no-select';
command.onHovered = jest.fn();
command.stopSelectComponent = jest.fn();
command.em = { setSelected: jest.fn() } as any;
command.toggleToolsEl = jest.fn();
command.editor = { stopCommand: jest.fn() };
command.stop();
expect(removeBadgeClass).toHaveBeenCalledWith('badge-warning');
expect(removeHighlighterClass).toHaveBeenCalledWith('highlighter-warning');
expect(wrapper.css).toHaveBeenCalledWith('cursor', '');
expect(wrapper.unbind).toHaveBeenCalledTimes(1);
expect(wrapper.removeClass).toHaveBeenCalledWith('no-select');
});
});

39
packages/core/test/specs/commands/view/OpenAssets.ts

@ -0,0 +1,39 @@
import OpenAssets from '../../../../src/commands/view/OpenAssets';
describe('OpenAssets command', () => {
test('open should open the modal', () => {
const onceClose = jest.fn();
const command = new OpenAssets({});
const editor = {
Modal: {
open: jest.fn(() => ({ onceClose })),
},
stopCommand: jest.fn(),
} as any;
command.editor = editor;
command.am = { __customData: jest.fn() };
command.config = { custom: false };
command.title = 'Assets';
command.open('content');
expect(editor.Modal.open).toHaveBeenCalledWith({ title: 'Assets', content: 'content' });
expect(onceClose).toHaveBeenCalledTimes(1);
});
test('stop should close the modal', () => {
const command = new OpenAssets({});
const editor = {
Modal: {
close: jest.fn(),
},
} as any;
command.editor = editor;
command.am = { __customData: jest.fn() };
command.config = { custom: false };
command.stop(editor);
expect(editor.Modal.close).toHaveBeenCalledTimes(1);
});
});

33
packages/core/test/specs/commands/view/OpenBlocks.ts

@ -0,0 +1,33 @@
import OpenBlocks from '../../../../src/commands/view/OpenBlocks';
describe('OpenBlocks command', () => {
test('open should show the container', () => {
const command = new OpenBlocks({});
const panels = {
getPanel: jest.fn(() => ({ set: jest.fn(() => ({ trigger: jest.fn() })) })),
addPanel: jest.fn(),
};
command.container = document.createElement('div');
command.editor = { Panels: panels } as any;
command.bm = { render: jest.fn(() => document.createElement('div')) };
command.config = { custom: false, appendTo: '' };
command.firstRender = false;
command.open();
expect(command.container.style.display).toBe('block');
});
test('stop should hide the container', () => {
const command = new OpenBlocks({});
command.container = document.createElement('div');
command.container.style.display = 'block';
command.bm = { __customData: jest.fn() };
command.config = { custom: false };
command.stop();
expect(command.container.style.display).toBe('none');
});
});

35
packages/core/test/specs/commands/view/OpenLayers.ts

@ -0,0 +1,35 @@
import OpenLayers from '../../../../src/commands/view/OpenLayers';
describe('OpenLayers command', () => {
test('should show layers container when opened', () => {
const command = new OpenLayers({});
const panelTrigger = jest.fn();
const panel = { set: jest.fn(() => ({ trigger: panelTrigger })) };
const render = jest.fn(() => document.createElement('div'));
const editor = {
LayerManager: {
getConfig: jest.fn(() => ({})),
render,
},
Panels: {
getPanel: jest.fn(() => panel),
addPanel: jest.fn(),
},
} as any;
command.run(editor);
expect(panel.set).toHaveBeenCalled();
expect(command.layers?.style.display).toBe('block');
});
test('stop should hide layers container', () => {
const command = new OpenLayers({});
command.layers = document.createElement('div');
command.layers.style.display = 'block';
command.stop();
expect(command.layers.style.display).toBe('none');
});
});

27
packages/core/test/specs/commands/view/OpenStyleManager.ts

@ -0,0 +1,27 @@
import OpenStyleManager from '../../../../src/commands/view/OpenStyleManager';
describe('OpenStyleManager command', () => {
test('toggleSm should show content when a target is selected', () => {
const command = new OpenStyleManager({});
command.sender = { get: jest.fn(() => true) };
command.sm = { getSelected: jest.fn(() => true) };
command.$cntInner = { show: jest.fn(), hide: jest.fn() };
command.$header = { show: jest.fn(), hide: jest.fn() };
command.toggleSm();
expect(command.$cntInner.show).toHaveBeenCalledTimes(1);
expect(command.$header.hide).toHaveBeenCalledTimes(1);
});
test('stop should hide content and header', () => {
const command = new OpenStyleManager({});
command.$cntInner = { hide: jest.fn() };
command.$header = { hide: jest.fn() };
command.stop();
expect(command.$cntInner.hide).toHaveBeenCalledTimes(1);
expect(command.$header.hide).toHaveBeenCalledTimes(1);
});
});

27
packages/core/test/specs/commands/view/OpenTraitManager.ts

@ -0,0 +1,27 @@
import OpenTraitManager from '../../../../src/commands/view/OpenTraitManager';
describe('OpenTraitManager command', () => {
test('toggleTm should show content when a single target is selected', () => {
const command = new OpenTraitManager({});
command.sender = { get: jest.fn(() => true) };
command.target = { getSelectedAll: jest.fn(() => [{}]) };
command.$cn2 = { show: jest.fn(), hide: jest.fn() };
command.$header = { show: jest.fn(), hide: jest.fn() };
command.toggleTm();
expect(command.$cn2.show).toHaveBeenCalledTimes(1);
expect(command.$header.hide).toHaveBeenCalledTimes(1);
});
test('stop should hide content and header', () => {
const command = new OpenTraitManager({});
command.$cn2 = { hide: jest.fn() };
command.$header = { hide: jest.fn() };
command.stop();
expect(command.$cn2.hide).toHaveBeenCalledTimes(1);
expect(command.$header.hide).toHaveBeenCalledTimes(1);
});
});

53
packages/core/test/specs/commands/view/PasteComponent.ts

@ -0,0 +1,53 @@
import { ComponentsEvents } from '../../../../src/dom_components/types';
import PasteComponent from '../../../../src/commands/view/PasteComponent';
describe('PasteComponent command', () => {
test('should paste a clone into the selected collection and emit paste event', () => {
const command = new PasteComponent({});
const added = { id: 'added' };
const collection = {
add: jest.fn(() => added),
};
const selected = {
collection,
index: jest.fn(() => 2),
get: jest.fn((key: string) => (key === 'copyable' ? true : undefined)),
clone: jest.fn(() => ({ id: 'clone' })),
parent: jest.fn(),
};
const clipboard = [selected];
const emitUpdate = jest.fn();
const trigger = jest.fn();
const editor = {
getModel: jest.fn(() => ({
get: jest.fn((key: string) => (key === 'clipboard' ? clipboard : undefined)),
})),
getSelected: jest.fn(() => ({ emitUpdate })),
getSelectedAll: jest.fn(() => [selected]),
trigger,
} as any;
command.run(editor, null, { action: 'clone-component' });
expect(collection.add).toHaveBeenCalled();
expect(trigger).toHaveBeenCalledWith(ComponentsEvents.paste, added);
expect(emitUpdate).toHaveBeenCalledTimes(1);
});
test('should do nothing without clipboard content or selection', () => {
const command = new PasteComponent({});
const trigger = jest.fn();
const editor = {
getModel: jest.fn(() => ({
get: jest.fn(() => null),
})),
getSelected: jest.fn(() => null),
getSelectedAll: jest.fn(() => []),
trigger,
} as any;
command.run(editor, null);
expect(trigger).not.toHaveBeenCalled();
});
});

53
packages/core/test/specs/commands/view/Preview.ts

@ -2,12 +2,27 @@ import Panel from '../../../../src/panels/model/Panel';
import Preview from '../../../../src/commands/view/Preview';
describe('Preview command', () => {
let command: Preview;
let fakePanels: Panel[];
let fakeEditor: any;
let fakeIsActive: any;
const obj: any = {};
beforeEach(() => {
command = new Preview({});
command.ppfx = '';
command.em = {
Canvas: {
getBody: jest.fn(() => ({
querySelectorAll: jest.fn(() => []),
})),
getToolbarEl: jest.fn(() => ({
style: {},
})),
},
on: jest.fn(),
off: jest.fn(),
} as any;
fakePanels = [new Panel(obj, obj), new Panel(obj, obj), new Panel(obj, obj)];
fakeIsActive = false;
@ -44,63 +59,63 @@ describe('Preview command', () => {
},
};
Preview.panels = undefined;
Preview.shouldRunSwVisibility = undefined;
command.panels = undefined;
command.shouldRunSwVisibility = undefined;
});
describe('.getPanels', () => {
test('it should return panels set with the editor panels if not already set', () => {
Preview.getPanels(fakeEditor);
expect(Preview.panels).toBe(fakePanels);
Preview.getPanels(fakeEditor);
command.getPanels(fakeEditor);
expect(command.panels).toBe(fakePanels);
command.getPanels(fakeEditor);
expect(fakeEditor.Panels.getPanels).toHaveBeenCalledTimes(1);
});
});
describe('.run', () => {
beforeEach(() => {
Preview.helper = { style: {} };
command.helper = { style: {} } as any;
});
it('should hide all panels', () => {
fakePanels.forEach((panel) => expect(panel.get('visible')).toEqual(true));
Preview.run!(fakeEditor, obj, obj);
command.run(fakeEditor, obj);
fakePanels.forEach((panel) => expect(panel.get('visible')).toEqual(false));
});
it("should stop the 'core:component-outline' command if active", () => {
Preview.run!(fakeEditor, obj, obj);
command.run(fakeEditor, obj);
expect(fakeEditor.stopCommand).not.toHaveBeenCalled();
fakeIsActive = true;
Preview.run!(fakeEditor, obj, obj);
command.run(fakeEditor, obj);
expect(fakeEditor.stopCommand).toHaveBeenCalledWith('core:component-outline');
});
it('should not reset the `shouldRunSwVisibility` state once active if run multiple times', () => {
expect(Preview.shouldRunSwVisibility).toBeUndefined();
expect(command.shouldRunSwVisibility).toBeUndefined();
fakeIsActive = true;
Preview.run!(fakeEditor, obj, obj);
expect(Preview.shouldRunSwVisibility).toEqual(true);
command.run(fakeEditor, obj);
expect(command.shouldRunSwVisibility).toEqual(true);
fakeIsActive = false;
Preview.run!(fakeEditor, obj, obj);
expect(Preview.shouldRunSwVisibility).toEqual(true);
command.run(fakeEditor, obj);
expect(command.shouldRunSwVisibility).toEqual(true);
});
});
describe('.stop', () => {
it('should show all panels', () => {
fakePanels.forEach((panel) => panel.set('visible', false));
Preview.stop!(fakeEditor, obj, obj);
command.stop(fakeEditor);
fakePanels.forEach((panel) => expect(panel.get('visible')).toEqual(true));
});
it("should run the 'core:component-outline' command if it was active before run", () => {
Preview.stop!(fakeEditor, obj, obj);
command.stop(fakeEditor);
expect(fakeEditor.runCommand).not.toHaveBeenCalled();
Preview.shouldRunSwVisibility = true;
Preview.stop!(fakeEditor, obj, obj);
command.shouldRunSwVisibility = true;
command.stop(fakeEditor);
expect(fakeEditor.runCommand).toHaveBeenCalledWith('core:component-outline');
expect(Preview.shouldRunSwVisibility).toEqual(false);
expect(command.shouldRunSwVisibility).toEqual(false);
});
});
});

30
packages/core/test/specs/commands/view/Resize.ts

@ -0,0 +1,30 @@
import Resize, { ConvertUnitsToPx } from '../../../../src/commands/view/Resize';
describe('Resize command', () => {
test('stop should blur the canvas resizer', () => {
const command = new Resize({});
const blur = jest.fn();
command.canvasResizer = { blur } as any;
command.stop();
expect(blur).toHaveBeenCalledTimes(1);
});
test('convertPxToUnit should keep method name and convert pixels to percentage', () => {
const command = new Resize({});
const parent = document.createElement('div');
Object.defineProperty(parent, 'offsetWidth', { configurable: true, value: 200 });
const el = document.createElement('div');
parent.appendChild(el);
const result = command.convertPxToUnit({
el,
valuePx: 50,
unit: ConvertUnitsToPx.perc,
elComputedStyle: window.getComputedStyle(el),
});
expect(result).toBe('25%');
});
});

30
packages/core/test/specs/commands/view/SelectComponent.ts

@ -0,0 +1,30 @@
import SelectComponent from '../../../../src/commands/view/SelectComponent';
describe('SelectComponent command', () => {
test('select should update editor selection and initialize resize', () => {
const command = new SelectComponent({});
const selected = { id: 'cmp-selected' };
const setSelected = jest.fn();
const getSelected = jest.fn(() => selected);
command.em = { setSelected, getSelected } as any;
command.initResize = jest.fn();
const model = { id: 'cmp-1' } as any;
const event = {} as MouseEvent;
command.select(model, event);
expect(setSelected).toHaveBeenCalledWith(model, { event, useValid: true });
expect(command.initResize).toHaveBeenCalledWith(selected);
});
test('hideBadge should hide the badge element', () => {
const command = new SelectComponent({});
const badge = document.createElement('div');
badge.style.display = 'block';
command.getBadge = jest.fn(() => badge);
command.hideBadge();
expect(badge.style.display).toBe('none');
});
});

31
packages/core/test/specs/commands/view/SelectPosition.ts

@ -0,0 +1,31 @@
import SelectPosition from '../../../../src/commands/view/SelectPosition';
describe('SelectPosition command', () => {
test('nearFloat should detect floating neighbors', () => {
const command = new SelectPosition({});
const dims = [
[0, 0, 0, 0, true],
[0, 0, 0, 0, false],
];
expect(command.nearFloat(1, 'before', dims)).toBe(1);
});
test('stop should cancel drag and reset wrapper cursor', () => {
const command = new SelectPosition({});
const wrapper: any = {};
wrapper.css = jest.fn(() => wrapper);
wrapper.unbind = jest.fn(() => wrapper);
command.$wrapper = wrapper;
command.sorter = { cancelDrag: jest.fn() };
command.cDim = [];
command.posMethod = 'before';
command.posIndex = 0;
command.stop();
expect(command.sorter.cancelDrag).toHaveBeenCalledTimes(1);
expect(wrapper.css).toHaveBeenCalledWith('cursor', '');
expect(wrapper.unbind).toHaveBeenCalledTimes(1);
});
});

34
packages/core/test/specs/commands/view/ShowOffset.ts

@ -0,0 +1,34 @@
import ShowOffset from '../../../../src/commands/view/ShowOffset';
describe('ShowOffset command', () => {
test('getOffsetMethod should build the method name from state', () => {
const command = new ShowOffset({});
expect(command.getOffsetMethod('Fixed')).toBe('getFixedOffsetViewerEl');
});
test('run should stop the command when offsets are disabled', () => {
const command = new ShowOffset({ em: { getZoomDecimal: jest.fn(() => 1) } });
command.id = 'core:component-offset';
const editor = {
getConfig: jest.fn(() => ({ showOffsets: false, showOffsetsSelected: true })),
stopCommand: jest.fn(),
};
command.run(editor as any, null, { el: document.createElement('div') });
expect(editor.stopCommand).toHaveBeenCalledWith('core:component-offset', { el: expect.any(HTMLElement) });
});
test('stop should hide the offset viewer', () => {
const command = new ShowOffset({});
const viewer = document.createElement('div');
command.canvas = {
getFixedOffsetViewerEl: jest.fn(() => viewer),
} as any;
command.stop({} as any, null, { state: 'Fixed' });
expect(viewer.style.opacity).toBe('0');
});
});

22
packages/core/test/specs/commands/view/SwitchVisibility.ts

@ -4,13 +4,16 @@ describe('SwitchVisibility command', () => {
let fakeEditor: any;
let fakeFrames: any;
let fakeIsActive: any;
let command: SwitchVisibility;
beforeEach(() => {
fakeFrames = [];
fakeIsActive = false;
command = new SwitchVisibility({ em: { Commands: { isActive: jest.fn(() => false) } }, pStylePrefix: 'gjs-' });
fakeEditor = {
Canvas: {
getModel: jest.fn(() => ({ on: jest.fn(), off: jest.fn() })),
getFrames: jest.fn(() => fakeFrames),
},
@ -24,8 +27,25 @@ describe('SwitchVisibility command', () => {
it('should do nothing if the preview command is active', () => {
expect(fakeEditor.Canvas.getFrames).not.toHaveBeenCalled();
fakeIsActive = true;
SwitchVisibility.toggleVis(fakeEditor);
command.toggleVis(fakeEditor);
expect(fakeEditor.Canvas.getFrames).not.toHaveBeenCalled();
});
it('should remove the dashed class on stop', () => {
const remove = jest.fn();
fakeFrames = [
{
view: {
loaded: true,
getBody: jest.fn(() => ({ classList: { add: jest.fn(), remove } })),
},
on: jest.fn(),
},
];
command.stop(fakeEditor);
expect(remove).toHaveBeenCalledWith('gjs-dashed');
});
});
});

2
packages/core/test/specs/grapesjs/index.ts

@ -606,7 +606,7 @@ describe('GrapesJS', () => {
config.plugins = [pluginName];
editor = grapesjs.init(config);
expect(editor.Commands.get('export-template')!.test).toEqual(1);
expect((editor.Commands.get('export-template') as any).test).toEqual(1);
});
describe('usePlugin', () => {

Loading…
Cancel
Save