ui-layout-manager-dev 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-layout-manager-dev",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A react component to manage layout and themes in single page applications.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -62,7 +62,8 @@
62
62
  "react-dom": "^18.2.0"
63
63
  },
64
64
  "dependencies": {
65
+ "@dnd-kit/core": "^6.3.1",
65
66
  "leaflet": "^1.9.4",
66
- "sample-ui-component-library": "^0.0.0-beta"
67
+ "sample-ui-component-library": "^0.0.16-dev"
67
68
  }
68
69
  }
@@ -22,6 +22,10 @@ export const HandleBar = ({orientation, parent, sibling1, sibling2}) => {
22
22
  * It does the following:
23
23
  * - Determines the dynamic prop being modified (width or height)
24
24
  * - Determines the mouse down property to track (clientY or clientX)
25
+ * - Gets the references to the sibling elements
26
+ * - Gets the sibling sizes from the layout
27
+ * - Gets the sibling sizes from the layout reference
28
+ * - Saves the information in drag start info ref for access
25
29
  * @param {MouseEvent} e
26
30
  */
27
31
  const handleMouseDown = (e) => {
@@ -110,7 +114,7 @@ export const HandleBar = ({orientation, parent, sibling1, sibling2}) => {
110
114
  sibling1: sibling1,
111
115
  sibling2: sibling2
112
116
  });
113
- }, 0.1);
117
+ }, 4);
114
118
 
115
119
  // Don't update container sizes we are past min or max values.
116
120
  const sibling1SizeKeys = Object.keys(startInfo.sibling1LayoutConfig);
@@ -154,10 +158,25 @@ export const HandleBar = ({orientation, parent, sibling1, sibling2}) => {
154
158
  e.stopPropagation();
155
159
  document.removeEventListener("mousemove", handleMouseMove);
156
160
  document.removeEventListener("mouseup", handleMouseUp);
157
- handleRef.current.classList.remove(dragStartInfo.current.hoverClass);
158
- dragStartInfo.current = null;
161
+ clearTimeout(timerRef.current);
162
+ if (handleRef.current && dragStartInfo.current) {
163
+ handleRef.current.classList.remove(dragStartInfo.current.hoverClass);
164
+ }
165
+ dragStartInfo.current = null;
159
166
  }
160
167
 
