Skip to content

Commit e7b66d2

Browse files
committed
component navigator
1 parent df7e953 commit e7b66d2

File tree

8 files changed

+385
-90
lines changed

8 files changed

+385
-90
lines changed

‎packages/canvas-runtime/src/hooks/useHintOverlays.tsx‎

Lines changed: 73 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import React, { useCallback, useEffect, useState } from "react";
22
import { canvasComponentStore } from "../CanvasComponentData";
33
import { BoxDimension, Dimension, Position } from "../types";
4-
import { getCoords } from "../utils";
4+
import { ComponentCoords, getCoords } from "../utils";
5+
6+
type HintDimension = {
7+
position: Position;
8+
dimension: { width: number; height: number };
9+
};
10+
11+
type BoxOverlay = (comp: { dimension: BoxDimension }) => HintDimension;
12+
513

614
export type HintOverlay = {
715
overlayId: string;
816
compId: string;
917
comp: React.ReactNode;
10-
box: (comp: { dimension: BoxDimension }) => {
11-
position: Position;
12-
dimension: { width: number; height: number };
13-
};
18+
box: BoxOverlay;
1419
};
1520

21+
type HintOverlayDimension = {
22+
box: HintDimension;
23+
bodyCoords: ComponentCoords;
24+
compCoords: ComponentCoords;
25+
}
26+
1627
let hintOverlays: { [overlayId: string]: HintOverlay } = {};
1728
let hintOverlaySubscriber: (() => void) | undefined;
1829

@@ -36,51 +47,67 @@ export function removeHintOverlays(overlayIds: string[]) {
3647
}
3748
}
3849

