ui-layout-manager-dev 0.0.18 → 0.0.20

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.
Files changed (34) hide show
  1. package/dist/cjs/index.js +3 -3
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/esm/Worker/LayoutWorker.js +38 -1
  4. package/dist/esm/index.js +3 -3
  5. package/dist/esm/index.js.map +1 -1
  6. package/package.json +3 -3
  7. package/src/components/LayoutManager/Components/Container/Container.jsx +1 -1
  8. package/src/components/LayoutManager/Components/HandleBar/HandleBar.jsx +6 -16
  9. package/src/components/LayoutManager/Components/LazyLoader/LazyLoader.js +74 -9
  10. package/src/components/LayoutManager/Components/LazyLoader/LazyLoader.scss +38 -1
  11. package/src/components/LayoutManager/Components/LazyLoader/MenuBar/MenuBar.js +40 -0
  12. package/src/components/LayoutManager/Components/LazyLoader/MenuBar/MenuBar.scss +22 -0
  13. package/src/components/LayoutManager/Components/LazyLoader/Tabs/Tabs.js +51 -0
  14. package/src/components/LayoutManager/Components/LazyLoader/Tabs/Tabs.scss +34 -0
  15. package/src/components/LayoutManager/Components/RootContainer/DragController.js +79 -0
  16. package/src/components/LayoutManager/Components/RootContainer/RootContainer.jsx +21 -21
  17. package/src/components/LayoutManager/Components/RootContainer/RootContainer.scss +1 -0
  18. package/src/components/LayoutManager/Controller/LAYOUT_WORKER_PROTOCOL.js +2 -1
  19. package/src/components/LayoutManager/Controller/LayoutController.js +14 -0
  20. package/src/components/LayoutManager/Controller/LayoutEventController.js +55 -0
  21. package/src/components/LayoutManager/Controller/Worker/HandleRulesEnforcer.js +4 -0
  22. package/src/components/LayoutManager/Controller/Worker/LayoutEditor.js +29 -0
  23. package/src/components/LayoutManager/Controller/Worker/LayoutWorker.js +3 -0
  24. package/src/components/LayoutManager/LayoutManager.jsx +5 -5
  25. package/src/components/LayoutManager/Providers/LayoutEventProvider.js +55 -0
  26. package/src/components/LayoutManager/index.js +1 -2
  27. package/src/stories/LayoutManager.stories.js +17 -0
  28. package/src/stories/layouts/vsCode/workbench.json +22 -5
  29. package/src/stories/layouts/vsCode/workbench2.json +176 -0
  30. package/src/stories/layouts/vsCode/workbench3.json +176 -0
  31. package/src/stories/sample_components/fileeditor/FileEditor.jsx +45 -21
  32. package/src/stories/sample_components/fileeditor/helper.js +22 -0
  33. package/src/stories/sample_components/filetree/FileTree.jsx +11 -5
  34. package/src/components/LayoutManager/Providers/DragProvider.js +0 -87
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-layout-manager-dev",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
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",
@@ -56,7 +56,8 @@
56
56
  "storybook": "^8.6.8",
57
57
  "style-loader": "^4.0.0",
58
58
  "vitest": "^4.0.18",
59
- "sample-ui-component-library": "^0.0.16-dev"
59
+ "sample-ui-component-library": "^0.0.25-dev",
60
+ "leaflet": "^1.9.4"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "@dnd-kit/core": "^6.3.1",
@@ -64,6 +65,5 @@
64
65
  "react-dom": "^18.2.0"
65
66
  },
66
67
  "dependencies": {
67
- "leaflet": "^1.9.4"
68
68
  }
69
69
  }
@@ -115,7 +115,7 @@ export const Container = ({node}) => {
115
115
  }
116
116
  }
117
117
 
118
- setChildElements(hasChildren?processContainer(node):<LazyLoader content={node} />);
118
+ setChildElements(hasChildren?processContainer(node):<LazyLoader node={node} />);
119
119
 
120
120
  controller.registerContainer(node.id, containerAPI, containerRef.current);
