From 351bf92d34d49b4e5070e1033c057cef71fbbb0f Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 20 May 2026 17:11:23 +0400 Subject: [PATCH] Refactor ComponentNext --- packages/core/src/commands/registry.ts | 2 + .../core/src/commands/view/ComponentNext.ts | 20 ++++++--- .../core/src/commands/view/SelectComponent.ts | 7 ++- .../test/specs/commands/view/ComponentNext.ts | 44 +++++++++++++++++++ 4 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 packages/core/test/specs/commands/view/ComponentNext.ts diff --git a/packages/core/src/commands/registry.ts b/packages/core/src/commands/registry.ts index be5944ff2..7874089a1 100644 --- a/packages/core/src/commands/registry.ts +++ b/packages/core/src/commands/registry.ts @@ -9,6 +9,7 @@ import type { 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 { CopyComponentCommandRegistryRun } from './view/CopyComponent'; import type { PasteComponentCommandRegistryRun } from './view/PasteComponent'; import type { CanvasMoveCommandRegistryRun, CanvasMoveCommandRegistryStop } from './view/CanvasMove'; @@ -41,6 +42,7 @@ export interface CommandRegistryRun extends FullscreenCommandRegistryRun, PreviewCommandRegistryRun, ResizeCommandRegistryRun, + ComponentNextCommandRegistryRun, CopyComponentCommandRegistryRun, PasteComponentCommandRegistryRun, CanvasMoveCommandRegistryRun, diff --git a/packages/core/src/commands/view/ComponentNext.ts b/packages/core/src/commands/view/ComponentNext.ts index 68241c8ff..ec032839a 100644 --- a/packages/core/src/commands/view/ComponentNext.ts +++ b/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; +} + +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; + } +} diff --git a/packages/core/src/commands/view/SelectComponent.ts b/packages/core/src/commands/view/SelectComponent.ts index 9ce4fafe3..b415aefd0 100644 --- a/packages/core/src/commands/view/SelectComponent.ts +++ b/packages/core/src/commands/view/SelectComponent.ts @@ -280,12 +280,15 @@ export default class CommandSelectComponent extends CommandAbstract { this.canvas.getHighlighter(view).style.opacity = 0; } - 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); diff --git a/packages/core/test/specs/commands/view/ComponentNext.ts b/packages/core/test/specs/commands/view/ComponentNext.ts new file mode 100644 index 000000000..209d5c7f1 --- /dev/null +++ b/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(); + }); +});