50+
function calculateBoxDimensions(props: HintOverlay): HintOverlayDimension | null {
51+
if (canvasComponentStore[props.compId]) {
52+
if (
53+
canvasComponentStore["body"].ref.current &&
54+
canvasComponentStore[props.compId].ref.current
55+
) {
56+
const body = canvasComponentStore["body"].ref.current;
57+
const comp = canvasComponentStore[props.compId].ref.current!;
58+
const bodyCoords = getCoords(body);
59+
const compCoords = getCoords(comp);
60+
const {
61+
marginBottom,
62+
marginTop,
63+
marginLeft,
64+
marginRight,
65+
paddingLeft,
66+
paddingRight,
67+
paddingTop,
68+
paddingBottom,
69+
} = getComputedStyle(comp);
70+
const box = props.box({
71+
dimension: {
72+
height: compCoords.height,
73+
width: compCoords.width,
74+
marginBottom: parseFloat(marginBottom),
75+
marginLeft: parseFloat(marginLeft),
76+
marginRight: parseFloat(marginRight),
77+
marginTop: parseFloat(marginTop),
78+
paddingBottom: parseFloat(paddingBottom),
79+
paddingTop: parseFloat(paddingTop),
80+
paddingLeft: parseFloat(paddingLeft),
81+
paddingRight: parseFloat(paddingRight),
82+
},
83+
});
84+
return { box, bodyCoords, compCoords };
85+
}
86+
}
87+
return null;
88+
}
89+
3990
const HintOverlayBox: React.FC<HintOverlay & { scale: number }> = (props) => {
40-
const [box, setBox] = useState<ReturnType<HintOverlay["box"]> | null>(null);
41-
const [bodyPosition, setBodyPosition] = useState<Position | null>(null);
42-
const [compPosition, setCompPosition] = useState<Position | null>(null);
91+
const boxDimensions = calculateBoxDimensions(props);
92+
let initialBox = null, initialBody = null, initialComp = null;
93+
if (boxDimensions) {
94+
initialBox = boxDimensions.box;
95+
initialBody = boxDimensions.bodyCoords;
96+
initialComp = boxDimensions.compCoords;
97+
}
98+
const [box, setBox] = useState<ReturnType<HintOverlay["box"]> | null>(initialBox);
99+
const [bodyPosition, setBodyPosition] = useState<Position | null>(initialBody);
100+
const [compPosition, setCompPosition] = useState<Position | null>(initialComp);
43101
useEffect(() => {
44-
if (canvasComponentStore[props.compId]) {
45-
if (
46-
canvasComponentStore["body"].ref.current &&
47-
canvasComponentStore[props.compId].ref.current
48-
) {
49-
const body = canvasComponentStore["body"].ref.current;
50-
const comp = canvasComponentStore[props.compId].ref.current!;
51-
const bodyCoords = getCoords(body);
52-
const compCoords = getCoords(comp);
53-
const {
54-
marginBottom,
55-
marginTop,
56-
marginLeft,
57-
marginRight,
58-
paddingLeft,
59-
paddingRight,
60-
paddingTop,
61-
paddingBottom,
62-
} = getComputedStyle(comp);
63-
const box = props.box({
64-
dimension: {
65-
height: compCoords.height,
66-
width: compCoords.width,
67-
marginBottom: parseFloat(marginBottom),
68-
marginLeft: parseFloat(marginLeft),
69-
marginRight: parseFloat(marginRight),
70-
marginTop: parseFloat(marginTop),
71-
paddingBottom: parseFloat(paddingBottom),
72-
paddingTop: parseFloat(paddingTop),
73-
paddingLeft: parseFloat(paddingLeft),
74-
paddingRight: parseFloat(paddingRight),
75-
},
76-
});
77-
setBox(box);
78-
setBodyPosition({ top: bodyCoords.top, left: bodyCoords.left });
79-
setCompPosition({ top: compCoords.top, left: compCoords.left });
80-
}
102+
const boxDimensions = calculateBoxDimensions(props);
103+
if (!boxDimensions) {
104+
return;
81105
}
106+
const { box, bodyCoords, compCoords } = boxDimensions;
107+
setBox(box);
108+
setBodyPosition({ top: bodyCoords.top, left: bodyCoords.left });
109+
setCompPosition({ top: compCoords.top, left: compCoords.left });
82110
}, [props]);
83-
84111
return (
85112
<React.Fragment>
86113
{box && compPosition && bodyPosition ? (
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const CaretDown = () => {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
aria-hidden="true"
6+
focusable="false"
7+
width="16"
8+
height="16"
9+
viewBox="0 0 16 16">
10+
<path
11+
d="M4 6l3 .01h2L12 6l-4 4-4-4z"
12+
fill="#ffffff"
13+
/>
14+
</svg>
15+
)
16+
}
17+
18+
export default CaretDown;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const CaretRight = () => {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
aria-hidden="true"
6+
focusable="false"
7+
width="16"
8+
height="16"
9+
viewBox="0 0 16 16">
10+
<path
11+
d="M6 12l.01-3V7L6 4l4 4-4 4z"
12+
fill="#ffffff"
13+
/>
14+
</svg>
15+
)
16+
}
17+
18+
export default CaretRight;
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { ComponentNode } from "../types";
22

3-
export type ComponentNavigatorProps = {
4-
rootNode: ComponentNode;
5-
// Call onChange everytime the selected node is repositioned
6-
onChange: (change: { id: string; parentId: string; index: number }) => void;
7-
// Call onHover everytime user hovers over a component
8-
onHover: (id: string) => void;
9-
// Call onSelect everytime user clicks on a component
10-
onSelect: (id: string) => void;
11-
// Call onDragStart whenever the drag process starts
12-
onDragStart: (id: string) => void;
13-
// Call onDragEnd whenever the drag process stops
14-
onDragEnd: (id: string) => void;
15-
};
3+
// export type ComponentNavigatorProps = {
4+
// rootNode: ComponentNode;
5+
// // Call onChange everytime the selected node is repositioned
6+
// onChange: (change: { id: string; parentId: string; index: number }) => void;
7+
// // Call onHover everytime user hovers over a component
8+
// onHover: (id: string) => void;
9+
// // Call onSelect everytime user clicks on a component
10+
// onSelect: (id: string) => void;
11+
// // Call onDragStart whenever the drag process starts
12+
// onDragStart: (id: string) => void;
13+
// // Call onDragEnd whenever the drag process stops
14+
// onDragEnd: (id: string) => void;
15+
// };
1616

17-
export const ComponentNavigator: React.FC<ComponentNavigatorProps> = (
18-
props
19-
) => {
20-
return <div></div>;
21-
};
17+
// export const ComponentNavigator: React.FC<ComponentNavigatorProps> = (
18+
// props
19+
// ) => {
20+
// return <div></div>;
21+
// };
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { useState, useEffect, useCallback } from "react";
2+
import { BrowserForestManager, manifestRegistryController, api } from "@atrilabs/core";
3+
import { PatchEvent, Tree } from "@atrilabs/forest";
4+
import ComponentTreeId from "@atrilabs/app-design-forest/lib/componentTree?id";
5+
import { ComponentNode } from '../types';
6+
7+
8+
export const useComponentNodes = () => {
9+
const { items: newTree, oldItems } = transformTreeToComponentNodes(BrowserForestManager.currentForest.tree(ComponentTreeId)!, {});
10+
const [oldItemsMap, setOldItemsMap] = useState<{ [id: string]: ComponentNode }>(oldItems);
11+
12+
const [items, setItems] = useState<ComponentNode[]>(newTree);
13+
useEffect(() => {
14+
const unsubscribe = BrowserForestManager.currentForest.subscribeForest((_) => {
15+
const newTree = BrowserForestManager.currentForest.tree(ComponentTreeId)!;
16+
const { items, oldItems } = transformTreeToComponentNodes(newTree, oldItemsMap);
17+
setItems(items);
18+
setOldItemsMap(oldItems);
19+
20+
return () => {
21+
unsubscribe();
22+
};
23+
});
24+
}, []);
25+
const patchCb = useCallback(
26+
(nodeId: string, parentId: any) => {
27+
const forestPkgId = BrowserForestManager.currentForest.forestPkgId;
28+
const forestId = BrowserForestManager.currentForest.forestId;
29+
const newParent = recursionFind(parentId, items);
30+
if (newParent?.type !== 'acceptsChild') {
31+
return;
32+
}
33+
const node = recursionFind(nodeId, items);
34+
if (!node) {
35+
return;
36+
}
37+
newParent.children = newParent.children!.concat(node);
38+
newParent.children?.forEach((curr, index) => {
39+
const patchEvent: PatchEvent = {
40+
type: `PATCH$$${ComponentTreeId}`,
41+
slice: {
42+
parent: {
43+
id: newParent?.id,
44+
index: index + 1,
45+
}
46+
},
47+
id: curr.id,
48+
};
49+
api.postNewEvent(forestPkgId, forestId, patchEvent, () => { });
50+
});
51+
},
52+
[items]
53+
);
54+
const toggleNode = useCallback(
55+
(nodeId: string) => {
56+
const newItems = items;
57+
const itemsMap = oldItemsMap;
58+
const node = recursionFind(nodeId, newItems);
59+
if (!node) {
60+
return;
61+
}
62+
const nodeMapItem = itemsMap[nodeId];
63+
if (!nodeMapItem) {
64+
return;
65+
}
66+
nodeMapItem.open = !nodeMapItem.open;
67+
node.open = nodeMapItem.open;
68+
itemsMap[nodeId] = nodeMapItem;
69+
setOldItemsMap(itemsMap);
70+
setItems([...newItems]);
71+
},
72+
[items]
73+
);
74+
return { items, patchCb, toggleNode };
75+
};
76+
77+
function recursionFind(nodeId: string, items: ComponentNode[]): ComponentNode | undefined {
78+
for (let i = 0; i < items.length; i++) {
79+
if (items[i].id === nodeId) {
80+
return items[i];
81+
}
82+
if (items[i].children) {
83+
const found = recursionFind(nodeId, items[i].children!);
84+
if (found) {
85+
return found;
86+
}
87+
}
88+
}
89+
}
90+
91+
function transformTreeToComponentNodes(tree: Tree, oldItemsMap: { [id: string]: ComponentNode }): { items: ComponentNode[], oldItems: { [id: string]: ComponentNode } } {
92+
const oldBody = oldItemsMap['body'];
93+
const itemsMap: { [id: string]: ComponentNode } = {
94+
'body': { type: 'acceptsChild', id: 'body', name: 'Root', open: oldBody ? oldBody.open : true, children: [], index: 1 },
95+
};
96+
const manifestRegistry = manifestRegistryController.readManifestRegistry();
97+
98+
Object.keys(tree.nodes).forEach((id) => {
99+
const node = tree.nodes[id];
100+
const manifest = manifestRegistry[node.meta.manifestSchemaId].components.find(
101+
(curr) => {
102+
return curr.pkg === node.meta.pkg && curr.component.meta.key === node.meta.key;
103+
}
104+
);
105+
const acceptsChild = manifest?.component?.dev?.acceptsChild ? 'acceptsChild' : 'normal';
106+
const oldBody = oldItemsMap[node.id];
107+
itemsMap[node.id] = { type: acceptsChild, id: node.id, name: node.state.alias, open: oldBody ? oldBody.open : true, children: [], index: node.state.parent.index };
108+
});
109+
const items: ComponentNode[] = [itemsMap['body']];
110+
Object.keys(itemsMap).forEach((id) => {
111+
const node = tree.nodes[id];
112+
if (!node) {
113+
return;
114+
}
115+
const parent = itemsMap[node.state.parent.id];
116+
if (parent && parent.type === 'acceptsChild') {
117+
parent.children?.push(itemsMap[id]);
118+
parent.children = parent.children?.sort((a, b) => a.index - b.index);
119+
}
120+
});
121+
return { items: items.sort((a, b) => a.name.localeCompare(b.name)), oldItems: itemsMap };
122+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getId } from "@atrilabs/core";
2+
import {
3+
addOrModifyHintOverlays,
4+
removeHintOverlays,
5+
} from "@atrilabs/canvas-runtime";
6+
import { orange600 } from "@atrilabs/design-system";
7+
8+
type FilledLineProps = {
9+
fill: string;
10+
};
11+
12+
const FilledLine: React.FC<FilledLineProps> = (props) => {
13+
return (
14+
<div
15+
style={{ border: `solid ${props.fill} 2px`, height: "100%", width: "100%" }}
16+
></div>
17+
);
18+
};
19+
20+
export const clickOverlay = getId();
21+
export const hoverOverlay = getId();
22+
23+
export const useMarginOverlay = (overlay: string) => {
24+
const createMarginOverlay = (compId: string) => {
25+
addOrModifyHintOverlays({
26+
[overlay]: {
27+
overlayId: overlay,
28+
compId,
29+
comp: <FilledLine fill={orange600} />,
30+
box: (dim) => {
31+
return {
32+
dimension: dim.dimension,
33+
position: { top: 0, left: 0 },
34+
};
35+
},
36+
},
37+
});
38+
};
39+
const removeMarginOverlay = () => {
40+
removeHintOverlays([overlay]);
41+
};
42+
return { createMarginOverlay, removeMarginOverlay };
43+
}

0 commit comments

Comments
 (0)