@vuu-ui/vuu-layout 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +1 -0
  2. package/package.json +30 -0
  3. package/src/Component.css +2 -0
  4. package/src/Component.tsx +20 -0
  5. package/src/DraggableLayout.css +18 -0
  6. package/src/DraggableLayout.tsx +29 -0
  7. package/src/__tests__/flexbox-utils.spec.js +90 -0
  8. package/src/action-buttons/action-buttons.css +12 -0
  9. package/src/action-buttons/action-buttons.tsx +30 -0
  10. package/src/action-buttons/index.ts +1 -0
  11. package/src/chest-of-drawers/Chest.css +36 -0
  12. package/src/chest-of-drawers/Chest.tsx +42 -0
  13. package/src/chest-of-drawers/Drawer.css +153 -0
  14. package/src/chest-of-drawers/Drawer.tsx +118 -0
  15. package/src/chest-of-drawers/index.ts +2 -0
  16. package/src/common-types.ts +9 -0
  17. package/src/debug.ts +16 -0
  18. package/src/dialog/Dialog.css +16 -0
  19. package/src/dialog/Dialog.tsx +59 -0
  20. package/src/dialog/index.ts +1 -0
  21. package/src/drag-drop/BoxModel.ts +546 -0
  22. package/src/drag-drop/DragState.ts +222 -0
  23. package/src/drag-drop/Draggable.ts +282 -0
  24. package/src/drag-drop/DropMenu.css +70 -0
  25. package/src/drag-drop/DropMenu.tsx +68 -0
  26. package/src/drag-drop/DropTarget.ts +392 -0
  27. package/src/drag-drop/DropTargetRenderer.css +40 -0
  28. package/src/drag-drop/DropTargetRenderer.tsx +284 -0
  29. package/src/drag-drop/dragDropTypes.ts +49 -0
  30. package/src/drag-drop/index.ts +4 -0
  31. package/src/editable-label/EditableLabel.css +28 -0
  32. package/src/editable-label/EditableLabel.tsx +99 -0
  33. package/src/editable-label/index.ts +1 -0
  34. package/src/flexbox/Flexbox.css +45 -0
  35. package/src/flexbox/Flexbox.tsx +70 -0
  36. package/src/flexbox/FlexboxLayout.jsx +26 -0
  37. package/src/flexbox/FluidGrid.css +134 -0
  38. package/src/flexbox/FluidGrid.tsx +84 -0
  39. package/src/flexbox/FluidGridLayout.tsx +10 -0
  40. package/src/flexbox/Splitter.css +140 -0
  41. package/src/flexbox/Splitter.tsx +135 -0
  42. package/src/flexbox/flexbox-utils.ts +128 -0
  43. package/src/flexbox/flexboxTypes.ts +63 -0
  44. package/src/flexbox/index.ts +4 -0
  45. package/src/flexbox/useResponsiveSizing.ts +85 -0
  46. package/src/flexbox/useSplitterResizing.ts +272 -0
  47. package/src/index.ts +20 -0
  48. package/src/layout-action.ts +21 -0
  49. package/src/layout-header/ActionButton.tsx +23 -0
  50. package/src/layout-header/Header.css +8 -0
  51. package/src/layout-header/Header.tsx +222 -0
  52. package/src/layout-header/index.ts +1 -0
  53. package/src/layout-provider/LayoutProvider.tsx +160 -0
  54. package/src/layout-provider/LayoutProviderContext.ts +17 -0
  55. package/src/layout-provider/index.ts +2 -0
  56. package/src/layout-provider/useLayoutDragDrop.ts +241 -0
  57. package/src/layout-reducer/flexUtils.ts +281 -0
  58. package/src/layout-reducer/index.ts +4 -0
  59. package/src/layout-reducer/insert-layout-element.ts +365 -0
  60. package/src/layout-reducer/layout-reducer.ts +255 -0
  61. package/src/layout-reducer/layoutTypes.ts +151 -0
  62. package/src/layout-reducer/layoutUtils.ts +302 -0
  63. package/src/layout-reducer/remove-layout-element.ts +240 -0
  64. package/src/layout-reducer/replace-layout-element.ts +118 -0
  65. package/src/layout-reducer/resize-flex-children.ts +56 -0
  66. package/src/layout-reducer/wrap-layout-element.ts +317 -0
  67. package/src/layout-view/View.css +58 -0
  68. package/src/layout-view/View.tsx +149 -0
  69. package/src/layout-view/ViewContext.ts +31 -0
  70. package/src/layout-view/index.ts +4 -0
  71. package/src/layout-view/useView.tsx +104 -0
  72. package/src/layout-view/useViewActionDispatcher.ts +133 -0
  73. package/src/layout-view/useViewResize.ts +53 -0
  74. package/src/layout-view/viewTypes.ts +37 -0
  75. package/src/palette/Palette.css +37 -0
  76. package/src/palette/Palette.tsx +140 -0
  77. package/src/palette/PaletteUitk.css +9 -0
  78. package/src/palette/PaletteUitk.tsx +79 -0
  79. package/src/palette/index.ts +2 -0
  80. package/src/placeholder/Placeholder.css +10 -0
  81. package/src/placeholder/Placeholder.tsx +39 -0
  82. package/src/placeholder/index.ts +1 -0
  83. package/src/registry/ComponentRegistry.ts +35 -0
  84. package/src/registry/index.ts +1 -0
  85. package/src/responsive/OverflowMenu.css +31 -0
  86. package/src/responsive/OverflowMenu.jsx +56 -0
  87. package/src/responsive/breakpoints.ts +48 -0
  88. package/src/responsive/index.ts +4 -0
  89. package/src/responsive/measureMinimumNodeSize.ts +23 -0
  90. package/src/responsive/overflowUtils.js +14 -0
  91. package/src/responsive/use-breakpoints.ts +100 -0
  92. package/src/responsive/useOverflowObserver.ts +606 -0
  93. package/src/responsive/useResizeObserver.ts +154 -0
  94. package/src/responsive/utils.ts +37 -0
  95. package/src/stack/Stack.css +39 -0
  96. package/src/stack/Stack.tsx +160 -0
  97. package/src/stack/StackLayout.tsx +137 -0
  98. package/src/stack/index.ts +3 -0
  99. package/src/stack/stackTypes.ts +19 -0
  100. package/src/tabs/TabPanel.css +12 -0
  101. package/src/tabs/TabPanel.tsx +17 -0
  102. package/src/tabs/index.ts +1 -0
  103. package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
  104. package/src/tools/config-wrapper/index.js +1 -0
  105. package/src/tools/devtools-box/layout-configurator.css +112 -0
  106. package/src/tools/devtools-box/layout-configurator.jsx +369 -0
  107. package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
  108. package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
  109. package/src/tools/index.js +3 -0
  110. package/src/use-persistent-state.ts +115 -0
  111. package/src/utils/componentFromLayout.tsx +30 -0
  112. package/src/utils/index.ts +6 -0
  113. package/src/utils/pathUtils.ts +294 -0
  114. package/src/utils/propUtils.ts +24 -0
  115. package/src/utils/refUtils.ts +16 -0
  116. package/src/utils/styleUtils.ts +14 -0
  117. package/src/utils/typeOf.ts +22 -0
@@ -0,0 +1,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,8 @@
1
+ .vuuHeader {
2
+ --uitkToolbar-background: var(--uitk-container-background-low);
3
+ }
4
+
5
+ .uitk-density-high .vuuHeader {
6
+ --uitkToolbarField-marginTop: 0;
7
+ }
8
+
@@ -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,2 @@
1
+ export * from './LayoutProvider';
2
+ export * from './LayoutProviderContext';
@@ -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
+ };