121
121
  return () => {
@@ -129,24 +129,14 @@ export const HandleBar = ({orientation, parent, sibling1, sibling2}) => {
129
129
  sibling2SizeKeys.includes("max") && newSibling2Size >= startInfo.sibling2LayoutConfig.max.value) {
130
130
  return;
131
131
  }
132
-
133
- // If both siblings are type fill, then set sizes. Set min size of sibling sizes to 50px;
134
- // TODO: Make into constants and I think this should be evaluated inside the controller.
135
- const sibling1Type = startInfo.sibling1LayoutConfig.initial.type;
136
- const sibling2Type = startInfo.sibling2LayoutConfig.initial.type;
137
- if (sibling1Type === "fill" && sibling2Type === "fill" && newSibling1Size > 50 && newSibling2Size > 50) {
138
- controller.containerRefs[sibling1].style[startInfo.propKey] = newSibling1Size + "px";
139
- controller.containerRefs[sibling2].style[startInfo.propKey] = newSibling2Size + "px";
140
- return;
141
- }
142
132
 
143
- // Don't update fill types, flex box will take care of that
144
- if (!(sibling1Type === "fill")) {
145
- controller.containerRefs[sibling1].style[startInfo.propKey] = newSibling1Size + "px";
146
- }
147
- if (!(sibling2Type === "fill")) {
148
- controller.containerRefs[sibling2].style[startInfo.propKey] = newSibling2Size + "px";
133
+ // If either of the containers are less than 50px, then don't update size.
134
+ if (newSibling1Size < MIN_PANEL_SIZE || newSibling2Size < MIN_PANEL_SIZE) {
135
+ return;
149
136
  }
137
+
138
+ controller.containerRefs[sibling1].style[startInfo.propKey] = newSibling1Size + "px";
139
+ controller.containerRefs[sibling2].style[startInfo.propKey] = newSibling2Size + "px";
150
140
  }
151
141
 
152
142
  /**
@@ -1,6 +1,8 @@
1
- import React, { lazy, useMemo, Suspense, useContext } from "react";
1
+ import React, { lazy, useMemo, Suspense, useContext, useEffect, useState } from "react";
2
2
  import PropTypes from 'prop-types';
3
3
  import ComponentRegistryContext from "../../Providers/ComponentRegistryContext";
4
+ import { MenuBar } from "./MenuBar/MenuBar";
5
+ import { Tabs } from "./Tabs/Tabs";
4
6
 
5
7
  import "./LazyLoader.scss"
6
8
 
@@ -10,20 +12,83 @@ import "./LazyLoader.scss"
10
12
  *
11
13
  * @param {Object} content
12
14
  */
13
- export const LazyLoader = ({content}) => {
15
+ export const LazyLoader = ({node}) => {
14
16
  const registry = useContext(ComponentRegistryContext);
17
+ const [showTitle, setShowTitle] = useState(false);
18
+ const [showTab, setShowTab] = useState(false);
15
19
 
20
+ const [lazyContainerTop, setLazyContainerTop] = useState(0);
21
+ const [tabsTop, setTabsTop] = useState(0);
22
+ const [selectedComponent, setSelectedComponent] = useState("");
23
+
24
+ // Lazy load the component when the selected component changes.
16
25
  const LazyComponent = useMemo(() => {
17
- if (registry && content && "component" in content && content["component"] in registry) {
18
- return lazy(registry[content["component"]]);
26
+ if (registry && selectedComponent in registry) {
27
+ return lazy(registry[selectedComponent]);
28
+ }
29
+ }, [registry, selectedComponent]);
30
+
31
+ /**
32
+ * Note: I am setting the top of the absolute position
33
+ * of the containers based on if the title and tabs are
34
+ * shown. This is a temporary implementation, I will be
35
+ * revisiting this and formally implementing it after.
36
+ */
37
+ useEffect(() => {
38
+ let _lazyContainerTop = 0;
39
+ let _tabsTop = 0;
40
+ if ("menuBar" in node) {
41
+ setShowTitle(true);
42
+ _tabsTop += 35;
43
+ _lazyContainerTop += 35;
19
44
  }
20
- }, [registry, content]);
45
+ if ("tabsBar" in node) {
46
+ setShowTab(true);
47
+ _lazyContainerTop += 35;
48
+ selectTab(node.tabsBar.tabs[0]);
49
+ } else {
50
+ setSelectedComponent(node.component);
51
+ }
52
+ setLazyContainerTop(_lazyContainerTop)
53
+ setTabsTop(_tabsTop);
54
+ }, [node]);
55
+
56
+ /**
57
+ * Selects the provided tab.
58
+ * @param {Object} selectedTab
59
+ */
60
+ const selectTab = (selectedTab) => {
61
+ node.tabsBar.tabs.forEach((tab) => {
62
+ if (tab === selectedTab) {
63
+ tab.selected = true;
64
+ setSelectedComponent(tab.component);
65
+ } else {
66
+ tab.selected = false;
67
+ }
68
+ });
69
+ }
21
70
 
22
71
  return (
23
- <div className="lazyContainer">
24
- <Suspense fallback={<div>Loading...</div>}>
25
- {LazyComponent && <LazyComponent />}
26
- </Suspense>
72
+ <div className="absoluteContainer">
73
+ <div className="contentContainer">
74
+ {
75
+ showTitle &&
76
+ <div className="menuContainer">
77
+ <MenuBar node={node}/>
78
+ </div>
79
+ }
80
+ {
81
+ showTab &&
82
+ <div className="tabsContainer" style={{ top: `${tabsTop}px`}}>
83
+ <Tabs node={node} onTabClick={selectTab}/>
84
+ </div>
85
+ }
86
+ <div className="lazycontainer" style={{ top: `${lazyContainerTop}px`}}>
87
+ <Suspense fallback={<div>Loading...</div>}>
88
+ {LazyComponent && <LazyComponent />}
89
+ </Suspense>
90
+ </div>
91
+ </div>
27
92
  </div>
28
93
  );
29
94
  }
@@ -1,4 +1,41 @@
1
- .lazyContainer {
1
+ /**
2
+ TODO: I agree that it is convoluted to use absolute, then relative and then absolute.
3
+ However, the reason I am doing it is, it makes the layout of the monaco editor trivial
4
+ and avoids resize observer loops when the container sizes change. I haven't thought
5
+ about this enough and I will revisit it later.
6
+ */
7
+
8
+ .absoluteContainer {
9
+ position: absolute;
10
+ top: 0px;
11
+ bottom: 0px;
12
+ left: 0px;
13
+ right: 0px;
14
+ }
15
+
16
+ .contentContainer {
17
+ position: relative;
18
+ width: 100%;
19
+ height: 100%;
20
+ }
21
+
22
+ .menuContainer {
23
+ position: absolute;
24
+ top: 0px;
25
+ height: 35px;
26
+ left: 0px;
27
+ right: 0px;
28
+ }
29
+
30
+ .tabsContainer {
31
+ position: absolute;
32
+ top: 0px;
33
+ height: 35px;
34
+ left: 0px;
35
+ right: 0px;
36
+ }
37
+
38
+ .lazycontainer {
2
39
  position: absolute;
3
40
  top: 0px;
4
41
  bottom: 0px;
@@ -0,0 +1,40 @@
1
+ import PropTypes from "prop-types";
2
+
3
+ import "./MenuBar.scss";
4
+
5
+ import { XLg } from "react-bootstrap-icons";
6
+
7
+ import { useLayoutController } from "../../../Providers/LayoutProvider";
8
+
9
+ /**
10
+ * MenuBar component for the containers.
11
+ *
12
+ * @param {Object} node
13
+ */
14
+ export const MenuBar = ({ node }) => {
15
+ const controller = useLayoutController();
16
+
17
+ const closeContainer = () => {
18
+ controller.invokeAction({
19
+ id: node?.menuBar?.closeContainerId,
20
+ action: "close",
21
+ args: {},
22
+ });
23
+ };
24
+
25
+ return (
26
+ <div className="titleContainer">
27
+ <div className="title">{node?.menuBar?.title}</div>
28
+ {
29
+ node?.menuBar?.showClose &&
30
+ <div onClick={closeContainer} className="close">
31
+ <XLg />
32
+ </div>
33
+ }
34
+ </div>
35
+ );
36
+ };
37
+
38
+ MenuBar.propTypes = {
39
+ node : PropTypes.object,
40
+ };
@@ -0,0 +1,22 @@
1
+ .titleContainer {
2
+ position:absolute;
3
+ width:100%;
4
+ top:0;
5
+ bottom:1px;
6
+ display: flex;
7
+ align-items: center;
8
+ color:#bbbbbb;
9
+ justify-content: space-between;
10
+ background: #222425;
11
+ }
12
+
13
+ .titleContainer > .title {
14
+ padding-left: 30px;
15
+ font-size: 11px;
16
+ }
17
+
18
+ .titleContainer > .close {
19
+ padding-right: 15px;
20
+ font-size: 11px;
21
+ cursor: pointer;
22
+ }
@@ -0,0 +1,51 @@
1
+ import PropTypes from 'prop-types';
2
+
3
+ import {XLg} from "react-bootstrap-icons";
4
+ import "./Tabs.scss"
5
+
6
+ import { useLayoutController } from "../../../Providers/LayoutProvider";
7
+
8
+ /**
9
+ * Tabs component for the containers.
10
+ *
11
+ * @param {Object} node
12
+ */
13
+ export const Tabs = ({node, onTabClick}) => {
14
+ const controller = useLayoutController();
15
+
16
+ const closeContainer = () => {
17
+ controller.invokeAction({
18
+ id: node?.tabsBar?.closeContainerId,
19
+ action: "close",
20
+ args: {},
21
+ });
22
+ };
23
+
24
+ return (
25
+ <div className="container-tabs-row">
26
+ <div className="container-tabs">
27
+ {node?.tabsBar?.tabs?.map((tab, index) => (
28
+ <div
29
+ key= {tab.name + String(index)}
30
+ style={{ borderBottom: tab.selected ? "solid 1px white" : "none" }}
31
+ onClick={(e) => onTabClick(tab)}
32
+ className="container-tab">
33
+ {tab.name}
34
+ </div>
35
+ ))}
36
+ </div>
37
+
38
+ {
39
+ node?.tabsBar?.showClose &&
40
+ <div className="container-close">
41
+ <XLg onClick={closeContainer}/>
42
+ </div>
43
+ }
44
+ </div>
45
+ );
46
+ }
47
+
48
+ Tabs.propTypes = {
49
+ node: PropTypes.array,
50
+ onTabsClick: PropTypes.func
51
+ }
@@ -0,0 +1,34 @@
1
+ .container-tabs-row{
2
+ position:relative;
3
+ width:100%;
4
+ height:100%;
5
+ background: #222425;
6
+ display:flex;
7
+ justify-content: space-between;
8
+ }
9
+
10
+ .container-tabs-row > .container-tabs {
11
+ display:flex;
12
+ flex-direction: row;
13
+ gap: 5px;
14
+ margin-left:20px;
15
+ }
16
+
17
+ .container-tabs-row > .container-tabs > .container-tab {
18
+ display:flex;
19
+ align-items: center;
20
+ cursor: pointer;
21
+ padding: 0 10px;
22
+ height: 28px;
23
+ font-size: 13px;
24
+ color:#bbbbbb;
25
+ }
26
+
27
+ .container-tabs-row > .container-close {
28
+ display:flex;
29
+ align-items: center;
30
+ cursor: pointer;
31
+ color:white;
32
+ font-size: 11px;
33
+ margin-right:10px;
34
+ }
@@ -0,0 +1,79 @@
1
+ import { useMemo } from "react";
2
+
3
+ import { useLayoutEventPublisher } from "../../Providers/LayoutEventProvider";
4
+
5
+ /**
6
+ * Drag controller exposed through the hook. It exposes
7
+ * callback functions which are connected to the dnd context.
8
+ *
9
+ * The controller is also initialized with the publish function
10
+ * exposed by the useLayoutEventPublisher so it can notify
11
+ * subscribers of any relevant events.
12
+ */
13
+ class DragController {
14
+ constructor({publish}) {
15
+ this.publish = publish;
16
+ this.resetDragState();
17
+ }
18
+
19
+ resetDragState = () => {
20
+ this.dragState = {
21
+ activeId: null,
22
+ activeData: null,
23
+ overId: null,
24
+ overData: null,
25
+ isDragging: false
26
+ }
27
+ }
28
+
29
+ onDragStart = (event) => {
30
+ this.dragState = {
31
+ activeId: event.active?.id ?? null,
32
+ activeData: event.active?.data?.current ?? null,
33
+ overId: null,
34
+ overData: null,
35
+ isDragging: true
36
+ };
37
+ }
38
+
39
+ onDragOver = (event) => {
40
+ this.dragState.overId = event.over?.id ?? null;
41
+ this.dragState.overData = event.over?.data?.current ?? null;
42
+ }
43
+
44
+ onDragEnd = (event) => {
45
+ this.publish({
46
+ type: "drag:drop",
47
+ payload: {
48
+ activeId: event.active?.id ?? null,
49
+ activeData: event.active?.data?.current ?? null,
50
+ overId: event.over?.id ?? null,
51
+ overData: event.over?.data?.current ?? null,
52
+ },
53
+ source: "drag-provider",
54
+ })
55
+ this.resetDragState();
56
+ }
57
+
58
+ onDragCancel = (event) => {
59
+ this.resetDragState();
60
+ }
61
+
62
+ isDragging = () => {
63
+ return this.dragState.isDragging;
64
+ }
65
+
66
+ getDragPreview = () => {
67
+ return this.dragState?.activeData?.preview;
68
+ }
69
+ }
70
+
71
+ const useDragEventController = function() {
72
+ const publish = useLayoutEventPublisher();
73
+
74
+ return useMemo(() => {
75
+ return new DragController({publish});
76
+ }, [publish]);
77
+ }
78
+
79
+ export default useDragEventController;
@@ -5,13 +5,13 @@ import { HandleBar } from "../HandleBar/HandleBar";
5
5
  import { useLayoutController } from "../../Providers/LayoutProvider";
6
6
  import {
7
7
  DndContext,
8
+ closestCenter,
8
9
  PointerSensor,
9
10
  useSensor,
10
11
  useSensors
11
12
  } from "@dnd-kit/core";
12
13
 
13
- import { useDragState } from "../../Providers/DragProvider";
14
-
14
+ import useDragEventController from "./DragController";
15
15
  import "./RootContainer.scss"
16
16
 
17
17
 
@@ -23,9 +23,8 @@ import "./RootContainer.scss"
23
23
  * @return {React.ReactElement}
24
24
  */
25
25
  export const RootContainer = () => {
26
- const controller = useLayoutController();
27
-
28
- const { dragState, handleDragStart, handleDragOver, clearDrag } = useDragState();
26
+ const layoutController = useLayoutController();
27
+ const dragController = useDragEventController();
29
28
 
30
29
  const rootRef = useRef(null);
31
30
  const timerRef = useRef(null);
@@ -46,7 +45,7 @@ export const RootContainer = () => {
46
45
  const childNode = node.children[index];
47
46
 
48
47
  if (childNode.type === "container") {
49
- const child = controller.ldf.containers[node.children[index].containerId];
48
+ const child = layoutController.ldf.containers[node.children[index].containerId];
50
49
  child.parent = node;
51
50
  childElements.push(
52
51
  <Container key={index} meta={node.children[index]} id={child.id} node={child} />
@@ -76,14 +75,14 @@ export const RootContainer = () => {
76
75
  }
77
76
  };
78
77
  return childElements;
79
- }, [controller]);
78
+ }, [layoutController]);
80
79
 
81
80
 
82
81
  useLayoutEffect(() => {
83
- if (controller) {
84
- const rootNode = controller.ldf.containers[controller.ldf.layoutRoot];
82
+ if (layoutController) {
83
+ const rootNode = layoutController.ldf.containers[layoutController.ldf.layoutRoot];
85
84
  const hasChildren = rootNode.children && rootNode.children.length > 0
86
- controller.registerContainer(rootNode.id, rootContainerAPI, rootRef.current);
85
+ layoutController.registerContainer(rootNode.id, rootContainerAPI, rootRef.current);
87
86
 
88
87
  if (hasChildren) {
89
88
  if (rootNode.orientation === "horizontal") {
@@ -107,7 +106,7 @@ export const RootContainer = () => {
107
106
 
108
107
  timerRef.current = setTimeout(() => {
109
108
  resizingRef.current = false;
110
- controller.handleRootResize(width, height);
109
+ layoutController.handleRootResize(width, height);
111
110
  }, 1);
112
111
  }
113
112
  });
@@ -115,11 +114,11 @@ export const RootContainer = () => {
115
114
  observer.observe(rootRef.current);
116
115
 
117
116
  return () => {
118
- controller.unregisterContainer(controller.ldf.layoutRoot);
117
+ layoutController.unregisterContainer(layoutController.ldf.layoutRoot);
119
118
  observer.disconnect();
120
119
  }
121
120
  }
122
- }, [controller]);
121
+ }, [layoutController]);
123
122
 
124
123
  const sensors = useSensors(
125
124
  useSensor(PointerSensor, {
@@ -132,20 +131,21 @@ export const RootContainer = () => {
132
131
  // Manually track the drag position for smooth overlay
133
132
  const [dragPos, setDragPos] = useState({ left: 0, top: 0 });
134
133
  useEffect(() => {
135
- if (!dragState.isDragging) return;
134
+ if (!dragController.isDragging()) return;
136
135
  const handleMove = (e) => {setDragPos({ left: e.clientX, top: e.clientY })};
137
136
  window.addEventListener("pointermove", handleMove);
138
137
  return () => {
139
138
  window.removeEventListener("pointermove", handleMove);
140
139
  };
141
- }, [dragState.isDragging]);
140
+ }, [dragController.isDragging()]);
142
141
 
143
142
  return (
144
143
  <DndContext sensors={sensors}
145
- onDragStart={handleDragStart}
146
- onDragOver={handleDragOver}
147
- onDragEnd={clearDrag}
148
- onDragCancel={clearDrag}>
144
+ collisionDetection={closestCenter}
145
+ onDragStart={dragController.onDragStart}
146
+ onDragOver={dragController.onDragOver}
147
+ onDragEnd={dragController.onDragEnd}
148
+ onDragCancel={dragController.onDragCancel}>
149
149
 
150
150
  <div className="root-container">
151
151
  <div ref={rootRef} className="relative-container">
@@ -153,9 +153,9 @@ export const RootContainer = () => {
153
153
  </div>
154
154
  </div>
155
155
 
156
- {dragState.isDragging && (
156
+ {dragController.isDragging() && (
157
157
  <div className="drag-overlay" style={dragPos}>
158
- {dragState?.activeData?.preview}
158
+ {dragController.getDragPreview()}
159
159
  </div>
160
160
  )}
161
161
  </DndContext>
@@ -2,6 +2,7 @@
2
2
  width: 100%;
3
3
  height: 100%;
4
4
  overflow: hidden;
5
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
5
6
  }
6
7
 
7
8
  .relative-container {
@@ -4,7 +4,8 @@ let LAYOUT_WORKER_PROTOCOL = {
4
4
  APPLY_SIZES: 3,
5
5
  ERROR: 4,
6
6
  TRANSFORMATIONS: 5,
7
- MOVE_HANDLE_BAR: 6
7
+ MOVE_HANDLE_BAR: 6,
8
+ INVOKE_ACTION: 7,
8
9
  };
9
10
  LAYOUT_WORKER_PROTOCOL = Object.freeze(LAYOUT_WORKER_PROTOCOL);
10
11
 
@@ -198,6 +198,20 @@ export class LayoutController {
198
198
  }
199
199
  }
200
200
 
201
+ /**
202
+ * Invoke a specific action on a container given the id.
203
+ */
204
+ invokeAction({id, action, args}) {
205
+ this.sendToWorker(
206
+ LAYOUT_WORKER_PROTOCOL.INVOKE_ACTION,
207
+ {
208
+ id: id,
209
+ action: action,
210
+ args: args
211
+ }
212
+ );
213
+ }
214
+
201
215
  /**
202
216
  * Performs cleanup when the controller is destroyed.
203
217
  */
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Layout Event Controller used to subscribe to events
3
+ * and publish events.
4
+ */
5
+ class LayoutEventController {
6
+
7
+ constructor() {
8
+ this.subscribers = new Map();
9
+ }
10
+
11
+ /**
12
+ * Subscribe to event type and provide handler
13
+ * @param {String} type
14
+ * @param {Function} handler
15
+ * @returns
16
+ */
17
+ subscribe(type, handler) {
18
+ // Create set for type if it doesn't exist
19
+ if (!this.subscribers.has(type)) {
20
+ this.subscribers.set(type, new Set());
21
+ }
22
+
23
+ // Add handler to type set
24
+ const handlers = this.subscribers.get(type);
25
+ handlers.add(handler);
26
+
27
+ // Return unsubscribe function
28
+ const unsubscribe = () => {
29
+ handlers.delete(handler);
30
+ if (handlers.size === 0) {
31
+ this.subscribers.delete(type);
32
+ }
33
+ }
34
+
35
+ return unsubscribe;
36
+ }
37
+
38
+ /**
39
+ * Publishes the event to the subscribers.
40
+ * @param {Object} event
41
+ * @returns
42
+ */
43
+ publish(event) {
44
+ const handlers = this.subscribers.get(event.type);
45
+ if (!handlers) return;
46
+
47
+ for (const handler of handlers) {
48
+ handler(event);
49
+ }
50
+ }
51
+
52
+ }
53
+
54
+ export default LayoutEventController;
55
+