diff --git a/packages/core/src/commands/registry.ts b/packages/core/src/commands/registry.ts index a01eb9586..6bb1da11c 100644 --- a/packages/core/src/commands/registry.ts +++ b/packages/core/src/commands/registry.ts @@ -13,6 +13,7 @@ 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 { CopyComponentCommandRegistryRun } from './view/CopyComponent'; import type { PasteComponentCommandRegistryRun } from './view/PasteComponent'; import type { CanvasMoveCommandRegistryRun, CanvasMoveCommandRegistryStop } from './view/CanvasMove'; @@ -49,6 +50,7 @@ export interface CommandRegistryRun ComponentPrevCommandRegistryRun, ComponentEnterCommandRegistryRun, ComponentExitCommandRegistryRun, + ComponentDeleteCommandRegistryRun, CopyComponentCommandRegistryRun, PasteComponentCommandRegistryRun, CanvasMoveCommandRegistryRun, diff --git a/packages/core/src/commands/view/ComponentDelete.ts b/packages/core/src/commands/view/ComponentDelete.ts index 909ccff9e..f3cc6ef99 100644 --- a/packages/core/src/commands/view/ComponentDelete.ts +++ b/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; +} + +export default class CommandComponentDelete extends CommandAbstract { + 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; + } +} diff --git a/packages/core/test/specs/commands/view/ComponentDelete.ts b/packages/core/test/specs/commands/view/ComponentDelete.ts new file mode 100644 index 000000000..ad5eb9d19 --- /dev/null +++ b/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([]); + }); +});