@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,255 @@
|
|
|
1
|
+
import React, { ReactElement } from "react";
|
|
2
|
+
import { isContainer } from "../registry/ComponentRegistry";
|
|
3
|
+
import {
|
|
4
|
+
findTarget,
|
|
5
|
+
followPath,
|
|
6
|
+
followPathToParent,
|
|
7
|
+
getProp,
|
|
8
|
+
getProps,
|
|
9
|
+
typeOf,
|
|
10
|
+
} from "../utils";
|
|
11
|
+
import { getIntrinsicSize } from "./flexUtils";
|
|
12
|
+
import {
|
|
13
|
+
getInsertTabBeforeAfter,
|
|
14
|
+
insertBesideChild,
|
|
15
|
+
insertIntoContainer,
|
|
16
|
+
} from "./insert-layout-element";
|
|
17
|
+
import {
|
|
18
|
+
AddAction,
|
|
19
|
+
DragDropAction,
|
|
20
|
+
LayoutReducerAction,
|
|
21
|
+
LayoutActionType,
|
|
22
|
+
SetTitleAction,
|
|
23
|
+
SwitchTabAction,
|
|
24
|
+
MaximizeAction,
|
|
25
|
+
} from "./layoutTypes";
|
|
26
|
+
import { LayoutProps } from "./layoutUtils";
|
|
27
|
+
import { removeChild } from "./remove-layout-element";
|
|
28
|
+
import {
|
|
29
|
+
replaceChild,
|
|
30
|
+
swapChild,
|
|
31
|
+
_replaceChild,
|
|
32
|
+
} from "./replace-layout-element";
|
|
33
|
+
import { resizeFlexChildren } from "./resize-flex-children";
|
|
34
|
+
import { wrap } from "./wrap-layout-element";
|
|
35
|
+
import { DropPos } from "../drag-drop/dragDropTypes";
|
|
36
|
+
import { DropTarget } from "../drag-drop/DropTarget";
|
|
37
|
+
|
|
38
|
+
// const handlers: Handlers = {
|
|
39
|
+
// [Action.MAXIMIZE]: setChildProps,
|
|
40
|
+
// [Action.MINIMIZE]: setChildProps,
|
|
41
|
+
// [Action.RESTORE]: setChildProps,
|
|
42
|
+
// };
|
|
43
|
+
|
|
44
|
+
export const layoutReducer = (
|
|
45
|
+
state: ReactElement,
|
|
46
|
+
action: LayoutReducerAction
|
|
47
|
+
): ReactElement => {
|
|
48
|
+
switch (action.type) {
|
|
49
|
+
case LayoutActionType.ADD:
|
|
50
|
+
return addChild(state, action);
|
|
51
|
+
case LayoutActionType.DRAG_DROP:
|
|
52
|
+
return dragDrop(state, action);
|
|
53
|
+
case LayoutActionType.MAXIMIZE:
|
|
54
|
+
return setChildProps(state, action);
|
|
55
|
+
case LayoutActionType.REMOVE:
|
|
56
|
+
return removeChild(state, action);
|
|
57
|
+
case LayoutActionType.REPLACE:
|
|
58
|
+
return replaceChild(state, action);
|
|
59
|
+
case LayoutActionType.SET_TITLE:
|
|
60
|
+
return setTitle(state, action);
|
|
61
|
+
case LayoutActionType.SPLITTER_RESIZE:
|
|
62
|
+
return resizeFlexChildren(state, action);
|
|
63
|
+
case LayoutActionType.SWITCH_TAB:
|
|
64
|
+
return switchTab(state, action);
|
|
65
|
+
default:
|
|
66
|
+
console.warn(
|
|
67
|
+
`layoutActionHandlers. No handler for action.type ${
|
|
68
|
+
(action as any).type
|
|
69
|
+
}`
|
|
70
|
+
);
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function switchTab(state: ReactElement, { path, nextIdx }: SwitchTabAction) {
|
|
76
|
+
var target = followPath(state, path, true);
|
|
77
|
+
const replacement = React.cloneElement<any>(target, {
|
|
78
|
+
active: nextIdx,
|
|
79
|
+
});
|
|
80
|
+
return swapChild(state, target, replacement);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setTitle(state: ReactElement, { path, title }: SetTitleAction) {
|
|
84
|
+
var target = followPath(state, path, true);
|
|
85
|
+
const replacement = React.cloneElement(target, {
|
|
86
|
+
title,
|
|
87
|
+
});
|
|
88
|
+
return swapChild(state, target, replacement);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setChildProps(state: ReactElement, { path, type }: MaximizeAction) {
|
|
92
|
+
if (path) {
|
|
93
|
+
// path will always be set here. Need to distinguisj ViewAction from LayoutAction
|
|
94
|
+
var target = followPath(state, path, true);
|
|
95
|
+
return swapChild(state, target, target, type);
|
|
96
|
+
} else {
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function dragDrop(
|
|
102
|
+
layoutRoot: ReactElement,
|
|
103
|
+
action: DragDropAction
|
|
104
|
+
): ReactElement {
|
|
105
|
+
console.log("drag drop");
|
|
106
|
+
const {
|
|
107
|
+
draggedReactElement: newComponent,
|
|
108
|
+
dragInstructions,
|
|
109
|
+
dropTarget,
|
|
110
|
+
} = action;
|
|
111
|
+
const existingComponent = dropTarget.component as ReactElement;
|
|
112
|
+
const { pos } = dropTarget;
|
|
113
|
+
const destinationTabstrip =
|
|
114
|
+
pos?.position?.Header && typeOf(existingComponent) === "Stack";
|
|
115
|
+
const { id, version } = getProps(newComponent);
|
|
116
|
+
const intrinsicSize = getIntrinsicSize(newComponent);
|
|
117
|
+
let newLayoutRoot: ReactElement;
|
|
118
|
+
if (destinationTabstrip) {
|
|
119
|
+
const [targetTab, insertionPosition] = getInsertTabBeforeAfter(
|
|
120
|
+
existingComponent!,
|
|
121
|
+
pos
|
|
122
|
+
);
|
|
123
|
+
if (targetTab === undefined) {
|
|
124
|
+
newLayoutRoot = insertIntoContainer(
|
|
125
|
+
layoutRoot,
|
|
126
|
+
existingComponent,
|
|
127
|
+
newComponent
|
|
128
|
+
);
|
|
129
|
+
} else {
|
|
130
|
+
newLayoutRoot = insertBesideChild(
|
|
131
|
+
layoutRoot,
|
|
132
|
+
targetTab,
|
|
133
|
+
newComponent,
|
|
134
|
+
insertionPosition
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} else if (!intrinsicSize && pos?.position?.Centre) {
|
|
138
|
+
newLayoutRoot = _replaceChild(
|
|
139
|
+
layoutRoot,
|
|
140
|
+
existingComponent as ReactElement,
|
|
141
|
+
newComponent
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
newLayoutRoot = dropLayoutIntoContainer(
|
|
145
|
+
layoutRoot,
|
|
146
|
+
dropTarget as DropTarget,
|
|
147
|
+
newComponent
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// return newLayoutRoot
|
|
152
|
+
|
|
153
|
+
if (dragInstructions.DoNotRemove) {
|
|
154
|
+
return newLayoutRoot;
|
|
155
|
+
} else {
|
|
156
|
+
const finalTarget = findTarget(
|
|
157
|
+
newLayoutRoot,
|
|
158
|
+
(props: LayoutProps) => props.id === id && props.version === version
|
|
159
|
+
) as ReactElement;
|
|
160
|
+
const finalPath = getProp(finalTarget, "path");
|
|
161
|
+
return removeChild(newLayoutRoot, { path: finalPath, type: "remove" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function addChild(
|
|
166
|
+
layoutRoot: ReactElement,
|
|
167
|
+
{ path: containerPath, component }: AddAction
|
|
168
|
+
) {
|
|
169
|
+
return insertIntoContainer(
|
|
170
|
+
layoutRoot,
|
|
171
|
+
followPath(layoutRoot, containerPath) as ReactElement,
|
|
172
|
+
component
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function dropLayoutIntoContainer(
|
|
177
|
+
layoutRoot: ReactElement,
|
|
178
|
+
dropTarget: DropTarget,
|
|
179
|
+
newComponent: ReactElement
|
|
180
|
+
): ReactElement {
|
|
181
|
+
const {
|
|
182
|
+
component: existingComponent,
|
|
183
|
+
pos,
|
|
184
|
+
clientRect,
|
|
185
|
+
dropRect,
|
|
186
|
+
} = dropTarget;
|
|
187
|
+
const existingComponentPath = getProp(existingComponent, "path");
|
|
188
|
+
// In a Draggable layout, 0.n is the top-level layout
|
|
189
|
+
if (
|
|
190
|
+
/* existingComponent.path === '0.0' || */ existingComponentPath === "0.0"
|
|
191
|
+
) {
|
|
192
|
+
return wrap(layoutRoot, existingComponent, newComponent, pos);
|
|
193
|
+
} else {
|
|
194
|
+
var targetContainer = followPathToParent(
|
|
195
|
+
layoutRoot,
|
|
196
|
+
existingComponentPath
|
|
197
|
+
) as ReactElement;
|
|
198
|
+
|
|
199
|
+
if (withTheGrain(pos, targetContainer)) {
|
|
200
|
+
const insertionPosition = pos.position.SouthOrEast ? "after" : "before";
|
|
201
|
+
return insertBesideChild(
|
|
202
|
+
layoutRoot,
|
|
203
|
+
existingComponent,
|
|
204
|
+
newComponent,
|
|
205
|
+
insertionPosition,
|
|
206
|
+
pos,
|
|
207
|
+
clientRect,
|
|
208
|
+
dropRect
|
|
209
|
+
);
|
|
210
|
+
} else if (!withTheGrain(pos, targetContainer)) {
|
|
211
|
+
return wrap(
|
|
212
|
+
layoutRoot,
|
|
213
|
+
existingComponent,
|
|
214
|
+
newComponent,
|
|
215
|
+
pos,
|
|
216
|
+
clientRect,
|
|
217
|
+
dropRect
|
|
218
|
+
);
|
|
219
|
+
} else if (isContainer(typeOf(targetContainer) as string)) {
|
|
220
|
+
return wrap(layoutRoot, existingComponent, newComponent, pos);
|
|
221
|
+
} else {
|
|
222
|
+
throw Error(`no support right now for position = ${pos.position}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return layoutRoot;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Note: withTheGrain is not the negative of againstTheGrain - the difference lies in the
|
|
230
|
+
// handling of non-Flexible containers, the response for which is always false;
|
|
231
|
+
function withTheGrain(pos: DropPos, container: ReactElement) {
|
|
232
|
+
if (pos.position.Centre) {
|
|
233
|
+
return isTerrace(container) || isTower(container);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return pos.position.NorthOrSouth
|
|
237
|
+
? isTower(container)
|
|
238
|
+
: pos.position.EastOrWest
|
|
239
|
+
? isTerrace(container)
|
|
240
|
+
: false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isTower(container: ReactElement) {
|
|
244
|
+
return (
|
|
245
|
+
typeOf(container) === "Flexbox" &&
|
|
246
|
+
container.props.style.flexDirection === "column"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isTerrace(container: ReactElement) {
|
|
251
|
+
return (
|
|
252
|
+
typeOf(container) === "Flexbox" &&
|
|
253
|
+
container.props.style.flexDirection !== "column"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { DropTarget } from "../drag-drop/DropTarget";
|
|
3
|
+
import { DragDropRect, DragInstructions, DropPos } from "../drag-drop";
|
|
4
|
+
|
|
5
|
+
export interface WithProps {
|
|
6
|
+
props?: { [key: string]: any };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WithType {
|
|
10
|
+
props?: any;
|
|
11
|
+
title?: string;
|
|
12
|
+
type: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LayoutRoot extends WithProps {
|
|
16
|
+
active?: number;
|
|
17
|
+
children?: ReactElement[];
|
|
18
|
+
type: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LayoutJSON extends WithType {
|
|
22
|
+
children?: LayoutJSON[];
|
|
23
|
+
id?: string;
|
|
24
|
+
props?: { [key: string]: any };
|
|
25
|
+
state?: any;
|
|
26
|
+
type: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WithActive {
|
|
30
|
+
active?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type LayoutModel = LayoutRoot | ReactElement | WithType;
|
|
34
|
+
|
|
35
|
+
export type layoutType = "Flexbox" | "View" | "DraggableLayout" | "Stack";
|
|
36
|
+
|
|
37
|
+
export const LayoutActionType = {
|
|
38
|
+
ADD: "add",
|
|
39
|
+
DRAG_START: "drag-start",
|
|
40
|
+
DRAG_DROP: "drag-drop",
|
|
41
|
+
MAXIMIZE: "maximize",
|
|
42
|
+
MINIMIZE: "minimize",
|
|
43
|
+
REMOVE: "remove",
|
|
44
|
+
REPLACE: "replace",
|
|
45
|
+
RESTORE: "restore",
|
|
46
|
+
SAVE: "save",
|
|
47
|
+
SET_TITLE: "set-title",
|
|
48
|
+
SPLITTER_RESIZE: "splitter-resize",
|
|
49
|
+
SWITCH_TAB: "switch-tab",
|
|
50
|
+
TEAROUT: "tearout",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export type AddAction = {
|
|
54
|
+
component: any;
|
|
55
|
+
path: string;
|
|
56
|
+
type: typeof LayoutActionType.ADD;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DragDropAction = {
|
|
60
|
+
draggedReactElement: ReactElement;
|
|
61
|
+
dragInstructions: any;
|
|
62
|
+
dropTarget: Partial<DropTarget>;
|
|
63
|
+
type: typeof LayoutActionType.DRAG_DROP;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type MaximizeAction = {
|
|
67
|
+
path?: string;
|
|
68
|
+
type: typeof LayoutActionType.MAXIMIZE;
|
|
69
|
+
};
|
|
70
|
+
export type MinimizeAction = {
|
|
71
|
+
path?: string;
|
|
72
|
+
type: typeof LayoutActionType.MINIMIZE;
|
|
73
|
+
};
|
|
74
|
+
export type RemoveAction = {
|
|
75
|
+
path?: string;
|
|
76
|
+
type: typeof LayoutActionType.REMOVE;
|
|
77
|
+
};
|
|
78
|
+
export type ReplaceAction = {
|
|
79
|
+
replacement: any;
|
|
80
|
+
target: any;
|
|
81
|
+
type: typeof LayoutActionType.REPLACE;
|
|
82
|
+
};
|
|
83
|
+
export type RestoreAction = {
|
|
84
|
+
path?: string;
|
|
85
|
+
type: typeof LayoutActionType.RESTORE;
|
|
86
|
+
};
|
|
87
|
+
export type SetTitleAction = {
|
|
88
|
+
path: string;
|
|
89
|
+
title: string;
|
|
90
|
+
type: typeof LayoutActionType.SET_TITLE;
|
|
91
|
+
};
|
|
92
|
+
export type SplitterResizeAction = {
|
|
93
|
+
path: string;
|
|
94
|
+
sizes: { currentSize: number; flexBasis: number }[];
|
|
95
|
+
type: typeof LayoutActionType.SPLITTER_RESIZE;
|
|
96
|
+
};
|
|
97
|
+
export type SwitchTabAction = {
|
|
98
|
+
nextIdx: number;
|
|
99
|
+
path: string;
|
|
100
|
+
type: typeof LayoutActionType.SWITCH_TAB;
|
|
101
|
+
};
|
|
102
|
+
export type TearoutAction = {
|
|
103
|
+
path?: string;
|
|
104
|
+
type: typeof LayoutActionType.TEAROUT;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type LayoutReducerAction =
|
|
108
|
+
| AddAction
|
|
109
|
+
| DragDropAction
|
|
110
|
+
| MaximizeAction
|
|
111
|
+
| MinimizeAction
|
|
112
|
+
| RemoveAction
|
|
113
|
+
| ReplaceAction
|
|
114
|
+
| RestoreAction
|
|
115
|
+
| SetTitleAction
|
|
116
|
+
| SplitterResizeAction
|
|
117
|
+
| SwitchTabAction;
|
|
118
|
+
|
|
119
|
+
export type SaveAction = {
|
|
120
|
+
type: typeof LayoutActionType.SAVE;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type AddToolbarContributionViewAction = {
|
|
124
|
+
content: ReactElement;
|
|
125
|
+
location: string;
|
|
126
|
+
type: "add-toolbar-contribution";
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type RemoveToolbarContributionViewAction = {
|
|
130
|
+
location: string;
|
|
131
|
+
type: "remove-toolbar-contribution";
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type MousedownViewAction = {
|
|
135
|
+
preDragActivity?: any;
|
|
136
|
+
index?: number;
|
|
137
|
+
type: "mousedown";
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// TODO split this out into separate actions for different drag scenarios
|
|
141
|
+
export type DragStartAction = {
|
|
142
|
+
payload?: ReactElement;
|
|
143
|
+
dragContainerPath?: string;
|
|
144
|
+
dragElement?: HTMLElement;
|
|
145
|
+
dragRect: DragDropRect;
|
|
146
|
+
dropTargets?: string[];
|
|
147
|
+
evt: MouseEvent;
|
|
148
|
+
instructions?: DragInstructions;
|
|
149
|
+
path: string;
|
|
150
|
+
type: typeof LayoutActionType.DRAG_START;
|
|
151
|
+
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { uuid } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { CSSProperties, ReactElement } from "react";
|
|
3
|
+
import React, { cloneElement } from "react";
|
|
4
|
+
import { dimension } from "../common-types";
|
|
5
|
+
import {
|
|
6
|
+
ComponentRegistry,
|
|
7
|
+
isContainer,
|
|
8
|
+
isLayoutComponent,
|
|
9
|
+
} from "../registry/ComponentRegistry";
|
|
10
|
+
import {
|
|
11
|
+
getPersistentState,
|
|
12
|
+
hasPersistentState,
|
|
13
|
+
setPersistentState,
|
|
14
|
+
} from "../use-persistent-state";
|
|
15
|
+
import { expandFlex, getProps, typeOf } from "../utils";
|
|
16
|
+
import { LayoutJSON, LayoutModel, layoutType } from "./layoutTypes";
|
|
17
|
+
|
|
18
|
+
export const getManagedDimension = (
|
|
19
|
+
style: CSSProperties
|
|
20
|
+
): [dimension, dimension] =>
|
|
21
|
+
style.flexDirection === "column" ? ["height", "width"] : ["width", "height"];
|
|
22
|
+
|
|
23
|
+
const theKidHasNoStyle: CSSProperties = {};
|
|
24
|
+
|
|
25
|
+
export const applyLayoutProps = (component: ReactElement, path = "0") => {
|
|
26
|
+
const [layoutProps, children] = getChildLayoutProps(
|
|
27
|
+
typeOf(component) as string,
|
|
28
|
+
component.props,
|
|
29
|
+
path
|
|
30
|
+
);
|
|
31
|
+
return React.cloneElement(component, layoutProps, children);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface LayoutProps {
|
|
35
|
+
active?: number;
|
|
36
|
+
"data-path"?: string;
|
|
37
|
+
children?: ReactElement[];
|
|
38
|
+
column?: any;
|
|
39
|
+
dropTarget?: any;
|
|
40
|
+
id: string;
|
|
41
|
+
key: string;
|
|
42
|
+
layout?: any;
|
|
43
|
+
path?: string;
|
|
44
|
+
resizeable?: boolean;
|
|
45
|
+
style: CSSProperties;
|
|
46
|
+
type?: string;
|
|
47
|
+
version?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const processLayoutElement = (
|
|
51
|
+
layoutElement: ReactElement,
|
|
52
|
+
previousLayout?: ReactElement
|
|
53
|
+
): ReactElement => {
|
|
54
|
+
const type = typeOf(layoutElement) as string;
|
|
55
|
+
const [layoutProps, children] = getChildLayoutProps(
|
|
56
|
+
type,
|
|
57
|
+
layoutElement.props,
|
|
58
|
+
"0",
|
|
59
|
+
undefined,
|
|
60
|
+
previousLayout
|
|
61
|
+
);
|
|
62
|
+
return cloneElement(layoutElement, layoutProps, children);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const applyLayout = (
|
|
66
|
+
type: layoutType,
|
|
67
|
+
props: LayoutProps,
|
|
68
|
+
previousLayout?: LayoutModel
|
|
69
|
+
): LayoutModel => {
|
|
70
|
+
// This works if the root layout is itself loaded from JSON
|
|
71
|
+
const [layoutProps, children] = getChildLayoutProps(
|
|
72
|
+
type,
|
|
73
|
+
props,
|
|
74
|
+
"0",
|
|
75
|
+
undefined,
|
|
76
|
+
previousLayout
|
|
77
|
+
);
|
|
78
|
+
return {
|
|
79
|
+
...props,
|
|
80
|
+
...layoutProps,
|
|
81
|
+
type,
|
|
82
|
+
children,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function getLayoutProps(
|
|
87
|
+
type: string,
|
|
88
|
+
props: LayoutProps,
|
|
89
|
+
path = "0",
|
|
90
|
+
parentType: string | null = null,
|
|
91
|
+
previousLayout?: LayoutModel
|
|
92
|
+
): LayoutProps {
|
|
93
|
+
const {
|
|
94
|
+
active: prevActive = 0,
|
|
95
|
+
"data-path": dataPath,
|
|
96
|
+
path: prevPath = dataPath,
|
|
97
|
+
id: prevId,
|
|
98
|
+
style: prevStyle,
|
|
99
|
+
} = getProps(previousLayout);
|
|
100
|
+
|
|
101
|
+
const prevMatch = typeOf(previousLayout) === type && path === prevPath;
|
|
102
|
+
// TODO is there anything else we can re-use from previousType ?
|
|
103
|
+
const id = prevMatch ? prevId : props.id ?? uuid();
|
|
104
|
+
const active = type === "Stack" ? props.active ?? prevActive : undefined;
|
|
105
|
+
|
|
106
|
+
const key = id;
|
|
107
|
+
//TODO this might be wrong if client has updated style ?
|
|
108
|
+
const style = prevMatch ? prevStyle : getStyle(type, props, parentType);
|
|
109
|
+
// TODO need two interfaces to cover these two scenarios
|
|
110
|
+
return isLayoutComponent(type)
|
|
111
|
+
? { id, key, path, style, type, active }
|
|
112
|
+
: { id, key, style, "data-path": path };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getChildLayoutProps(
|
|
116
|
+
type: string,
|
|
117
|
+
props: LayoutProps,
|
|
118
|
+
path: string,
|
|
119
|
+
parentType: string | null = null,
|
|
120
|
+
previousLayout?: LayoutModel
|
|
121
|
+
): [LayoutProps, ReactElement[]] {
|
|
122
|
+
const layoutProps = getLayoutProps(
|
|
123
|
+
type,
|
|
124
|
+
props,
|
|
125
|
+
path,
|
|
126
|
+
parentType,
|
|
127
|
+
previousLayout
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (props.layout && !previousLayout) {
|
|
131
|
+
// reconstitute children from layout. Will always be a single child,
|
|
132
|
+
// but return as array to make subsequent processing more consistent
|
|
133
|
+
return [layoutProps, [layoutFromJson(props.layout, `${path}.0`)]];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const previousChildren =
|
|
137
|
+
(previousLayout as any)?.children ?? previousLayout?.props?.children;
|
|
138
|
+
const hasDynamicChildren = props.dropTarget && previousChildren;
|
|
139
|
+
const children = hasDynamicChildren
|
|
140
|
+
? previousChildren
|
|
141
|
+
: getLayoutChildren(type, props.children, path, previousChildren);
|
|
142
|
+
return [layoutProps, children];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getLayoutChildren(
|
|
146
|
+
type: string,
|
|
147
|
+
children?: ReactElement[],
|
|
148
|
+
path = "0",
|
|
149
|
+
previousChildren?: ReactElement[]
|
|
150
|
+
) {
|
|
151
|
+
// Avoid React.Children.map here, it messes with the keys.
|
|
152
|
+
const kids = Array.isArray(children)
|
|
153
|
+
? children
|
|
154
|
+
: React.isValidElement(children)
|
|
155
|
+
? [children]
|
|
156
|
+
: [];
|
|
157
|
+
return isContainer(type) /*|| isView(type)*/
|
|
158
|
+
? kids.map((child, i) => {
|
|
159
|
+
const childType = typeOf(child) as string;
|
|
160
|
+
const previousType = typeOf(previousChildren?.[i]);
|
|
161
|
+
if (!previousType || childType === previousType) {
|
|
162
|
+
const [layoutProps, children] = getChildLayoutProps(
|
|
163
|
+
childType,
|
|
164
|
+
child.props,
|
|
165
|
+
`${path}.${i}`,
|
|
166
|
+
type,
|
|
167
|
+
previousChildren?.[i]
|
|
168
|
+
);
|
|
169
|
+
return React.cloneElement(child, layoutProps, children);
|
|
170
|
+
} else {
|
|
171
|
+
//TODO is this always correct ?
|
|
172
|
+
return previousChildren?.[i];
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
: // TODO should we check the types of children ?
|
|
176
|
+
// : previousChildren ?? children;
|
|
177
|
+
//TODO this is new - is it dangerous ?
|
|
178
|
+
children;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const getStyle = (
|
|
182
|
+
type: string,
|
|
183
|
+
props: LayoutProps,
|
|
184
|
+
parentType?: string | null
|
|
185
|
+
) => {
|
|
186
|
+
let { style = theKidHasNoStyle } = props;
|
|
187
|
+
if (type === "Flexbox") {
|
|
188
|
+
style = {
|
|
189
|
+
flexDirection: props.column ? "column" : "row",
|
|
190
|
+
...style,
|
|
191
|
+
display: "flex",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (style.flex) {
|
|
196
|
+
const { flex, ...otherStyles } = style;
|
|
197
|
+
style = {
|
|
198
|
+
...otherStyles,
|
|
199
|
+
...expandFlex(flex),
|
|
200
|
+
};
|
|
201
|
+
} else if (parentType === "Stack") {
|
|
202
|
+
style = {
|
|
203
|
+
...style,
|
|
204
|
+
...expandFlex(1),
|
|
205
|
+
};
|
|
206
|
+
} else if (
|
|
207
|
+
parentType === "Flexbox" &&
|
|
208
|
+
(style.width || style.height) &&
|
|
209
|
+
style.flexBasis === undefined
|
|
210
|
+
) {
|
|
211
|
+
// strictly, this should depend on flexDirection
|
|
212
|
+
style = {
|
|
213
|
+
...style,
|
|
214
|
+
flexBasis: "auto",
|
|
215
|
+
flexGrow: 0,
|
|
216
|
+
flexShrink: 0,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return style;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
//TODO we don't need id beyond view
|
|
224
|
+
export function layoutFromJson(
|
|
225
|
+
{ id = uuid(), type, children, props, state }: LayoutJSON,
|
|
226
|
+
path: string
|
|
227
|
+
): ReactElement {
|
|
228
|
+
// if (type === "DraggableLayout") {
|
|
229
|
+
// return layoutFromJson(children[0], "0");
|
|
230
|
+
// }
|
|
231
|
+
|
|
232
|
+
const componentType = type.match(/^[a-z]/) ? type : ComponentRegistry[type];
|
|
233
|
+
|
|
234
|
+
if (componentType === undefined) {
|
|
235
|
+
throw Error(`Unable to create component from JSON, unknown type ${type}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (state) {
|
|
239
|
+
setPersistentState(id, state);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return React.createElement(
|
|
243
|
+
componentType,
|
|
244
|
+
{
|
|
245
|
+
...props,
|
|
246
|
+
id,
|
|
247
|
+
key: id,
|
|
248
|
+
path,
|
|
249
|
+
},
|
|
250
|
+
children
|
|
251
|
+
? children.map((child, i) => layoutFromJson(child, `${path}.${i}`))
|
|
252
|
+
: undefined
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function layoutToJSON(component: ReactElement) {
|
|
257
|
+
return componentToJson(component);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function componentToJson(component: ReactElement): LayoutJSON {
|
|
261
|
+
const type = typeOf(component) as string;
|
|
262
|
+
const { id, children, type: _omit, ...props } = getProps(component);
|
|
263
|
+
|
|
264
|
+
const state = hasPersistentState(id) ? getPersistentState(id) : undefined;
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
id,
|
|
268
|
+
type,
|
|
269
|
+
props: serializeProps(props as LayoutProps),
|
|
270
|
+
state,
|
|
271
|
+
children: React.Children.map(children, componentToJson),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function serializeProps(props?: LayoutProps) {
|
|
276
|
+
if (props) {
|
|
277
|
+
const { path, ...otherProps } = props;
|
|
278
|
+
const result: { [key: string]: any } = {};
|
|
279
|
+
for (let [key, value] of Object.entries(otherProps)) {
|
|
280
|
+
result[key] = serializeValue(value);
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function serializeValue(value: unknown): any {
|
|
287
|
+
if (
|
|
288
|
+
typeof value === "string" ||
|
|
289
|
+
typeof value === "number" ||
|
|
290
|
+
typeof value === "boolean"
|
|
291
|
+
) {
|
|
292
|
+
return value;
|
|
293
|
+
} else if (Array.isArray(value)) {
|
|
294
|
+
return value.map(serializeValue);
|
|
295
|
+
} else if (typeof value === "object" && value !== null) {
|
|
296
|
+
const result: { [key: string]: any } = {};
|
|
297
|
+
for (let [k, v] of Object.entries(value)) {
|
|
298
|
+
result[k] = serializeValue(v);
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
}
|