@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,21 @@
|
|
|
1
|
+
export const Action = {
|
|
2
|
+
ADD: 'add',
|
|
3
|
+
BLUR: 'blur',
|
|
4
|
+
BLUR_SPLITTER: 'blur-splitter',
|
|
5
|
+
DRAG_START: 'drag-start',
|
|
6
|
+
DRAG_STARTED: 'drag-started',
|
|
7
|
+
DRAG_DROP: 'drag-drop',
|
|
8
|
+
FOCUS: 'focus',
|
|
9
|
+
FOCUS_SPLITTER: 'focus-splitter',
|
|
10
|
+
INITIALIZE: 'initialize',
|
|
11
|
+
MAXIMIZE: 'maximize',
|
|
12
|
+
MINIMIZE: 'minimize',
|
|
13
|
+
REMOVE: 'remove',
|
|
14
|
+
REPLACE: 'replace',
|
|
15
|
+
RESTORE: 'restore',
|
|
16
|
+
SAVE: 'save',
|
|
17
|
+
SET_TITLE: 'set-title',
|
|
18
|
+
SPLITTER_RESIZE: 'splitter-resize',
|
|
19
|
+
SWITCH_TAB: 'switch-tab',
|
|
20
|
+
TEAR_OUT: 'tear-out'
|
|
21
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { HTMLAttributes, MouseEvent } from 'react';
|
|
2
|
+
import classnames from 'classnames';
|
|
3
|
+
|
|
4
|
+
export interface ActionButtonProps extends Omit<HTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
|
5
|
+
actionId: 'maximize' | 'minimize' | 'restore' | 'tearout';
|
|
6
|
+
iconName?: string;
|
|
7
|
+
onClick: (evt: MouseEvent, actionId: 'maximize' | 'minimize' | 'restore' | 'tearout') => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ActionButton = ({ actionId, className, iconName, onClick, ...props }: ActionButtonProps) => {
|
|
11
|
+
const handleClick = (evt: MouseEvent) => {
|
|
12
|
+
onClick(evt, actionId);
|
|
13
|
+
};
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
{...props}
|
|
17
|
+
className={classnames('ActionButton', className)}
|
|
18
|
+
onClick={handleClick}
|
|
19
|
+
title="Close View"></button>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default ActionButton;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
import React, {
|
|
3
|
+
HTMLAttributes,
|
|
4
|
+
KeyboardEvent,
|
|
5
|
+
MouseEvent,
|
|
6
|
+
ReactElement,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { Contribution, useViewDispatch } from "../layout-view";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
EditableLabel,
|
|
14
|
+
Toolbar,
|
|
15
|
+
ToolbarButton,
|
|
16
|
+
ToolbarField,
|
|
17
|
+
Tooltray,
|
|
18
|
+
} from "@heswell/uitk-lab";
|
|
19
|
+
import { CloseIcon } from "@heswell/uitk-icons";
|
|
20
|
+
|
|
21
|
+
import "./Header.css";
|
|
22
|
+
|
|
23
|
+
export interface HeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
24
|
+
collapsed?: boolean;
|
|
25
|
+
contributions?: Contribution[];
|
|
26
|
+
expanded?: boolean;
|
|
27
|
+
closeable?: boolean;
|
|
28
|
+
onEditTitle: (value: string) => void;
|
|
29
|
+
orientation?: "horizontal" | "vertical";
|
|
30
|
+
tearOut?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Header = ({
|
|
34
|
+
className: classNameProp,
|
|
35
|
+
contributions,
|
|
36
|
+
collapsed,
|
|
37
|
+
expanded,
|
|
38
|
+
closeable,
|
|
39
|
+
onEditTitle,
|
|
40
|
+
orientation: orientationProp = "horizontal",
|
|
41
|
+
style,
|
|
42
|
+
tearOut,
|
|
43
|
+
title = "Untitled",
|
|
44
|
+
}: HeaderProps) => {
|
|
45
|
+
const labelFieldRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const [value, setValue] = useState<string>(title);
|
|
47
|
+
const [editing, setEditing] = useState<boolean>(false);
|
|
48
|
+
|
|
49
|
+
const viewDispatch = useViewDispatch();
|
|
50
|
+
const handleAction = (
|
|
51
|
+
evt: MouseEvent,
|
|
52
|
+
actionId: "maximize" | "restore" | "minimize" | "tearout"
|
|
53
|
+
) => viewDispatch?.({ type: actionId }, evt);
|
|
54
|
+
const handleClose = (evt: MouseEvent) =>
|
|
55
|
+
viewDispatch?.({ type: "remove" }, evt);
|
|
56
|
+
const classBase = "vuuHeader";
|
|
57
|
+
|
|
58
|
+
const handleTitleMouseDown = (e: MouseEvent) => {
|
|
59
|
+
labelFieldRef.current?.focus();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleButtonMouseDown = (evt: MouseEvent) => {
|
|
63
|
+
// do not allow drag to be initiated
|
|
64
|
+
evt.stopPropagation();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const orientation = collapsed || orientationProp;
|
|
68
|
+
|
|
69
|
+
const className = classnames(
|
|
70
|
+
classBase,
|
|
71
|
+
classNameProp,
|
|
72
|
+
`${classBase}-${orientation}`
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const handleEnterEditMode = () => {
|
|
76
|
+
setEditing(true);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleTitleKeyDown = (evt: KeyboardEvent<HTMLDivElement>) => {
|
|
80
|
+
if (evt.key === "Enter") {
|
|
81
|
+
setEditing(true);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleExitEditMode = (
|
|
86
|
+
originalValue = "",
|
|
87
|
+
finalValue = "",
|
|
88
|
+
allowDeactivation = true,
|
|
89
|
+
editCancelled = false
|
|
90
|
+
) => {
|
|
91
|
+
setEditing(false);
|
|
92
|
+
if (editCancelled) {
|
|
93
|
+
setValue(originalValue);
|
|
94
|
+
} else if (finalValue !== originalValue) {
|
|
95
|
+
setValue(finalValue);
|
|
96
|
+
onEditTitle?.(finalValue);
|
|
97
|
+
}
|
|
98
|
+
if (allowDeactivation === false) {
|
|
99
|
+
labelFieldRef.current?.focus();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
104
|
+
viewDispatch?.({ type: "mousedown" }, e);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const toolbarItems: ReactElement[] = [];
|
|
108
|
+
const contributedItems: ReactElement[] = [];
|
|
109
|
+
const actionButtons: ReactElement[] = [];
|
|
110
|
+
|
|
111
|
+
title &&
|
|
112
|
+
toolbarItems.push(
|
|
113
|
+
<ToolbarField className="vuuHeader-title" key="title">
|
|
114
|
+
<EditableLabel
|
|
115
|
+
editing={editing}
|
|
116
|
+
key="title"
|
|
117
|
+
value={value}
|
|
118
|
+
onChange={setValue}
|
|
119
|
+
onMouseDownCapture={handleTitleMouseDown}
|
|
120
|
+
onEnterEditMode={handleEnterEditMode}
|
|
121
|
+
onExitEditMode={handleExitEditMode}
|
|
122
|
+
onKeyDown={handleTitleKeyDown}
|
|
123
|
+
ref={labelFieldRef}
|
|
124
|
+
tabIndex={0}
|
|
125
|
+
/>
|
|
126
|
+
</ToolbarField>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
contributions?.forEach((contribution, i) => {
|
|
130
|
+
contributedItems.push(React.cloneElement(contribution.content, { key: i }));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
closeable &&
|
|
134
|
+
actionButtons.push(
|
|
135
|
+
<ToolbarButton
|
|
136
|
+
key="close"
|
|
137
|
+
onClick={handleClose}
|
|
138
|
+
onMouseDown={handleButtonMouseDown}
|
|
139
|
+
>
|
|
140
|
+
<CloseIcon /> Close
|
|
141
|
+
</ToolbarButton>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
contributedItems.length > 0 &&
|
|
145
|
+
toolbarItems.push(
|
|
146
|
+
<Tooltray data-align-end key="contributions">
|
|
147
|
+
{contributedItems}
|
|
148
|
+
</Tooltray>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
actionButtons.length > 0 &&
|
|
152
|
+
toolbarItems.push(
|
|
153
|
+
<Tooltray data-align-end key="actions">
|
|
154
|
+
{actionButtons}
|
|
155
|
+
</Tooltray>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Toolbar
|
|
160
|
+
className={className}
|
|
161
|
+
orientation={orientationProp}
|
|
162
|
+
style={style}
|
|
163
|
+
onMouseDown={handleMouseDown}
|
|
164
|
+
>
|
|
165
|
+
{toolbarItems}
|
|
166
|
+
{/*
|
|
167
|
+
{collapsed === false ? (
|
|
168
|
+
<ActionButton
|
|
169
|
+
aria-label="Minimize View"
|
|
170
|
+
actionId="minimize"
|
|
171
|
+
iconName="minimize"
|
|
172
|
+
onClick={handleAction}
|
|
173
|
+
onMouseDown={handleButtonMouseDown}
|
|
174
|
+
/>
|
|
175
|
+
) : null}
|
|
176
|
+
{collapsed ? (
|
|
177
|
+
<ActionButton
|
|
178
|
+
aria-label="Restore View"
|
|
179
|
+
actionId="restore"
|
|
180
|
+
iconName="double-chevron-right"
|
|
181
|
+
onClick={handleAction}
|
|
182
|
+
onMouseDown={handleButtonMouseDown}
|
|
183
|
+
/>
|
|
184
|
+
) : null}
|
|
185
|
+
{expanded === false ? (
|
|
186
|
+
<ActionButton
|
|
187
|
+
aria-label="Maximize View"
|
|
188
|
+
actionId="maximize"
|
|
189
|
+
iconName="maximize"
|
|
190
|
+
onClick={handleAction}
|
|
191
|
+
onMouseDown={handleButtonMouseDown}
|
|
192
|
+
/>
|
|
193
|
+
) : null}
|
|
194
|
+
{expanded ? (
|
|
195
|
+
<ActionButton
|
|
196
|
+
aria-label="Restore View"
|
|
197
|
+
actionId="restore"
|
|
198
|
+
iconName="restore"
|
|
199
|
+
onClick={handleAction}
|
|
200
|
+
onMouseDown={handleButtonMouseDown}
|
|
201
|
+
/>
|
|
202
|
+
) : null}
|
|
203
|
+
{tearOut ? (
|
|
204
|
+
<ActionButton
|
|
205
|
+
aria-label="Tear out View"
|
|
206
|
+
actionId="tearout"
|
|
207
|
+
iconName="tear-out"
|
|
208
|
+
onClick={handleAction}
|
|
209
|
+
onMouseDown={handleButtonMouseDown}
|
|
210
|
+
/>
|
|
211
|
+
) : null}
|
|
212
|
+
{closeable ? (
|
|
213
|
+
<Button
|
|
214
|
+
aria-label="close"
|
|
215
|
+
data-icon
|
|
216
|
+
onClick={handleClose}
|
|
217
|
+
onMouseDown={handleButtonMouseDown}
|
|
218
|
+
/>
|
|
219
|
+
) : null} */}
|
|
220
|
+
</Toolbar>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Header';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
MutableRefObject,
|
|
3
|
+
ReactElement,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
LayoutActionType,
|
|
12
|
+
layoutFromJson,
|
|
13
|
+
LayoutJSON,
|
|
14
|
+
layoutReducer,
|
|
15
|
+
LayoutReducerAction,
|
|
16
|
+
layoutToJSON,
|
|
17
|
+
processLayoutElement,
|
|
18
|
+
} from "../layout-reducer";
|
|
19
|
+
import { findTarget, getChildProp, getProps, typeOf } from "../utils";
|
|
20
|
+
import {
|
|
21
|
+
LayoutProviderContext,
|
|
22
|
+
LayoutProviderDispatch,
|
|
23
|
+
} from "./LayoutProviderContext";
|
|
24
|
+
import { useLayoutDragDrop } from "./useLayoutDragDrop";
|
|
25
|
+
|
|
26
|
+
const withDropTarget = (props: any) => props.dropTarget;
|
|
27
|
+
const shouldSave = (action: LayoutReducerAction) =>
|
|
28
|
+
[
|
|
29
|
+
"drag-drop",
|
|
30
|
+
"remove",
|
|
31
|
+
"set-title",
|
|
32
|
+
"splitter-resize",
|
|
33
|
+
"switch-tab",
|
|
34
|
+
].includes(action.type);
|
|
35
|
+
|
|
36
|
+
type LayoutChangeHandler = (layout: LayoutJSON, source: string) => void;
|
|
37
|
+
|
|
38
|
+
export interface LayoutProviderProps {
|
|
39
|
+
children: ReactElement;
|
|
40
|
+
layout?: LayoutJSON;
|
|
41
|
+
onLayoutChange?: LayoutChangeHandler;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const LayoutProviderVersion = () => {
|
|
45
|
+
const version = useLayoutProviderVersion();
|
|
46
|
+
return <div>{`Context: ${version} `}</div>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const LayoutProvider = (props: LayoutProviderProps): ReactElement => {
|
|
50
|
+
const { children, layout, onLayoutChange } = props;
|
|
51
|
+
const state = useRef<ReactElement | undefined>(undefined);
|
|
52
|
+
const childrenRef = useRef<ReactElement>(children);
|
|
53
|
+
|
|
54
|
+
const [, forceRefresh] = useState<any>(null);
|
|
55
|
+
|
|
56
|
+
const serializeState = useCallback(
|
|
57
|
+
(source) => {
|
|
58
|
+
if (onLayoutChange) {
|
|
59
|
+
const targetContainer =
|
|
60
|
+
findTarget(source, withDropTarget) || state.current;
|
|
61
|
+
const isDraggableLayout = typeOf(targetContainer) === "DraggableLayout";
|
|
62
|
+
const target = isDraggableLayout
|
|
63
|
+
? getProps(targetContainer).children[0]
|
|
64
|
+
: targetContainer;
|
|
65
|
+
const serializedModel = layoutToJSON(target);
|
|
66
|
+
onLayoutChange(serializedModel, "drag-root");
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[onLayoutChange]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const dispatchLayoutAction = useCallback(
|
|
73
|
+
(action: LayoutReducerAction, suppressSave = false) => {
|
|
74
|
+
const nextState = layoutReducer(state.current as ReactElement, action);
|
|
75
|
+
if (nextState !== state.current) {
|
|
76
|
+
state.current = nextState;
|
|
77
|
+
forceRefresh({});
|
|
78
|
+
if (!suppressSave && shouldSave(action)) {
|
|
79
|
+
serializeState(nextState);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[serializeState]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const layoutActionDispatcher: LayoutProviderDispatch = useCallback(
|
|
87
|
+
(action) => {
|
|
88
|
+
// onsole.log(
|
|
89
|
+
// `%cdispatchLayoutProviderAction ${action.type}`,
|
|
90
|
+
// "color: blue; font-weight: bold;"
|
|
91
|
+
// );
|
|
92
|
+
|
|
93
|
+
if (action.type === "drag-start") {
|
|
94
|
+
prepareToDragLayout(action);
|
|
95
|
+
} else if (action.type === "save") {
|
|
96
|
+
serializeState(state.current);
|
|
97
|
+
} else if (state.current) {
|
|
98
|
+
dispatchLayoutAction(action);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[dispatchLayoutAction, serializeState]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const prepareToDragLayout = useLayoutDragDrop(
|
|
105
|
+
state as MutableRefObject<ReactElement>,
|
|
106
|
+
layoutActionDispatcher
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (layout) {
|
|
111
|
+
const targetContainer = findTarget(
|
|
112
|
+
state.current as any,
|
|
113
|
+
withDropTarget
|
|
114
|
+
) as ReactElement;
|
|
115
|
+
const target = getChildProp(targetContainer);
|
|
116
|
+
const newLayout = layoutFromJson(
|
|
117
|
+
layout,
|
|
118
|
+
`${targetContainer.props.path}.0`
|
|
119
|
+
);
|
|
120
|
+
const action = target
|
|
121
|
+
? {
|
|
122
|
+
type: LayoutActionType.REPLACE,
|
|
123
|
+
target,
|
|
124
|
+
replacement: newLayout,
|
|
125
|
+
}
|
|
126
|
+
: {
|
|
127
|
+
type: LayoutActionType.ADD,
|
|
128
|
+
path: targetContainer.props.path,
|
|
129
|
+
component: newLayout,
|
|
130
|
+
};
|
|
131
|
+
dispatchLayoutAction(action, true);
|
|
132
|
+
}
|
|
133
|
+
}, [dispatchLayoutAction, layout]);
|
|
134
|
+
|
|
135
|
+
if (state.current === undefined) {
|
|
136
|
+
state.current = processLayoutElement(children);
|
|
137
|
+
} else if (children !== childrenRef.current) {
|
|
138
|
+
state.current = processLayoutElement(children, state.current);
|
|
139
|
+
childrenRef.current = children;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<LayoutProviderContext.Provider
|
|
144
|
+
value={{ dispatchLayoutProvider: layoutActionDispatcher, version: 0 }}
|
|
145
|
+
>
|
|
146
|
+
{state.current}
|
|
147
|
+
</LayoutProviderContext.Provider>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const useLayoutProviderDispatch = () => {
|
|
152
|
+
const { dispatchLayoutProvider } = useContext(LayoutProviderContext);
|
|
153
|
+
return dispatchLayoutProvider;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const useLayoutProviderVersion = () => {
|
|
157
|
+
console.log({ LayoutProviderContext });
|
|
158
|
+
const { version } = useContext(LayoutProviderContext);
|
|
159
|
+
return version;
|
|
160
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext, Dispatch } from 'react';
|
|
2
|
+
import { DragStartAction, LayoutReducerAction, SaveAction } from '../layout-reducer';
|
|
3
|
+
|
|
4
|
+
const unconfiguredLayoutProviderDispatch: LayoutProviderDispatch = (action) =>
|
|
5
|
+
console.log(`dispatch ${action.type}, have you forgotten to provide a LayoutProvider ?`);
|
|
6
|
+
|
|
7
|
+
export type LayoutProviderDispatch = Dispatch<LayoutReducerAction | SaveAction | DragStartAction>;
|
|
8
|
+
|
|
9
|
+
export interface LayoutProviderContextProps {
|
|
10
|
+
dispatchLayoutProvider: LayoutProviderDispatch;
|
|
11
|
+
version: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const LayoutProviderContext = createContext<LayoutProviderContextProps>({
|
|
15
|
+
dispatchLayoutProvider: unconfiguredLayoutProviderDispatch,
|
|
16
|
+
version: -1
|
|
17
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { MutableRefObject, ReactElement, useCallback, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DragDropRect,
|
|
4
|
+
DragEndCallback,
|
|
5
|
+
Draggable,
|
|
6
|
+
DragInstructions,
|
|
7
|
+
} from "../drag-drop";
|
|
8
|
+
import { DragStartAction } from "../layout-reducer";
|
|
9
|
+
import { getIntrinsicSize } from "../layout-reducer/flexUtils";
|
|
10
|
+
import { followPath } from "../utils";
|
|
11
|
+
import { LayoutProviderDispatch } from "./LayoutProviderContext";
|
|
12
|
+
|
|
13
|
+
const NO_INSTRUCTIONS = {} as DragInstructions;
|
|
14
|
+
const NO_OFFSETS: [number, number] = [0, 0];
|
|
15
|
+
|
|
16
|
+
interface CurrentDragAction extends Omit<DragStartAction, "evt" | "type"> {
|
|
17
|
+
dragContainerPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DragOperation {
|
|
21
|
+
payload: ReactElement;
|
|
22
|
+
originalCSS: string;
|
|
23
|
+
dragRect: any;
|
|
24
|
+
dragInstructions: DragInstructions;
|
|
25
|
+
dragOffsets: [number, number];
|
|
26
|
+
targetPosition: { left: number; top: number };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create a temporary object for dragging, where we don not have an existing object
|
|
30
|
+
// e.g dragging a non-selected tab from a Stack or an item from Palette
|
|
31
|
+
const getDragElement = (
|
|
32
|
+
rect: DragDropRect,
|
|
33
|
+
id: string,
|
|
34
|
+
dragElement?: HTMLElement
|
|
35
|
+
): [HTMLElement, string, number, number] => {
|
|
36
|
+
const wrapper = document.createElement("div");
|
|
37
|
+
wrapper.className = "vuuSimpleDraggableWrapper";
|
|
38
|
+
// TODO caller needs to supply the uitk classes
|
|
39
|
+
wrapper.classList.add(
|
|
40
|
+
"vuuSimpleDraggableWrapper",
|
|
41
|
+
"uitk-theme",
|
|
42
|
+
"uitk-density-medium"
|
|
43
|
+
);
|
|
44
|
+
wrapper.dataset.dragging = "true";
|
|
45
|
+
|
|
46
|
+
const div = dragElement ?? document.createElement("div");
|
|
47
|
+
// this seems wrong the id is the payload id
|
|
48
|
+
div.id = id;
|
|
49
|
+
|
|
50
|
+
wrapper.appendChild(div);
|
|
51
|
+
document.body.appendChild(wrapper);
|
|
52
|
+
const cssText = `top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;`;
|
|
53
|
+
return [wrapper, cssText, rect.left, rect.top];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const determineDragOffsets = (
|
|
57
|
+
draggedElement: HTMLElement
|
|
58
|
+
): [number, number] => {
|
|
59
|
+
const { offsetParent } = draggedElement;
|
|
60
|
+
if (offsetParent === null) {
|
|
61
|
+
return NO_OFFSETS;
|
|
62
|
+
} else {
|
|
63
|
+
const { left: offsetLeft, top: offsetTop } =
|
|
64
|
+
offsetParent.getBoundingClientRect();
|
|
65
|
+
return [offsetLeft, offsetTop];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const useLayoutDragDrop = (
|
|
70
|
+
rootLayoutRef: MutableRefObject<ReactElement>,
|
|
71
|
+
dispatch: LayoutProviderDispatch
|
|
72
|
+
) => {
|
|
73
|
+
const dragActionRef = useRef<CurrentDragAction>();
|
|
74
|
+
const dragOperationRef = useRef<DragOperation>();
|
|
75
|
+
const draggableHTMLElementRef = useRef<HTMLElement>();
|
|
76
|
+
|
|
77
|
+
const handleDrag = useCallback((x, y) => {
|
|
78
|
+
if (dragOperationRef.current && draggableHTMLElementRef.current) {
|
|
79
|
+
const {
|
|
80
|
+
dragOffsets: [offsetX, offsetY],
|
|
81
|
+
targetPosition,
|
|
82
|
+
} = dragOperationRef.current;
|
|
83
|
+
const left = typeof x === "number" ? x - offsetX : targetPosition.left;
|
|
84
|
+
const top = typeof y === "number" ? y - offsetY : targetPosition.top;
|
|
85
|
+
if (left !== targetPosition.left || top !== targetPosition.top) {
|
|
86
|
+
dragOperationRef.current.targetPosition.left = left;
|
|
87
|
+
dragOperationRef.current.targetPosition.top = top;
|
|
88
|
+
draggableHTMLElementRef.current.style.top = top + "px";
|
|
89
|
+
draggableHTMLElementRef.current.style.left = left + "px";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const handleDrop: DragEndCallback = useCallback((dropTarget) => {
|
|
95
|
+
if (dragOperationRef.current) {
|
|
96
|
+
const {
|
|
97
|
+
dragInstructions,
|
|
98
|
+
payload: draggedReactElement,
|
|
99
|
+
originalCSS,
|
|
100
|
+
} = dragOperationRef.current;
|
|
101
|
+
dispatch({
|
|
102
|
+
type: "drag-drop",
|
|
103
|
+
draggedReactElement,
|
|
104
|
+
dragInstructions,
|
|
105
|
+
dropTarget,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log(`[useLayoutDragDrop]`, {
|
|
109
|
+
dragInstructions,
|
|
110
|
+
});
|
|
111
|
+
if (draggableHTMLElementRef.current) {
|
|
112
|
+
if (dragInstructions.RemoveDraggableOnDragEnd) {
|
|
113
|
+
document.body.removeChild(draggableHTMLElementRef.current);
|
|
114
|
+
} else {
|
|
115
|
+
draggableHTMLElementRef.current.style.cssText = originalCSS;
|
|
116
|
+
delete draggableHTMLElementRef.current.dataset.dragging;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
dragActionRef.current = undefined;
|
|
121
|
+
dragOperationRef.current = undefined;
|
|
122
|
+
draggableHTMLElementRef.current = undefined;
|
|
123
|
+
}
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* This will be called when Draggable has established that a drag operation is
|
|
128
|
+
* underway. There may be a delay between the initial mousedown and the call to
|
|
129
|
+
* this function - while we wait for either a drag timeout to fire or a minumum
|
|
130
|
+
* mouse move threshold to be reached.
|
|
131
|
+
*/
|
|
132
|
+
const handleDragStart = useCallback(
|
|
133
|
+
(evt: MouseEvent) => {
|
|
134
|
+
if (dragActionRef.current) {
|
|
135
|
+
const {
|
|
136
|
+
payload: component,
|
|
137
|
+
dragContainerPath,
|
|
138
|
+
dragElement,
|
|
139
|
+
dragRect,
|
|
140
|
+
// dropTargets,
|
|
141
|
+
instructions = NO_INSTRUCTIONS,
|
|
142
|
+
path,
|
|
143
|
+
// preDragActivity,
|
|
144
|
+
// resolveDragStart // see View drag
|
|
145
|
+
} = dragActionRef.current;
|
|
146
|
+
const { current: rootLayout } = rootLayoutRef;
|
|
147
|
+
const dragPos = { x: evt.clientX, y: evt.clientY };
|
|
148
|
+
const dragPayload = component ?? followPath(rootLayout, path, true);
|
|
149
|
+
const { id: dragPayloadId } = dragPayload.props;
|
|
150
|
+
const intrinsicSize = getIntrinsicSize(dragPayload);
|
|
151
|
+
let originalCSS = "",
|
|
152
|
+
dragCSS = "",
|
|
153
|
+
dragTransform = "";
|
|
154
|
+
let dragInstructions = instructions;
|
|
155
|
+
|
|
156
|
+
let dragStartLeft = -1;
|
|
157
|
+
let dragStartTop = -1;
|
|
158
|
+
let dragOffsets: [number, number] = NO_OFFSETS;
|
|
159
|
+
|
|
160
|
+
// TODO this has a bearing on offsets we apply to (absolutely positioned) dragged element.
|
|
161
|
+
// If we are creating the element here, offset parent will be document body.
|
|
162
|
+
let element = document.getElementById(dragPayloadId);
|
|
163
|
+
|
|
164
|
+
if (element === null) {
|
|
165
|
+
// This may bew the case where, for example, we drag a Tab (non selected) from a Tabstrip.
|
|
166
|
+
[element, dragCSS, dragStartLeft, dragStartTop] = getDragElement(
|
|
167
|
+
dragRect,
|
|
168
|
+
dragPayloadId,
|
|
169
|
+
dragElement
|
|
170
|
+
);
|
|
171
|
+
dragInstructions = {
|
|
172
|
+
...dragInstructions,
|
|
173
|
+
RemoveDraggableOnDragEnd: true,
|
|
174
|
+
};
|
|
175
|
+
} else {
|
|
176
|
+
dragOffsets = determineDragOffsets(element);
|
|
177
|
+
const [offsetLeft, offsetTop] = dragOffsets;
|
|
178
|
+
const { width, height, left, top } = element.getBoundingClientRect();
|
|
179
|
+
dragStartLeft = left - offsetLeft;
|
|
180
|
+
dragStartTop = top - offsetTop;
|
|
181
|
+
dragCSS = `width:${width}px;height:${height}px;left:${dragStartLeft}px;top:${dragStartTop}px;z-index: 100;background-color:#ccc;opacity: 0.6;`;
|
|
182
|
+
// Important that this is set before we call initDrag
|
|
183
|
+
// this just enables position: absolute
|
|
184
|
+
element.dataset.dragging = "true";
|
|
185
|
+
|
|
186
|
+
// resolveDragStart && resolveDragStart(true);
|
|
187
|
+
|
|
188
|
+
// if (preDragActivity) {
|
|
189
|
+
// await preDragActivity();
|
|
190
|
+
// }
|
|
191
|
+
|
|
192
|
+
originalCSS = element.style.cssText;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dragTransform = Draggable.initDrag(
|
|
196
|
+
rootLayoutRef.current,
|
|
197
|
+
dragContainerPath,
|
|
198
|
+
dragRect,
|
|
199
|
+
dragPos,
|
|
200
|
+
{
|
|
201
|
+
drag: handleDrag,
|
|
202
|
+
drop: handleDrop,
|
|
203
|
+
},
|
|
204
|
+
intrinsicSize
|
|
205
|
+
// dropTargets
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
element.style.cssText = dragCSS + dragTransform;
|
|
209
|
+
draggableHTMLElementRef.current = element;
|
|
210
|
+
|
|
211
|
+
dragOperationRef.current = {
|
|
212
|
+
payload: dragPayload,
|
|
213
|
+
originalCSS,
|
|
214
|
+
dragRect,
|
|
215
|
+
dragOffsets,
|
|
216
|
+
dragInstructions: instructions,
|
|
217
|
+
targetPosition: { left: dragStartLeft, top: dragStartTop },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
[handleDrag, handleDrop, rootLayoutRef]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const prepareToDrag = useCallback(
|
|
225
|
+
(action: DragStartAction) => {
|
|
226
|
+
const { evt, ...options } = action;
|
|
227
|
+
console.log(`prepare to drag`, {
|
|
228
|
+
options,
|
|
229
|
+
});
|
|
230
|
+
dragActionRef.current = {
|
|
231
|
+
...options,
|
|
232
|
+
dragContainerPath: "",
|
|
233
|
+
// dragContainerPath: '0.0.1.1'
|
|
234
|
+
};
|
|
235
|
+
Draggable.handleMousedown(evt, handleDragStart, options.instructions);
|
|
236
|
+
},
|
|
237
|
+
[handleDragStart]
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return prepareToDrag;
|
|
241
|
+
};
|