@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,392 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BoxModel,
|
|
4
|
+
positionValues,
|
|
5
|
+
getPosition,
|
|
6
|
+
Position,
|
|
7
|
+
RelativeDropPosition,
|
|
8
|
+
Measurements,
|
|
9
|
+
} from "./BoxModel";
|
|
10
|
+
import { getProps, typeOf } from "../utils";
|
|
11
|
+
import { DragDropRect, DropPos, DropPosTab } from "./dragDropTypes";
|
|
12
|
+
import { LayoutModel } from "../layout-reducer";
|
|
13
|
+
import { DragState } from "./DragState";
|
|
14
|
+
import { rect, rectTuple } from "../common-types";
|
|
15
|
+
|
|
16
|
+
export const isTabstrip = (dropTarget: DropTarget) =>
|
|
17
|
+
dropTarget.pos.tab &&
|
|
18
|
+
typeOf(dropTarget.component) === "Stack" &&
|
|
19
|
+
dropTarget.pos.position.Header;
|
|
20
|
+
|
|
21
|
+
const { north, south, east, west } = positionValues;
|
|
22
|
+
const eastwest = east + west;
|
|
23
|
+
const northsouth = north + south;
|
|
24
|
+
|
|
25
|
+
export interface DropTargetProps {
|
|
26
|
+
component: LayoutModel;
|
|
27
|
+
pos: DropPos;
|
|
28
|
+
clientRect: DragDropRect;
|
|
29
|
+
nextDropTarget: DropTarget | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type GuideLine = [
|
|
33
|
+
number,
|
|
34
|
+
number,
|
|
35
|
+
number,
|
|
36
|
+
number,
|
|
37
|
+
number,
|
|
38
|
+
number,
|
|
39
|
+
number,
|
|
40
|
+
number
|
|
41
|
+
];
|
|
42
|
+
export interface TargetDropOutline {
|
|
43
|
+
l: number;
|
|
44
|
+
r: number;
|
|
45
|
+
t: number;
|
|
46
|
+
b: number;
|
|
47
|
+
tabLeft?: number;
|
|
48
|
+
tabWidth?: number;
|
|
49
|
+
tabHeight?: number;
|
|
50
|
+
guideLines?: GuideLine;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class DropTarget {
|
|
54
|
+
private active: boolean;
|
|
55
|
+
|
|
56
|
+
public box: any;
|
|
57
|
+
public clientRect: DragDropRect;
|
|
58
|
+
public component: LayoutModel;
|
|
59
|
+
public dropRect: rectTuple | undefined;
|
|
60
|
+
public nextDropTarget: DropTarget | null;
|
|
61
|
+
public pos: DropPos;
|
|
62
|
+
|
|
63
|
+
constructor({
|
|
64
|
+
component,
|
|
65
|
+
pos,
|
|
66
|
+
clientRect /*, closeToTheEdge*/,
|
|
67
|
+
nextDropTarget,
|
|
68
|
+
}: DropTargetProps) {
|
|
69
|
+
this.component = component;
|
|
70
|
+
this.pos = pos;
|
|
71
|
+
this.clientRect = clientRect;
|
|
72
|
+
this.nextDropTarget = nextDropTarget;
|
|
73
|
+
this.active = false;
|
|
74
|
+
this.dropRect = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
targetTabPos(tab: DropPosTab) {
|
|
78
|
+
const { left: tabLeft, width: tabWidth, positionRelativeToTab } = tab;
|
|
79
|
+
return positionRelativeToTab === RelativeDropPosition.BEFORE
|
|
80
|
+
? tabLeft
|
|
81
|
+
: tabLeft + tabWidth;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Determine what will be rendered by the dropTargetRenderer
|
|
86
|
+
*
|
|
87
|
+
* @param {*} lineWidth
|
|
88
|
+
* @param {*} dragState
|
|
89
|
+
* @returns {l, t, r, b, tabLeft, tabWidth, tabHeight}
|
|
90
|
+
*/
|
|
91
|
+
getTargetDropOutline(
|
|
92
|
+
lineWidth: number,
|
|
93
|
+
dragState?: DragState
|
|
94
|
+
): TargetDropOutline {
|
|
95
|
+
if (this.pos.tab) {
|
|
96
|
+
return this.getDropTabOutline(lineWidth, this.pos.tab);
|
|
97
|
+
} else if (dragState && dragState.hasIntrinsicSize()) {
|
|
98
|
+
return this.getIntrinsicDropRect(dragState);
|
|
99
|
+
} else {
|
|
100
|
+
const [l, t, r, b] = this.getDropRectOutline(
|
|
101
|
+
lineWidth,
|
|
102
|
+
dragState
|
|
103
|
+
) as rectTuple;
|
|
104
|
+
return { l, t, r, b };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getDropTabOutline(lineWidth: number, tab: DropPosTab): TargetDropOutline {
|
|
109
|
+
const {
|
|
110
|
+
clientRect: { top, left, right, bottom, header },
|
|
111
|
+
} = this;
|
|
112
|
+
|
|
113
|
+
const inset = 0;
|
|
114
|
+
const gap = Math.round(lineWidth / 2) + inset;
|
|
115
|
+
|
|
116
|
+
const t = Math.round(top);
|
|
117
|
+
const l = Math.round(left + gap);
|
|
118
|
+
const r = Math.round(right - gap);
|
|
119
|
+
const b = Math.round(bottom - gap);
|
|
120
|
+
const tabLeft = this.targetTabPos(tab);
|
|
121
|
+
const tabWidth = 60; // should really measure text
|
|
122
|
+
const tabHeight = header!.bottom - header!.top;
|
|
123
|
+
return { l, t, r, b, tabLeft, tabWidth, tabHeight };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getIntrinsicDropRect(dragState: DragState): TargetDropOutline {
|
|
127
|
+
const { pos, clientRect: rect } = this;
|
|
128
|
+
|
|
129
|
+
let { x, y } = dragState;
|
|
130
|
+
|
|
131
|
+
let height = dragState.intrinsicSize?.height ?? 0;
|
|
132
|
+
let width = dragState.intrinsicSize?.height ?? 0;
|
|
133
|
+
|
|
134
|
+
if (height && height > rect.height) {
|
|
135
|
+
console.log(`DropTarget: we're going to blow the gaff`);
|
|
136
|
+
height = rect.height;
|
|
137
|
+
} else if (width && width > rect.width) {
|
|
138
|
+
console.log(`DropTarget: we're going to blow the gaff`);
|
|
139
|
+
width = rect.width;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const left = Math.min(
|
|
143
|
+
rect.right - width,
|
|
144
|
+
Math.max(rect.left, Math.round(pos.x - x.mousePct * width))
|
|
145
|
+
);
|
|
146
|
+
const top = Math.min(
|
|
147
|
+
rect.bottom - height,
|
|
148
|
+
Math.max(rect.top, Math.round(pos.y - y.mousePct * height))
|
|
149
|
+
);
|
|
150
|
+
const [l, t, r, b] = (this.dropRect = [
|
|
151
|
+
left,
|
|
152
|
+
top,
|
|
153
|
+
left + width,
|
|
154
|
+
top + height,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const guideLines: GuideLine = pos.position.EastOrWest
|
|
158
|
+
? [l, rect.top, l, rect.bottom, r, rect.top, r, rect.bottom]
|
|
159
|
+
: [rect.left, t, rect.right, t, rect.left, b, rect.right, b];
|
|
160
|
+
|
|
161
|
+
return { l, r, t, b, guideLines };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @returns [left, top, right, bottom]
|
|
166
|
+
*/
|
|
167
|
+
getDropRectOutline(lineWidth: number, dragState?: DragState) {
|
|
168
|
+
const { pos, clientRect: rect } = this;
|
|
169
|
+
const { width: suggestedWidth, height: suggestedHeight, position } = pos;
|
|
170
|
+
|
|
171
|
+
const { width: intrinsicWidth, height: intrinsicHeight } =
|
|
172
|
+
dragState?.intrinsicSize ?? {};
|
|
173
|
+
const sizeHeight = intrinsicHeight ?? suggestedHeight ?? 0;
|
|
174
|
+
const sizeWidth = intrinsicWidth ?? suggestedWidth ?? 0;
|
|
175
|
+
|
|
176
|
+
this.dropRect = undefined;
|
|
177
|
+
|
|
178
|
+
const { top: t, left: l, right: r, bottom: b } = rect;
|
|
179
|
+
|
|
180
|
+
const inset = 0;
|
|
181
|
+
const gap = Math.round(lineWidth / 2) + inset;
|
|
182
|
+
|
|
183
|
+
switch (position) {
|
|
184
|
+
case Position.North:
|
|
185
|
+
case Position.Header: {
|
|
186
|
+
const halfHeight = Math.round((b - t) / 2);
|
|
187
|
+
const height = sizeHeight
|
|
188
|
+
? Math.min(halfHeight, Math.round(sizeHeight))
|
|
189
|
+
: halfHeight;
|
|
190
|
+
return sizeWidth && l + sizeWidth < r
|
|
191
|
+
? [l + gap, t + gap, l + sizeWidth - gap, t + gap + height] // need flex direction indicator
|
|
192
|
+
: [l + gap, t + gap, r - gap, t + gap + height];
|
|
193
|
+
}
|
|
194
|
+
case Position.West: {
|
|
195
|
+
const halfWidth = Math.round((r - l) / 2);
|
|
196
|
+
const width = sizeWidth
|
|
197
|
+
? Math.min(halfWidth, Math.round(sizeWidth))
|
|
198
|
+
: halfWidth;
|
|
199
|
+
return sizeHeight && t + sizeHeight < b
|
|
200
|
+
? [l + gap, t + gap, l - gap + width, t + sizeHeight + gap] // need flex direction indicator
|
|
201
|
+
: [l + gap, t + gap, l - gap + width, b - gap];
|
|
202
|
+
}
|
|
203
|
+
case Position.East: {
|
|
204
|
+
const halfWidth = Math.round((r - l) / 2);
|
|
205
|
+
const width = sizeWidth
|
|
206
|
+
? Math.min(halfWidth, Math.round(sizeWidth))
|
|
207
|
+
: halfWidth;
|
|
208
|
+
return sizeHeight && t + sizeHeight < b
|
|
209
|
+
? [r - gap - width, t + gap, r - gap, t + sizeHeight + gap] // need flex direction indicator
|
|
210
|
+
: [r - gap - width, t + gap, r - gap, b - gap];
|
|
211
|
+
}
|
|
212
|
+
case Position.South: {
|
|
213
|
+
const halfHeight = Math.round((b - t) / 2);
|
|
214
|
+
const height = sizeHeight
|
|
215
|
+
? Math.min(halfHeight, Math.round(sizeHeight))
|
|
216
|
+
: halfHeight;
|
|
217
|
+
|
|
218
|
+
return sizeWidth && l + sizeWidth < r
|
|
219
|
+
? [l + gap, b - gap - height, l + sizeWidth - gap, b - gap] // need flex direction indicator
|
|
220
|
+
: [l + gap, b - gap - height, r - gap, b - gap];
|
|
221
|
+
}
|
|
222
|
+
case Position.Centre: {
|
|
223
|
+
return [l + gap, t + gap, r - gap, b - gap];
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
console.warn(`DropTarget does not recognize position ${position}`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
activate() {
|
|
232
|
+
this.active = true;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
toArray(this: DropTarget) {
|
|
237
|
+
let dropTarget: DropTarget | null = this;
|
|
238
|
+
const dropTargets = [dropTarget];
|
|
239
|
+
// eslint-disable-next-line no-cond-assign
|
|
240
|
+
while ((dropTarget = dropTarget.nextDropTarget)) {
|
|
241
|
+
dropTargets.push(dropTarget);
|
|
242
|
+
}
|
|
243
|
+
return dropTargets;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static getActiveDropTarget(dropTarget: DropTarget | null): DropTarget | null {
|
|
247
|
+
return dropTarget === null
|
|
248
|
+
? null
|
|
249
|
+
: dropTarget?.active
|
|
250
|
+
? dropTarget
|
|
251
|
+
: DropTarget.getActiveDropTarget(dropTarget.nextDropTarget);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Initial entry to this method is always via the app (may be it should be *on* the app)
|
|
256
|
+
export function identifyDropTarget(
|
|
257
|
+
x: number,
|
|
258
|
+
y: number,
|
|
259
|
+
rootLayout: LayoutModel,
|
|
260
|
+
measurements: Measurements,
|
|
261
|
+
intrinsicSize?: number,
|
|
262
|
+
validDropTargets?: string[]
|
|
263
|
+
) {
|
|
264
|
+
let dropTarget = null;
|
|
265
|
+
|
|
266
|
+
var allBoxesContainingPoint = BoxModel.allBoxesContainingPoint(
|
|
267
|
+
rootLayout,
|
|
268
|
+
measurements,
|
|
269
|
+
x,
|
|
270
|
+
y,
|
|
271
|
+
validDropTargets
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (allBoxesContainingPoint.length) {
|
|
275
|
+
const [component, ...containers] = allBoxesContainingPoint;
|
|
276
|
+
const {
|
|
277
|
+
"data-path": dataPath,
|
|
278
|
+
path = dataPath,
|
|
279
|
+
"data-row-placeholder": isRowPlaceholder,
|
|
280
|
+
} = getProps(component);
|
|
281
|
+
const clientRect = measurements[path];
|
|
282
|
+
const placeholderOrientation =
|
|
283
|
+
intrinsicSize && isRowPlaceholder ? "row" : undefined;
|
|
284
|
+
const pos = getPosition(x, y, clientRect, placeholderOrientation);
|
|
285
|
+
const box = measurements[path];
|
|
286
|
+
|
|
287
|
+
function nextDropTarget([nextTarget, ...targets]: LayoutModel[]):
|
|
288
|
+
| DropTarget
|
|
289
|
+
| undefined {
|
|
290
|
+
if (pos.position?.Header || pos.closeToTheEdge) {
|
|
291
|
+
const targetPosition = getTargetPosition(
|
|
292
|
+
nextTarget,
|
|
293
|
+
pos,
|
|
294
|
+
box,
|
|
295
|
+
measurements,
|
|
296
|
+
x,
|
|
297
|
+
y
|
|
298
|
+
);
|
|
299
|
+
if (targetPosition) {
|
|
300
|
+
const [containerPos, clientRect] = targetPosition;
|
|
301
|
+
|
|
302
|
+
return new DropTarget({
|
|
303
|
+
component: nextTarget,
|
|
304
|
+
pos: containerPos,
|
|
305
|
+
clientRect,
|
|
306
|
+
nextDropTarget: nextDropTarget(targets) ?? null,
|
|
307
|
+
});
|
|
308
|
+
} else if (targets.length) {
|
|
309
|
+
return nextDropTarget(targets);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
dropTarget = new DropTarget({
|
|
314
|
+
component,
|
|
315
|
+
pos,
|
|
316
|
+
clientRect,
|
|
317
|
+
nextDropTarget: nextDropTarget(containers) ?? null,
|
|
318
|
+
}).activate();
|
|
319
|
+
}
|
|
320
|
+
return dropTarget;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getTargetPosition(
|
|
324
|
+
container: LayoutModel,
|
|
325
|
+
{ closeToTheEdge, position }: DropPos,
|
|
326
|
+
box: rect,
|
|
327
|
+
measurements: Measurements,
|
|
328
|
+
x: number,
|
|
329
|
+
y: number
|
|
330
|
+
): [DropPos, DragDropRect] | undefined {
|
|
331
|
+
if (!container || container.type === "DraggableLayout") {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const containingBox = measurements[container.props.path];
|
|
336
|
+
const closeToTop = closeToTheEdge & positionValues.north;
|
|
337
|
+
const closeToRight = closeToTheEdge & positionValues.east;
|
|
338
|
+
const closeToBottom = closeToTheEdge & positionValues.south;
|
|
339
|
+
const closeToLeft = closeToTheEdge & positionValues.west;
|
|
340
|
+
|
|
341
|
+
const atTop =
|
|
342
|
+
(closeToTop || position.Header) &&
|
|
343
|
+
Math.round(box.top) === Math.round(containingBox.top);
|
|
344
|
+
const atRight =
|
|
345
|
+
closeToRight && Math.round(box.right) === Math.round(containingBox.right);
|
|
346
|
+
const atBottom =
|
|
347
|
+
closeToBottom &&
|
|
348
|
+
Math.round(box.bottom) === Math.round(containingBox.bottom);
|
|
349
|
+
const atLeft =
|
|
350
|
+
closeToLeft && Math.round(box.left) === Math.round(containingBox.left);
|
|
351
|
+
|
|
352
|
+
if (atTop || atRight || atBottom || atLeft) {
|
|
353
|
+
const { "data-path": dataPath, path = dataPath } = container.props;
|
|
354
|
+
const clientRect = measurements[path];
|
|
355
|
+
let containerPos = getPosition(x, y, clientRect);
|
|
356
|
+
|
|
357
|
+
// if its a VBox and we're close to left or right ...
|
|
358
|
+
if (
|
|
359
|
+
(isVBox(container) || isTabbedContainer(container)) &&
|
|
360
|
+
closeToTheEdge & eastwest
|
|
361
|
+
) {
|
|
362
|
+
containerPos.width = 120;
|
|
363
|
+
return [containerPos, clientRect];
|
|
364
|
+
}
|
|
365
|
+
// if it's a HBox and we're close to top or bottom ...
|
|
366
|
+
else if (
|
|
367
|
+
(isHBox(container) || isTabbedContainer(container)) &&
|
|
368
|
+
(position.Header || closeToTheEdge & northsouth)
|
|
369
|
+
) {
|
|
370
|
+
containerPos.height = 120;
|
|
371
|
+
return [containerPos, clientRect];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isTabbedContainer(component: LayoutModel) {
|
|
377
|
+
return typeOf(component) === "Stack";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isVBox(component: LayoutModel) {
|
|
381
|
+
return (
|
|
382
|
+
typeOf(component) === "Flexbox" &&
|
|
383
|
+
component.props.style.flexDirection === "column"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isHBox(component: LayoutModel) {
|
|
388
|
+
return (
|
|
389
|
+
typeOf(component) === "Flexbox" &&
|
|
390
|
+
component.props.style.flexDirection === "row"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#hw-drag-canvas {
|
|
2
|
+
visibility: hidden;
|
|
3
|
+
z-index: 1;
|
|
4
|
+
position: absolute;
|
|
5
|
+
top: 0px;
|
|
6
|
+
left: 0;
|
|
7
|
+
right: 0;
|
|
8
|
+
bottom: 0;
|
|
9
|
+
background-color: transparent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#hw-drag-canvas > svg {
|
|
13
|
+
position: absolute;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.drawing #hw-drag-canvas {
|
|
17
|
+
visibility: visible;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
path.drop-target {
|
|
21
|
+
stroke: blue;
|
|
22
|
+
stroke-width: 4px;
|
|
23
|
+
fill: transparent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
path.drop-target.centre {
|
|
27
|
+
stroke: red;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#hw-drop-outline {
|
|
31
|
+
fill: none;
|
|
32
|
+
stroke: red;
|
|
33
|
+
stroke-dasharray: 4 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#hw-drop-guides {
|
|
37
|
+
fill: none;
|
|
38
|
+
stroke: rgba(0, 0, 0, 0.3);
|
|
39
|
+
stroke-dasharray: 2 3;
|
|
40
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PopupService } from "@vuu-ui/ui-controls";
|
|
3
|
+
import { DropMenu, computeMenuPosition } from "./DropMenu";
|
|
4
|
+
import { RelativeDropPosition } from "./BoxModel";
|
|
5
|
+
|
|
6
|
+
import "./DropTargetRenderer.css";
|
|
7
|
+
import { DropTarget, GuideLine } from "./DropTarget";
|
|
8
|
+
import { DragDropRect } from "./dragDropTypes";
|
|
9
|
+
import { DragState } from "./DragState";
|
|
10
|
+
|
|
11
|
+
type Point = [number, number];
|
|
12
|
+
type TabMode = "full-view" | "tab-only";
|
|
13
|
+
|
|
14
|
+
let _multiDropOptions = false;
|
|
15
|
+
let _hoverDropTarget: DropTarget | null = null;
|
|
16
|
+
let _shiftedTab: HTMLElement | null = null;
|
|
17
|
+
|
|
18
|
+
const onHoverDropTarget = (dropTarget: DropTarget | null) =>
|
|
19
|
+
(_hoverDropTarget = dropTarget);
|
|
20
|
+
|
|
21
|
+
const start = ([x, y]: Point) => `M${x},${y}`;
|
|
22
|
+
const point = ([x, y]: Point) => `L${x},${y}`;
|
|
23
|
+
const pathFromPoints = ([p1, ...points]: Point[]) =>
|
|
24
|
+
`${start(p1)} ${points.map(point)}Z`;
|
|
25
|
+
|
|
26
|
+
const pathFromGuideLines = (guideLines?: GuideLine) => {
|
|
27
|
+
if (guideLines) {
|
|
28
|
+
const [x1, y1, x2, y2, x3, y3, x4, y4] = guideLines;
|
|
29
|
+
return `M${x1},${y1} L${x2},${y2} M${x3},${y3} L${x4},${y4}`;
|
|
30
|
+
} else {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function insertSVGRoot() {
|
|
36
|
+
if (document.getElementById("hw-drag-canvas") === null) {
|
|
37
|
+
const root = document.getElementById("root");
|
|
38
|
+
const container = document.createElement("div");
|
|
39
|
+
container.id = "hw-drag-canvas";
|
|
40
|
+
container.innerHTML = `
|
|
41
|
+
<svg width="100%" height="100%">
|
|
42
|
+
<path id="hw-drop-guides" />
|
|
43
|
+
<path
|
|
44
|
+
id="hw-drop-outline"
|
|
45
|
+
d="M300,132 L380,132 L380,100 L460,100 L460,132, L550,132 L550,350 L300,350z">
|
|
46
|
+
<animate
|
|
47
|
+
attributeName="d"
|
|
48
|
+
id="hw-drop-outline-animate"
|
|
49
|
+
begin="indefinite"
|
|
50
|
+
dur="300ms"
|
|
51
|
+
fill="freeze"
|
|
52
|
+
to="M255,33 L255,33,L255,1,L315,1,L315,1,L794,1,L794,164,L255,164Z"
|
|
53
|
+
/>
|
|
54
|
+
</path>
|
|
55
|
+
</svg>
|
|
56
|
+
`;
|
|
57
|
+
document.body.insertBefore(container, root);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export default class DropTargetCanvas {
|
|
61
|
+
private currentPath: string | null = null;
|
|
62
|
+
private tabMode: TabMode | null = null;
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
insertSVGRoot();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
prepare(dragRect: DragDropRect, tabMode: TabMode = "full-view") {
|
|
69
|
+
// don't do this on body
|
|
70
|
+
document.body.classList.add("drawing");
|
|
71
|
+
this.currentPath = null;
|
|
72
|
+
this.tabMode = tabMode;
|
|
73
|
+
|
|
74
|
+
const { top, left, right, bottom } = dragRect;
|
|
75
|
+
const width = right - left;
|
|
76
|
+
const height = bottom - top;
|
|
77
|
+
|
|
78
|
+
const points = this.getPoints(0, 0, 0, 0);
|
|
79
|
+
// const points = this.getPoints(left, top, width, height);
|
|
80
|
+
const d = pathFromPoints(points);
|
|
81
|
+
|
|
82
|
+
const dropOutlinePath = document.getElementById("hw-drop-outline");
|
|
83
|
+
dropOutlinePath?.setAttribute("d", d);
|
|
84
|
+
this.currentPath = d;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
clear() {
|
|
88
|
+
// don't do this on body
|
|
89
|
+
_hoverDropTarget = null;
|
|
90
|
+
clearShiftedTab();
|
|
91
|
+
document.body.classList.remove("drawing");
|
|
92
|
+
PopupService.hidePopup();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get hoverDropTarget() {
|
|
96
|
+
return _hoverDropTarget;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getPoints(
|
|
100
|
+
x: number,
|
|
101
|
+
y: number,
|
|
102
|
+
width: number,
|
|
103
|
+
height: number,
|
|
104
|
+
tabLeft = 0,
|
|
105
|
+
tabWidth = 0,
|
|
106
|
+
tabHeight = 0
|
|
107
|
+
): Point[] {
|
|
108
|
+
const tabOnly = this.tabMode === "tab-only";
|
|
109
|
+
if (tabWidth === 0) {
|
|
110
|
+
return [
|
|
111
|
+
[x, y + tabHeight],
|
|
112
|
+
[x, y + tabHeight],
|
|
113
|
+
[x, y],
|
|
114
|
+
[x + tabWidth, y],
|
|
115
|
+
[x + tabWidth, y],
|
|
116
|
+
[x + width, y],
|
|
117
|
+
[x + width, y + height],
|
|
118
|
+
[x, y + height],
|
|
119
|
+
];
|
|
120
|
+
} else if (tabOnly) {
|
|
121
|
+
const left = tabLeft;
|
|
122
|
+
return [
|
|
123
|
+
[left, y],
|
|
124
|
+
[left, y],
|
|
125
|
+
[left + tabWidth, y],
|
|
126
|
+
[left + tabWidth, y],
|
|
127
|
+
[left + tabWidth, y + tabHeight],
|
|
128
|
+
[left + tabWidth, y + tabHeight],
|
|
129
|
+
[left, y + tabHeight],
|
|
130
|
+
[left, y + tabHeight],
|
|
131
|
+
];
|
|
132
|
+
} else if (tabLeft === 0) {
|
|
133
|
+
return [
|
|
134
|
+
[x, y + tabHeight],
|
|
135
|
+
[x, y + tabHeight],
|
|
136
|
+
[x, y],
|
|
137
|
+
[x + tabWidth, y],
|
|
138
|
+
[x + tabWidth, y + tabHeight],
|
|
139
|
+
[x + width, y + tabHeight],
|
|
140
|
+
[x + width, y + height],
|
|
141
|
+
[x, y + height],
|
|
142
|
+
];
|
|
143
|
+
} else {
|
|
144
|
+
return [
|
|
145
|
+
[x, y + tabHeight],
|
|
146
|
+
[x + tabLeft, y + tabHeight],
|
|
147
|
+
[x + tabLeft, y],
|
|
148
|
+
[x + tabLeft, y],
|
|
149
|
+
[x + tabLeft, y + tabHeight],
|
|
150
|
+
[x + width, y + tabHeight],
|
|
151
|
+
[x + width, y + height],
|
|
152
|
+
[x, y + height],
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
draw(dropTarget: DropTarget, dragState: DragState) {
|
|
158
|
+
const sameDropTarget = false;
|
|
159
|
+
const wasMultiDrop = _multiDropOptions;
|
|
160
|
+
|
|
161
|
+
if (_hoverDropTarget !== null) {
|
|
162
|
+
this.drawTarget(_hoverDropTarget);
|
|
163
|
+
} else {
|
|
164
|
+
if (sameDropTarget === false) {
|
|
165
|
+
_multiDropOptions = dropTarget.nextDropTarget != null;
|
|
166
|
+
if (dropTarget.pos.tab) {
|
|
167
|
+
moveExistingTabs(dropTarget);
|
|
168
|
+
} else if (_shiftedTab) {
|
|
169
|
+
clearShiftedTab();
|
|
170
|
+
}
|
|
171
|
+
this.drawTarget(dropTarget, dragState);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (_multiDropOptions) {
|
|
175
|
+
const [left, top, orientation] = computeMenuPosition(dropTarget);
|
|
176
|
+
if (!wasMultiDrop || !sameDropTarget) {
|
|
177
|
+
const component = (
|
|
178
|
+
<DropMenu
|
|
179
|
+
dropTarget={dropTarget}
|
|
180
|
+
onHover={onHoverDropTarget}
|
|
181
|
+
orientation={orientation}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
PopupService.showPopup({
|
|
185
|
+
left,
|
|
186
|
+
top,
|
|
187
|
+
component,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
PopupService.movePopupTo(left, top);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
PopupService.hidePopup();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
drawTarget(dropTarget: DropTarget, dragState?: DragState) {
|
|
199
|
+
const lineWidth = 6;
|
|
200
|
+
|
|
201
|
+
const targetDropOutline = dropTarget.getTargetDropOutline(
|
|
202
|
+
lineWidth,
|
|
203
|
+
dragState
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (targetDropOutline) {
|
|
207
|
+
const { l, t, r, b, tabLeft, tabWidth, tabHeight, guideLines } =
|
|
208
|
+
targetDropOutline;
|
|
209
|
+
const w = r - l;
|
|
210
|
+
const h = b - t;
|
|
211
|
+
|
|
212
|
+
if (this.currentPath) {
|
|
213
|
+
const path = document.getElementById("hw-drop-outline");
|
|
214
|
+
path?.setAttribute("d", this.currentPath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const points = this.getPoints(l, t, w, h, tabLeft, tabWidth, tabHeight);
|
|
218
|
+
const path = pathFromPoints(points);
|
|
219
|
+
const animation = document.getElementById(
|
|
220
|
+
"hw-drop-outline-animate"
|
|
221
|
+
) as unknown as SVGAnimateElement;
|
|
222
|
+
animation?.setAttribute("to", path);
|
|
223
|
+
animation?.beginElement();
|
|
224
|
+
this.currentPath = path;
|
|
225
|
+
|
|
226
|
+
const dropGuidePath = document.getElementById("hw-drop-guides");
|
|
227
|
+
dropGuidePath?.setAttribute("d", pathFromGuideLines(guideLines));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const cssShiftRight = "transition:margin-left .4s ease-out;margin-left: 63px";
|
|
233
|
+
const cssShiftBack = "transition:margin-left .4s ease-out;margin-left: 0px";
|
|
234
|
+
|
|
235
|
+
function moveExistingTabs(dropTarget: DropTarget) {
|
|
236
|
+
const { AFTER, BEFORE } = RelativeDropPosition;
|
|
237
|
+
const {
|
|
238
|
+
clientRect: { Stack },
|
|
239
|
+
pos: {
|
|
240
|
+
// tab: { index: tabIndex, positionRelativeToTab }
|
|
241
|
+
tab,
|
|
242
|
+
},
|
|
243
|
+
} = dropTarget;
|
|
244
|
+
|
|
245
|
+
const { id } = dropTarget.component.props;
|
|
246
|
+
let tabEl = null;
|
|
247
|
+
// console.log(`tabPos = ${tabPos} (width=${tabWidth}) x=${x}`)
|
|
248
|
+
if (Stack && tab && tab.positionRelativeToTab !== AFTER) {
|
|
249
|
+
const tabOffset = tab.positionRelativeToTab === BEFORE ? 1 : 2;
|
|
250
|
+
const selector = `:scope .hwTabstrip > .hwTabstrip-inner > .hwTab:nth-child(${
|
|
251
|
+
tab.index + tabOffset
|
|
252
|
+
})`;
|
|
253
|
+
tabEl = document.getElementById(id)?.querySelector(selector) as HTMLElement;
|
|
254
|
+
if (tabEl) {
|
|
255
|
+
if (_shiftedTab === null || _shiftedTab !== tabEl) {
|
|
256
|
+
tabEl.style.cssText = cssShiftRight;
|
|
257
|
+
if (_shiftedTab) {
|
|
258
|
+
_shiftedTab.style.cssText = cssShiftBack;
|
|
259
|
+
}
|
|
260
|
+
_shiftedTab = tabEl;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
clearShiftedTab();
|
|
264
|
+
}
|
|
265
|
+
} else if (tab?.positionRelativeToTab === BEFORE) {
|
|
266
|
+
if (_shiftedTab === null) {
|
|
267
|
+
const selector = ".vuuHeader-title";
|
|
268
|
+
tabEl = document
|
|
269
|
+
.getElementById(id)
|
|
270
|
+
?.querySelector(selector) as HTMLElement;
|
|
271
|
+
tabEl.style.cssText = cssShiftRight;
|
|
272
|
+
_shiftedTab = tabEl;
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
clearShiftedTab();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function clearShiftedTab() {
|
|
280
|
+
if (_shiftedTab) {
|
|
281
|
+
_shiftedTab.style.cssText = cssShiftBack;
|
|
282
|
+
_shiftedTab = null;
|
|
283
|
+
}
|
|
284
|
+
}
|