diff --git a/packages/core/AnotherCanvas.tsx b/packages/core/AnotherCanvas.tsx new file mode 100644 index 000000000..c111e7e6a --- /dev/null +++ b/packages/core/AnotherCanvas.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { connectNode } from "./nodes"; +import { ConnectedNode, ConnectedPublicNode, Node } from "./interfaces"; + +type AnotherCanvas = { + children: React.ReactChildren +} & ConnectedPublicNode + +const AnotherCanvas: React.FC = ({children, craft: {node, connectTarget}}: AnotherCanvas) => { + return connectTarget( +
{children}
, + { + incoming: (incomingNode: Node) => { + // if ( incomingNode.data.props.children === 'Order1') return false; + return true; + } + } + ) +} + +export default connectNode(AnotherCanvas); \ No newline at end of file diff --git a/packages/core/App.tsx b/packages/core/App.tsx index 334a5f872..cabbc62c7 100644 --- a/packages/core/App.tsx +++ b/packages/core/App.tsx @@ -6,6 +6,7 @@ import { MsgBox } from "./MsgBox"; import { Heading } from "./Heading"; import { Renderer } from "./render/Renderer"; import { Craft } from "./Craft"; +import AnotherCanvas from "./AnotherCanvas"; class App extends React.Component { render() { @@ -13,9 +14,10 @@ class App extends React.Component {
+ +

Alright

+
- -
diff --git a/packages/core/MsgBox.tsx b/packages/core/MsgBox.tsx index 290b604b7..0b15d4db5 100644 --- a/packages/core/MsgBox.tsx +++ b/packages/core/MsgBox.tsx @@ -2,8 +2,9 @@ import React from "react"; import { Canvas } from "./nodes/Canvas"; import { Heading } from "./Heading"; import { PublicManagerMethods } from "./manager/methods"; -import { connectNode, ConnectedPublicNode } from "./nodes/connectors"; +import { connectNode } from "./nodes/connectors"; import MsgCanvas from "./MsgCanvas"; +import { ConnectedPublicNode } from "./interfaces"; export type MsgBox = { text: string @@ -14,10 +15,10 @@ const Msg = ({craft:{node, manager, connectTarget}, text}: MsgBox) => { return connectTarget(

MESSAGE{text}

- {/* +

Order1

Order2

-
*/} +
) } diff --git a/packages/core/MsgCanvas.tsx b/packages/core/MsgCanvas.tsx index 107ceaabf..532cc593b 100644 --- a/packages/core/MsgCanvas.tsx +++ b/packages/core/MsgCanvas.tsx @@ -1,7 +1,20 @@ import React from "react"; +import { connectNode } from "./nodes"; +import { ConnectedNode, ConnectedPublicNode, Node } from "./interfaces"; -export default function MsgCanvas({children}) { - return ( -
{children}
+type MsgCanvas = { + children: React.ReactChildren +} & ConnectedPublicNode + +const MsgCanvas: React.FC = ({children, craft: {node, connectTarget}}: MsgCanvas) => { + return connectTarget( +
{children}
, + { + incoming: (incomingNode: Node) => { + return true; + } + } ) -} \ No newline at end of file +} + +export default connectNode(MsgCanvas); \ No newline at end of file diff --git a/packages/core/events/EventsManager.tsx b/packages/core/events/EventsManager.tsx index 2db453e2e..dc7e21fd3 100644 --- a/packages/core/events/EventsManager.tsx +++ b/packages/core/events/EventsManager.tsx @@ -1,8 +1,8 @@ import { connectManager, ConnectedManager } from "../manager"; import React, { useState, useRef, useCallback } from "react"; -import { CanvasNode, PlaceholderInfo } from "../interfaces"; +import { CanvasNode, PlaceholderInfo, Nodes, Node } from "../interfaces"; import { NodeId, DropAction } from "~types"; -import { getDOMInfo, getDeepChildrenNodes } from "../utils"; +import { getDOMInfo, getDeepChildrenNodes, getAllCanvas } from "../utils"; import findPosition from "./findPosition"; import movePlaceholder from "./movePlaceholder"; import { useEventListener } from "../utils/hooks"; @@ -17,8 +17,7 @@ export const EventsManager = connectManager(({ children, manager: [state, method const placeholderRef = useRef(null); const placeBestPosition = (e: MouseEvent) => { const { nodes } = state; - const nearestTargets = getNearestTarget(e), - nearestTargetId = nearestTargets.pop(); + const [nearestTargetId, possibleNodes] = getNearestTarget(e); if (nearestTargetId) { const targetNode = nodes[nearestTargetId], targetParent = (targetNode as CanvasNode).data.nodes ? targetNode as CanvasNode : nodes[targetNode.data.parent] as CanvasNode; @@ -32,6 +31,8 @@ export const EventsManager = connectManager(({ children, manager: [state, method const bestTarget = findPosition(targetParent, dimensionsInContainer, e.clientX, e.clientY); const bestTargetNode = targetParent.data.nodes.length ? nodes[targetParent.data.nodes[bestTarget.index]] : targetParent; + + if ( !possibleNodes.includes(bestTargetNode.id) ) return; const output: PlaceholderInfo = { position: movePlaceholder( @@ -49,17 +50,33 @@ export const EventsManager = connectManager(({ children, manager: [state, method } } - const getNearestTarget = (e: MouseEvent) => { + const getNodesInAcceptedCanvas = (nodes: Nodes, incomingNode: Node): NodeId[] => { + const canvases = getAllCanvas(nodes); + const nodesToConsider = canvases.reduce((res: NodeId[], id) => { + const canvas = nodes[id] as CanvasNode; + if ( canvas.ref.incoming(incomingNode) ) { + if ( !res.includes(canvas.id) ) res = [...res, canvas.id]; + res = [...res, ...canvas.data.nodes]; + + } + return res; + }, []); + + return nodesToConsider; + } + + const getNearestTarget = (e: MouseEvent): [NodeId, NodeId[]]=> { const { nodes, events } = state; const pos = { x: e.clientX, y: e.clientY }; const deepChildren = getDeepChildrenNodes(nodes, events.active.id); - const nodesWithinBounds = Object.keys(nodes).filter(nodeId => { - return nodeId !== "rootNode" && nodes[nodeId].ref.dom && !deepChildren.includes(nodeId) + const possibleNodeIds = getNodesInAcceptedCanvas(nodes, state.events.active); + const nodesWithinBounds = possibleNodeIds.filter(nodeId => { + return nodes[nodeId].ref.dom && + !deepChildren.includes(nodeId) }); - - return nodesWithinBounds.filter((nodeId: NodeId) => { + const nearestTargets = nodesWithinBounds.filter((nodeId: NodeId) => { const { top, left, width, height } = getDOMInfo(nodes[nodeId].ref.dom); return ( @@ -67,6 +84,8 @@ export const EventsManager = connectManager(({ children, manager: [state, method (pos.y >= top && pos.y <= top + height) ); }); + + return [nearestTargets.length ? nearestTargets.pop() : null, possibleNodeIds] }; const onDrag = useCallback((e: MouseEvent) => { diff --git a/packages/core/interfaces/nodes.d.ts b/packages/core/interfaces/nodes.d.ts index b944b5e06..9fe92f2b6 100644 --- a/packages/core/interfaces/nodes.d.ts +++ b/packages/core/interfaces/nodes.d.ts @@ -20,14 +20,21 @@ export interface NodeData { export interface CanvasNode extends Node { data: CanvasNodeData; + ref: CanvasNodeRef } export interface CanvasNodeData extends NodeData { nodes: NodeId[] } -export type NodeRef = { +export interface NodeRef { dom: HTMLElement; + canDrag(node: Node): void; +} + +export interface CanvasNodeRef extends NodeRef { + incoming(incoming: Node): boolean; + outgoing(outgoing: Node): boolean; } export interface NodeEvent { diff --git a/packages/core/manager/methods.ts b/packages/core/manager/methods.ts index 2afe44764..d5ca15fb0 100644 --- a/packages/core/manager/methods.ts +++ b/packages/core/manager/methods.ts @@ -1,6 +1,7 @@ import { NodeId, Node, CanvasNode } from "../interfaces"; import { CallbacksFor } from "use-methods"; import { ManagerState } from "../interfaces"; +import { isCanvas } from "../utils"; export const PublicManagerMethods = (state: ManagerState) => { return { @@ -33,11 +34,15 @@ export const PublicManagerMethods = (state: ManagerState) => { }; const ManagerMethods = (state: ManagerState) => ({ - setDOM: (id: NodeId, dom: HTMLElement) => { - state.nodes[id].ref.dom = dom; + setRef: (id: NodeId, ref: "dom" | "outgoing" | "incoming" | "canDrag", value: any) => { + if ( !["dom", "outgoing", "incoming", "canDrag"].includes(ref)) { throw new Error(); } + let node = state.nodes[id]; + if ( isCanvas(node) ) (node as CanvasNode).ref[ref] = value; + else node.ref[ref as "dom" | "canDrag"] = value; }, pushChildCanvas(id: NodeId, canvasName: string, newNode: Node) { if (!state.nodes[id].data._childCanvas) state.nodes[id].data._childCanvas = {}; + newNode.data.closestParent = id; state.nodes[id].data._childCanvas[canvasName] = newNode.id; state.nodes[newNode.id] = newNode; }, diff --git a/packages/core/nodes/Canvas.tsx b/packages/core/nodes/Canvas.tsx index 9d70e9f15..521637b73 100644 --- a/packages/core/nodes/Canvas.tsx +++ b/packages/core/nodes/Canvas.tsx @@ -9,8 +9,9 @@ const shortid = require("shortid"); export interface Canvas extends ConnectedInternalNode, React.Props { id?: NodeId, + style?: any, + className?: any, is?: React.ElementType - children?: React.ReactNode } export const Canvas = connectInternalNode(({ craft: { node, manager }, children, is="div", id, ...props}: Canvas) => { @@ -19,15 +20,19 @@ export const Canvas = connectInternalNode(({ craft: { node, manager }, children, let canvasId = `canvas-${shortid.generate()}`; if (node.data.type === Canvas) { - canvasId = internal.current.id = node.id; - const childNodes = mapChildrenToNodes(children, canvasId); - manager.add(node.id, childNodes); + if ( !(node as CanvasNode).data.nodes ) { // don't recreate nodes from children after initial hydration + canvasId = internal.current.id = node.id; + const childNodes = mapChildrenToNodes(children, canvasId); + manager.add(node.id, childNodes); + } } else { if (!id) throw new Error("Root Canvas cannot ommit `id` prop"); if (!node.data._childCanvas || (node.data._childCanvas && !node.data._childCanvas[id])) { const rootNode = createNode(Canvas, { is, children } as any, canvasId, null); internal.current.id = canvasId; manager.pushChildCanvas(node.id, id, rootNode); + } else { + internal.current.id = node.data._childCanvas[id]; } } }, []); diff --git a/packages/core/nodes/useNode.ts b/packages/core/nodes/useNode.ts index 9a40d9ae8..1803457b6 100644 --- a/packages/core/nodes/useNode.ts +++ b/packages/core/nodes/useNode.ts @@ -3,6 +3,7 @@ import { NodeContext } from "./NodeContext"; import { ManagerContext } from "../manager"; import { ManagerMethods, PublicManagerMethods } from "../manager/methods"; import { CraftNodeAPI } from "../interfaces"; +import { isCanvas } from "../utils"; const useNode = () : CraftNodeAPI => { const nodeContext = useContext(NodeContext); @@ -23,7 +24,7 @@ const useNode = () : CraftNodeAPI => { ) }, [state.nodes[id]]); - const connectTarget = useCallback((render) => { + const connectTarget = useCallback((render, nodeMethods) => { return cloneElement(render, { onMouseDown: (e) => { e.stopPropagation(); @@ -31,7 +32,14 @@ const useNode = () : CraftNodeAPI => { }, ref: (ref: any) => { if ( ref ) { - manager.setDOM(id, ref); + manager.setRef(id, "dom", ref); + if ( nodeMethods ) { + if ( nodeMethods.canDrag ) manager.setRef(id, "canDrag", nodeMethods.canDrag); + if ( isCanvas(node) ) { + if ( nodeMethods.incoming ) manager.setRef(id, "incoming", nodeMethods.incoming) + if ( nodeMethods.outgoing ) manager.setRef(id, "outgoing", nodeMethods.outgoing); + } + } } if ( render.ref ) render.ref(ref); } diff --git a/packages/core/render/Renderer.tsx b/packages/core/render/Renderer.tsx index 0076f8329..f7572285b 100644 --- a/packages/core/render/Renderer.tsx +++ b/packages/core/render/Renderer.tsx @@ -7,7 +7,7 @@ import { Canvas } from "../nodes/Canvas"; export const Renderer = ({ children }: any) => { const [state, methods] = useContext(ManagerContext); useEffect(() => { - let node = mapChildrenToNodes({children}, null, "rootNode"); + let node = mapChildrenToNodes({children}, null, "rootNode"); methods.add(null, node); console.log("added root node"); }, []); diff --git a/packages/core/utils/index.tsx b/packages/core/utils/index.tsx index d5aba0225..7d4e7676f 100644 --- a/packages/core/utils/index.tsx +++ b/packages/core/utils/index.tsx @@ -6,7 +6,6 @@ import { TextNode } from "./nodes"; export * from "./nodes"; export * from "./element"; -export const isCanvas = (node: any) => !!node.nodes export const isCraftComponent = (type: any): boolean => !!type.editor; export const defineReactiveProperty = (obj: any, key: string, val?: string, cb?: Function) => { diff --git a/packages/core/utils/nodes.ts b/packages/core/utils/nodes.ts index 0c9f559c2..4ffd84d4e 100644 --- a/packages/core/utils/nodes.ts +++ b/packages/core/utils/nodes.ts @@ -7,6 +7,9 @@ import produce from "immer"; const shortid = require("shortid"); +export const isCanvas = (node: Node) => node.data.type === Canvas + + export const createNode = (component: React.ElementType, props: React.Props, id: NodeId, parent?: NodeId): Node => { let node = produce({}, (node: Node) => { node.id = id; @@ -22,7 +25,12 @@ export const createNode = (component: React.ElementType, props: React.Props } }; node.ref = { - dom: null + dom: null, + canDrag: () => true + }; + if ( isCanvas(node) ) { + (node as CanvasNode).ref.incoming = () => true; + (node as CanvasNode).ref.outgoing = () => true; } }) as Node; @@ -143,4 +151,11 @@ export const getAllParents = (nodes: Nodes, nodeId: NodeId, result:NodeId[] = [] } return result; +} + +export const getAllCanvas = (nodes: Nodes) => { + return Object.keys(nodes).filter(id => { + if (isCanvas(nodes[id]) ) return true; + return false; + }) } \ No newline at end of file