168
+
169
+ useEffect(() => {
170
+ return () => {
171
+ document.removeEventListener("mousemove", handleMouseMove);
172
+ document.removeEventListener("mouseup", handleMouseUp);
173
+ if (timerRef.current) {
174
+ clearTimeout(timerRef.current);
175
+ timerRef.current = null;
176
+ }
177
+ }
178
+ }, []);
179
+
161
180
  return (
162
181
  <React.Fragment >
163
182
  {
@@ -1,9 +1,20 @@
1
1
  import React, { useEffect, useLayoutEffect, useState, useRef, useCallback, useContext } from "react";
2
2
  import PropTypes from 'prop-types';
3
3
  import { Container } from "../Container/Container";
4
+ import { HandleBar } from "../HandleBar/HandleBar";
4
5
  import { useLayoutController } from "../../Providers/LayoutProvider";
6
+ import {
7
+ DndContext,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors
11
+ } from "@dnd-kit/core";
12
+
13
+ import { useDragState } from "../../Providers/DragProvider";
5
14
 
6
15
  import "./RootContainer.scss"
16
+
17
+
7
18
  /**
8
19
  * Root node for the layout tree. This component will start
9
20
  * rendering the tree and it will also watch for changes in the
@@ -14,21 +25,23 @@ import "./RootContainer.scss"
14
25
  export const RootContainer = () => {
15
26
  const controller = useLayoutController();
16
27
 
28
+ const { dragState, handleDragStart, handleDragOver, clearDrag } = useDragState();
29
+
17
30
  const rootRef = useRef(null);
18
31
  const timerRef = useRef(null);
19
32
  const resizingRef = useRef(false);
20
-
33
+
21
34
  // Create the container API that will be used by the controller.
22
35
  const rootContainerAPI = useRef({});
23
36
  rootContainerAPI.current = {};
24
37
 
25
38
  const [childElements, setChildElements] = useState(null);
26
-
39
+
27
40
  /**
28
41
  * Renders child containers recursively.
29
42
  */
30
43
  const processContainer = useCallback((node) => {
31
- const childElements = [];
44
+ const childElements = [];
32
45
  for (let index = 0; index < node.children.length; index++) {
33
46
  const childNode = node.children[index];
34
47
 
@@ -36,31 +49,43 @@ export const RootContainer = () => {
36
49
  const child = controller.ldf.containers[node.children[index].containerId];
37
50
  child.parent = node;
38
51
  childElements.push(
39
- <Container key={index} meta={node.children[index]} id={child.id} node={child}/>
52
+ <Container key={index} meta={node.children[index]} id={child.id} node={child} />
40
53
  );
41
54
  } else if (childNode.type === "handleBar") {
42
55
  if (node.orientation === "horizontal") {
43
56
  childElements.push(
44
- <div className="verticalLine"></div>
57
+ <HandleBar
58
+ key={index}
59
+ orientation="vertical"
60
+ parent={node.id}
61
+ sibling1={childNode.sibling1}
62
+ sibling2={childNode.sibling2}
63
+ />
45
64
  );
46
65
  } else if (node.orientation === "vertical") {
47
66
  childElements.push(
48
- <div className="horizontalLine"></div>
67
+ <HandleBar
68
+ key={index}
69
+ orientation="horizontal"
70
+ parent={node.id}
71
+ sibling1={childNode.sibling1}
72
+ sibling2={childNode.sibling2}
73
+ />
49
74
  );
50
75
  }
51
76
  }
52
77
  };
53
78
  return childElements;
54
- },[controller]);
79
+ }, [controller]);
55
80
 
56
81
 
