Skip to content

Commit be2cdf3

Browse files
tree getting close
1 parent 7718220 commit be2cdf3

File tree

6 files changed

+170
-16
lines changed

6 files changed

+170
-16
lines changed

app/src/components/bottom/BottomTabs.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useContext } from 'react';
22
import { makeStyles } from '@material-ui/core/styles';
3+
import { stateContext } from '../../context/context';
34
import Tabs from '@material-ui/core/Tabs';
45
import Tab from '@material-ui/core/Tab';
56
// import Tree from 'react-d3-tree';
67
import CodePreview from './CodePreview';
78
import Box from '@material-ui/core/Box';
9+
import Tree from '../../tree/TreeChart';
810

911
const BottomTabs = () => {
1012
// state that controls which tab the user is on
13+
const [state, dispatch] = useContext(stateContext);
1114
const [tab, setTab] = useState(0);
1215
const classes = useStyles();
1316
treeWrapper: HTMLDivElement;
@@ -17,6 +20,9 @@ const BottomTabs = () => {
1720
setTab(value);
1821
};
1922

23+
const { HTMLTypes } = state;
24+
const { components } = state;
25+
2026
return (
2127
<div className={classes.root}>
2228
<Box display="flex" justifyContent="space-between">
@@ -33,9 +39,15 @@ const BottomTabs = () => {
3339
classes={{ root: classes.tabRoot, selected: classes.tabSelected }}
3440
label="Code Preview"
3541
/>
42+
<Tab
43+
disableRipple
44+
classes={{ root: classes.tabRoot, selected: classes.tabSelected }}
45+
label="Component Tree"
46+
/>
3647
</Tabs>
3748
</Box>
3849
{tab === 0 && <CodePreview />}
50+
{tab === 1 && <Tree data={components} />}
3951
</div>
4052
);
4153
};

app/src/context/initialState.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export const initialState: State = {
1111
name: 'index',
1212
style: {},
1313
code: '<div>Drag in a component or HTML element into the canvas!</div>',
14-
children: []
14+
children: [],
15+
isPage: true
1516
}
1617
],
1718
projectType: 'Next.js',

