@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,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
+ }