Browse Source

Initial setup for typed commands

typed-command-runs
Artur Arseniev 5 months ago
parent
commit
0f60c0bf58
  1. 2
      packages/core/package.json
  2. 15
      packages/core/src/commands/config/config.ts
  3. 63
      packages/core/src/commands/index.ts
  4. 73
      packages/core/src/commands/registry.ts
  5. 9
      packages/core/src/commands/registryHelpers.ts
  6. 73
      packages/core/src/commands/view/CommandAbstract.ts
  7. 123
      packages/core/src/commands/view/Fullscreen.ts
  8. 9
      packages/core/src/editor/index.ts
  9. 1
      packages/core/src/index.ts
  10. 58
      packages/core/test/specs/commands/view/Fullscreen.ts
  11. 86
      packages/core/test/types/commandRegistries.ts
  12. 18
      packages/core/test/types/tsconfig.json

2
packages/core/package.json

@ -69,7 +69,7 @@
"build:mjs": "cross-env BUILD_MODULE=true node node_modules/grapesjs-cli/dist/cli.js build --dts='skip' --patch=false --targets=\"defaults\"",
"build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules",
"ts:build": "node node_modules/grapesjs-cli/dist/cli.js build --dts='only' --patch=false",
"ts:check": "tsc --noEmit --esModuleInterop dist/index.d.ts",
"ts:check": "tsc -p test/types/tsconfig.json",
"start": "cross-env NODE_ENV=development run-p start:*",
"start:js": "node node_modules/grapesjs-cli/dist/cli.js serve",
"start:css": "npm run build:css -- --watch",

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 = () => ({

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

@ -36,16 +36,32 @@
*/
import { isFunction, includes } from 'underscore';
import CommandAbstract, { Command, CommandOptions, CommandObject, CommandFunction } from './view/CommandAbstract';
import CommandAbstract, {
Command,
CommandConstructor,
CommandOptions,
CommandStored,
} from './view/CommandAbstract';
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'],
@ -99,7 +115,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 +135,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 +196,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;
@ -237,8 +253,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 +274,7 @@ 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;
}
@ -260,8 +287,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 +312,7 @@ 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) {
@ -327,8 +354,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 +366,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 +407,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 +432,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 +456,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);
}

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

@ -0,0 +1,73 @@
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';
type CommandPublicHandler = (...args: any[]) => any;
type CommandRegistryEntry<TRegistry, TId extends string> = TId extends keyof TRegistry
? TRegistry[TId] extends CommandPublicHandler
? TRegistry[TId]
: CommandPublicHandler
: CommandPublicHandler;
export interface CommandRegistryRun extends FullscreenCommandRegistryRun {}
export interface CommandRegistryStop extends FullscreenCommandRegistryStop {}
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>
>;

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

@ -0,0 +1,9 @@
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];

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;
}
}

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 }>;
}
}

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';

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

@ -0,0 +1,58 @@
import CommandFullscreen from '../../../../src/commands/view/Fullscreen';
import Editor from '../../../../src/editor';
describe('Fullscreen command', () => {
let editor: Editor;
let container: HTMLElement & { requestFullscreen: jest.Mock };
let isFullscreen: boolean;
let exitFullscreen: jest.Mock;
beforeEach(() => {
isFullscreen = false;
container = document.createElement('div') as HTMLElement & { requestFullscreen: jest.Mock };
container.requestFullscreen = jest.fn(() => {
isFullscreen = true;
});
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(container.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(container.requestFullscreen).toHaveBeenCalledTimes(2);
editor.stopCommand('fullscreen');
expect(exitFullscreen).toHaveBeenCalledTimes(2);
});
});

86
packages/core/test/types/commandRegistries.ts

@ -0,0 +1,86 @@
import type { Editor } from 'grapesjs';
interface MyCommandOptions {
value: number;
}
interface MyCommandResult {
done: boolean;
}
interface MyCommandStopOptions {
reason: string;
}
declare module 'grapesjs' {
interface CommandRegistryRun {
'my:command': (options: MyCommandOptions) => MyCommandResult;
'my:stateless': () => number;
}
interface CommandRegistryStop {
'my:command': (options: MyCommandStopOptions) => void;
}
}
const editor = {} as Editor;
const fullscreenResult: void = editor.runCommand('core:fullscreen');
editor.runCommand('fullscreen', { target: document.body });
// @ts-expect-error Fullscreen target must be an element or selector string
editor.runCommand('core:fullscreen', { target: 1 });
const customResult: MyCommandResult = editor.runCommand('my:command', { value: 1 });
customResult.done;
editor.stopCommand('my:command', { reason: 'done' });
// @ts-expect-error Missing required run options
editor.runCommand('my:command');
// @ts-expect-error Stop options do not match the registry
editor.stopCommand('my:command', { value: 1 });
const statelessResult: number = editor.runCommand('my:stateless');
statelessResult.toFixed();
// @ts-expect-error Stateless commands should not accept options
editor.runCommand('my:stateless', {});
editor.Commands.add('my:command', {
run(_editor, _sender, options) {
options.value.toFixed();
return { done: true };
},
stop(_editor, _sender, options) {
options.reason.toUpperCase();
},
});
editor.Commands.add('my:command', (_editor, _sender, options) => {
options.value.toFixed();
return { done: true };
});
editor.Commands.add('my:stateless', () => 1);
editor.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 };
},
});
editor.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' };
},
},
};
editor.Commands.config.defaultOptions['my:command']?.run?.({ value: 1 });

18
packages/core/test/types/tsconfig.json

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"module": "esnext",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noEmit": true,
"baseUrl": "../..",
"paths": {
"grapesjs": ["dist/index.d.ts"]
}
},
"files": ["commandRegistries.ts"]
}
Loading…
Cancel
Save