@vuu-ui/vuu-layout 0.0.27

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 (117) hide show
  1. package/README.md +1 -0
  2. package/package.json +30 -0
  3. package/src/Component.css +2 -0
  4. package/src/Component.tsx +20 -0
  5. package/src/DraggableLayout.css +18 -0
  6. package/src/DraggableLayout.tsx +29 -0
  7. package/src/__tests__/flexbox-utils.spec.js +90 -0
  8. package/src/action-buttons/action-buttons.css +12 -0
  9. package/src/action-buttons/action-buttons.tsx +30 -0
  10. package/src/action-buttons/index.ts +1 -0
  11. package/src/chest-of-drawers/Chest.css +36 -0
  12. package/src/chest-of-drawers/Chest.tsx +42 -0
  13. package/src/chest-of-drawers/Drawer.css +153 -0
  14. package/src/chest-of-drawers/Drawer.tsx +118 -0
  15. package/src/chest-of-drawers/index.ts +2 -0
  16. package/src/common-types.ts +9 -0
  17. package/src/debug.ts +16 -0
  18. package/src/dialog/Dialog.css +16 -0
  19. package/src/dialog/Dialog.tsx +59 -0
  20. package/src/dialog/index.ts +1 -0
  21. package/src/drag-drop/BoxModel.ts +546 -0
  22. package/src/drag-drop/DragState.ts +222 -0
  23. package/src/drag-drop/Draggable.ts +282 -0
  24. package/src/drag-drop/DropMenu.css +70 -0
  25. package/src/drag-drop/DropMenu.tsx +68 -0
  26. package/src/drag-drop/DropTarget.ts +392 -0
  27. package/src/drag-drop/DropTargetRenderer.css +40 -0
  28. package/src/drag-drop/DropTargetRenderer.tsx +284 -0
  29. package/src/drag-drop/dragDropTypes.ts +49 -0
  30. package/src/drag-drop/index.ts +4 -0
  31. package/src/editable-label/EditableLabel.css +28 -0
  32. package/src/editable-label/EditableLabel.tsx +99 -0
  33. package/src/editable-label/index.ts +1 -0
  34. package/src/flexbox/Flexbox.css +45 -0
  35. package/src/flexbox/Flexbox.tsx +70 -0
  36. package/src/flexbox/FlexboxLayout.jsx +26 -0
  37. package/src/flexbox/FluidGrid.css +134 -0
  38. package/src/flexbox/FluidGrid.tsx +84 -0
  39. package/src/flexbox/FluidGridLayout.tsx +10 -0
  40. package/src/flexbox/Splitter.css +140 -0
  41. package/src/flexbox/Splitter.tsx +135 -0
  42. package/src/flexbox/flexbox-utils.ts +128 -0
  43. package/src/flexbox/flexboxTypes.ts +63 -0
  44. package/src/flexbox/index.ts +4 -0
  45. package/src/flexbox/useResponsiveSizing.ts +85 -0
  46. package/src/flexbox/useSplitterResizing.ts +272 -0
  47. package/src/index.ts +20 -0
  48. package/src/layout-action.ts +21 -0
  49. package/src/layout-header/ActionButton.tsx +23 -0
  50. package/src/layout-header/Header.css +8 -0
  51. package/src/layout-header/Header.tsx +222 -0
  52. package/src/layout-header/index.ts +1 -0
  53. package/src/layout-provider/LayoutProvider.tsx +160 -0
  54. package/src/layout-provider/LayoutProviderContext.ts +17 -0
  55. package/src/layout-provider/index.ts +2 -0
  56. package/src/layout-provider/useLayoutDragDrop.ts +241 -0
  57. package/src/layout-reducer/flexUtils.ts +281 -0
  58. package/src/layout-reducer/index.ts +4 -0
  59. package/src/layout-reducer/insert-layout-element.ts +365 -0
  60. package/src/layout-reducer/layout-reducer.ts +255 -0
  61. package/src/layout-reducer/layoutTypes.ts +151 -0
  62. package/src/layout-reducer/layoutUtils.ts +302 -0
  63. package/src/layout-reducer/remove-layout-element.ts +240 -0
  64. package/src/layout-reducer/replace-layout-element.ts +118 -0
  65. package/src/layout-reducer/resize-flex-children.ts +56 -0
  66. package/src/layout-reducer/wrap-layout-element.ts +317 -0
  67. package/src/layout-view/View.css +58 -0
  68. package/src/layout-view/View.tsx +149 -0
  69. package/src/layout-view/ViewContext.ts +31 -0
  70. package/src/layout-view/index.ts +4 -0
  71. package/src/layout-view/useView.tsx +104 -0
  72. package/src/layout-view/useViewActionDispatcher.ts +133 -0
  73. package/src/layout-view/useViewResize.ts +53 -0
  74. package/src/layout-view/viewTypes.ts +37 -0
  75. package/src/palette/Palette.css +37 -0
  76. package/src/palette/Palette.tsx +140 -0
  77. package/src/palette/PaletteUitk.css +9 -0
  78. package/src/palette/PaletteUitk.tsx +79 -0
  79. package/src/palette/index.ts +2 -0
  80. package/src/placeholder/Placeholder.css +10 -0
  81. package/src/placeholder/Placeholder.tsx +39 -0
  82. package/src/placeholder/index.ts +1 -0
  83. package/src/registry/ComponentRegistry.ts +35 -0
  84. package/src/registry/index.ts +1 -0
  85. package/src/responsive/OverflowMenu.css +31 -0
  86. package/src/responsive/OverflowMenu.jsx +56 -0
  87. package/src/responsive/breakpoints.ts +48 -0
  88. package/src/responsive/index.ts +4 -0
  89. package/src/responsive/measureMinimumNodeSize.ts +23 -0
  90. package/src/responsive/overflowUtils.js +14 -0
  91. package/src/responsive/use-breakpoints.ts +100 -0
  92. package/src/responsive/useOverflowObserver.ts +606 -0
  93. package/src/responsive/useResizeObserver.ts +154 -0
  94. package/src/responsive/utils.ts +37 -0
  95. package/src/stack/Stack.css +39 -0
  96. package/src/stack/Stack.tsx +160 -0
  97. package/src/stack/StackLayout.tsx +137 -0
  98. package/src/stack/index.ts +3 -0
  99. package/src/stack/stackTypes.ts +19 -0
  100. package/src/tabs/TabPanel.css +12 -0
  101. package/src/tabs/TabPanel.tsx +17 -0
  102. package/src/tabs/index.ts +1 -0
  103. package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
  104. package/src/tools/config-wrapper/index.js +1 -0
  105. package/src/tools/devtools-box/layout-configurator.css +112 -0
  106. package/src/tools/devtools-box/layout-configurator.jsx +369 -0
  107. package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
  108. package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
  109. package/src/tools/index.js +3 -0
  110. package/src/use-persistent-state.ts +115 -0
  111. package/src/utils/componentFromLayout.tsx +30 -0
  112. package/src/utils/index.ts +6 -0
  113. package/src/utils/pathUtils.ts +294 -0
  114. package/src/utils/propUtils.ts +24 -0
  115. package/src/utils/refUtils.ts +16 -0
  116. package/src/utils/styleUtils.ts +14 -0
  117. package/src/utils/typeOf.ts +22 -0
