Browse Source

Add Canvas Component Dragging API (#6184)

* Add API for dragging components onto the canvas

* Replace canvas.startSort API

* Update canvas.startSort

* Refactor types

* Fix image dropping

* Add getDefinition method

* Rename property

* Change block properties interface

* Remove old dragContent

* refactor startsort

* Merge branch 'add-canvas-sorter-api' of https://github.com/GrapesJS/grapesjs into add-canvas-sorter-api

* remove unused code

* Fix types
data-var-listen-memory
mohamed yahia 1 year ago
committed by GitHub
parent
commit
d94f2c1f2c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      packages/core/src/block_manager/index.ts
  2. 17
      packages/core/src/block_manager/model/Block.ts
  3. 19
      packages/core/src/canvas/index.ts
  4. 24
      packages/core/src/utils/Droppable.ts
  5. 5
      packages/core/src/utils/sorter/BaseComponentNode.ts
  6. 89
      packages/core/src/utils/sorter/CanvasNewComponentNode.ts
  7. 10
      packages/core/src/utils/sorter/ComponentSorter.ts
  8. 14
      packages/core/src/utils/sorter/DropLocationDeterminer.ts
  9. 13
      packages/core/src/utils/sorter/SortableTreeNode.ts
  10. 9
      packages/core/src/utils/sorter/Sorter.ts
  11. 24
      packages/core/src/utils/sorter/types.ts

10
packages/core/src/block_manager/index.ts

@ -108,7 +108,13 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
const { em, events, blocks } = this;
const content = block.getContent ? block.getContent() : block;
this._dragBlock = block;
em.set({ dragResult: null, dragContent: content });
em.set({
dragResult: null,
dragSource: {
content,
dragDef: block.getDragDef(),
},
});
[em, blocks].map((i) => i.trigger(events.dragStart, block, ev));
}
@ -145,7 +151,7 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
}
}
em.set({ dragResult: null, dragContent: null });
em.set({ dragResult: null, dragSource: undefined });
if (block) {
[em, blocks].map((i) => i.trigger(events.dragEnd, cmp, block));

17
packages/core/src/block_manager/model/Block.ts

@ -2,19 +2,15 @@ import { Model } from '../../common';
import { isFunction } from 'underscore';
import Editor from '../../editor';
import Category, { CategoryProperties } from '../../abstract/ModuleCategory';
import { ComponentDefinition } from '../../dom_components/model/types';
import Blocks from './Blocks';
import { DraggableContent } from '../../utils/sorter/types';
/** @private */
export interface BlockProperties {
export interface BlockProperties extends DraggableContent {
/**
* Block label, eg. `My block`
*/
label: string;
/**
* The content of the block. Might be an HTML string or a [Component Defintion](/modules/Components.html#component-definition)
*/
content: string | ComponentDefinition | (string | ComponentDefinition)[];
/**
* HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* @default ''
@ -91,6 +87,7 @@ export default class Block extends Model<BlockProperties> {
disable: false,
onClick: undefined,
attributes: {},
dragDef: {},
};
}
@ -135,6 +132,14 @@ export default class Block extends Model<BlockProperties> {
return this.get('content');
}
/**
* Get block component dragDef
* @returns {ComponentDefinition}
*/
getDragDef() {
return this.get('dragDef');
}
/**
* Get block category label
* @returns {String}

19
packages/core/src/canvas/index.ts

@ -43,6 +43,7 @@ import Frame from './model/Frame';
import { CanvasEvents, CanvasRefreshOptions, ToWorldOption } from './types';
import CanvasView, { FitViewportOptions } from './view/CanvasView';
import FrameView from './view/FrameView';
import { DragSource } from '../utils/sorter/types';
export type CanvasEvent = `${CanvasEvents}`;
@ -508,6 +509,24 @@ export default class CanvasModule extends Module<CanvasConfig> {
};
}
/**
* Sets the drag source in the editor so it's used in Droppable.ts.
* This method can be used for custom drag-and-drop content by passing in a `DragSource` object.
*
* @param {DragSource<Component>} dragSource - The source object for the drag operation, containing the component being dragged.
*/
startDrag(dragSource: DragSource<Component>) {
this.em.set('dragSource', dragSource);
}
/**
* Ends the drag-and-drop process, resetting the drag source and clearing any drag results.
* This method can be used to finalize custom drag-and-drop content operations.
*/
endDrag() {
this.em.set({ dragResult: null, dragSource: undefined });
}
/**
* Check if the canvas is focused
* @returns {Boolean}

24
packages/core/src/utils/Droppable.ts

@ -3,9 +3,10 @@ import CanvasModule from '../canvas';
import { ObjectStrings } from '../common';
import EditorModel from '../editor/model/Editor';
import { getDocumentScroll, off, on } from './dom';
import { DragDirection } from './sorter/types';
import { DragDirection, DragSource } from './sorter/types';
import CanvasNewComponentNode from './sorter/CanvasNewComponentNode';
import ComponentSorter from './sorter/ComponentSorter';
import Component from '../dom_components/model/Component';
// TODO move in sorter
type SorterOptions = {
@ -107,9 +108,9 @@ export default class Droppable {
handleDragEnter(ev: DragEvent | Event) {
const { em, canvas } = this;
const dt = (ev as DragEvent).dataTransfer;
const dragContentOrigin = em.get('dragContent');
const dragSourceOrigin: DragSource<Component> = em.get('dragSource');
if (!dragContentOrigin && !canvas.getConfig().allowExternalDrop) {
if (!dragSourceOrigin?.content && !canvas.getConfig().allowExternalDrop) {
return;
}
@ -118,11 +119,11 @@ export default class Droppable {
this.over = true;
const utils = em.Utils;
// For security reason I can't read the drag data on dragenter, but
// as I need it for the Sorter context I will use `dragContent` or just
// as I need it for the Sorter context I will use `dragSource` or just
// any not empty element
let content = dragContentOrigin || '<br>';
let content = dragSourceOrigin?.content || '<br>';
let dragStop: DragStop;
let dragContent;
let dragSource;
em.stopDefault();
// Select the right drag provider
@ -155,7 +156,7 @@ export default class Droppable {
},
});
dragStop = (cancel?: boolean) => dragger.stop(ev, { cancel });
dragContent = (cnt: any) => (content = cnt);
dragSource = (cnt: any) => (content = cnt);
} else {
const sorter = new utils.ComponentSorter({
em,
@ -195,7 +196,8 @@ export default class Droppable {
);
let dropModel = this.getTempDropModel(content);
const el = dropModel.view?.el;
sorter.startSort(el ? [{ element: el, content }] : []);
const sources = el ? [{ element: el, dragSource: dragSourceOrigin }] : [];
sorter.startSort(sources);
this.sorter = sorter;
this.draggedNode = sorter.sourceNodes?.[0];
dragStop = (cancel?: boolean) => {
@ -267,7 +269,7 @@ export default class Droppable {
const em = this.em;
const types = dt && dt.types;
const files = (dt && dt.files) || [];
const dragContent = em.get('dragContent');
const dragSource: DragSource<Component> = em.get('dragSource');
let content = dt && dt.getData('text');
if (files.length) {
@ -284,8 +286,8 @@ export default class Droppable {
});
}
}
} else if (dragContent) {
content = dragContent;
} else if (dragSource?.content) {
content = dragSource.content;
} else if (indexOf(types, 'text/html') >= 0) {
content = dt && dt.getData('text/html').replace(/<\/?meta[^>]*>/g, '');
} else if (indexOf(types, 'text/uri-list') >= 0) {

5
packages/core/src/utils/sorter/BaseComponentNode.ts

@ -1,4 +1,3 @@
import { View } from '../../common';
import Component from '../../dom_components/model/Component';
import { SortableTreeNode } from './SortableTreeNode';
@ -9,10 +8,6 @@ import { SortableTreeNode } from './SortableTreeNode';
* Subclasses must implement the `view` and `element` methods.
*/
export abstract class BaseComponentNode extends SortableTreeNode<Component> {
constructor(model: Component, content?: any) {
super(model, content);
}
/**
* Get the list of child components.
* @returns {BaseComponentNode[] | null} - The list of children wrapped in

89
packages/core/src/utils/sorter/CanvasNewComponentNode.ts

@ -1,27 +1,96 @@
import { isFunction } from 'underscore';
import CanvasComponentNode from './CanvasComponentNode';
import { getSymbolMain, getSymbolTop, isSymbol, isSymbolMain } from '../../dom_components/model/SymbolUtils';
import Component from '../../dom_components/model/Component';
import { ContentElement, ContentType } from './types';
type CanMoveSource = Component | ContentType;
export default class CanvasNewComponentNode extends CanvasComponentNode {
/**
* **Note:** For new components, this method will not directly add them to the target collection.
* Instead, the adding logic is handled in `Droppable.ts` to accommodate dragging various content types,
* such as images.
*/
canMove(source: CanvasNewComponentNode, index: number): boolean {
const realIndex = this.getRealIndex(index);
const { model: symbolModel, content, dragDef } = source._dragSource;
const canMoveSymbol = !symbolModel || !this.isSourceSameSymbol(symbolModel);
const sourceContent: CanMoveSource = (isFunction(content) ? dragDef : content) || source.model;
if (Array.isArray(sourceContent)) {
return (
sourceContent.every((contentItem, i) => this.canMoveSingleContent(contentItem, realIndex + i)) && canMoveSymbol
);
}
return this.canMoveSingleContent(sourceContent, realIndex) && canMoveSymbol;
}
private canMoveSingleContent(contentItem: ContentElement | Component, index: number): boolean {
return this.model.em.Components.canMove(this.model, contentItem, index).result;
}
addChildAt(node: CanvasNewComponentNode, index: number): CanvasNewComponentNode {
const dragSource = node._dragSource;
const dragSourceContent = dragSource.content!;
const insertingTextableIntoText = this.isTextNode() && node.isTextable();
const content = isFunction(node._content) ? node._content() : node._content;
const content = isFunction(dragSourceContent) ? dragSourceContent() : dragSourceContent;
if (Array.isArray(content)) {
return this.addMultipleChildren(content, index, insertingTextableIntoText);
}
return this.addSingleChild(content, index, insertingTextableIntoText);
}
private addSingleChild(
content: ContentType,
index: number,
insertingTextableIntoText: boolean,
): CanvasNewComponentNode {
let model;
if (insertingTextableIntoText) {
// @ts-ignore
model = this.model?.getView?.()?.insertComponent?.(content, { action: 'add-component' });
} else {
model = this.model.components().add(content, { at: this.getRealIndex(index || -1), action: 'add-component' });
model = this.model.components().add(content, { at: this.getRealIndex(index), action: 'add-component' });
}
return new (this.constructor as any)(model);
}
set content(content: any) {
this._content = content;
/**
* Adds multiple content items as children, looping through the array.
* @param {any[]} contentArray - Array of content items
* @param {number} index - Index to start adding children
* @param {boolean} insertingTextableIntoText - Whether inserting textable content
* @returns {CanvasNewComponentNode} The last added node
*/
private addMultipleChildren(
contentArray: ContentType[],
index: number,
insertingTextableIntoText: boolean,
): CanvasNewComponentNode {
let lastNode: CanvasNewComponentNode | undefined;
contentArray.forEach((contentItem, i) => {
lastNode = this.addSingleChild(contentItem, index + i, insertingTextableIntoText);
});
return lastNode!;
}
/**
* Checks if the source component belongs to the same symbol model as the current component.
* @param {Component | undefined} symbolModel - Symbol model to compare
* @returns {boolean} Whether the source is the same symbol
*/
private isSourceSameSymbol(symbolModel: Component | undefined) {
if (isSymbol(this.model)) {
const targetRootSymbol = getSymbolTop(this.model);
const targetMainSymbol = isSymbolMain(targetRootSymbol) ? targetRootSymbol : getSymbolMain(targetRootSymbol);
if (targetMainSymbol === symbolModel) {
return true;
}
}
return false;
}
set content(content: ContentType | (() => ContentType)) {
this._dragSource.content = content;
}
}

10
packages/core/src/utils/sorter/ComponentSorter.ts

@ -4,7 +4,13 @@ import EditorModel from '../../editor/model/Editor';
import { getPointerEvent } from '../dom';
import { BaseComponentNode } from './BaseComponentNode';
import Sorter from './Sorter';
import { SorterContainerContext, PositionOptions, SorterDragBehaviorOptions, SorterEventHandlers } from './types';
import {
SorterContainerContext,
PositionOptions,
SorterDragBehaviorOptions,
SorterEventHandlers,
DragSource,
} from './types';
const targetSpotType = CanvasSpotBuiltInTypes.Target;
@ -24,7 +30,7 @@ export default class ComponentSorter<NodeType extends BaseComponentNode> extends
eventHandlers = {},
}: {
em: EditorModel;
treeClass: new (model: Component, content?: any) => NodeType;
treeClass: new (model: Component, dragSource?: DragSource<Component>) => NodeType;
containerContext: SorterContainerContext;
dragBehavior: SorterDragBehaviorOptions;
positionOptions?: PositionOptions;

14
packages/core/src/utils/sorter/DropLocationDeterminer.ts

@ -3,7 +3,15 @@ import { $, View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { isTextNode, off, on } from '../dom';
import { SortableTreeNode } from './SortableTreeNode';
import { Dimension, Placement, PositionOptions, DragDirection, SorterEventHandlers, CustomTarget } from './types';
import {
Dimension,
Placement,
PositionOptions,
DragDirection,
SorterEventHandlers,
CustomTarget,
DragSource,
} from './types';
import { bindAll, each } from 'underscore';
import { matches, findPosition, offset, isInFlow } from './SorterUtils';
@ -16,7 +24,7 @@ type ContainerContext = {
interface DropLocationDeterminerOptions<T, NodeType extends SortableTreeNode<T>> {
em: EditorModel;
treeClass: new (model: T, content?: any) => NodeType;
treeClass: new (model: T, dragSource?: DragSource<T>) => NodeType;
containerContext: ContainerContext;
positionOptions: PositionOptions;
dragDirection: DragDirection;
@ -44,7 +52,7 @@ type LastMoveData<NodeType> = {
export class DropLocationDeterminer<T, NodeType extends SortableTreeNode<T>> extends View {
em: EditorModel;
treeClass: new (model: any) => NodeType;
treeClass: new (model: any, dragSource?: DragSource<T>) => NodeType;
positionOptions: PositionOptions;
containerContext: ContainerContext;

13
packages/core/src/utils/sorter/SortableTreeNode.ts

@ -1,4 +1,5 @@
import { $, View } from '../../common';
import { View } from '../../common';
import { DragSource } from './types';
/**
* Base class for managing tree-like structures with sortable nodes.
@ -7,10 +8,10 @@ import { $, View } from '../../common';
*/
export abstract class SortableTreeNode<T> {
protected _model: T;
protected _content: any;
constructor(model: T, content?: any) {
protected _dragSource: DragSource<T>;
constructor(model: T, dragSource: DragSource<T> = {}) {
this._model = model;
this._content = content;
this._dragSource = dragSource;
}
/**
* Get the list of children of this node.
@ -82,8 +83,8 @@ export abstract class SortableTreeNode<T> {
return this._model;
}
get content(): T {
return this._content;
get dragSource() {
return this._dragSource;
}
equals(node?: SortableTreeNode<T>): boolean {

9
packages/core/src/utils/sorter/Sorter.ts

@ -3,6 +3,7 @@ import { $ } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { off, on } from '../dom';
import { SortableTreeNode } from './SortableTreeNode';
import { DragSource } from './types';
import { DropLocationDeterminer } from './DropLocationDeterminer';
import { PlaceholderClass } from './PlaceholderClass';
import { getMergedOptions, getDocument, matches, closest, sortDom } from './SorterUtils';
@ -18,7 +19,7 @@ import { SorterOptions } from './types';
export default class Sorter<T, NodeType extends SortableTreeNode<T>> {
em: EditorModel;
treeClass: new (model: T, content?: any) => NodeType;
treeClass: new (model: T, dragSource?: DragSource<T>) => NodeType;
placeholder: PlaceholderClass;
dropLocationDeterminer: DropLocationDeterminer<T, NodeType>;
@ -69,13 +70,13 @@ export default class Sorter<T, NodeType extends SortableTreeNode<T>> {
* Picking components to move
* @param {HTMLElement[]} sources[]
* */
startSort(sources: { element?: HTMLElement; content?: any }[]) {
const validSources = sources.filter((source) => !!source.content || this.findValidSourceElement(source.element));
startSort(sources: { element?: HTMLElement; dragSource?: DragSource<T> }[]) {
const validSources = sources.filter((source) => !!source.dragSource || this.findValidSourceElement(source.element));
const sourcesWithModel: { model: T; content?: any }[] = validSources.map((source) => {
return {
model: $(source.element)?.data('model'),
content: source.content,
content: source.dragSource,
};
});
const sortedSources = sourcesWithModel.sort((a, b) => {

24
packages/core/src/utils/sorter/types.ts

@ -1,7 +1,29 @@
import CanvasModule from '../../canvas';
import { ComponentDefinition } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';
import { SortableTreeNode } from './SortableTreeNode';
export type ContentElement = string | ComponentDefinition;
export type ContentType = ContentElement | ContentElement[];
export interface DraggableContent {
/**
* Determines if a block can be moved inside a given component when the content is a function.
*
* This property is used to determine the validity of the drag operation.
* @type {ComponentDefinition | undefined}
*/
dragDef?: ComponentDefinition;
/**
* The content being dragged. Might be an HTML string or a [Component Defintion](/modules/Components.html#component-definition)
*/
content?: ContentType | (() => ContentType);
}
export type DragSource<T> = DraggableContent & {
model?: T;
};
export interface Dimension {
top: number;
left: number;
@ -96,7 +118,7 @@ export interface SorterDragBehaviorOptions {
export interface SorterOptions<T, NodeType extends SortableTreeNode<T>> {
em: EditorModel;
treeClass: new (model: T, content?: any) => NodeType;
treeClass: new (model: T, dragSource?: DragSource<T>) => NodeType;
containerContext: SorterContainerContext;
positionOptions: PositionOptions;

Loading…
Cancel
Save