diff --git a/src/dom_components/index.ts b/src/dom_components/index.ts index 295dec987..079434f8d 100644 --- a/src/dom_components/index.ts +++ b/src/dom_components/index.ts @@ -100,6 +100,7 @@ import { ItemManagerModule } from '../abstract/Module'; import EditorModel from '../editor/model/Editor'; import { ComponentAdd, ComponentDefinition, ComponentDefinitionDefined } from './model/types'; import { AddOptions } from '../common'; +import { isComponent } from '../utils/mixins'; export type ComponentEvent = | 'component:create' @@ -139,6 +140,29 @@ export interface AddComponentTypeOptions { extendFnView?: string[]; } +/** @private */ +export enum CanMoveReason { + /** + * Invalid source. This is a default value and should be ignored in case the `result` is true + */ + InvalidSource = 0, + /** + * Source doesn't accept target as destination. + */ + SourceReject = 1, + /** + * Target doesn't accept source. + */ + TargetReject = 2, +} + +export interface CanMoveResult { + result: boolean; + reason: CanMoveReason; + target: Component; + source?: Component | null; +} + export default class ComponentManager extends ItemManagerModule { componentTypes = [ { @@ -629,36 +653,44 @@ export default class ComponentManager extends ItemManagerModule...'); */ - canMove(target: Component, source?: Component, index?: number) { - const at = index || index === 0 ? index : null; - const result = { + canMove(target: Component, source?: Component | ComponentDefinition | string, index?: number): CanMoveResult { + const result: CanMoveResult = { result: false, - reason: 0, + reason: CanMoveReason.InvalidSource, target, source: null, }; if (!source || !target) return result; - //@ts-ignore - let srcModel = source.toHTML ? source : null; + let srcModel = isComponent(source) ? source : null; if (!srcModel) { const wrapper = this.getShallowWrapper(); srcModel = wrapper?.append(source)[0] || null; } - //@ts-ignore result.source = srcModel; if (!srcModel) return result; @@ -667,20 +699,20 @@ export default class ComponentManager extends ItemManagerModule -1; - if (!droppable || isTargetInside) return { ...result, reason: 2 }; + if (!droppable || isTargetInside) return { ...result, reason: CanMoveReason.TargetReject }; return { ...result, result: true }; } diff --git a/src/dom_components/model/types.ts b/src/dom_components/model/types.ts index 9162cf9b1..cb986ae53 100644 --- a/src/dom_components/model/types.ts +++ b/src/dom_components/model/types.ts @@ -11,6 +11,8 @@ import { ToolbarButtonProps } from './ToolbarButton'; export type DragMode = 'translate' | 'absolute' | ''; +export type DraggableDroppableFn = (source: Component, target: Component, index?: number) => boolean | void; + export interface ComponentProperties { /** * Component type, eg. `text`, `image`, `video`, etc. @@ -44,13 +46,13 @@ export interface ComponentProperties { containing `some-class` class and `Hello` title, and `column` components. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drag is possible. Default: `true` * @default true */ - draggable?: boolean | string | ((...params: any[]) => any); + draggable?: boolean | string | DraggableDroppableFn; /** * Indicates if it's possible to drop other components inside. You can use a query string as with `draggable`. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drop is possible. Default: `true` * @default true */ - droppable?: boolean | string | ((...params: any[]) => any); + droppable?: boolean | string | DraggableDroppableFn; /** * Set to false if you don't want to see the badge (with the name) over the component. Default: `true` * @default true diff --git a/src/utils/mixins.ts b/src/utils/mixins.ts index 9a70354d3..e6e0f835a 100644 --- a/src/utils/mixins.ts +++ b/src/utils/mixins.ts @@ -2,6 +2,7 @@ import { isArray, isElement, isUndefined, keys } from 'underscore'; import ComponentView from '../dom_components/view/ComponentView'; import EditorModel from '../editor/model/Editor'; import { isTextNode } from './dom'; +import Component from '../dom_components/model/Component'; export const isDef = (value: any) => typeof value !== 'undefined'; @@ -191,11 +192,12 @@ const isObject = (val: any): val is Object => val !== null && !Array.isArray(val const isEmptyObj = (val: Record) => Object.keys(val).length <= 0; const capitalize = (str: string = '') => str && str.charAt(0).toUpperCase() + str.substring(1); -const isComponent = (obj: any) => obj && obj.toHTML; const isRule = (obj: any) => obj && obj.toCSS; const getViewEl = (el?: Node): T | undefined => (el as any)?.__gjsv; +export const isComponent = (obj: any): obj is Component => !!obj?.toHTML; + export const getComponentView = (el?: Node) => getViewEl(el); export const getComponentModel = (el?: Node) => getComponentView(el)?.model; @@ -246,7 +248,6 @@ export { appendStyles, isObject, isEmptyObj, - isComponent, createId, isRule, };