@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.
- package/README.md +1 -0
- package/package.json +30 -0
- package/src/Component.css +2 -0
- package/src/Component.tsx +20 -0
- package/src/DraggableLayout.css +18 -0
- package/src/DraggableLayout.tsx +29 -0
- package/src/__tests__/flexbox-utils.spec.js +90 -0
- package/src/action-buttons/action-buttons.css +12 -0
- package/src/action-buttons/action-buttons.tsx +30 -0
- package/src/action-buttons/index.ts +1 -0
- package/src/chest-of-drawers/Chest.css +36 -0
- package/src/chest-of-drawers/Chest.tsx +42 -0
- package/src/chest-of-drawers/Drawer.css +153 -0
- package/src/chest-of-drawers/Drawer.tsx +118 -0
- package/src/chest-of-drawers/index.ts +2 -0
- package/src/common-types.ts +9 -0
- package/src/debug.ts +16 -0
- package/src/dialog/Dialog.css +16 -0
- package/src/dialog/Dialog.tsx +59 -0
- package/src/dialog/index.ts +1 -0
- package/src/drag-drop/BoxModel.ts +546 -0
- package/src/drag-drop/DragState.ts +222 -0
- package/src/drag-drop/Draggable.ts +282 -0
- package/src/drag-drop/DropMenu.css +70 -0
- package/src/drag-drop/DropMenu.tsx +68 -0
- package/src/drag-drop/DropTarget.ts +392 -0
- package/src/drag-drop/DropTargetRenderer.css +40 -0
- package/src/drag-drop/DropTargetRenderer.tsx +284 -0
- package/src/drag-drop/dragDropTypes.ts +49 -0
- package/src/drag-drop/index.ts +4 -0
- package/src/editable-label/EditableLabel.css +28 -0
- package/src/editable-label/EditableLabel.tsx +99 -0
- package/src/editable-label/index.ts +1 -0
- package/src/flexbox/Flexbox.css +45 -0
- package/src/flexbox/Flexbox.tsx +70 -0
- package/src/flexbox/FlexboxLayout.jsx +26 -0
- package/src/flexbox/FluidGrid.css +134 -0
- package/src/flexbox/FluidGrid.tsx +84 -0
- package/src/flexbox/FluidGridLayout.tsx +10 -0
- package/src/flexbox/Splitter.css +140 -0
- package/src/flexbox/Splitter.tsx +135 -0
- package/src/flexbox/flexbox-utils.ts +128 -0
- package/src/flexbox/flexboxTypes.ts +63 -0
- package/src/flexbox/index.ts +4 -0
- package/src/flexbox/useResponsiveSizing.ts +85 -0
- package/src/flexbox/useSplitterResizing.ts +272 -0
- package/src/index.ts +20 -0
- package/src/layout-action.ts +21 -0
- package/src/layout-header/ActionButton.tsx +23 -0
- package/src/layout-header/Header.css +8 -0
- package/src/layout-header/Header.tsx +222 -0
- package/src/layout-header/index.ts +1 -0
- package/src/layout-provider/LayoutProvider.tsx +160 -0
- package/src/layout-provider/LayoutProviderContext.ts +17 -0
- package/src/layout-provider/index.ts +2 -0
- package/src/layout-provider/useLayoutDragDrop.ts +241 -0
- package/src/layout-reducer/flexUtils.ts +281 -0
- package/src/layout-reducer/index.ts +4 -0
- package/src/layout-reducer/insert-layout-element.ts +365 -0
- package/src/layout-reducer/layout-reducer.ts +255 -0
- package/src/layout-reducer/layoutTypes.ts +151 -0
- package/src/layout-reducer/layoutUtils.ts +302 -0
- package/src/layout-reducer/remove-layout-element.ts +240 -0
- package/src/layout-reducer/replace-layout-element.ts +118 -0
- package/src/layout-reducer/resize-flex-children.ts +56 -0
- package/src/layout-reducer/wrap-layout-element.ts +317 -0
- package/src/layout-view/View.css +58 -0
- package/src/layout-view/View.tsx +149 -0
- package/src/layout-view/ViewContext.ts +31 -0
- package/src/layout-view/index.ts +4 -0
- package/src/layout-view/useView.tsx +104 -0
- package/src/layout-view/useViewActionDispatcher.ts +133 -0
- package/src/layout-view/useViewResize.ts +53 -0
- package/src/layout-view/viewTypes.ts +37 -0
- package/src/palette/Palette.css +37 -0
- package/src/palette/Palette.tsx +140 -0
- package/src/palette/PaletteUitk.css +9 -0
- package/src/palette/PaletteUitk.tsx +79 -0
- package/src/palette/index.ts +2 -0
- package/src/placeholder/Placeholder.css +10 -0
- package/src/placeholder/Placeholder.tsx +39 -0
- package/src/placeholder/index.ts +1 -0
- package/src/registry/ComponentRegistry.ts +35 -0
- package/src/registry/index.ts +1 -0
- package/src/responsive/OverflowMenu.css +31 -0
- package/src/responsive/OverflowMenu.jsx +56 -0
- package/src/responsive/breakpoints.ts +48 -0
- package/src/responsive/index.ts +4 -0
- package/src/responsive/measureMinimumNodeSize.ts +23 -0
- package/src/responsive/overflowUtils.js +14 -0
- package/src/responsive/use-breakpoints.ts +100 -0
- package/src/responsive/useOverflowObserver.ts +606 -0
- package/src/responsive/useResizeObserver.ts +154 -0
- package/src/responsive/utils.ts +37 -0
- package/src/stack/Stack.css +39 -0
- package/src/stack/Stack.tsx +160 -0
- package/src/stack/StackLayout.tsx +137 -0
- package/src/stack/index.ts +3 -0
- package/src/stack/stackTypes.ts +19 -0
- package/src/tabs/TabPanel.css +12 -0
- package/src/tabs/TabPanel.tsx +17 -0
- package/src/tabs/index.ts +1 -0
- package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
- package/src/tools/config-wrapper/index.js +1 -0
- package/src/tools/devtools-box/layout-configurator.css +112 -0
- package/src/tools/devtools-box/layout-configurator.jsx +369 -0
- package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
- package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
- package/src/tools/index.js +3 -0
- package/src/use-persistent-state.ts +115 -0
- package/src/utils/componentFromLayout.tsx +30 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/pathUtils.ts +294 -0
- package/src/utils/propUtils.ts +24 -0
- package/src/utils/refUtils.ts +16 -0
- package/src/utils/styleUtils.ts +14 -0
- 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,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
|
+
};
|