@vuu-ui/grid-layout 4.0.0-alpha.1
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/LICENSE +201 -0
- package/README.md +141 -0
- package/package.json +40 -0
- package/src/GridCommand.js +395 -0
- package/src/GridComponentSettings.js +163 -0
- package/src/GridController.js +359 -0
- package/src/GridDragCoordinator.js +293 -0
- package/src/GridGeometry.js +1298 -0
- package/src/GridLayout.css.js +119 -0
- package/src/GridLayout.js +120 -0
- package/src/GridLayoutContext.js +58 -0
- package/src/GridLayoutDocument.js +307 -0
- package/src/GridLayoutDocumentIds.js +110 -0
- package/src/GridLayoutItem.css.js +46 -0
- package/src/GridLayoutItem.js +147 -0
- package/src/GridLayoutLegacyCompatibility.js +66 -0
- package/src/GridLayoutModel.js +138 -0
- package/src/GridLayoutProvider.js +263 -0
- package/src/GridLayoutStackedItem.css.js +26 -0
- package/src/GridLayoutStackedtem.js +140 -0
- package/src/GridModel.js +1289 -0
- package/src/GridPlaceholder.css.js +9 -0
- package/src/GridPlaceholder.js +24 -0
- package/src/GridSnapshot.js +11 -0
- package/src/GridSplitter.css.js +62 -0
- package/src/GridSplitter.js +26 -0
- package/src/GridStack.js +277 -0
- package/src/Icon.css.js +8 -0
- package/src/Icon.js +26 -0
- package/src/IconButton.css.js +9 -0
- package/src/IconButton.js +28 -0
- package/src/TabDialog.css.js +10 -0
- package/src/TabDialog.js +77 -0
- package/src/TabMenu.css.js +8 -0
- package/src/TabMenu.js +78 -0
- package/src/componentToJson.js +41 -0
- package/src/drag-drop-next/DragContextNext.js +173 -0
- package/src/drag-drop-next/DragDropProviderNext.css.js +72 -0
- package/src/drag-drop-next/DragDropProviderNext.js +91 -0
- package/src/drag-drop-next/SpaceMan.js +256 -0
- package/src/drag-drop-next/TemplateDragSession.js +36 -0
- package/src/drag-drop-next/tabstrip-drag-drop.js +251 -0
- package/src/grid-dom-utils.js +57 -0
- package/src/grid-layout-utils.js +39 -0
- package/src/grid-snapshot-adapters.js +435 -0
- package/src/index.js +24 -0
- package/src/json-value.js +56 -0
- package/src/layoutFromJson.js +15 -0
- package/src/layoutToJson.js +5 -0
- package/src/propUtils.js +15 -0
- package/src/react-element-utils.js +15 -0
- package/src/typeOf.js +15 -0
- package/src/useAsDropTarget.js +229 -0
- package/src/useDraggable.js +46 -0
- package/src/useEditTabName.js +70 -0
- package/src/useGridChildProps.js +47 -0
- package/src/useGridControllerSnapshot.js +21 -0
- package/src/useGridLayout.js +727 -0
- package/src/useGridSplitterResizing.js +262 -0
- package/src/useNotDropTarget.js +2 -0
- package/types/AddTabDialog.d.ts +5 -0
- package/types/GridCommand.d.ts +146 -0
- package/types/GridComponentSettings.d.ts +71 -0
- package/types/GridController.d.ts +50 -0
- package/types/GridDragCoordinator.d.ts +80 -0
- package/types/GridGeometry.d.ts +331 -0
- package/types/GridLayout.d.ts +20 -0
- package/types/GridLayoutContext.d.ts +141 -0
- package/types/GridLayoutDocument.d.ts +61 -0
- package/types/GridLayoutDocumentIds.d.ts +15 -0
- package/types/GridLayoutItem.d.ts +28 -0
- package/types/GridLayoutLegacyCompatibility.d.ts +57 -0
- package/types/GridLayoutModel.d.ts +72 -0
- package/types/GridLayoutProvider.d.ts +59 -0
- package/types/GridLayoutStackedtem.d.ts +10 -0
- package/types/GridModel.d.ts +504 -0
- package/types/GridPlaceholder.d.ts +5 -0
- package/types/GridSnapshot.d.ts +49 -0
- package/types/GridSplitter.d.ts +10 -0
- package/types/GridStack.d.ts +160 -0
- package/types/Icon.d.ts +6 -0
- package/types/IconButton.d.ts +6 -0
- package/types/TabDialog.d.ts +9 -0
- package/types/TabMenu.d.ts +7 -0
- package/types/componentToJson.d.ts +38 -0
- package/types/drag-drop-next/DragContextNext.d.ts +92 -0
- package/types/drag-drop-next/DragDropProviderNext.d.ts +23 -0
- package/types/drag-drop-next/SpaceMan.d.ts +35 -0
- package/types/drag-drop-next/TemplateDragSession.d.ts +21 -0
- package/types/drag-drop-next/tabstrip-drag-drop.d.ts +3 -0
- package/types/grid-dom-utils.d.ts +17 -0
- package/types/grid-layout-utils.d.ts +18 -0
- package/types/grid-matrix.d.ts +5 -0
- package/types/grid-snapshot-adapters.d.ts +22 -0
- package/types/index.d.ts +24 -0
- package/types/json-value.d.ts +23 -0
- package/types/layoutFromJson.d.ts +4 -0
- package/types/layoutToJson.d.ts +3 -0
- package/types/propUtils.d.ts +5 -0
- package/types/react-element-utils.d.ts +3 -0
- package/types/src/AddTabDialog.d.ts +5 -0
- package/types/src/GridLayout.d.ts +14 -0
- package/types/src/GridLayoutContext.d.ts +88 -0
- package/types/src/GridLayoutItem.d.ts +28 -0
- package/types/src/GridLayoutModel.d.ts +59 -0
- package/types/src/GridLayoutProvider.d.ts +37 -0
- package/types/src/GridLayoutStackedtem.d.ts +10 -0
- package/types/src/GridModel.d.ts +350 -0
- package/types/src/GridPlaceholder.d.ts +5 -0
- package/types/src/GridSplitter.d.ts +9 -0
- package/types/src/Icon.d.ts +6 -0
- package/types/src/IconButton.d.ts +6 -0
- package/types/src/TabMenu.d.ts +6 -0
- package/types/src/componentToJson.d.ts +32 -0
- package/types/src/drag-drop-next/DragContextNext.d.ts +73 -0
- package/types/src/drag-drop-next/DragDropProviderNext.d.ts +20 -0
- package/types/src/drag-drop-next/SpaceMan.d.ts +35 -0
- package/types/src/drag-drop-next/tabstrip-drag-drop.d.ts +3 -0
- package/types/src/grid-dom-utils.d.ts +17 -0
- package/types/src/grid-layout-utils.d.ts +80 -0
- package/types/src/grid-matrix.d.ts +5 -0
- package/types/src/index.d.ts +10 -0
- package/types/src/layoutFromJson.d.ts +3 -0
- package/types/src/layoutToJson.d.ts +2 -0
- package/types/src/propUtils.d.ts +5 -0
- package/types/src/react-element-utils.d.ts +3 -0
- package/types/src/typeOf.d.ts +6 -0
- package/types/src/useAsDropTarget.d.ts +7 -0
- package/types/src/useDraggable.d.ts +23 -0
- package/types/src/useGridChildProps.d.ts +13 -0
- package/types/src/useGridLayout.d.ts +43 -0
- package/types/src/useGridSplitterResizing.d.ts +12 -0
- package/types/src/useNotDropTarget.d.ts +1 -0
- package/types/typeOf.d.ts +6 -0
- package/types/useAsDropTarget.d.ts +7 -0
- package/types/useDraggable.d.ts +24 -0
- package/types/useEditTabName.d.ts +11 -0
- package/types/useGridChildProps.d.ts +13 -0
- package/types/useGridControllerSnapshot.d.ts +7 -0
- package/types/useGridLayout.d.ts +53 -0
- package/types/useGridSplitterResizing.d.ts +14 -0
- package/types/useNotDropTarget.d.ts +1 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuGridLayout {
|
|
3
|
+
--vuu-layout-tabs-height: 32px;
|
|
4
|
+
--vuu-grid-gap: 6px;
|
|
5
|
+
--vuu-grid-header-height: 25px;
|
|
6
|
+
padding: var(--vuu-grid-gap);
|
|
7
|
+
display: grid;
|
|
8
|
+
|
|
9
|
+
&.vuuResizing {
|
|
10
|
+
transition: all .3s;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&.vuuFullPage {
|
|
14
|
+
width: 100vw;
|
|
15
|
+
height: 100vh;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.vuu-detached {
|
|
20
|
+
visibility: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.vuu-stacked {
|
|
24
|
+
&.has-h-splitter {
|
|
25
|
+
margin-top: calc(var(--vuu-layout-tabs-height) + 7px);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&:not(.has-h-splitter) {
|
|
29
|
+
margin-top: var(--vuu-layout-tabs-height);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.vuuGridLayoutItemHeader-close {
|
|
34
|
+
margin-left: auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.vuuGridLayoutStackedItemContent, .vuuGridLayoutItemContent {
|
|
38
|
+
flex: auto;
|
|
39
|
+
position: relative;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.vuuGridLayoutStackedItemContent > *, .vuuGridLayoutItemContent > * {
|
|
43
|
+
position: absolute;
|
|
44
|
+
inset: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.vuuDropTarget-east {
|
|
48
|
+
--grid-dropzone-top: 0px;
|
|
49
|
+
--grid-dropzone-left: 50%;
|
|
50
|
+
--grid-dropzone-bottom: 0px;
|
|
51
|
+
--grid-dropzone-right: 0px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.vuuDropTarget-west {
|
|
55
|
+
--grid-dropzone-top: 0px;
|
|
56
|
+
--grid-dropzone-left: 0px;
|
|
57
|
+
--grid-dropzone-bottom: 0px;
|
|
58
|
+
--grid-dropzone-right: 50%;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.vuuDropTarget-north {
|
|
62
|
+
--grid-dropzone-top: 0px;
|
|
63
|
+
--grid-dropzone-left: 0px;
|
|
64
|
+
--grid-dropzone-bottom: 50%;
|
|
65
|
+
--grid-dropzone-right: 0px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.vuuDropTarget-south {
|
|
69
|
+
--grid-dropzone-top: 50%;
|
|
70
|
+
--grid-dropzone-left: 0px;
|
|
71
|
+
--grid-dropzone-bottom: 0px;
|
|
72
|
+
--grid-dropzone-right: 0px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.vuuDropTarget-centre {
|
|
76
|
+
--grid-dropzone-top: 0px;
|
|
77
|
+
--grid-dropzone-left: 0px;
|
|
78
|
+
--grid-dropzone-bottom: 0px;
|
|
79
|
+
--grid-dropzone-right: 0px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.vuu-stacked {
|
|
83
|
+
& > .vuuDropTarget-north, & > .vuuDropTarget-east, & > .vuuDropTarget-west {
|
|
84
|
+
--grid-dropzone-top: -32px;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.vuuDropTarget-centre:after, .vuuDropTarget-north:after, .vuuDropTarget-east:after, .vuuDropTarget-south:after, .vuuDropTarget-west:after {
|
|
89
|
+
content: "";
|
|
90
|
+
opacity: .3;
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
top: var(--grid-dropzone-top);
|
|
93
|
+
right: var(--grid-dropzone-right);
|
|
94
|
+
bottom: var(--grid-dropzone-bottom);
|
|
95
|
+
left: var(--grid-dropzone-left);
|
|
96
|
+
z-index: 100;
|
|
97
|
+
background-color: #6495ed;
|
|
98
|
+
transition-property: top, left, right, bottom;
|
|
99
|
+
transition-duration: .3s;
|
|
100
|
+
transition-timing-function: ease-in-out;
|
|
101
|
+
position: absolute;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.vuuDropTarget-header:after {
|
|
105
|
+
content: "";
|
|
106
|
+
opacity: .3;
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
z-index: 100;
|
|
109
|
+
background-color: red;
|
|
110
|
+
transition-property: top, left, right, bottom;
|
|
111
|
+
transition-duration: .3s;
|
|
112
|
+
transition-timing-function: ease-in-out;
|
|
113
|
+
position: absolute;
|
|
114
|
+
inset: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
`;
|
|
119
|
+
export default css;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { registerComponent } from "@vuu-ui/vuu-utils";
|
|
3
|
+
import { useIdMemo } from "@salt-ds/core";
|
|
4
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
5
|
+
import { useWindow } from "@salt-ds/window";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import { cloneElement } from "react";
|
|
8
|
+
import { DragDropProviderNext } from "./drag-drop-next/DragDropProviderNext.js";
|
|
9
|
+
import { getGridArea } from "./grid-layout-utils.js";
|
|
10
|
+
import { GridLayoutContext } from "./GridLayoutContext.js";
|
|
11
|
+
import { GridLayoutStackedItem } from "./GridLayoutStackedtem.js";
|
|
12
|
+
import { GridPlaceholder } from "./GridPlaceholder.js";
|
|
13
|
+
import { useGridLayout } from "./useGridLayout.js";
|
|
14
|
+
import { useGridSplitterResizing } from "./useGridSplitterResizing.js";
|
|
15
|
+
import { GridSplitter } from "./GridSplitter.js";
|
|
16
|
+
import GridLayout from "./GridLayout.css";
|
|
17
|
+
const classBase = "vuuGridLayout";
|
|
18
|
+
const startsAtHorizontalSplitter = (splitter, splitters)=>"vertical" === splitter.ariaOrientation && splitters.some((candidate)=>"horizontal" === candidate.ariaOrientation && candidate.row.start === splitter.row.start && candidate.column.start <= splitter.column.start && candidate.column.end > splitter.column.start);
|
|
19
|
+
const NO_DRAG_SOURCES = {};
|
|
20
|
+
const GridLayout_GridLayout = ({ id: idProp, children: childrenProp, className, "full-page": fullPage, colsAndRows, onClick, onChange, placeholderContent, rowResizeDistribution = "adjacent", style: styleProp, ...htmlAttributes })=>{
|
|
21
|
+
const targetWindow = useWindow();
|
|
22
|
+
useComponentCssInjection({
|
|
23
|
+
testId: "vuu-grid-layout",
|
|
24
|
+
css: GridLayout,
|
|
25
|
+
window: targetWindow
|
|
26
|
+
});
|
|
27
|
+
const id = useIdMemo(idProp);
|
|
28
|
+
const { children, containerCallback, dispatchGridLayoutAction, gridController, gridLayoutModel, gridModel, gridSnapshot, nonContentGridItems: { placeholders, splitters, stackIds }, onCancelDrag, onCancelTabDrag, onDetachTab, onDragEnd, onDragLeave, onDragPreview, onDragStart, onDrop, onDropStackedItem, stackTemplates } = useGridLayout({
|
|
29
|
+
children: childrenProp,
|
|
30
|
+
id,
|
|
31
|
+
colsAndRows,
|
|
32
|
+
onChange
|
|
33
|
+
});
|
|
34
|
+
const splitterLayoutProps = useGridSplitterResizing({
|
|
35
|
+
gridLayoutModel,
|
|
36
|
+
gridModel,
|
|
37
|
+
gridController,
|
|
38
|
+
id,
|
|
39
|
+
onClick,
|
|
40
|
+
rowResizeDistribution
|
|
41
|
+
});
|
|
42
|
+
const style = {
|
|
43
|
+
gridTemplateColumns: gridSnapshot.columns.map(({ size })=>size).join(" "),
|
|
44
|
+
gridTemplateRows: gridSnapshot.rows.map(({ size })=>size).join(" "),
|
|
45
|
+
...styleProp
|
|
46
|
+
};
|
|
47
|
+
const snapshotItemById = new Map(gridSnapshot.items.map((item)=>[
|
|
48
|
+
item.id,
|
|
49
|
+
item
|
|
50
|
+
]));
|
|
51
|
+
return /*#__PURE__*/ jsx(GridLayoutContext.Provider, {
|
|
52
|
+
value: {
|
|
53
|
+
dispatchGridLayoutAction,
|
|
54
|
+
gridController,
|
|
55
|
+
gridLayoutModel,
|
|
56
|
+
gridModel,
|
|
57
|
+
gridSnapshot,
|
|
58
|
+
id,
|
|
59
|
+
onDragEnd,
|
|
60
|
+
onDragLeave,
|
|
61
|
+
onDragPreview,
|
|
62
|
+
onDragStart,
|
|
63
|
+
onDrop
|
|
64
|
+
},
|
|
65
|
+
children: /*#__PURE__*/ jsx(DragDropProviderNext, {
|
|
66
|
+
dragSources: NO_DRAG_SOURCES,
|
|
67
|
+
onCancelDrag: onCancelDrag,
|
|
68
|
+
onCancelTabDrag: onCancelTabDrag,
|
|
69
|
+
onDetachTab: onDetachTab,
|
|
70
|
+
onDrop: onDropStackedItem,
|
|
71
|
+
children: /*#__PURE__*/ jsxs("div", {
|
|
72
|
+
...htmlAttributes,
|
|
73
|
+
...splitterLayoutProps,
|
|
74
|
+
id: id,
|
|
75
|
+
ref: containerCallback,
|
|
76
|
+
style: style,
|
|
77
|
+
className: clsx(classBase, className, {
|
|
78
|
+
vuuFullPage: fullPage
|
|
79
|
+
}),
|
|
80
|
+
children: [
|
|
81
|
+
stackIds.map((stackId)=>{
|
|
82
|
+
const template = stackTemplates.get(stackId);
|
|
83
|
+
return template ? /*#__PURE__*/ cloneElement(template, {
|
|
84
|
+
key: stackId
|
|
85
|
+
}) : /*#__PURE__*/ jsx(GridLayoutStackedItem, {
|
|
86
|
+
id: stackId
|
|
87
|
+
}, stackId);
|
|
88
|
+
}),
|
|
89
|
+
children,
|
|
90
|
+
placeholders.map(({ id: placeholderId, snapshotId })=>{
|
|
91
|
+
const placeholder = snapshotItemById.get(snapshotId);
|
|
92
|
+
if (!placeholder) throw Error(`[GridLayout] canonical placeholder #${placeholderId} not found`);
|
|
93
|
+
return /*#__PURE__*/ jsx(GridPlaceholder, {
|
|
94
|
+
id: placeholderId,
|
|
95
|
+
style: {
|
|
96
|
+
gridArea: `${placeholder.row.start}/${placeholder.column.start}/${placeholder.row.start + placeholder.row.span}/${placeholder.column.start + placeholder.column.span}`
|
|
97
|
+
},
|
|
98
|
+
children: placeholderContent
|
|
99
|
+
}, placeholderId);
|
|
100
|
+
}),
|
|
101
|
+
splitters.map((splitter)=>/*#__PURE__*/ jsx(GridSplitter, {
|
|
102
|
+
"aria-controls": splitter.controls,
|
|
103
|
+
ariaOrientation: splitter.ariaOrientation,
|
|
104
|
+
"data-resized-child-items-after": splitter.resizedChildItems.after.join(" "),
|
|
105
|
+
"data-resized-child-items-before": splitter.resizedChildItems.before.join(" "),
|
|
106
|
+
id: splitter.id,
|
|
107
|
+
offsetStart: startsAtHorizontalSplitter(splitter, splitters),
|
|
108
|
+
orientation: splitter.orientation,
|
|
109
|
+
style: {
|
|
110
|
+
gridArea: getGridArea(splitter)
|
|
111
|
+
}
|
|
112
|
+
}, splitter.id))
|
|
113
|
+
]
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
GridLayout_GridLayout.displayName = "Grid";
|
|
119
|
+
registerComponent("Grid", GridLayout_GridLayout, "container");
|
|
120
|
+
export { GridLayout_GridLayout as GridLayout };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
const unconfiguredGridLayoutDispatch = (action)=>console.log(`dispatch ${action.type}, have you forgotten to provide a GridLayoutProvider ?`);
|
|
3
|
+
const isTypedComponentTemplate = (template)=>"component" in template;
|
|
4
|
+
const sourceIsComponent = (source)=>source?.type === "component";
|
|
5
|
+
const sourceIsTabbedComponent = (source)=>source?.type === "tabbed-component";
|
|
6
|
+
const sourceIsTemplate = (source)=>source?.type === "template";
|
|
7
|
+
const GridLayoutContext = createContext({
|
|
8
|
+
dispatchGridLayoutAction: unconfiguredGridLayoutDispatch,
|
|
9
|
+
id: "",
|
|
10
|
+
onDragLeave: ()=>void 0,
|
|
11
|
+
onDragPreview: ()=>false,
|
|
12
|
+
onDragStart: ()=>console.log("no GridLayoutProvider"),
|
|
13
|
+
onDrop: ()=>false
|
|
14
|
+
});
|
|
15
|
+
const useGridLayoutDispatch = ()=>{
|
|
16
|
+
const { dispatchGridLayoutAction } = useContext(GridLayoutContext);
|
|
17
|
+
return dispatchGridLayoutAction;
|
|
18
|
+
};
|
|
19
|
+
const useGridLayoutDropHandler = ()=>{
|
|
20
|
+
const { onDrop } = useContext(GridLayoutContext);
|
|
21
|
+
return onDrop;
|
|
22
|
+
};
|
|
23
|
+
const useGridLayoutDragPreviewHandler = ()=>{
|
|
24
|
+
const { onDragPreview } = useContext(GridLayoutContext);
|
|
25
|
+
return onDragPreview;
|
|
26
|
+
};
|
|
27
|
+
const useGridLayoutDragLeaveHandler = ()=>{
|
|
28
|
+
const { onDragLeave } = useContext(GridLayoutContext);
|
|
29
|
+
return onDragLeave;
|
|
30
|
+
};
|
|
31
|
+
const useGridLayoutDragEndHandler = ()=>{
|
|
32
|
+
const { onDragEnd } = useContext(GridLayoutContext);
|
|
33
|
+
return onDragEnd;
|
|
34
|
+
};
|
|
35
|
+
const useGridLayoutDragStartHandler = ()=>{
|
|
36
|
+
const { onDragStart } = useContext(GridLayoutContext);
|
|
37
|
+
return onDragStart;
|
|
38
|
+
};
|
|
39
|
+
const useGridModel = ()=>{
|
|
40
|
+
const { gridModel } = useContext(GridLayoutContext);
|
|
41
|
+
if (gridModel) return gridModel;
|
|
42
|
+
throw Error("[useGridModel] no gridModel, did you forget to use a GridLayout");
|
|
43
|
+
};
|
|
44
|
+
const useGridController = ()=>{
|
|
45
|
+
const { gridController } = useContext(GridLayoutContext);
|
|
46
|
+
if (gridController) return gridController;
|
|
47
|
+
throw Error("[useGridController] no gridController, did you forget to use a GridLayout");
|
|
48
|
+
};
|
|
49
|
+
const useGridSnapshot = ()=>{
|
|
50
|
+
const { gridSnapshot } = useContext(GridLayoutContext);
|
|
51
|
+
if (gridSnapshot) return gridSnapshot;
|
|
52
|
+
throw Error("[useGridSnapshot] no gridSnapshot, did you forget to use a GridLayout");
|
|
53
|
+
};
|
|
54
|
+
const useGridLayoutId = ()=>{
|
|
55
|
+
const { id } = useContext(GridLayoutContext);
|
|
56
|
+
return id;
|
|
57
|
+
};
|
|
58
|
+
export { GridLayoutContext, isTypedComponentTemplate, sourceIsComponent, sourceIsTabbedComponent, sourceIsTemplate, useGridController, useGridLayoutDispatch, useGridLayoutDragEndHandler, useGridLayoutDragLeaveHandler, useGridLayoutDragPreviewHandler, useGridLayoutDragStartHandler, useGridLayoutDropHandler, useGridLayoutId, useGridModel, useGridSnapshot };
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { normalizeGridSnapshot } from "./grid-snapshot-adapters.js";
|
|
2
|
+
import { toJsonValue } from "./json-value.js";
|
|
3
|
+
const GRID_LAYOUT_DOCUMENT_KIND = "grid-layout";
|
|
4
|
+
const GRID_LAYOUT_DOCUMENT_VERSION = 2;
|
|
5
|
+
const cloneAndFreeze = (value)=>{
|
|
6
|
+
if ("object" != typeof value || null === value) return value;
|
|
7
|
+
if (Array.isArray(value)) return Object.freeze(value.map(cloneAndFreeze));
|
|
8
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry])=>[
|
|
9
|
+
key,
|
|
10
|
+
cloneAndFreeze(entry)
|
|
11
|
+
])));
|
|
12
|
+
};
|
|
13
|
+
const immutableGridLayoutDocument = (document)=>cloneAndFreeze(document);
|
|
14
|
+
class GridLayoutDocumentCodecError extends Error {
|
|
15
|
+
issue;
|
|
16
|
+
constructor(issue){
|
|
17
|
+
super(`${issue.path}: ${issue.message}`), this.issue = issue;
|
|
18
|
+
this.name = "GridLayoutDocumentCodecError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const failure = (code, message, path, version)=>({
|
|
22
|
+
error: {
|
|
23
|
+
code,
|
|
24
|
+
message,
|
|
25
|
+
path,
|
|
26
|
+
version
|
|
27
|
+
},
|
|
28
|
+
ok: false
|
|
29
|
+
});
|
|
30
|
+
const isRecord = (value)=>"object" == typeof value && null !== value && !Array.isArray(value);
|
|
31
|
+
const compareIds = (left, right)=>left < right ? -1 : left > right ? 1 : 0;
|
|
32
|
+
const durableItem = ({ column, componentInstanceId, contentVisible, dropTarget, header, id, minHeight, minWidth, resizeable, row, title })=>({
|
|
33
|
+
column: {
|
|
34
|
+
...column
|
|
35
|
+
},
|
|
36
|
+
...void 0 === componentInstanceId ? {} : {
|
|
37
|
+
componentInstanceId
|
|
38
|
+
},
|
|
39
|
+
...void 0 === contentVisible ? {} : {
|
|
40
|
+
contentVisible
|
|
41
|
+
},
|
|
42
|
+
...void 0 === dropTarget ? {} : {
|
|
43
|
+
dropTarget
|
|
44
|
+
},
|
|
45
|
+
...void 0 === header ? {} : {
|
|
46
|
+
header
|
|
47
|
+
},
|
|
48
|
+
id,
|
|
49
|
+
...void 0 === minHeight ? {} : {
|
|
50
|
+
minHeight
|
|
51
|
+
},
|
|
52
|
+
...void 0 === minWidth ? {} : {
|
|
53
|
+
minWidth
|
|
54
|
+
},
|
|
55
|
+
...void 0 === resizeable ? {} : {
|
|
56
|
+
resizeable
|
|
57
|
+
},
|
|
58
|
+
row: {
|
|
59
|
+
...row
|
|
60
|
+
},
|
|
61
|
+
...void 0 === title ? {} : {
|
|
62
|
+
title
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const persistedLayoutToSnapshot = (layout)=>normalizeGridSnapshot({
|
|
66
|
+
columns: layout.columns.map((size)=>({
|
|
67
|
+
size: size
|
|
68
|
+
})),
|
|
69
|
+
gridId: layout.id,
|
|
70
|
+
items: layout.items,
|
|
71
|
+
revision: 0,
|
|
72
|
+
rows: layout.rows.map((size)=>({
|
|
73
|
+
size: size
|
|
74
|
+
})),
|
|
75
|
+
stacks: layout.stacks
|
|
76
|
+
});
|
|
77
|
+
const encodeGridLayoutDocument = (snapshot, registry, { componentSettings, placeholderIds = [] })=>{
|
|
78
|
+
let normalized;
|
|
79
|
+
try {
|
|
80
|
+
normalized = normalizeGridSnapshot(snapshot);
|
|
81
|
+
} catch (cause) {
|
|
82
|
+
return failure("INVALID_DOCUMENT", cause instanceof Error ? cause.message : "invalid grid snapshot", "$.layout");
|
|
83
|
+
}
|
|
84
|
+
const placeholderIdSet = new Set(placeholderIds);
|
|
85
|
+
if (placeholderIdSet.size !== placeholderIds.length) return failure("INVALID_DOCUMENT", "placeholder ids must be unique", "$.layout.placeholderIds");
|
|
86
|
+
const layoutItemIds = new Set(normalized.items.map(({ id })=>id));
|
|
87
|
+
for (const placeholderId of placeholderIdSet)if (!layoutItemIds.has(placeholderId)) return failure("INVALID_DOCUMENT", `placeholder "${placeholderId}" has no matching layout item`, "$.layout.placeholderIds");
|
|
88
|
+
const componentReferenceIds = new Set(normalized.items.filter(({ id })=>!placeholderIdSet.has(id)).map(({ componentInstanceId, id })=>componentInstanceId ?? id));
|
|
89
|
+
const referencedComponents = new Map();
|
|
90
|
+
for (const { componentInstanceId, id } of normalized.items){
|
|
91
|
+
if (placeholderIdSet.has(id)) continue;
|
|
92
|
+
const componentId = componentInstanceId ?? id;
|
|
93
|
+
const previousItemId = referencedComponents.get(componentId);
|
|
94
|
+
if (previousItemId) return failure("DUPLICATE_COMPONENT_ID", `component "${componentId}" is referenced by items "${previousItemId}" and "${id}"`, `$.layout.items.${id}.componentInstanceId`);
|
|
95
|
+
referencedComponents.set(componentId, id);
|
|
96
|
+
}
|
|
97
|
+
const componentIds = new Set();
|
|
98
|
+
const components = [];
|
|
99
|
+
for (const component of [
|
|
100
|
+
...componentSettings
|
|
101
|
+
].sort((a, b)=>compareIds(a.id, b.id))){
|
|
102
|
+
if (componentIds.has(component.id)) return failure("DUPLICATE_COMPONENT_ID", `component id "${component.id}" is duplicated`, "$.components");
|
|
103
|
+
componentIds.add(component.id);
|
|
104
|
+
if (!componentReferenceIds.has(component.id)) return failure("UNEXPECTED_COMPONENT_SETTINGS", `component "${component.id}" has no matching layout item reference`, "$.components");
|
|
105
|
+
const encoded = registry.encode(component);
|
|
106
|
+
if (!encoded.ok) return {
|
|
107
|
+
error: {
|
|
108
|
+
code: "COMPONENT_SETTINGS_ERROR",
|
|
109
|
+
componentError: encoded.error,
|
|
110
|
+
message: encoded.error.message,
|
|
111
|
+
path: `$.components.${component.id}${encoded.error.path.slice(1)}`,
|
|
112
|
+
version: encoded.error.version
|
|
113
|
+
},
|
|
114
|
+
ok: false
|
|
115
|
+
};
|
|
116
|
+
components.push(encoded.value);
|
|
117
|
+
}
|
|
118
|
+
for (const { componentInstanceId, id } of normalized.items){
|
|
119
|
+
if (placeholderIdSet.has(id) && void 0 !== componentInstanceId) return failure("INVALID_DOCUMENT", `placeholder "${id}" must not reference a component`, `$.layout.items.${id}.componentInstanceId`);
|
|
120
|
+
const componentId = componentInstanceId ?? id;
|
|
121
|
+
if (!componentIds.has(componentId) && !placeholderIdSet.has(id)) return failure("MISSING_COMPONENT_SETTINGS", `layout item "${id}" references missing component "${componentId}"`, `$.layout.items.${id}`);
|
|
122
|
+
}
|
|
123
|
+
const document = {
|
|
124
|
+
components,
|
|
125
|
+
kind: GRID_LAYOUT_DOCUMENT_KIND,
|
|
126
|
+
layout: {
|
|
127
|
+
columns: normalized.columns.map(({ size })=>size),
|
|
128
|
+
id: normalized.gridId,
|
|
129
|
+
items: normalized.items.map(durableItem).sort((a, b)=>compareIds(a.id, b.id)),
|
|
130
|
+
placeholderIds: [
|
|
131
|
+
...placeholderIdSet
|
|
132
|
+
].sort(),
|
|
133
|
+
rows: normalized.rows.map(({ size })=>size),
|
|
134
|
+
stacks: normalized.stacks.map((stack)=>({
|
|
135
|
+
id: stack.id,
|
|
136
|
+
itemIds: [
|
|
137
|
+
...stack.itemIds
|
|
138
|
+
],
|
|
139
|
+
selectedItemId: stack.selectedItemId
|
|
140
|
+
})).sort((a, b)=>compareIds(a.id, b.id))
|
|
141
|
+
},
|
|
142
|
+
version: GRID_LAYOUT_DOCUMENT_VERSION
|
|
143
|
+
};
|
|
144
|
+
const json = toJsonValue(document);
|
|
145
|
+
return json.ok ? {
|
|
146
|
+
ok: true,
|
|
147
|
+
value: immutableGridLayoutDocument(document)
|
|
148
|
+
} : failure("INVALID_DOCUMENT", json.error.message, json.error.path);
|
|
149
|
+
};
|
|
150
|
+
const migrateDocument = (value)=>{
|
|
151
|
+
if ("number" != typeof value.version || !Number.isInteger(value.version)) return failure("INVALID_DOCUMENT", "document version must be an integer", "$.version");
|
|
152
|
+
if (value.version === GRID_LAYOUT_DOCUMENT_VERSION) return {
|
|
153
|
+
ok: true,
|
|
154
|
+
value
|
|
155
|
+
};
|
|
156
|
+
if (1 !== value.version) return failure("UNSUPPORTED_DOCUMENT_VERSION", `GridLayout document version ${String(value.version)} is not supported`, "$.version", "number" == typeof value.version ? value.version : void 0);
|
|
157
|
+
if (!isRecord(value.layout) || "string" != typeof value.layout.gridId) return failure("DOCUMENT_MIGRATION_FAILED", "version 1 layout.gridId must be a string", "$.layout.gridId", 1);
|
|
158
|
+
const { gridId, stacks = [], ...layout } = value.layout;
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
value: {
|
|
162
|
+
...value,
|
|
163
|
+
layout: {
|
|
164
|
+
...layout,
|
|
165
|
+
id: gridId,
|
|
166
|
+
placeholderIds: [],
|
|
167
|
+
stacks
|
|
168
|
+
},
|
|
169
|
+
version: GRID_LAYOUT_DOCUMENT_VERSION
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
const parseComponent = (value, index)=>{
|
|
174
|
+
const path = `$.components[${index}]`;
|
|
175
|
+
if (!isRecord(value)) return failure("INVALID_DOCUMENT", "component must be an object", path);
|
|
176
|
+
const unexpectedField = Object.keys(value).find((field)=>![
|
|
177
|
+
"id",
|
|
178
|
+
"settings",
|
|
179
|
+
"type",
|
|
180
|
+
"version"
|
|
181
|
+
].includes(field));
|
|
182
|
+
if (unexpectedField) return failure("INVALID_DOCUMENT", `unexpected component field "${unexpectedField}"`, `${path}.${unexpectedField}`);
|
|
183
|
+
if ("string" != typeof value.id || !value.id || "string" != typeof value.type || !value.type || !Number.isInteger(value.version) || "number" != typeof value.version) return failure("INVALID_DOCUMENT", "component id, type and integer version are required", path);
|
|
184
|
+
const settings = toJsonValue(value.settings, `${path}.settings`);
|
|
185
|
+
if (!settings.ok) return failure("INVALID_DOCUMENT", settings.error.message, settings.error.path);
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
value: {
|
|
189
|
+
id: value.id,
|
|
190
|
+
settings: settings.value,
|
|
191
|
+
type: value.type,
|
|
192
|
+
version: value.version
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const decodeGridLayoutDocument = (input, registry)=>{
|
|
197
|
+
const detached = toJsonValue(input);
|
|
198
|
+
if (!detached.ok || !isRecord(detached.value)) return failure("INVALID_DOCUMENT", detached.ok ? "document must be an object" : detached.error.message, detached.ok ? "$" : detached.error.path);
|
|
199
|
+
if (detached.value.kind !== GRID_LAYOUT_DOCUMENT_KIND) return failure("INVALID_DISCRIMINATOR", `expected document kind "${GRID_LAYOUT_DOCUMENT_KIND}"`, "$.kind");
|
|
200
|
+
const migrated = migrateDocument(detached.value);
|
|
201
|
+
if (!migrated.ok) return migrated;
|
|
202
|
+
const value = migrated.value;
|
|
203
|
+
const unexpectedDocumentField = Object.keys(value).find((field)=>![
|
|
204
|
+
"components",
|
|
205
|
+
"kind",
|
|
206
|
+
"layout",
|
|
207
|
+
"version"
|
|
208
|
+
].includes(field));
|
|
209
|
+
if (unexpectedDocumentField) return failure("INVALID_DOCUMENT", `unexpected document field "${unexpectedDocumentField}"`, `$.${unexpectedDocumentField}`);
|
|
210
|
+
if (!isRecord(value.layout) || !Array.isArray(value.components)) return failure("INVALID_DOCUMENT", "document layout and components are required", "$");
|
|
211
|
+
const unexpectedLayoutField = Object.keys(value.layout).find((field)=>![
|
|
212
|
+
"columns",
|
|
213
|
+
"id",
|
|
214
|
+
"items",
|
|
215
|
+
"placeholderIds",
|
|
216
|
+
"rows",
|
|
217
|
+
"stacks"
|
|
218
|
+
].includes(field));
|
|
219
|
+
if (unexpectedLayoutField) return failure("INVALID_DOCUMENT", `unexpected layout field "${unexpectedLayoutField}"`, `$.layout.${unexpectedLayoutField}`);
|
|
220
|
+
const { columns, id, items, placeholderIds, rows, stacks } = value.layout;
|
|
221
|
+
if ("string" != typeof id || !Array.isArray(columns) || !Array.isArray(rows) || !Array.isArray(items) || !Array.isArray(placeholderIds) || !Array.isArray(stacks)) return failure("INVALID_DOCUMENT", "layout id, columns, rows, items, placeholderIds and stacks are required", "$.layout");
|
|
222
|
+
let snapshot;
|
|
223
|
+
try {
|
|
224
|
+
snapshot = persistedLayoutToSnapshot({
|
|
225
|
+
columns: columns,
|
|
226
|
+
id,
|
|
227
|
+
items: items,
|
|
228
|
+
placeholderIds: placeholderIds,
|
|
229
|
+
rows: rows,
|
|
230
|
+
stacks: stacks
|
|
231
|
+
});
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
return failure("INVALID_DOCUMENT", cause instanceof Error ? cause.message : "invalid canonical layout", "$.layout");
|
|
234
|
+
}
|
|
235
|
+
const itemIds = new Set(snapshot.items.map(({ id: itemId })=>itemId));
|
|
236
|
+
const parsedPlaceholderIds = new Set();
|
|
237
|
+
for(let index = 0; index < placeholderIds.length; index += 1){
|
|
238
|
+
const placeholderId = placeholderIds[index];
|
|
239
|
+
if ("string" != typeof placeholderId || !itemIds.has(placeholderId) || parsedPlaceholderIds.has(placeholderId)) return failure("INVALID_DOCUMENT", "placeholder id must identify one unique layout item", `$.layout.placeholderIds[${index}]`);
|
|
240
|
+
parsedPlaceholderIds.add(placeholderId);
|
|
241
|
+
}
|
|
242
|
+
const componentItemById = new Map();
|
|
243
|
+
for (const { componentInstanceId, id: itemId } of snapshot.items){
|
|
244
|
+
if (parsedPlaceholderIds.has(itemId)) {
|
|
245
|
+
if (void 0 !== componentInstanceId) return failure("INVALID_DOCUMENT", `placeholder "${itemId}" must not reference a component`, `$.layout.items.${itemId}.componentInstanceId`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const componentId = componentInstanceId ?? itemId;
|
|
249
|
+
const previousItemId = componentItemById.get(componentId);
|
|
250
|
+
if (previousItemId) return failure("DUPLICATE_COMPONENT_ID", `component "${componentId}" is referenced by items "${previousItemId}" and "${itemId}"`, `$.layout.items.${itemId}.componentInstanceId`);
|
|
251
|
+
componentItemById.set(componentId, itemId);
|
|
252
|
+
}
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const components = [];
|
|
255
|
+
const encodedComponents = [];
|
|
256
|
+
for(let index = 0; index < value.components.length; index += 1){
|
|
257
|
+
const parsed = parseComponent(value.components[index], index);
|
|
258
|
+
if (!parsed.ok) return parsed;
|
|
259
|
+
if (seen.has(parsed.value.id)) return failure("DUPLICATE_COMPONENT_ID", `component id "${parsed.value.id}" is duplicated`, `$.components[${index}].id`);
|
|
260
|
+
if (!componentItemById.has(parsed.value.id)) return failure("UNEXPECTED_COMPONENT_SETTINGS", `component "${parsed.value.id}" has no matching layout item reference`, `$.components[${index}].id`);
|
|
261
|
+
seen.add(parsed.value.id);
|
|
262
|
+
encodedComponents.push(parsed.value);
|
|
263
|
+
const decoded = registry.decode(parsed.value);
|
|
264
|
+
if (!decoded.ok) return {
|
|
265
|
+
error: {
|
|
266
|
+
code: "COMPONENT_SETTINGS_ERROR",
|
|
267
|
+
componentError: decoded.error,
|
|
268
|
+
message: decoded.error.message,
|
|
269
|
+
path: `$.components[${index}]${decoded.error.path.slice(1)}`,
|
|
270
|
+
version: decoded.error.version
|
|
271
|
+
},
|
|
272
|
+
ok: false
|
|
273
|
+
};
|
|
274
|
+
components.push(decoded.value);
|
|
275
|
+
}
|
|
276
|
+
for (const [componentId, itemId] of componentItemById)if (!seen.has(componentId)) return failure("MISSING_COMPONENT_SETTINGS", `layout item "${itemId}" references missing component "${componentId}"`, `$.layout.items.${itemId}`);
|
|
277
|
+
for (const placeholderId of parsedPlaceholderIds)if (seen.has(placeholderId)) return failure("INVALID_DOCUMENT", `placeholder "${placeholderId}" also has component settings`, "$.layout.placeholderIds");
|
|
278
|
+
const document = {
|
|
279
|
+
components: encodedComponents.sort((a, b)=>compareIds(a.id, b.id)),
|
|
280
|
+
kind: GRID_LAYOUT_DOCUMENT_KIND,
|
|
281
|
+
layout: {
|
|
282
|
+
columns: snapshot.columns.map(({ size })=>size),
|
|
283
|
+
id: snapshot.gridId,
|
|
284
|
+
items: snapshot.items.map(durableItem).sort((a, b)=>compareIds(a.id, b.id)),
|
|
285
|
+
placeholderIds: [
|
|
286
|
+
...parsedPlaceholderIds
|
|
287
|
+
].sort(),
|
|
288
|
+
rows: snapshot.rows.map(({ size })=>size),
|
|
289
|
+
stacks: snapshot.stacks.map((stack)=>({
|
|
290
|
+
...stack,
|
|
291
|
+
itemIds: [
|
|
292
|
+
...stack.itemIds
|
|
293
|
+
]
|
|
294
|
+
})).sort((a, b)=>compareIds(a.id, b.id))
|
|
295
|
+
},
|
|
296
|
+
version: GRID_LAYOUT_DOCUMENT_VERSION
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
value: {
|
|
301
|
+
components,
|
|
302
|
+
document: immutableGridLayoutDocument(document),
|
|
303
|
+
snapshot
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
export { GRID_LAYOUT_DOCUMENT_KIND, GRID_LAYOUT_DOCUMENT_VERSION, GridLayoutDocumentCodecError, decodeGridLayoutDocument, encodeGridLayoutDocument, immutableGridLayoutDocument };
|