Browse Source

Refactor SelectComponent

typed-command-runs
Artur Arseniev 5 months ago
parent
commit
16c2162c82
  1. 3
      packages/core/src/commands/registry.ts
  2. 13
      packages/core/src/commands/view/MoveComponent.ts
  3. 304
      packages/core/src/commands/view/SelectComponent.ts
  4. 30
      packages/core/test/specs/commands/view/SelectComponent.ts

3
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 {}

13
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<CommandMoveComponent['run']>;
'move-comp': CommandPublicFnFromHandler<CommandMoveComponent['run']>;
@ -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<string, unknown>,
SelectComponent as Record<string, unknown>,
SelectComponentProto as Record<string, unknown>,
].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<string, unknown>)[key] = source[key];
}

304
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,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<any, { [k: string]: any }>;
}
}

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');
});
});
Loading…
Cancel
Save