57
82
  useLayoutEffect(() => {
58
- if (controller) {
83
+ if (controller) {
59
84
  const rootNode = controller.ldf.containers[controller.ldf.layoutRoot];
60
85
  const hasChildren = rootNode.children && rootNode.children.length > 0
61
86
  controller.registerContainer(rootNode.id, rootContainerAPI, rootRef.current);
62
87
 
63
- if (hasChildren) {
88
+ if (hasChildren) {
64
89
  if (rootNode.orientation === "horizontal") {
65
90
  rootRef.current.style.flexDirection = "row";
66
91
  } else if (rootNode.orientation === "vertical") {
@@ -68,7 +93,7 @@ export const RootContainer = () => {
68
93
  }
69
94
  }
70
95
 
71
- setChildElements(hasChildren?processContainer(rootNode):null);
96
+ setChildElements(hasChildren ? processContainer(rootNode) : null);
72
97
 
73
98
  // Create resize observer to monitor changes in the root container size.
74
99
  const observer = new ResizeObserver((entries) => {
@@ -96,11 +121,43 @@ export const RootContainer = () => {
96
121
  }
97
122
  }, [controller]);
98
123
 
124
+ const sensors = useSensors(
125
+ useSensor(PointerSensor, {
126
+ activationConstraint: {
127
+ distance: 8
128
+ }
129
+ })
130
+ );
131
+
132
+ // Manually track the drag position for smooth overlay
133
+ const [dragPos, setDragPos] = useState({ left: 0, top: 0 });
134
+ useEffect(() => {
135
+ if (!dragState.isDragging) return;
136
+ const handleMove = (e) => {setDragPos({ left: e.clientX, top: e.clientY })};
137
+ window.addEventListener("pointermove", handleMove);
138
+ return () => {
139
+ window.removeEventListener("pointermove", handleMove);
140
+ };
141
+ }, [dragState.isDragging]);
142
+
99
143
  return (
100
- <div className="root-container">
101
- <div ref={rootRef} className="relative-container">
102
- {childElements}
144
+ <DndContext sensors={sensors}
145
+ onDragStart={handleDragStart}
146
+ onDragOver={handleDragOver}
147
+ onDragEnd={clearDrag}
148
+ onDragCancel={clearDrag}>
149
+
150
+ <div className="root-container">
151
+ <div ref={rootRef} className="relative-container">
152
+ {childElements}
153
+ </div>
103
154
  </div>
104
- </div>
155
+
156
+ {dragState.isDragging && (
157
+ <div className="drag-overlay" style={dragPos}>
158
+ {dragState?.activeData?.preview}
159
+ </div>
160
+ )}
161
+ </DndContext>
105
162
  );
106
163
  }
@@ -11,3 +11,9 @@
11
11
  height: 100%;
12
12
  overflow: hidden;
13
13
  }
14
+
15
+ .drag-overlay {
16
+ position: fixed;
17
+ pointer-events: none;
18
+ z-index: 9999;
19
+ }
@@ -11,6 +11,9 @@ import TRANSFORMATION_TYPES from "../TRANSFORMATION_TYPES";
11
11
  * So rather than fully collapsing the container, it sets up a
12
12
  * minimum size, this willl be useful for accordians and other containers.
13
13
  */
14
+
15
+ const COLLAPSE_THRESHOLD_PX = 50
16
+
14
17
  export class HandleRulesEnforcer {
15
18
  /**
16
19
  * Initialize the child rule enforcer.
@@ -69,11 +72,13 @@ export class HandleRulesEnforcer {
69
72
  return;
70
73
  }
71
74
 
72
- if (this.handleMetadata.handle.x < 100) {
75
+ // Hide and unhide containers from left of container.
76
+ if (this.handleMetadata.handle.x < COLLAPSE_THRESHOLD_PX) {
77
+ // Handle is less than collapse threshold from the left
73
78
  this.args = {style: {"display":"none", "min-width":0}}
74
79
  this.sibling1.hidden = true;
75
80
  this.activeSibling = this.sibling1.id;
76
- } else if (this.handleMetadata.handle.x > 100 && this.sibling1.hidden) {
81
+ } else if (this.handleMetadata.handle.x > COLLAPSE_THRESHOLD_PX && this.sibling1.hidden) {
77
82
  this.args = {style: {"display":"flex"}}
78
83
  const sibling = this.getSiblingProps(this.sibling1.id);
79
84
  if ("min" in sibling.size) {
@@ -81,14 +86,15 @@ export class HandleRulesEnforcer {
81
86
  }
82
87
  this.sibling1.hidden = false;
83
88
  this.activeSibling = this.sibling1.id;
84
- }
85
-
89
+ }
86
90
 
87
- if (this.handleMetadata.handle.x > totalWidth - 100) {
91
+ // Hide and unhide containrs from right of container.
92
+ if (this.handleMetadata.handle.x > totalWidth - COLLAPSE_THRESHOLD_PX) {
93
+ // Handle is greater than collapse threshold from right, so unhide container if it is hidden.
88
94
  this.args = {style: {"display":"none", "min-width":0}}
89
95
  this.sibling2.hidden = true;
90
96
  this.activeSibling = this.sibling2.id;
91
- } else if (this.handleMetadata.handle.x < totalWidth - 100 && this.sibling2.hidden) {
97
+ } else if (this.handleMetadata.handle.x < totalWidth - COLLAPSE_THRESHOLD_PX && this.sibling2.hidden) {
92
98
  this.args = {style: {"display":"flex"}}
93
99
  const sibling = this.getSiblingProps(this.sibling2.id);
94
100
  if ("min" in sibling.size) {
@@ -101,7 +107,7 @@ export class HandleRulesEnforcer {
101
107
 
102
108
 
103
109
  /**
104
- * Process the vertical containers.
110
+ * Process the horizontal containers.
105
111
  */
106
112
  processHorizontalContainers () {
107
113
  const totalHeight = this.handleMetadata.sizes[this.parent.id].height;
@@ -113,11 +119,14 @@ export class HandleRulesEnforcer {
113
119
  return;
114
120
  }
115
121
 
116
- if (this.handleMetadata.handle.y < 100) {
122
+ // Hide and unhide containers from top of container.
123
+ if (this.handleMetadata.handle.y < COLLAPSE_THRESHOLD_PX) {
124
+ // Handle is less than collapse threshold from the top
117
125
  this.args = {style: {"display":"none", "min-height":0}}
118
126
  this.sibling1.hidden = true;
119
127
  this.activeSibling = this.sibling1.id;
120
- } else if (this.handleMetadata.handle.y > 100 && this.sibling1.hidden) {
128
+ } else if (this.handleMetadata.handle.y > COLLAPSE_THRESHOLD_PX && this.sibling1.hidden) {
129
+ // Handle is greater than collapse threshold from top, so unhide container if it is hidden.
121
130
  this.args = {style: {"display":"flex"}}
122
131
  const sibling = this.getSiblingProps(this.sibling1.id);
123
132
  if ("min" in sibling.size) {
@@ -127,12 +136,14 @@ export class HandleRulesEnforcer {
127
136
  this.activeSibling = this.sibling1.id;
128
137
  }
129
138
 
130
-
131
- if (this.handleMetadata.handle.y > totalHeight - 100) {
139
+ // Hide and unhide containrs from bottom of container.
140
+ if (this.handleMetadata.handle.y > totalHeight - COLLAPSE_THRESHOLD_PX) {
141
+ // Handle is less than collapse threshold from the bottom
132
142
  this.args = {style: {"display":"none", "min-height":0}}
133
143
  this.sibling2.hidden = true;
134
144
  this.activeSibling = this.sibling2.id;
135
- } else if (this.handleMetadata.handle.y < totalHeight - 100 && this.sibling2.hidden) {
145
+ } else if (this.handleMetadata.handle.y < totalHeight - COLLAPSE_THRESHOLD_PX && this.sibling2.hidden) {
146
+ // Handle is greater than collapse threshold from bottom, so unhide container if it is hidden.
136
147
  this.args = {style: {"display":"flex"}}
137
148
  const sibling = this.getSiblingProps(this.sibling2.id);
138
149
  if ("min" in sibling.size) {
@@ -75,7 +75,7 @@ export class LayoutEditor {
75
75
  childStyle["flex"] = "0 0 auto";
76
76
  break;
77
77
  case "fill":
78
- childStyle["flexGrow"] = 1;
78
+ childStyle["flex-grow"] = 1;
79
79
  break;
80
80
  default:
81
81
  throw new Error(`Unknown size type "${child.size.initial.type}" in LDF configuration`);
@@ -154,7 +154,7 @@ export class LayoutEditor {
154
154
  const parent = this.ldf.containers[containerId];
155
155
 
156
156
  // If node is not split, then it has no children and is a leaf node, so we return.
157
- if ((!parent.type ? parent.type === "split": false) || (!("children" in parent))) {
157
+ if (parent.type !== "split" || !("children" in parent)) {
158
158
  return;
159
159
  }
160
160
 
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3
3
  import { RootContainer } from "./Components/RootContainer/RootContainer";
4
4
  import ComponentRegistryContext from "./Providers/ComponentRegistryContext";
5
5
  import { LayoutControllerProvider } from "./Providers/LayoutProvider";
6
+ import { DragProvider } from "./Providers/DragProvider";
6
7
 
7
8
  import "./LayoutManager.scss";
8
9
 
@@ -15,13 +16,16 @@ import "./LayoutManager.scss";
15
16
  * @return {React.ReactElement}
16
17
  */
17
18
  export const LayoutManager = ({ldf, registry}) => {
19
+
18
20
 
19
21
  return (
20
- <LayoutControllerProvider layout={ldf}>
21
- <ComponentRegistryContext.Provider value={registry}>
22
- <RootContainer/>
23
- </ComponentRegistryContext.Provider>
24
- </LayoutControllerProvider>
22
+ <DragProvider>
23
+ <LayoutControllerProvider layout={ldf}>
24
+ <ComponentRegistryContext.Provider value={registry}>
25
+ <RootContainer/>
26
+ </ComponentRegistryContext.Provider>
27
+ </LayoutControllerProvider>
28
+ </DragProvider>
25
29
  );
26
30
  }
27
31
 
@@ -0,0 +1,87 @@
1
+ import React, { createContext, useState, useCallback, useMemo } from "react";
2
+
3
+ const DragContext = createContext(null);
4
+
5
+
6
+ /**
7
+ * Exposes the drag state through a hook. The state is updated
8
+ * by linking the drage events to dnd kit. The consuming appplication
9
+ * uses the hook and access the latest state. This will enable the
10
+ * consumer to react to drag start and over to preview interactions.
11
+ *
12
+ * TODO:
13
+ * In the initial implementation, the child components will use the
14
+ * useDrag hook and react to the state change on drop. However,
15
+ * eventually, I will modify it so that I only trigger the drop
16
+ * on the component in which it was dropped. For now, all the components
17
+ * will check to see if the drop was for them and ignore it if it isn't.
18
+ */
19
+ export const DragProvider = ({ children }) => {
20
+
21
+ const [dragState, setDragState] = useState({
22
+ activeId: null,
23
+ activeData: null,
24
+ overId: null,
25
+ overData: null,
26
+ isDragging: false
27
+ });
28
+
29
+
30
+ const [drop, setDrop] = useState(null);
31
+
32
+ const handleDragStart = useCallback((event) => {
33
+ setDragState({
34
+ activeId: event.active?.id ?? null,
35
+ activeData: event.active?.data?.current ?? null,
36
+ overId: null,
37
+ overData: null,
38
+ isDragging: true
39
+ });
40
+ }, []);
41
+
42
+ const handleDragOver = useCallback((event) => {
43
+ setDragState((prev) => ({
44
+ ...prev,
45
+ overId: event.over?.id ?? null,
46
+ overData: event.over?.data?.current ?? null,
47
+ }));
48
+ }, []);
49
+
50
+ const clearDrag = useCallback((event) => {
51
+ setDrop({
52
+ activeId: event.active?.id ?? null,
53
+ activeData: event.active?.data?.current ?? null,
54
+ overId: event.over?.id ?? null,
55
+ overData: event.over?.data?.current ?? null,
56
+ });
57
+ setDragState({
58
+ activeId: null,
59
+ activeData: null,
60
+ overId: null,
61
+ overData: null,
62
+ isDragging: false,
63
+ });
64
+ }, []);
65
+
66
+ const value = useMemo(() => ({
67
+ dragState,
68
+ drop,
69
+ handleDragStart,
70
+ handleDragOver,
71
+ clearDrag,
72
+ }), [dragState, drop, handleDragStart, handleDragOver, clearDrag]);
73
+
74
+ return (
75
+ <DragContext.Provider value={value}>
76
+ {children}
77
+ </DragContext.Provider>
78
+ );
79
+ }
80
+
81
+ export const useDragState = () => {
82
+ const ctx = React.useContext(DragContext);
83
+ if (!ctx) {
84
+ throw new Error("useDragState must be used inside DragProvider");
85
+ }
86
+ return ctx;
87
+ }
@@ -3,6 +3,7 @@ import { useArgs } from "@storybook/preview-api";
3
3
  import { LayoutManager } from "../components/LayoutManager";
4
4
  import defaultLayoutJSON from "./layouts/vsCode/default.json";
5
5
  import sample1JSON from "./layouts/vsCode/sample1.json";
6
+ import workbenchJSON from "./layouts/vsCode/workbench.json";
6
7
 
7
8
  import "./LayoutManager.stories.scss";
8
9
 
@@ -20,8 +21,8 @@ const Template = (args) => {
20
21
  const [, updateArgs] = useArgs();
21
22
 
22
23
  const registry = useMemo(() => ({
23
- EditorVSCode: () =>
24
- import('./sample_components/editor/EditorVSCode').then((m) => ({
24
+ FileEditor: () =>
25
+ import('./sample_components/fileeditor/FileEditor').then((m) => ({
25
26
  default: m.default,
26
27
  })),
27
28
  Stack: () =>
@@ -36,6 +37,10 @@ const Template = (args) => {
36
37
  import('./sample_components/map/MapSample').then((m) => ({
37
38
  default: m.default,
38
39
  })),
40
+ FileTree: () =>
41
+ import('./sample_components/filetree/FileTree').then((m) => ({
42
+ default: m.default,
43
+ })),
39
44
  }), []);
40
45
 
41
46
  useEffect(() => {
@@ -62,3 +67,10 @@ sample1.args = {
62
67
  ldf: sample1JSON
63
68
  }
64
69
 
70
+
71
+ export const workbench = Template.bind({})
72
+
73
+ workbench.args = {
74
+ ldf: workbenchJSON
75
+ }
76
+
@@ -39,8 +39,7 @@
39
39
  "containerId": "sidebar",
40
40
  "type": "container",
41
41
  "size": { "initial": { "value": 50, "unit": "px", "type": "fixed" }},
42
- "collapse": { "value": 400, "condition": "lessThan", "relative": "parent" },
43
- "showHandlebar": true
42
+ "collapse": { "value": 400, "condition": "lessThan", "relative": "parent" }
44
43
  },
45
44
  {
46
45
  "containerId": "contentContainer",
@@ -165,24 +164,12 @@
165
164
  "containerId": "stackContainer",
166
165
  "type": "container",
167
166
  "size": { "initial": { "type": "fill" }}
168
- },
169
- {
170
- "type": "handleBar",
171
- "sibling1": "stackContainer",
172
- "sibling2": "stackContainer2"
173
- },
174
- {
175
- "containerId": "stackContainer2",
176
- "type": "container",
177
- "size": {
178
- "initial": { "value": 200, "unit": "px", "type": "fixed" }
179
- },
180
- "collapse": { "value": 500, "condition": "lessThan", "relative": "parent" }
181
167
  }
182
168
  ]
183
169
  },
184
170
  "fileTabsContainer": {
185
171
  "id": "fileTabsContainer",
172
+ "component": "FileBrowser",
186
173
  "background": "#1e1e1e"
187
174
  },
188
175
  "stackContainer": {
@@ -190,11 +177,6 @@
190
177
  "component": "Stack",
191
178
  "background": "#1e1e1e"
192
179
  },
193
- "stackContainer2": {
194
- "id": "stackContainer2",
195
- "component": "Stack",
196
- "background": "#1e1e1e"
197
- },
198
180
  "editorContainer": {
199
181
  "id": "editorContainer",
200
182
  "component": "EditorVSCode",
@@ -145,12 +145,16 @@
145
145
  {
146
146
  "containerId": "stackContainer2",
147
147
  "type": "container",
148
- "size": { "initial": { "type": "fill" }}
148
+ "size": {
149
+ "initial": { "value": 200, "unit": "px", "type": "fixed" }
150
+ },
151
+ "collapse": { "value": 500, "condition": "lessThan", "relative": "parent" }
149
152
  }
150
153
  ]
151
154
  },
152
155
  "fileTabsContainer": {
153
156
  "id": "fileTabsContainer",
157
+ "component": "FileBrowser",
154
158
  "background": "#1e1e1e"
155
159
  },
156
160
  "stackContainer": {