diff --git a/packages/core/src/commands/registry.ts b/packages/core/src/commands/registry.ts index 1d9776ad6..be5944ff2 100644 --- a/packages/core/src/commands/registry.ts +++ b/packages/core/src/commands/registry.ts @@ -18,6 +18,7 @@ import type { OpenAssetsCommandRegistryRun, OpenAssetsCommandRegistryStop } from 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 { @@ -51,6 +52,7 @@ export interface CommandRegistryRun OpenLayersCommandRegistryRun, OpenStyleManagerCommandRegistryRun, OpenTraitManagerCommandRegistryRun, + SelectComponentCommandRegistryRun, ShowOffsetCommandRegistryRun, SwitchVisibilityCommandRegistryRun {} @@ -66,6 +68,7 @@ export interface CommandRegistryStop OpenLayersCommandRegistryStop, OpenStyleManagerCommandRegistryStop, OpenTraitManagerCommandRegistryStop, + SelectComponentCommandRegistryStop, ShowOffsetCommandRegistryStop, SwitchVisibilityCommandRegistryStop {} diff --git a/packages/core/src/commands/view/MoveComponent.ts b/packages/core/src/commands/view/MoveComponent.ts index 475fe3762..5c27309f7 100644 --- a/packages/core/src/commands/view/MoveComponent.ts +++ b/packages/core/src/commands/view/MoveComponent.ts @@ -7,6 +7,8 @@ import CommandAbstract from './CommandAbstract'; import SelectComponent from './SelectComponent'; import SelectPosition from './SelectPosition'; +const SelectComponentProto = SelectComponent.prototype as any; + export interface MoveComponentCommandRegistryRun { 'core:component-move': CommandPublicFnFromHandler; 'move-comp': CommandPublicFnFromHandler; @@ -21,7 +23,7 @@ export default class CommandMoveComponent extends CommandAbstract { [key: string]: any; init(o: any) { - (SelectComponent.init as any).apply(this, arguments as any); + SelectComponentProto.init.apply(this, arguments as any); bindAll(this, 'initSorter', 'rollback', 'onEndMove'); this.opt = o; this.hoverClass = `${this.ppfx}highlighter-warning`; @@ -34,7 +36,7 @@ export default class CommandMoveComponent extends CommandAbstract { } enable(...args: any[]) { - SelectComponent.enable.apply(this, args); + SelectComponentProto.enable.apply(this, args); this.getBadgeEl().addClass(this.badgeClass); this.getHighlighterEl().addClass(this.hoverClass); const wp = this.$wrapper; @@ -167,7 +169,7 @@ export default class CommandMoveComponent extends CommandAbstract { } stop(...args: any[]) { - (SelectComponent.stop as any).apply(this, args); + SelectComponentProto.stop.apply(this, args); this.getBadgeEl().removeClass(this.badgeClass); this.getHighlighterEl().removeClass(this.hoverClass); const wp = this.$wrapper; @@ -177,9 +179,10 @@ export default class CommandMoveComponent extends CommandAbstract { [ SelectPosition as Record, - SelectComponent as Record, + SelectComponentProto as Record, ].forEach((source) => { - Object.keys(source).forEach((key) => { + Object.getOwnPropertyNames(source).forEach((key) => { + if (key === 'constructor') return; if (!(key in CommandMoveComponent.prototype)) { (CommandMoveComponent.prototype as Record)[key] = source[key]; } diff --git a/packages/core/src/commands/view/SelectComponent.ts b/packages/core/src/commands/view/SelectComponent.ts index 1d9ee978f..9ce4fafe3 100644 --- a/packages/core/src/commands/view/SelectComponent.ts +++ b/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; + 'select-comp': CommandPublicFnFromHandler; +} + +export interface SelectComponentCommandRegistryStop { + 'core:component-select': CommandPublicFnFromHandler; + 'select-comp': CommandPublicFnFromHandler; +} + /** * 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,53 +256,30 @@ 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) { ev.stopPropagation(); ev.preventDefault(); @@ -313,38 +302,22 @@ 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) + ((!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; + } +} diff --git a/packages/core/test/specs/commands/view/SelectComponent.ts b/packages/core/test/specs/commands/view/SelectComponent.ts new file mode 100644 index 000000000..f5865eab6 --- /dev/null +++ b/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'); + }); +});