app/src/interfaces/Interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface State {
1515
export interface ChildElement {
1616
type: string;
1717
typeId: number;
18+
name: string;
1819
childId: number;
1920
// update this interface later so that we enforce that each value of style object is a string
2021
style: object;
@@ -28,6 +29,7 @@ export interface Component {
2829
style: object;
2930
code: string;
3031
children: ChildElement[];
32+
isPage: boolean;
3133
}
3234

3335
export interface Action {

app/src/reducers/componentReducer.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import initialState from '../context/initialState';
88
import generateCode from '../helperFunctions/generateCode';
99
import cloneDeep from '../helperFunctions/cloneDeep';
10+
import HTMLTypes from '../context/HTMLTypes';
1011

1112
const reducer = (state: State, action: Action) => {
1213
// if the project type is set as Next.js, next component code should be generated
@@ -93,7 +94,6 @@ const reducer = (state: State, action: Action) => {
9394
// TODO: output parent name and id to refocus canvas on parent
9495
let isChild: boolean = false;
9596
state.components.forEach(comp => {
96-
console.log('comp =>', comp);
9797
comp.children.forEach(child => {
9898
if (child.type === 'Component' && child.typeId === id) {
9999
isChild = true;
@@ -109,6 +109,8 @@ const reducer = (state: State, action: Action) => {
109109
components.forEach((comp, i) => comp.id = i + 1);
110110
}
111111

112+
const deleteById = (id: number): Component[] => [...state.components].filter(comp => comp.id != id);
113+
112114
switch (action.type) {
113115
// Add a new component type
114116
// add component to the component array and increment our counter for the componentId
@@ -126,7 +128,8 @@ const reducer = (state: State, action: Action) => {
126128
nextChildId: 1,
127129
style: {},
128130
code: '',
129-
children: []
131+
children: [],
132+
isPage: action.payload.root
130133
};
131134
const components = [...state.components];
132135
components.push(newComponent);
@@ -169,9 +172,15 @@ const reducer = (state: State, action: Action) => {
169172
return state;
170173
}
171174

175+
let newName = HTMLTypes.reduce((name, el) => {
176+
if (typeId === el.id) name = el.tag;
177+
return name;
178+
},'');
179+
172180
const newChild: ChildElement = {
173181
type,
174182
typeId,
183+
name: newName,
175184
childId: state.nextChildId,
176185
style: {},
177186
children: []
@@ -187,7 +196,6 @@ const reducer = (state: State, action: Action) => {
187196
}
188197
// if there is a childId (childId here references the direct parent of the new child) find that child and a new child to its children array
189198

190-
191199
else {
192200
const directParent = findChild(parentComponent, childId);
193201
directParent.children.push(newChild);
@@ -294,15 +302,12 @@ const reducer = (state: State, action: Action) => {
294302
components,
295303
state.canvasFocus.componentId
296304
);
297-
console.log('curr comp', component);
298305
// find the moved element's former parent
299306
// delete the element from it's former parent's children array
300307
const { directParent, childIndexValue } = findParent(
301308
component,
302309
state.canvasFocus.childId
303310
);
304-
console.log('direct parent', directParent);
305-
console.log('child index', childIndexValue);
306311
const child = { ...directParent.children[childIndexValue] };
307312
directParent.children.splice(childIndexValue, 1);
308313
component.code = generateCode(
@@ -318,15 +323,21 @@ const reducer = (state: State, action: Action) => {
318323

319324
case 'DELETE PAGE': {
320325
const id: number = state.canvasFocus.componentId;
321-
// console.log('id: ', id);
322326

323-
const components = [...state.components].filter(comp => comp.id != id);
324-
// console.log('components: ', components);
327+
// remove component and update ids
328+
const components: Component[] = deleteById(id);
325329
updateIds(components);
326-
// // console.log('all components', state.components);
327330

331+
// rebuild root components
332+
const rootComponents: number[] = [];
333+
components.forEach(comp => {
334+
if (comp.isPage) rootComponents.push(comp.id);
335+
});
336+
337+
//TODO: where should canvas focus after deleting comp?
328338
const canvasFocus = { componentId: 1, childId: null }
329-
return {...state, components, canvasFocus}
339+
340+
return {...state, rootComponents, components, canvasFocus}
330341
}
331342
case 'DELETE REUSABLE COMPONENT' : {
332343
// TODO: bug when deleting element inside page
@@ -335,16 +346,17 @@ const reducer = (state: State, action: Action) => {
335346

336347
const id: number = state.canvasFocus.componentId;
337348
// check if component is a child element of a page
349+
// check if id is inside root components
338350
if(isChildOfPage(id)) {
339351
// TODO: include name of parent in alert
340352
// TODO: change canvas focus to parent
341-
//dialog.showErrorBox('error','Reusable components inside of a page must be deleted from the page');
342-
alert('Reusable components inside of a page must be deleted from the page');
353+
// TODO: modal
354+
console.log('Reusable components inside of a page must be deleted from the page');
343355
//const canvasFocus:Object = { componentId: id, childId: null };
344356
return { ...state }
345357
}
346358
// filter out components that don't match id
347-
const components: Component[] = [...state.components].filter(comp => comp.id != id);
359+
const components: Component[] = deleteById(id);
348360

349361
updateIds(components);
350362

app/src/tree/TreeChart.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useRef, useEffect, useContext } from "react";
2+
import { select, hierarchy, tree, linkHorizontal } from "d3";
3+
import { stateContext } from '../context/context';
4+
import useResizeObserver from "./useResizeObserver";
5+
6+
function usePrevious(value) {
7+
const ref = useRef();
8+
useEffect(() => {
9+
ref.current = value;
10+
});
11+
return ref.current;
12+
}
13+
14+
function TreeChart({ data }) {
15+
const [state, dispatch] = useContext(stateContext);
16+
const svgRef = useRef();
17+
const wrapperRef = useRef();
18+
const dimensions = useResizeObserver(wrapperRef);
19+
20+
// we save data to see if it changed
21+
const previouslyRenderedData = usePrevious(data);
22+
23+
// will be called initially and on every data change
24+
useEffect(() => {
25+
const svg = select(svgRef.current);
26+
27+
// use dimensions from useResizeObserver,
28+
// but use getBoundingClientRect on initial render
29+
// (dimensions are null for the first render)
30+
const { width, height } =
31+
dimensions || wrapperRef.current.getBoundingClientRect();
32+
33+
// transform hierarchical data
34+
const root = hierarchy(data[0]);
35+
const treeLayout = tree().size([height, width]);
36+
37+
// Returns a new link generator with horizontal tangents.
38+
// To visualize links in a tree diagram rooted on the left edge of the display
39+
const linkGenerator = linkHorizontal()
40+
.x(link => link.y)
41+
.y(link => link.x);
42+
43+
// insert our data into the tree layout
44+
treeLayout(root);
45+
46+
// nodes
47+
svg
48+
.selectAll(".node") // grab all nodes
49+
.data(root.descendants())
50+
.join(enter => enter.append("circle").attr("opacity", 0))
51+
.attr("class", "node")
52+
/*
53+
The cx, cy attributes are associated with the circle and ellipse elements and designate the centre of each shape. The coordinates are set from the top, left hand corner of the web page.
54+
cx: The position of the centre of the element in the x axis measured from the left side of the screen.
55+
cy: The position of the centre of the element in the y axis measured from the top of the screen.
56+
*/
57+
.attr("cx", node => node.y)
58+
.attr("cy", node => node.x)
59+
.attr("r", 4)
60+
.attr("opacity", 1);
61+
62+
// links
63+
const enteringAndUpdatingLinks = svg
64+
.selectAll(".link")
65+
.data(root.links())
66+
.join("path")
67+
.attr("class", "link")
68+
.attr("d", linkGenerator)
69+
.attr("stroke-dasharray", function() {
70+
const length = this.getTotalLength();
71+
return `${length} ${length}`;
72+
})
73+
.attr("stroke", "black")
74+
.attr("fill", "none")
75+
.attr("opacity", 1);
76+
77+
if (data !== previouslyRenderedData) {
78+
enteringAndUpdatingLinks
79+
.attr("stroke-dashoffset", function() {
80+
return this.getTotalLength();
81+
})
82+
.attr("stroke-dashoffset", 0);
83+
}
84+
85+
// labels
86+
svg
87+
.selectAll(".label")
88+
.data(root.descendants())
89+
.join(enter => enter.append("text").attr("opacity", 0))
90+
.attr("class", "label")
91+
.attr("x", node => node.y)
92+
.attr("y", node => node.x - 12)
93+
.attr("text-anchor", "middle")
94+
.attr("font-size", 24)
95+
.text(node => node.data.name)
96+
.attr("opacity", 1);
97+
}, [data, dimensions, previouslyRenderedData]);
98+
99+
return (
100+
<div ref={wrapperRef} style={{ marginBottom: "2rem" }}>
101+
<svg ref={svgRef}></svg>
102+
</div>
103+
);
104+
}
105+
106+
export default TreeChart;

app/src/tree/useResizeObserver.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useEffect, useState } from "react";
2+
import ResizeObserver from "resize-observer-polyfill";
3+
4+
const useResizeObserver = ref => {
5+
const [dimensions, setDimensions] = useState(null);
6+
useEffect(() => {
7+
const observeTarget = ref.current;
8+
const resizeObserver = new ResizeObserver(entries => {
9+
entries.forEach(entry => {
10+
setDimensions(entry.contentRect);
11+
});
12+
});
13+
resizeObserver.observe(observeTarget);
14+
return () => {
15+
resizeObserver.unobserve(observeTarget);
16+
};
17+
}, [ref]);
18+
return dimensions;
19+
};
20+
21+
export default useResizeObserver;

0 commit comments

Comments
 (0)