@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,59 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
HTMLAttributes,
|
|
3
|
+
useCallback,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
} from "react";
|
|
7
|
+
import cx from "classnames";
|
|
8
|
+
import { Flexbox } from "../flexbox";
|
|
9
|
+
import { View } from "../layout-view";
|
|
10
|
+
|
|
11
|
+
import { Toolbar, ToolbarButton } from "@heswell/uitk-lab";
|
|
12
|
+
import { CloseIcon } from "@heswell/uitk-icons";
|
|
13
|
+
|
|
14
|
+
import "./Dialog.css";
|
|
15
|
+
|
|
16
|
+
export interface DialogProps extends HTMLAttributes<HTMLDialogElement> {
|
|
17
|
+
isOpen?: boolean;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Dialog = ({
|
|
22
|
+
children,
|
|
23
|
+
className,
|
|
24
|
+
isOpen = false,
|
|
25
|
+
onClose,
|
|
26
|
+
...props
|
|
27
|
+
}: DialogProps) => {
|
|
28
|
+
const classRoot = "hwDialog";
|
|
29
|
+
const root = useRef<HTMLDialogElement>(null);
|
|
30
|
+
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
if (isOpen) {
|
|
33
|
+
root.current?.showModal();
|
|
34
|
+
}
|
|
35
|
+
}, [isOpen]);
|
|
36
|
+
|
|
37
|
+
const close = useCallback(() => {
|
|
38
|
+
root.current?.close();
|
|
39
|
+
onClose?.();
|
|
40
|
+
}, [onClose]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<dialog {...props} className={cx(classRoot, className)} ref={root}>
|
|
44
|
+
{/* <Flexbox style={{ flexDirection: "column", width: "fit-content" }}> */}
|
|
45
|
+
<Flexbox
|
|
46
|
+
style={{ flexDirection: "column", width: "100%", height: "100%" }}
|
|
47
|
+
>
|
|
48
|
+
<Toolbar style={{ height: 32 }}>
|
|
49
|
+
<ToolbarButton key="close" onClick={close} data-align="right">
|
|
50
|
+
<CloseIcon /> Close
|
|
51
|
+
</ToolbarButton>
|
|
52
|
+
</Toolbar>
|
|
53
|
+
<View style={{ flex: 1 }}>{children}</View>
|
|
54
|
+
</Flexbox>
|
|
55
|
+
</dialog>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default Dialog;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Dialog } from './Dialog';
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { getProps, typeOf } from "../utils";
|
|
3
|
+
import { isContainer } from "../registry/ComponentRegistry";
|
|
4
|
+
import { LayoutModel } from "../layout-reducer";
|
|
5
|
+
import { DragDropRect, DropPos, RelativePosition } from "./dragDropTypes";
|
|
6
|
+
import { rect } from "../common-types";
|
|
7
|
+
|
|
8
|
+
export var positionValues = {
|
|
9
|
+
north: 1,
|
|
10
|
+
east: 2,
|
|
11
|
+
south: 4,
|
|
12
|
+
west: 8,
|
|
13
|
+
header: 16,
|
|
14
|
+
centre: 32,
|
|
15
|
+
absolute: 64,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const RelativeDropPosition = {
|
|
19
|
+
AFTER: "after" as RelativePosition,
|
|
20
|
+
BEFORE: "before" as RelativePosition,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export var Position = Object.freeze({
|
|
24
|
+
North: _position("north"),
|
|
25
|
+
East: _position("east"),
|
|
26
|
+
South: _position("south"),
|
|
27
|
+
West: _position("west"),
|
|
28
|
+
Header: _position("header"),
|
|
29
|
+
Centre: _position("centre"),
|
|
30
|
+
Absolute: _position("absolute"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function _position(str: keyof typeof positionValues) {
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
offset:
|
|
36
|
+
str === "north" || str === "west"
|
|
37
|
+
? 0
|
|
38
|
+
: str === "south" || str === "east"
|
|
39
|
+
? 1
|
|
40
|
+
: NaN,
|
|
41
|
+
valueOf: function () {
|
|
42
|
+
return positionValues[str];
|
|
43
|
+
},
|
|
44
|
+
toString: function () {
|
|
45
|
+
return str;
|
|
46
|
+
},
|
|
47
|
+
North: str === "north",
|
|
48
|
+
South: str === "south",
|
|
49
|
+
East: str === "east",
|
|
50
|
+
West: str === "west",
|
|
51
|
+
Header: str === "header",
|
|
52
|
+
Centre: str === "centre",
|
|
53
|
+
NorthOrSouth: str === "north" || str === "south",
|
|
54
|
+
EastOrWest: str === "east" || str === "west",
|
|
55
|
+
NorthOrWest: str === "north" || str === "west",
|
|
56
|
+
SouthOrEast: str === "east" || str === "south",
|
|
57
|
+
Absolute: str === "absolute",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var NORTH = Position.North,
|
|
62
|
+
SOUTH = Position.South,
|
|
63
|
+
EAST = Position.East,
|
|
64
|
+
WEST = Position.West,
|
|
65
|
+
HEADER = Position.Header,
|
|
66
|
+
CENTRE = Position.Centre;
|
|
67
|
+
|
|
68
|
+
export interface Measurements {
|
|
69
|
+
[key: string]: DragDropRect;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class BoxModel {
|
|
73
|
+
//TODO we should accept initial let,top offsets here
|
|
74
|
+
// if dropTargets are supplied, we will only allow drop operations directly on these targets
|
|
75
|
+
// TODO we will need to make this more flexible e.g allowing drop anywhere within these target
|
|
76
|
+
static measure(
|
|
77
|
+
model: ReactElement,
|
|
78
|
+
dropTargetPaths: string[] = []
|
|
79
|
+
): Measurements {
|
|
80
|
+
var measurements: Measurements = {};
|
|
81
|
+
measureRootComponent(model, measurements, dropTargetPaths);
|
|
82
|
+
return measurements;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static allBoxesContainingPoint(
|
|
86
|
+
layout: LayoutModel,
|
|
87
|
+
measurements: Measurements,
|
|
88
|
+
x: number,
|
|
89
|
+
y: number,
|
|
90
|
+
validDropTargets?: string[]
|
|
91
|
+
) {
|
|
92
|
+
return allBoxesContainingPoint(
|
|
93
|
+
layout,
|
|
94
|
+
measurements,
|
|
95
|
+
x,
|
|
96
|
+
y,
|
|
97
|
+
validDropTargets
|
|
98
|
+
).reverse();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function pointPositionWithinRect(
|
|
103
|
+
x: number,
|
|
104
|
+
y: number,
|
|
105
|
+
rect: DragDropRect,
|
|
106
|
+
borderZone = 30
|
|
107
|
+
) {
|
|
108
|
+
const width = rect.right - rect.left;
|
|
109
|
+
const height = rect.bottom - rect.top;
|
|
110
|
+
const posX = x - rect.left;
|
|
111
|
+
const posY = y - rect.top;
|
|
112
|
+
let closeToTheEdge = 0;
|
|
113
|
+
|
|
114
|
+
if (posX < borderZone) closeToTheEdge += 8;
|
|
115
|
+
if (posX > width - borderZone) closeToTheEdge += 2;
|
|
116
|
+
if (posY < borderZone) closeToTheEdge += 1;
|
|
117
|
+
if (posY > height - borderZone) closeToTheEdge += 4;
|
|
118
|
+
|
|
119
|
+
return { pctX: posX / width, pctY: posY / height, closeToTheEdge };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getPosition(
|
|
123
|
+
x: number,
|
|
124
|
+
y: number,
|
|
125
|
+
rect: DragDropRect,
|
|
126
|
+
targetOrientation?: "row" | "column"
|
|
127
|
+
): DropPos {
|
|
128
|
+
const { BEFORE, AFTER } = RelativeDropPosition;
|
|
129
|
+
const { pctX, pctY, closeToTheEdge } = pointPositionWithinRect(x, y, rect);
|
|
130
|
+
let position;
|
|
131
|
+
let tab;
|
|
132
|
+
|
|
133
|
+
if (targetOrientation === "row") {
|
|
134
|
+
position = pctX < 0.5 ? WEST : EAST;
|
|
135
|
+
} else if (rect.header && containsPoint(rect.header, x, y)) {
|
|
136
|
+
position = HEADER;
|
|
137
|
+
|
|
138
|
+
if (rect.Stack) {
|
|
139
|
+
const tabCount = rect.Stack.length;
|
|
140
|
+
if (tabCount === 0) {
|
|
141
|
+
tab = {
|
|
142
|
+
index: -1,
|
|
143
|
+
left: rect.left,
|
|
144
|
+
positionRelativeToTab: AFTER,
|
|
145
|
+
width: 0,
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
//TODO account for gaps between tabs
|
|
149
|
+
const targetTab = rect.Stack.find(
|
|
150
|
+
({ left, right }) => x >= left && x <= right
|
|
151
|
+
);
|
|
152
|
+
if (targetTab) {
|
|
153
|
+
const tabWidth = targetTab.right - targetTab.left;
|
|
154
|
+
tab = {
|
|
155
|
+
index: rect.Stack.indexOf(targetTab),
|
|
156
|
+
left: targetTab.left,
|
|
157
|
+
positionRelativeToTab:
|
|
158
|
+
(x - targetTab.left) / tabWidth < 0.5 ? BEFORE : AFTER,
|
|
159
|
+
width: tabWidth,
|
|
160
|
+
};
|
|
161
|
+
} else {
|
|
162
|
+
const lastTab = rect.Stack[tabCount - 1];
|
|
163
|
+
tab = {
|
|
164
|
+
left: lastTab.right,
|
|
165
|
+
width: 0,
|
|
166
|
+
index: tabCount,
|
|
167
|
+
positionRelativeToTab: AFTER,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else if (rect.header.titleWidth) {
|
|
172
|
+
const tabWidth = rect.header.titleWidth;
|
|
173
|
+
tab = {
|
|
174
|
+
index: -1,
|
|
175
|
+
left: rect.left,
|
|
176
|
+
positionRelativeToTab:
|
|
177
|
+
(x - rect.left) / tabWidth < 0.5 ? BEFORE : AFTER,
|
|
178
|
+
width: tabWidth,
|
|
179
|
+
};
|
|
180
|
+
} else {
|
|
181
|
+
tab = {
|
|
182
|
+
left: rect.left,
|
|
183
|
+
width: 0,
|
|
184
|
+
positionRelativeToTab: BEFORE,
|
|
185
|
+
index: -1,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
position = getPositionWithinBox(x, y, rect, pctX, pctY);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { position: position!, x, y, closeToTheEdge, tab };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getPositionWithinBox(
|
|
196
|
+
x: number,
|
|
197
|
+
y: number,
|
|
198
|
+
rect: DragDropRect,
|
|
199
|
+
pctX: number,
|
|
200
|
+
pctY: number
|
|
201
|
+
) {
|
|
202
|
+
const centerBox = getCenteredBox(rect, 0.2);
|
|
203
|
+
if (containsPoint(centerBox, x, y)) {
|
|
204
|
+
return CENTRE;
|
|
205
|
+
} else {
|
|
206
|
+
const quadrant = `${pctY < 0.5 ? "north" : "south"}${
|
|
207
|
+
pctX < 0.5 ? "west" : "east"
|
|
208
|
+
}`;
|
|
209
|
+
|
|
210
|
+
switch (quadrant) {
|
|
211
|
+
case "northwest":
|
|
212
|
+
return pctX > pctY ? NORTH : WEST;
|
|
213
|
+
case "northeast":
|
|
214
|
+
return 1 - pctX > pctY ? NORTH : EAST;
|
|
215
|
+
case "southeast":
|
|
216
|
+
return pctX > pctY ? EAST : SOUTH;
|
|
217
|
+
case "southwest":
|
|
218
|
+
return 1 - pctX > pctY ? WEST : SOUTH;
|
|
219
|
+
default:
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getCenteredBox(
|
|
225
|
+
{ right, left, top, bottom }: DragDropRect,
|
|
226
|
+
pctSize: number
|
|
227
|
+
) {
|
|
228
|
+
const pctOffset = (1 - pctSize) / 2;
|
|
229
|
+
const w = (right - left) * pctOffset;
|
|
230
|
+
const h = (bottom - top) * pctOffset;
|
|
231
|
+
return { left: left + w, top: top + h, right: right - w, bottom: bottom - h };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function measureRootComponent(
|
|
235
|
+
rootComponent: ReactElement,
|
|
236
|
+
measurements: Measurements,
|
|
237
|
+
dropTargets: any[]
|
|
238
|
+
) {
|
|
239
|
+
const {
|
|
240
|
+
id,
|
|
241
|
+
"data-path": dataPath,
|
|
242
|
+
path = dataPath,
|
|
243
|
+
} = getProps(rootComponent);
|
|
244
|
+
const type = typeOf(rootComponent) as string;
|
|
245
|
+
|
|
246
|
+
if (id && path) {
|
|
247
|
+
const [rect, el] = measureComponentDomElement(rootComponent);
|
|
248
|
+
measureComponent(rootComponent, rect, el, measurements);
|
|
249
|
+
if (isContainer(type)) {
|
|
250
|
+
collectChildMeasurements(rootComponent, measurements, dropTargets);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function measureComponent(
|
|
256
|
+
component: LayoutModel,
|
|
257
|
+
rect: DragDropRect,
|
|
258
|
+
el: HTMLElement,
|
|
259
|
+
measurements: Measurements
|
|
260
|
+
) {
|
|
261
|
+
const {
|
|
262
|
+
"data-path": dataPath,
|
|
263
|
+
path = dataPath,
|
|
264
|
+
header,
|
|
265
|
+
} = getProps(component);
|
|
266
|
+
|
|
267
|
+
measurements[path] = rect;
|
|
268
|
+
|
|
269
|
+
const type = typeOf(component);
|
|
270
|
+
if (header || type === "Stack") {
|
|
271
|
+
const headerEl = el.querySelector(".vuuHeader");
|
|
272
|
+
if (headerEl) {
|
|
273
|
+
const { top, left, right, bottom } = headerEl.getBoundingClientRect();
|
|
274
|
+
measurements[path].header = {
|
|
275
|
+
top: Math.round(top),
|
|
276
|
+
left: Math.round(left),
|
|
277
|
+
right: Math.round(right),
|
|
278
|
+
bottom: Math.round(bottom),
|
|
279
|
+
};
|
|
280
|
+
if (type === "Stack") {
|
|
281
|
+
measurements[path].Stack = Array.from(
|
|
282
|
+
headerEl.querySelectorAll(".uitkTab")
|
|
283
|
+
)
|
|
284
|
+
.map((tab) => tab.getBoundingClientRect())
|
|
285
|
+
.map(({ left, right }) => ({ left, right }));
|
|
286
|
+
} else {
|
|
287
|
+
const titleEl = headerEl.querySelector('[class^="vuuHeader-title"]');
|
|
288
|
+
const { header } = measurements[path];
|
|
289
|
+
if (titleEl && header) {
|
|
290
|
+
header.titleWidth = titleEl.clientWidth;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return measurements[path];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function collectChildMeasurements(
|
|
300
|
+
component: LayoutModel,
|
|
301
|
+
measurements: Measurements,
|
|
302
|
+
dropTargets: string[],
|
|
303
|
+
preX = 0,
|
|
304
|
+
posX = 0,
|
|
305
|
+
preY = 0,
|
|
306
|
+
posY = 0
|
|
307
|
+
) {
|
|
308
|
+
const {
|
|
309
|
+
children,
|
|
310
|
+
"data-path": dataPath,
|
|
311
|
+
path = dataPath,
|
|
312
|
+
style,
|
|
313
|
+
active = 0,
|
|
314
|
+
} = getProps(component);
|
|
315
|
+
|
|
316
|
+
const type = typeOf(component);
|
|
317
|
+
const isFlexbox = type === "Flexbox";
|
|
318
|
+
const isStack = type === "Stack";
|
|
319
|
+
const isTower = isFlexbox && style.flexDirection === "column";
|
|
320
|
+
const isTerrace = isFlexbox && style.flexDirection === "row";
|
|
321
|
+
|
|
322
|
+
const childrenToMeasure = isStack
|
|
323
|
+
? children.filter((_child: ReactElement, idx: number) => idx === active)
|
|
324
|
+
: children.filter(omitDragging);
|
|
325
|
+
|
|
326
|
+
type measuredTuple = [DragDropRect, HTMLElement, ReactElement];
|
|
327
|
+
// Collect all the measurements in first pass ...
|
|
328
|
+
const childMeasurements: measuredTuple[] = childrenToMeasure.map(
|
|
329
|
+
(child: ReactElement) => {
|
|
330
|
+
const [rect, el] = measureComponentDomElement(child);
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
...rect,
|
|
335
|
+
top: rect.top - preY,
|
|
336
|
+
right: rect.right + posX,
|
|
337
|
+
bottom: rect.bottom + posY,
|
|
338
|
+
left: rect.left - preX,
|
|
339
|
+
},
|
|
340
|
+
el,
|
|
341
|
+
child,
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// ...so that, in the second pass, we can identify gaps ...
|
|
347
|
+
const expandedMeasurements = childMeasurements.map(
|
|
348
|
+
([rect, el, child], i, all) => {
|
|
349
|
+
// generate a 'local' splitter adjustment for children adjacent to splitters
|
|
350
|
+
let localPreX;
|
|
351
|
+
let localPosX;
|
|
352
|
+
let localPreY;
|
|
353
|
+
let localPosY;
|
|
354
|
+
let gapPre;
|
|
355
|
+
let gapPos;
|
|
356
|
+
const n = all.length - 1;
|
|
357
|
+
if (isTerrace) {
|
|
358
|
+
gapPre = i === 0 ? 0 : rect.left - all[i - 1][0].right;
|
|
359
|
+
gapPos = i === n ? 0 : all[i + 1][0].left - rect.right;
|
|
360
|
+
// we don't need to divide the leading gap, as half the gap will
|
|
361
|
+
// already have been assigned to the preceeding child in the
|
|
362
|
+
// previous loop iteration.
|
|
363
|
+
localPreX = i === 0 ? 0 : gapPre === 0 ? 0 : gapPre;
|
|
364
|
+
localPosX = i === n ? 0 : gapPos === 0 ? 0 : gapPos - gapPos / 2;
|
|
365
|
+
rect.left -= localPreX;
|
|
366
|
+
rect.right += localPosX;
|
|
367
|
+
localPreY = preY;
|
|
368
|
+
localPosY = posY;
|
|
369
|
+
} else if (isTower) {
|
|
370
|
+
gapPre = i === 0 ? 0 : rect.top - all[i - 1][0].bottom;
|
|
371
|
+
gapPos = i === n ? 0 : all[i + 1][0].top - rect.bottom;
|
|
372
|
+
// we don't need to divide the leading gap, as half the gap will
|
|
373
|
+
// already have been assigned to the preceeding child in the
|
|
374
|
+
// previous loop iteration.
|
|
375
|
+
localPreY = i === 0 ? 0 : gapPre === 0 ? 0 : gapPre;
|
|
376
|
+
localPosY = i === n ? 0 : gapPos === 0 ? 0 : gapPos - gapPos / 2;
|
|
377
|
+
rect.top -= localPreY;
|
|
378
|
+
rect.bottom += localPosY;
|
|
379
|
+
localPreX = preX;
|
|
380
|
+
localPosX = posX;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const componentMeasurements = measureComponent(
|
|
384
|
+
child,
|
|
385
|
+
rect,
|
|
386
|
+
el,
|
|
387
|
+
measurements
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const childType = typeOf(child) as string;
|
|
391
|
+
if (isContainer(childType)) {
|
|
392
|
+
collectChildMeasurements(
|
|
393
|
+
child,
|
|
394
|
+
measurements,
|
|
395
|
+
dropTargets,
|
|
396
|
+
localPreX,
|
|
397
|
+
localPosX,
|
|
398
|
+
localPreY,
|
|
399
|
+
localPosY
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return componentMeasurements;
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
if (childMeasurements.length) {
|
|
406
|
+
measurements[path].children = expandedMeasurements;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function omitDragging(component: ReactElement) {
|
|
411
|
+
const { id } = getProps(component);
|
|
412
|
+
const el = document.getElementById(id);
|
|
413
|
+
if (el) {
|
|
414
|
+
return el.dataset.dragging !== "true";
|
|
415
|
+
} else {
|
|
416
|
+
console.warn(`BoxModel: no element found with id #${id}`);
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function measureComponentDomElement(
|
|
422
|
+
component: LayoutModel
|
|
423
|
+
): [DragDropRect, HTMLElement, LayoutModel] {
|
|
424
|
+
const { id } = getProps(component);
|
|
425
|
+
const type = typeOf(component) as string;
|
|
426
|
+
const el = document.getElementById(id);
|
|
427
|
+
if (!el) {
|
|
428
|
+
throw Error(`No DOM for ${type} ${id}`);
|
|
429
|
+
}
|
|
430
|
+
// Note: height and width are not required for dropTarget identification, but
|
|
431
|
+
// are used in sizing calculations on drop
|
|
432
|
+
let { top, left, right, bottom, height, width } = el.getBoundingClientRect();
|
|
433
|
+
let scrolling = undefined;
|
|
434
|
+
if (isContainer(type)) {
|
|
435
|
+
const scrollHeight = el.scrollHeight;
|
|
436
|
+
if (scrollHeight > height) {
|
|
437
|
+
scrolling = { id, scrollHeight, scrollTop: el.scrollTop };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return [
|
|
441
|
+
{
|
|
442
|
+
top: Math.round(top),
|
|
443
|
+
left: Math.round(left),
|
|
444
|
+
right: Math.round(right),
|
|
445
|
+
bottom: Math.round(bottom),
|
|
446
|
+
height: Math.round(height),
|
|
447
|
+
width: Math.round(width),
|
|
448
|
+
scrolling,
|
|
449
|
+
},
|
|
450
|
+
el,
|
|
451
|
+
component,
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function allBoxesContainingPoint(
|
|
456
|
+
component: LayoutModel,
|
|
457
|
+
measurements: Measurements,
|
|
458
|
+
x: number,
|
|
459
|
+
y: number,
|
|
460
|
+
dropTargets?: string[],
|
|
461
|
+
boxes: LayoutModel[] = []
|
|
462
|
+
): LayoutModel[] {
|
|
463
|
+
const {
|
|
464
|
+
children,
|
|
465
|
+
"data-path": dataPath,
|
|
466
|
+
path = dataPath,
|
|
467
|
+
} = getProps(component);
|
|
468
|
+
|
|
469
|
+
const type = typeOf(component) as string;
|
|
470
|
+
var rect = measurements[path];
|
|
471
|
+
if (!containsPoint(rect, x, y)) return boxes;
|
|
472
|
+
|
|
473
|
+
if (dropTargets && dropTargets.length) {
|
|
474
|
+
if (dropTargets.includes(path)) {
|
|
475
|
+
boxes.push(component);
|
|
476
|
+
} else if (
|
|
477
|
+
dropTargets.some((dropTargetPath) => dropTargetPath.startsWith(path))
|
|
478
|
+
) {
|
|
479
|
+
// keep going
|
|
480
|
+
} else {
|
|
481
|
+
return boxes;
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
boxes.push(component);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!isContainer(type)) {
|
|
488
|
+
return boxes;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (rect.header && containsPoint(rect.header, x, y)) {
|
|
492
|
+
return boxes;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (rect.scrolling) {
|
|
496
|
+
scrollIntoViewIfNeccesary(rect, x, y);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
for (var i = 0; i < children.length; i++) {
|
|
500
|
+
if (type === "Stack" && component.props.active !== i) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const nestedBoxes = allBoxesContainingPoint(
|
|
504
|
+
children[i],
|
|
505
|
+
measurements,
|
|
506
|
+
x,
|
|
507
|
+
y,
|
|
508
|
+
dropTargets
|
|
509
|
+
);
|
|
510
|
+
if (nestedBoxes.length) {
|
|
511
|
+
return boxes.concat(nestedBoxes);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return boxes;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function containsPoint(rect: rect, x: number, y: number) {
|
|
518
|
+
if (rect) {
|
|
519
|
+
return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function scrollIntoViewIfNeccesary(
|
|
524
|
+
{ top, bottom, scrolling }: DragDropRect,
|
|
525
|
+
x: number,
|
|
526
|
+
y: number
|
|
527
|
+
) {
|
|
528
|
+
if (scrolling) {
|
|
529
|
+
const { id, scrollTop, scrollHeight } = scrolling;
|
|
530
|
+
const height = bottom - top;
|
|
531
|
+
if (scrollTop === 0 && bottom - y < 50) {
|
|
532
|
+
const scrollMax = scrollHeight - height;
|
|
533
|
+
const el = document.getElementById(id) as HTMLElement;
|
|
534
|
+
el.scrollTo({ left: 0, top: scrollMax, behavior: "smooth" });
|
|
535
|
+
scrolling.scrollTop = scrollMax;
|
|
536
|
+
} else if (scrollTop > 0 && y - top < 50) {
|
|
537
|
+
const el = document.getElementById(id) as HTMLElement;
|
|
538
|
+
el.scrollTo({ left: 0, top: 0, behavior: "smooth" });
|
|
539
|
+
scrolling.scrollTop = 0;
|
|
540
|
+
} else {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|