@@ -0,0 +1,317 @@
1
+ import React, { ReactElement } from "react";
2
+ import { uuid } from "@vuu-ui/vuu-utils";
3
+ import { getProp, getProps, nextStep, resetPath, typeOf } from "../utils";
4
+ import { ComponentRegistry } from "../registry/ComponentRegistry";
5
+ import {
6
+ createFlexbox,
7
+ createPlaceHolder,
8
+ flexDirection,
9
+ getFlexStyle,
10
+ getIntrinsicSize,
11
+ wrapIntrinsicSizeComponentWithFlexbox,
12
+ } from "./flexUtils";
13
+ import { applyLayoutProps, LayoutProps } from "./layoutUtils";
14
+ import { LayoutModel } from "./layoutTypes";
15
+ import { DropPos } from "../drag-drop/dragDropTypes";
16
+ import { rectTuple } from "../common-types";
17
+ import { DropTarget } from "../drag-drop/DropTarget";
18
+
19
+ export interface LayoutSpec {
20
+ type: "Stack" | "Flexbox";
21
+ flexDirection: "column" | "row";
22
+ showTabs?: boolean;
23
+ }
24
+
25
+ const isHtmlElement = (component: LayoutModel) => {
26
+ const [firstLetter] = typeOf(component) as string;
27
+ return firstLetter === firstLetter.toLowerCase();
28
+ };
29
+
30
+ // newComponent has been dropped onto an existingComponent. A wrapper container will be inserted
31
+ // into the layout tree, wrapping the existingComponent. newComponent will be injected into the
32
+ // new wrapper, so existingComponent and newComponent will be siblings. Putting it another way,
33
+ // wrapper will replace existingComponent in the layout tree and it will contain existingComponent
34
+ // and newComponent.
35
+ export function wrap(
36
+ container: ReactElement,
37
+ existingComponent: any,
38
+ newComponent: any,
39
+ pos: DropPos,
40
+ clientRect?: DropTarget["clientRect"],
41
+ dropRect?: DropTarget["dropRect"]
42
+ ): ReactElement {
43
+ const { children: containerChildren, path: containerPath } =
44
+ getProps(container);
45
+
46
+ const existingComponentPath = getProp(existingComponent, "path");
47
+ const { idx, finalStep } = nextStep(containerPath, existingComponentPath);
48
+ const children = finalStep
49
+ ? updateChildren(
50
+ container,
51
+ containerChildren,
52
+ existingComponent,
53
+ newComponent,
54
+ pos,
55
+ clientRect,
56
+ dropRect
57
+ )
58
+ : containerChildren.map((child: ReactElement, index: number) =>
59
+ index === idx
60
+ ? wrap(
61
+ child,
62
+ existingComponent,
63
+ newComponent,
64
+ pos,
65
+ clientRect,
66
+ dropRect
67
+ )
68
+ : child
69
+ );
70
+
71
+ return React.cloneElement(container, undefined, children);
72
+ }
73
+
74
+ function updateChildren(
75
+ container: LayoutModel,
76
+ containerChildren: ReactElement[],
77
+ existingComponent: ReactElement,
78
+ newComponent: ReactElement,
79
+ pos: DropPos,
80
+ clientRect?: DropTarget["clientRect"],
81
+ dropRect?: rectTuple
82
+ ) {
83
+ const intrinsicSize = getIntrinsicSize(newComponent);
84
+
85
+ if (intrinsicSize?.width && intrinsicSize?.height) {
86
+ if (clientRect === undefined || dropRect === undefined) {
87
+ throw Error(
88
+ "wrap-layout-element, updateChildren clientRect and dropRect must both be available"
89
+ );
90
+ }
91
+ return wrapIntrinsicSizedComponent(
92
+ containerChildren,
93
+ existingComponent,
94
+ newComponent,
95
+ pos,
96
+ clientRect,
97
+ dropRect
98
+ );
99
+ } else {
100
+ return wrapFlexComponent(
101
+ container,
102
+ containerChildren,
103
+ existingComponent,
104
+ newComponent,
105
+ pos
106
+ );
107
+ }
108
+ }
109
+
110
+ function wrapFlexComponent(
111
+ container: LayoutModel,
112
+ containerChildren: ReactElement[],
113
+ existingComponent: ReactElement,
114
+ newComponent: ReactElement,
115
+ pos: DropPos
116
+ ) {
117
+ const { version = 0 } = getProps(newComponent);
118
+ const existingComponentPath = getProp(existingComponent, "path");
119
+ const {
120
+ type,
121
+ flexDirection,
122
+ showTabs: showTabsProp,
123
+ } = getLayoutSpecForWrapper(pos);
124
+ const [style, existingComponentStyle, newComponentStyle] =
125
+ getWrappedFlexStyles(
126
+ type,
127
+ existingComponent,
128
+ newComponent,
129
+ flexDirection,
130
+ pos
131
+ );
132
+ const targetFirst = isTargetFirst(pos);
133
+ const active = targetFirst ? 1 : 0; // double check this
134
+
135
+ // TODO how do we decide whether children should be resizable ?
136
+ const newComponentProps = {
137
+ resizeable: true,
138
+ style: newComponentStyle,
139
+ version: version + 1,
140
+ };
141
+ const resizeProp = isHtmlElement(existingComponent)
142
+ ? "data-resizeable"
143
+ : "resizeable";
144
+ const existingComponentProps = {
145
+ [resizeProp]: true,
146
+ style: existingComponentStyle,
147
+ };
148
+
149
+ const showTabs = type === "Stack" ? { showTabs: showTabsProp } : undefined;
150
+ const splitterSize =
151
+ type === "Flexbox"
152
+ ? {
153
+ splitterSize:
154
+ (typeOf(container) === "Flexbox" && container.props.splitterSize) ??
155
+ undefined,
156
+ }
157
+ : undefined;
158
+
159
+ const id = uuid();
160
+ var wrapper = React.createElement(
161
+ ComponentRegistry[type],
162
+ {
163
+ active,
164
+ id,
165
+ key: id,
166
+ path: getProp(existingComponent, "path"),
167
+ flexFill: getProp(existingComponent, "flexFill"),
168
+ // TODO we should be able to configure this in setDefaultLayoutProps
169
+ ...splitterSize,
170
+ ...showTabs,
171
+ style,
172
+ resizeable: getProp(existingComponent, "resizeable"),
173
+ } as LayoutProps,
174
+ targetFirst
175
+ ? [
176
+ resetPath(
177
+ existingComponent,
178
+ `${existingComponentPath}.0`,
179
+ existingComponentProps
180
+ ),
181
+ // resetPath(newComponent, `${existingComponentPath}.1`, newComponentProps),
182
+ applyLayoutProps(
183
+ React.cloneElement(newComponent, newComponentProps),
184
+ `${existingComponentPath}.1`
185
+ ),
186
+ ]
187
+ : [
188
+ applyLayoutProps(
189
+ React.cloneElement(newComponent, newComponentProps),
190
+ `${existingComponentPath}.0`
191
+ ),
192
+ // resetPath(newComponent, `${existingComponentPath}.0`, newComponentProps),
193
+ resetPath(
194
+ existingComponent,
195
+ `${existingComponentPath}.1`,
196
+ existingComponentProps
197
+ ),
198
+ ]
199
+ );
200
+ return containerChildren.map((child: ReactElement) =>
201
+ child === existingComponent ? wrapper : child
202
+ );
203
+ }
204
+
205
+ function wrapIntrinsicSizedComponent(
206
+ containerChildren: ReactElement[],
207
+ existingComponent: ReactElement,
208
+ newComponent: ReactElement,
209
+ pos: DropPos,
210
+ clientRect: DropTarget["clientRect"],
211
+ dropRect: rectTuple
212
+ ) {
213
+ const { flexDirection } = getLayoutSpecForWrapper(pos);
214
+ const contraDirection = flexDirection === "column" ? "row" : "column";
215
+ const targetFirst = isTargetFirst(pos);
216
+
217
+ const [dropLeft, dropTop, dropRight, dropBottom] = dropRect;
218
+ const [startPlaceholder, endPlaceholder] =
219
+ flexDirection === "column"
220
+ ? [dropTop - clientRect.top, clientRect.bottom - dropBottom]
221
+ : [dropLeft - clientRect.left, clientRect.right - dropRight];
222
+ const pathRoot = getProp(existingComponent, "path");
223
+ let pathIndex = 0;
224
+
225
+ const resizeProp = isHtmlElement(existingComponent)
226
+ ? "data-resizeable"
227
+ : "resizeable";
228
+
229
+ const wrappedChildren = [];
230
+ if (startPlaceholder) {
231
+ wrappedChildren.push(
232
+ targetFirst
233
+ ? resetPath(existingComponent, `${pathRoot}.${pathIndex++}`, {
234
+ [resizeProp]: true,
235
+ style: { flexBasis: startPlaceholder, flexGrow: 1, flexShrink: 1 },
236
+ })
237
+ : createPlaceHolder(`${pathRoot}.${pathIndex++}`, startPlaceholder, {
238
+ flexGrow: 0,
239
+ flexShrink: 0,
240
+ })
241
+ );
242
+ }
243
+ wrappedChildren.push(
244
+ wrapIntrinsicSizeComponentWithFlexbox(
245
+ newComponent,
246
+ contraDirection,
247
+ `${pathRoot}.${pathIndex++}`,
248
+ clientRect,
249
+ dropRect
250
+ )
251
+ );
252
+ if (endPlaceholder) {
253
+ wrappedChildren.push(
254
+ targetFirst
255
+ ? createPlaceHolder(`${pathRoot}.${pathIndex++}`, 0)
256
+ : resetPath(existingComponent, `${pathRoot}.${pathIndex++}`, {
257
+ [resizeProp]: true,
258
+ style: { flexBasis: 0, flexGrow: 1, flexShrink: 1 },
259
+ })
260
+ );
261
+ }
262
+
263
+ const wrapper = createFlexbox(
264
+ flexDirection,
265
+ existingComponent.props,
266
+ wrappedChildren,
267
+ pathRoot
268
+ );
269
+ return containerChildren.map((child) =>
270
+ child === existingComponent ? wrapper : child
271
+ );
272
+ }
273
+
274
+ //TODO we need to respect styles on the source, full-on flex might not be appropriate
275
+ function getWrappedFlexStyles(
276
+ type: string,
277
+ existingComponent: ReactElement,
278
+ newComponent: ReactElement,
279
+ flexDirection: flexDirection,
280
+ pos: DropPos
281
+ ) {
282
+ const style = {
283
+ ...existingComponent.props.style,
284
+ flexDirection,
285
+ };
286
+
287
+ const dimension =
288
+ type === "Flexbox" && flexDirection === "column" ? "height" : "width";
289
+ const newComponentStyle = getFlexStyle(newComponent, dimension, pos);
290
+ const existingComponentStyle = getFlexStyle(existingComponent, dimension);
291
+
292
+ return [style, existingComponentStyle, newComponentStyle];
293
+ }
294
+
295
+ const isTargetFirst = (pos: DropPos) =>
296
+ pos.position.SouthOrEast
297
+ ? true
298
+ : pos?.tab?.positionRelativeToTab === "before"
299
+ ? false
300
+ : pos.position.Header
301
+ ? true
302
+ : false;
303
+
304
+ function getLayoutSpecForWrapper(pos: DropPos): LayoutSpec {
305
+ if (pos.position.Header) {
306
+ return {
307
+ type: "Stack",
308
+ flexDirection: "column",
309
+ showTabs: true,
310
+ };
311
+ } else {
312
+ return {
313
+ type: "Flexbox",
314
+ flexDirection: pos.position.EastOrWest ? "row" : "column",
315
+ };
316
+ }
317
+ }
@@ -0,0 +1,58 @@
1
+ .vuuView {
2
+ border: var(--vuuView-border, solid 1px var(--uitk-container-borderColor-medium));
3
+ display: flex;
4
+ flex-direction: column;
5
+ margin: var(--vuuView-margin, 3px);
6
+ min-height: 50px;
7
+ min-width: 50px;
8
+ outline: none;
9
+ overflow: hidden;
10
+ position: relative;
11
+ }
12
+
13
+ .vuuView.focus-visible:after {
14
+ content: '';
15
+ position: absolute;
16
+ top: 0;
17
+ left: 0;
18
+ right: 0;
19
+ bottom: 0;
20
+ border: dotted cornflowerblue 2px;
21
+ }
22
+
23
+ .vuuView.dragging {
24
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
25
+ }
26
+
27
+ .vuuView-main {
28
+ /* height: var(--view-content-height);
29
+ width: var(--view-content-width); */
30
+ display: flex;
31
+ flex-direction: var(--vuuView-flexDirection, column);
32
+ flex-wrap: var(--vuuView-flex-wrap, nowrap);
33
+ flex: 1;
34
+ overflow: hidden;
35
+ position: relative;
36
+ }
37
+
38
+ .vuuView-main > * {
39
+ flex-basis: auto;
40
+ flex-grow: var(--vuuView-flex-grow, 1);
41
+ flex-shrink: var(--vuuView-flex-shrink, 1);
42
+ }
43
+
44
+ .vuuView-collapsed .vuuView-main {
45
+ display: none;
46
+ }
47
+
48
+ .vuuView-collapsed + .Splitter {
49
+ display: none;
50
+ }
51
+
52
+ .vuuView-collapsed .Toolbar-vertical {
53
+ border-right: solid 1px var(--grey40);
54
+ }
55
+
56
+ .vuuView-collapsed .Toolbar-vertical .toolbar-title {
57
+ display: none;
58
+ }
@@ -0,0 +1,149 @@
1
+ import { useForkRef, useIdMemo as useId } from "@heswell/uitk-core";
2
+ import cx from "classnames";
3
+ import React, { ForwardedRef, forwardRef, useMemo, useRef } from "react";
4
+ import { Header } from "../layout-header/Header";
5
+ import { registerComponent } from "../registry/ComponentRegistry";
6
+ import { ViewContext } from "./ViewContext";
7
+ import { ViewProps } from "./viewTypes";
8
+ import { useView } from "./useView";
9
+ import { useViewResize } from "./useViewResize";
10
+
11
+ import "./View.css";
12
+
13
+ const View = forwardRef(function View(
14
+ props: ViewProps,
15
+ forwardedRef: ForwardedRef<HTMLDivElement>
16
+ ) {
17
+ const {
18
+ children,
19
+ className,
20
+ collapsed, // "vertical" | "horizontal" | false | undefined
21
+ closeable,
22
+ "data-resizeable": dataResizeable,
23
+ dropTargets,
24
+ expanded,
25
+ flexFill, // use data-flexfill instead
26
+ id: idProp,
27
+ header,
28
+ orientation = "horizontal",
29
+ path,
30
+ resize = "responsive", // maybe throttle or debounce ?
31
+ resizeable = dataResizeable,
32
+ tearOut,
33
+ style = {},
34
+ title: titleProp,
35
+ ...restProps
36
+ } = props;
37
+
38
+ // A View within a managed layout will always be passed an id
39
+ const id = useId(idProp);
40
+ const rootRef = useRef<HTMLDivElement>(null);
41
+ const mainRef = useRef<HTMLDivElement>(null);
42
+
43
+ const {
44
+ contributions,
45
+ dispatchViewAction,
46
+ load,
47
+ loadSession,
48
+ onConfigChange,
49
+ onEditTitle,
50
+ purge,
51
+ restoredState,
52
+ save,
53
+ saveSession,
54
+ title,
55
+ } = useView({
56
+ id,
57
+ rootRef,
58
+ path,
59
+ dropTargets,
60
+ title: titleProp,
61
+ });
62
+
63
+ useViewResize({ mainRef, resize, rootRef });
64
+
65
+ const classBase = "vuuView";
66
+
67
+ const getContent = () => {
68
+ // We only inject restored state as props if child is a single element. Maybe we
69
+ // should take this further and only do it if the component has opted into this
70
+ // behaviour.
71
+ if (React.isValidElement(children) && restoredState) {
72
+ return React.cloneElement(children, restoredState);
73
+ } else {
74
+ return children;
75
+ }
76
+ };
77
+
78
+ const viewContextValue = useMemo(
79
+ () => ({
80
+ dispatch: dispatchViewAction,
81
+ id,
82
+ path,
83
+ title,
84
+ load,
85
+ loadSession,
86
+ onConfigChange,
87
+ purge,
88
+ save,
89
+ saveSession,
90
+ }),
91
+ [
92
+ dispatchViewAction,
93
+ id,
94
+ load,
95
+ loadSession,
96
+ onConfigChange,
97
+ path,
98
+ purge,
99
+ save,
100
+ saveSession,
101
+ title,
102
+ ]
103
+ );
104
+
105
+ const headerProps = typeof header === "object" ? header : {};
106
+
107
+ return (
108
+ <div
109
+ {...restProps}
110
+ className={cx(classBase, className, {
111
+ [`${classBase}-collapsed`]: collapsed,
112
+ [`${classBase}-expanded`]: expanded,
113
+ [`${classBase}-resize-defer`]: resize === "defer",
114
+ })}
115
+ data-resizeable={resizeable}
116
+ id={id}
117
+ ref={useForkRef(forwardedRef, rootRef)}
118
+ style={style}
119
+ tabIndex={-1}
120
+ >
121
+ <ViewContext.Provider value={viewContextValue}>
122
+ {header ? (
123
+ <Header
124
+ {...headerProps}
125
+ collapsed={collapsed}
126
+ contributions={contributions}
127
+ expanded={expanded}
128
+ closeable={closeable}
129
+ onEditTitle={onEditTitle}
130
+ orientation={/*collapsed || */ orientation}
131
+ tearOut={tearOut}
132
+ // title={`${title} v${version} #${id}`}
133
+ title={title}
134
+ />
135
+ ) : null}
136
+ <div className={`${classBase}-main`} ref={mainRef}>
137
+ {getContent()}
138
+ </div>
139
+ </ViewContext.Provider>
140
+ </div>
141
+ );
142
+ });
143
+ View.displayName = "View";
144
+
145
+ const MemoView = React.memo(View) as React.FunctionComponent<ViewProps>;
146
+ MemoView.displayName = "View";
147
+ registerComponent("View", MemoView, "view");
148
+
149
+ export { MemoView as View };
@@ -0,0 +1,31 @@
1
+ import path from "path";
2
+ import React, { SyntheticEvent, useContext } from "react";
3
+ import { ViewAction } from "./viewTypes";
4
+
5
+ export type ViewDispatch = <Action extends ViewAction = ViewAction>(
6
+ action: Action,
7
+ evt?: SyntheticEvent
8
+ ) => Promise<boolean | void>;
9
+
10
+ export interface ViewContextProps {
11
+ dispatch: ViewDispatch | null;
12
+ id: string;
13
+ load: (key?: string) => any;
14
+ loadSession: (key?: string) => any;
15
+ onConfigChange?: (config: any) => void;
16
+ path?: string;
17
+ purge: (key: string) => void;
18
+ save: (state: any, key: string) => void;
19
+ saveSession: (state: any, key: string) => void;
20
+ title?: string;
21
+ }
22
+
23
+ const NO_CONTEXT = { dispatch: null } as ViewContextProps;
24
+ export const ViewContext = React.createContext<ViewContextProps>(NO_CONTEXT);
25
+
26
+ export const useViewDispatch = () => {
27
+ const context = useContext(ViewContext);
28
+ return context?.dispatch ?? null;
29
+ };
30
+
31
+ export const useViewContext = () => useContext(ViewContext);
@@ -0,0 +1,4 @@
1
+ export * from './ViewContext';
2
+ export * from './View';
3
+ export * from './useViewActionDispatcher';
4
+ export * from './viewTypes';
@@ -0,0 +1,104 @@
1
+ import { RefObject, useCallback, useMemo } from "react";
2
+ import { useLayoutProviderDispatch } from "../layout-provider";
3
+ import { usePersistentState } from "../use-persistent-state";
4
+ import { useViewActionDispatcher } from "./useViewActionDispatcher";
5
+
6
+ export interface ViewHookProps {
7
+ id: string;
8
+ rootRef: RefObject<HTMLDivElement>;
9
+ path?: string;
10
+ dropTargets?: string[];
11
+ title?: string;
12
+ }
13
+
14
+ export const useView = ({
15
+ id,
16
+ rootRef,
17
+ path,
18
+ dropTargets,
19
+ title: titleProp,
20
+ }: ViewHookProps) => {
21
+ const layoutDispatch = useLayoutProviderDispatch();
22
+
23
+ const {
24
+ loadState,
25
+ loadSessionState,
26
+ purgeState,
27
+ saveState,
28
+ saveSessionState,
29
+ } = usePersistentState();
30
+
31
+ const [dispatchViewAction, contributions] = useViewActionDispatcher(
32
+ id,
33
+ rootRef,
34
+ path,
35
+ dropTargets
36
+ );
37
+
38
+ const title = useMemo(
39
+ () => loadState("view-title") ?? titleProp,
40
+ [loadState, titleProp]
41
+ );
42
+
43
+ const onEditTitle = useCallback(
44
+ (title: string) => {
45
+ if (path) {
46
+ layoutDispatch({ type: "set-title", path, title });
47
+ }
48
+ },
49
+ [layoutDispatch, path]
50
+ );
51
+
52
+ const restoredState = useMemo(() => loadState(id), [id, loadState]);
53
+
54
+ const load = useCallback(
55
+ (key?: string) => loadState(id, key),
56
+ [id, loadState]
57
+ );
58
+
59
+ const purge = useCallback(
60
+ (key) => {
61
+ purgeState(id, key);
62
+ layoutDispatch({ type: "save" });
63
+ },
64
+ [id, purgeState]
65
+ );
66
+
67
+ const save = useCallback(
68
+ (state, key) => {
69
+ saveState(id, key, state);
70
+ layoutDispatch({ type: "save" });
71
+ },
72
+ [id, layoutDispatch, saveState]
73
+ );
74
+ const loadSession = useCallback(
75
+ (key?: string) => loadSessionState(id, key),
76
+ [id, loadSessionState]
77
+ );
78
+ const saveSession = useCallback(
79
+ (state, key) => saveSessionState(id, key, state),
80
+ [id, saveSessionState]
81
+ );
82
+
83
+ const onConfigChange = useCallback(
84
+ ({ type: key, ...config }) => {
85
+ const { [key]: data } = config;
86
+ save(data, key);
87
+ },
88
+ [save]
89
+ );
90
+
91
+ return {
92
+ contributions,
93
+ dispatchViewAction,
94
+ load,
95
+ loadSession,
96
+ onConfigChange,
97
+ onEditTitle,
98
+ purge,
99
+ restoredState,
100
+ save,
101
+ saveSession,
102
+ title,
103
+ };
104
+ };