@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,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
+ }