Browse Source

Improve `editor.Components.canMove`

pull/5337/head
Artur Arseniev 3 years ago
parent
commit
4f0c5b1bea
  1. 64
      src/dom_components/index.ts
  2. 6
      src/dom_components/model/types.ts
  3. 5
      src/utils/mixins.ts

64
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<DomComponentsConfig, any> {
componentTypes = [
{
@ -629,36 +653,44 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
}
/**
* Check if the component can be moved inside another.
* @param {[Component]} target The target Component is the one that is supposed to receive the source one.
* @param {[Component]|String} source The source can be another Component or an HTML string.
* @param {Number} [index] Index position. If not specified, the check will perform against appending the source to target.
* Check if a component can be moved inside another one.
* @param {[Component]} target The target component is the one that is supposed to receive the source one.
* @param {[Component]|String} source The source can be another component, a component definition or an HTML string.
* @param {Number} [index] Index position, if not specified, the check will be performed against appending the source to the target.
* @returns {Object} Object containing the `result` (Boolean), `source`, `target` (as Components), and a `reason` (Number) with these meanings:
* * `0` - Invalid source. This is a default value and should be ignored in case the `result` is true.
* * `1` - Source doesn't accept target as destination.
* * `2` - Target doesn't accept source.
* @private
* @example
* const rootComponent = editor.getWrapper();
* const someComponent = editor.getSelected();
*
* // Check with two components
* editor.Components.canMove(rootComponent, someComponent);
*
* // Check with component definition
* editor.Components.canMove(rootComponent, { tagName: 'a', draggable: false });
*
* // Check with HTML string
* editor.Components.canMove(rootComponent, '<form>...</form>');
*/
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<DomComponentsCon
let draggable = srcModel.get('draggable');
if (isFunction(draggable)) {
draggable = !!draggable(srcModel, target, at);
draggable = !!draggable(srcModel, target, index);
} else {
const el = target.getEl();
draggable = isArray(draggable) ? draggable.join(',') : draggable;
draggable = isString(draggable) ? el?.matches(draggable) : draggable;
}
if (!draggable) return { ...result, reason: 1 };
if (!draggable) return { ...result, reason: CanMoveReason.SourceReject };
// Check if the target accepts the source
let droppable = target.get('droppable');
if (isFunction(droppable)) {
droppable = !!droppable(srcModel, target, at);
droppable = !!droppable(srcModel, target, index);
} else {
if (droppable === false && target.isInstanceOf('text') && srcModel.get('textable')) {
droppable = true;
@ -694,7 +726,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
// Ensure the target is not inside the source
const isTargetInside = [target].concat(target.parents()).indexOf(srcModel) > -1;
if (!droppable || isTargetInside) return { ...result, reason: 2 };
if (!droppable || isTargetInside) return { ...result, reason: CanMoveReason.TargetReject };
return { ...result, result: true };
}

6
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

5
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<string, any>) => 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 = <T extends any>(el?: Node): T | undefined => (el as any)?.__gjsv;
export const isComponent = (obj: any): obj is Component => !!obj?.toHTML;
export const getComponentView = (el?: Node) => getViewEl<ComponentView>(el);
export const getComponentModel = (el?: Node) => getComponentView(el)?.model;
@ -246,7 +248,6 @@ export {
appendStyles,
isObject,
isEmptyObj,
isComponent,
createId,
isRule,
};

Loading…
Cancel
Save