Skip to content

Commit 2b8f763

Browse files
authored
Merge pull request #8 from diegovazquezny/tree
Tree
2 parents 8d330d1 + 24d0044 commit 2b8f763

File tree

7 files changed

+180
-16
lines changed

7 files changed

+180
-16
lines changed

app/src/components/bottom/BottomTabs.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
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
import { emitKeypressEvents } from 'readline';
911

1012
const BottomTabs = () => {
1113
// state that controls which tab the user is on
14+
const [state, dispatch] = useContext(stateContext);
1215
const [tab, setTab] = useState(0);
1316
const classes = useStyles();
1417
treeWrapper: HTMLDivElement;
@@ -18,6 +21,9 @@ const BottomTabs = () => {
1821
setTab(value);
1922
};
2023

24+
const { HTMLTypes } = state;
25+
const { components } = state;
26+
2127
return (
2228
<div className={classes.root}>
2329
<Box display="flex" justifyContent="space-between">
@@ -34,9 +40,15 @@ const BottomTabs = () => {
3440
classes={{ root: classes.tabRoot, selected: classes.tabSelected }}
3541
label="Code Preview"
3642
/>
43+
<Tab
44+
disableRipple
45+
classes={{ root: classes.tabRoot, selected: classes.tabSelected }}
46+
label="Component Tree"
47+
/>
3748
</Tabs>
3849
</Box>
3950
{tab === 0 && <CodePreview />}
51+
{tab === 1 && <Tree data={components} />}
4052
</div>
4153
);
4254
};

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"connected-react-router": "^6.8.0",
111111
"cookie-parser": "^1.4.5",
112112
"cors": "^2.8.5",
113+
"d3": "^6.2.0",
113114
"electron-debug": "^3.1.0",
114115
"electron-devtools-installer": "^2.2.4",
115116
"electron-splashscreen": "^1.0.0",
@@ -130,6 +131,7 @@
130131
"react-dnd-html5-backend": "^11.1.3",
131132
"react-dom": "^16.4.1",
132133
"react-router-dom": "^5.2.0",
134+
"resize-observer-polyfill": "^1.5.1",
133135
"seamless-immutable": "^7.1.4",
134136
"source-map-support": "^0.5.19",
135137
"uuid": "^8.2.0"

0 commit comments

Comments
 (0)