From d94f2c1f2c12395641373525b2e565b0b1b16cd4 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Thu, 3 Oct 2024 13:20:40 +0300 Subject: [PATCH] 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 --- packages/core/src/block_manager/index.ts | 10 ++- .../core/src/block_manager/model/Block.ts | 17 ++-- packages/core/src/canvas/index.ts | 19 ++++ packages/core/src/utils/Droppable.ts | 24 ++--- .../src/utils/sorter/BaseComponentNode.ts | 5 -- .../utils/sorter/CanvasNewComponentNode.ts | 89 ++++++++++++++++--- .../core/src/utils/sorter/ComponentSorter.ts | 10 ++- .../utils/sorter/DropLocationDeterminer.ts | 14 ++- .../core/src/utils/sorter/SortableTreeNode.ts | 13 +-- packages/core/src/utils/sorter/Sorter.ts | 9 +- packages/core/src/utils/sorter/types.ts | 24 ++++- 11 files changed, 184 insertions(+), 50 deletions(-) diff --git a/packages/core/src/block_manager/index.ts b/packages/core/src/block_manager/index.ts index 6796fc281..eb3a5ecdb 100644 --- a/packages/core/src/block_manager/index.ts +++ b/packages/core/src/block_manager/index.ts @@ -108,7 +108,13 @@ export default class BlockManager extends ItemManagerModule i.trigger(events.dragStart, block, ev)); } @@ -145,7 +151,7 @@ export default class BlockManager extends ItemManagerModule i.trigger(events.dragEnd, cmp, block)); diff --git a/packages/core/src/block_manager/model/Block.ts b/packages/core/src/block_manager/model/Block.ts index b3414c60e..93c4a808b 100644 --- a/packages/core/src/block_manager/model/Block.ts +++ b/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. ` { disable: false, onClick: undefined, attributes: {}, + dragDef: {}, }; } @@ -135,6 +132,14 @@ export default class Block extends Model { return this.get('content'); } + /** + * Get block component dragDef + * @returns {ComponentDefinition} + */ + getDragDef() { + return this.get('dragDef'); + } + /** * Get block category label * @returns {String} diff --git a/packages/core/src/canvas/index.ts b/packages/core/src/canvas/index.ts index 01520648a..ae2b5fa4e 100644 --- a/packages/core/src/canvas/index.ts +++ b/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 { }; } + /** + * 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} dragSource - The source object for the drag operation, containing the component being dragged. + */ + startDrag(dragSource: DragSource) { + 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} diff --git a/packages/core/src/utils/Droppable.ts b/packages/core/src/utils/Droppable.ts index 130bfd3f2..5a0fa10a0 100644 --- a/packages/core/src/utils/Droppable.ts +++ b/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 = 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 || '
'; + let content = dragSourceOrigin?.content || '
'; 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 = 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) { diff --git a/packages/core/src/utils/sorter/BaseComponentNode.ts b/packages/core/src/utils/sorter/BaseComponentNode.ts index 4c53f114e..2247f09df 100644 --- a/packages/core/src/utils/sorter/BaseComponentNode.ts +++ b/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 { - constructor(model: Component, content?: any) { - super(model, content); - } - /** * Get the list of child components. * @returns {BaseComponentNode[] | null} - The list of children wrapped in diff --git a/packages/core/src/utils/sorter/CanvasNewComponentNode.ts b/packages/core/src/utils/sorter/CanvasNewComponentNode.ts index b2915a494..8447001a8 100644 --- a/packages/core/src/utils/sorter/CanvasNewComponentNode.ts +++ b/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; } } diff --git a/packages/core/src/utils/sorter/ComponentSorter.ts b/packages/core/src/utils/sorter/ComponentSorter.ts index 47135aa41..dba5e9316 100644 --- a/packages/core/src/utils/sorter/ComponentSorter.ts +++ b/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 extends eventHandlers = {}, }: { em: EditorModel; - treeClass: new (model: Component, content?: any) => NodeType; + treeClass: new (model: Component, dragSource?: DragSource) => NodeType; containerContext: SorterContainerContext; dragBehavior: SorterDragBehaviorOptions; positionOptions?: PositionOptions; diff --git a/packages/core/src/utils/sorter/DropLocationDeterminer.ts b/packages/core/src/utils/sorter/DropLocationDeterminer.ts index 7e146463f..fef8fb80c 100644 --- a/packages/core/src/utils/sorter/DropLocationDeterminer.ts +++ b/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> { em: EditorModel; - treeClass: new (model: T, content?: any) => NodeType; + treeClass: new (model: T, dragSource?: DragSource) => NodeType; containerContext: ContainerContext; positionOptions: PositionOptions; dragDirection: DragDirection; @@ -44,7 +52,7 @@ type LastMoveData = { export class DropLocationDeterminer> extends View { em: EditorModel; - treeClass: new (model: any) => NodeType; + treeClass: new (model: any, dragSource?: DragSource) => NodeType; positionOptions: PositionOptions; containerContext: ContainerContext; diff --git a/packages/core/src/utils/sorter/SortableTreeNode.ts b/packages/core/src/utils/sorter/SortableTreeNode.ts index 68ca7033d..5bcdc12e5 100644 --- a/packages/core/src/utils/sorter/SortableTreeNode.ts +++ b/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 { protected _model: T; - protected _content: any; - constructor(model: T, content?: any) { + protected _dragSource: DragSource; + constructor(model: T, dragSource: DragSource = {}) { 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 { return this._model; } - get content(): T { - return this._content; + get dragSource() { + return this._dragSource; } equals(node?: SortableTreeNode): boolean { diff --git a/packages/core/src/utils/sorter/Sorter.ts b/packages/core/src/utils/sorter/Sorter.ts index 9300ea38f..7c852f9b5 100644 --- a/packages/core/src/utils/sorter/Sorter.ts +++ b/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> { em: EditorModel; - treeClass: new (model: T, content?: any) => NodeType; + treeClass: new (model: T, dragSource?: DragSource) => NodeType; placeholder: PlaceholderClass; dropLocationDeterminer: DropLocationDeterminer; @@ -69,13 +70,13 @@ export default class Sorter> { * 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 }[]) { + 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) => { diff --git a/packages/core/src/utils/sorter/types.ts b/packages/core/src/utils/sorter/types.ts index fc15f78ea..450c35ee2 100644 --- a/packages/core/src/utils/sorter/types.ts +++ b/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 = DraggableContent & { + model?: T; +}; + export interface Dimension { top: number; left: number; @@ -96,7 +118,7 @@ export interface SorterDragBehaviorOptions { export interface SorterOptions> { em: EditorModel; - treeClass: new (model: T, content?: any) => NodeType; + treeClass: new (model: T, dragSource?: DragSource) => NodeType; containerContext: SorterContainerContext; positionOptions: PositionOptions;