@vuu-ui/vuu-layout 0.5.14 → 0.5.15

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 (114) hide show
  1. package/package.json +10 -13
  2. package/src/Component.css +0 -0
  3. package/src/Component.tsx +20 -0
  4. package/src/DraggableLayout.css +18 -0
  5. package/src/DraggableLayout.tsx +26 -0
  6. package/src/__tests__/flexbox-utils.spec.js +90 -0
  7. package/src/chest-of-drawers/Chest.css +36 -0
  8. package/src/chest-of-drawers/Chest.tsx +42 -0
  9. package/src/chest-of-drawers/Drawer.css +159 -0
  10. package/src/chest-of-drawers/Drawer.tsx +118 -0
  11. package/src/chest-of-drawers/index.ts +2 -0
  12. package/src/common-types.ts +9 -0
  13. package/src/debug.ts +16 -0
  14. package/src/drag-drop/BoxModel.ts +551 -0
  15. package/src/drag-drop/DragState.ts +219 -0
  16. package/src/drag-drop/Draggable.ts +282 -0
  17. package/src/drag-drop/DropMenu.css +71 -0
  18. package/src/drag-drop/DropMenu.tsx +61 -0
  19. package/src/drag-drop/DropTarget.ts +393 -0
  20. package/src/drag-drop/DropTargetRenderer.css +40 -0
  21. package/src/drag-drop/DropTargetRenderer.tsx +277 -0
  22. package/src/drag-drop/dragDropTypes.ts +47 -0
  23. package/src/drag-drop/index.ts +5 -0
  24. package/src/editable-label/EditableLabel.css +28 -0
  25. package/src/editable-label/EditableLabel.tsx +99 -0
  26. package/src/editable-label/index.ts +1 -0
  27. package/src/flexbox/Flexbox.css +45 -0
  28. package/src/flexbox/Flexbox.tsx +70 -0
  29. package/src/flexbox/FlexboxLayout.tsx +28 -0
  30. package/src/flexbox/FluidGrid.css +134 -0
  31. package/src/flexbox/FluidGrid.tsx +82 -0
  32. package/src/flexbox/FluidGridLayout.tsx +9 -0
  33. package/src/flexbox/Splitter.css +140 -0
  34. package/src/flexbox/Splitter.tsx +127 -0
  35. package/src/flexbox/flexbox-utils.ts +128 -0
  36. package/src/flexbox/flexboxTypes.ts +68 -0
  37. package/src/flexbox/index.ts +5 -0
  38. package/src/flexbox/useResponsiveSizing.ts +82 -0
  39. package/src/flexbox/useSplitterResizing.ts +270 -0
  40. package/src/index.ts +19 -0
  41. package/src/layout-action.ts +21 -0
  42. package/src/layout-header/ActionButton.tsx +23 -0
  43. package/src/layout-header/Header.css +8 -0
  44. package/src/layout-header/Header.tsx +216 -0
  45. package/src/layout-header/index.ts +1 -0
  46. package/src/layout-provider/LayoutProvider.tsx +161 -0
  47. package/src/layout-provider/LayoutProviderContext.ts +17 -0
  48. package/src/layout-provider/index.ts +3 -0
  49. package/src/layout-provider/useLayoutDragDrop.ts +210 -0
  50. package/src/layout-reducer/flexUtils.ts +276 -0
  51. package/src/layout-reducer/index.ts +5 -0
  52. package/src/layout-reducer/insert-layout-element.ts +365 -0
  53. package/src/layout-reducer/layout-reducer.ts +237 -0
  54. package/src/layout-reducer/layoutTypes.ts +159 -0
  55. package/src/layout-reducer/layoutUtils.ts +288 -0
  56. package/src/layout-reducer/remove-layout-element.ts +226 -0
  57. package/src/layout-reducer/replace-layout-element.ts +113 -0
  58. package/src/layout-reducer/resize-flex-children.ts +55 -0
  59. package/src/layout-reducer/wrap-layout-element.ts +307 -0
  60. package/src/layout-view/View.css +61 -0
  61. package/src/layout-view/View.tsx +143 -0
  62. package/src/layout-view/ViewContext.ts +30 -0
  63. package/src/layout-view/index.ts +5 -0
  64. package/src/layout-view/useView.tsx +104 -0
  65. package/src/layout-view/useViewActionDispatcher.ts +123 -0
  66. package/src/layout-view/useViewResize.ts +53 -0
  67. package/src/layout-view/viewTypes.ts +35 -0
  68. package/src/palette/Palette.css +33 -0
  69. package/src/palette/Palette.tsx +140 -0
  70. package/src/palette/PaletteSalt.css +9 -0
  71. package/src/palette/PaletteSalt.tsx +79 -0
  72. package/src/palette/index.ts +3 -0
  73. package/src/placeholder/Placeholder.css +10 -0
  74. package/src/placeholder/Placeholder.tsx +38 -0
  75. package/src/placeholder/index.ts +1 -0
  76. package/src/registry/ComponentRegistry.ts +44 -0
  77. package/src/registry/index.ts +1 -0
  78. package/src/responsive/breakpoints.ts +62 -0
  79. package/src/responsive/index.ts +3 -0
  80. package/src/responsive/measureMinimumNodeSize.ts +23 -0
  81. package/src/responsive/overflowUtils.js +14 -0
  82. package/src/responsive/use-breakpoints.ts +101 -0
  83. package/src/responsive/useResizeObserver.ts +154 -0
  84. package/src/responsive/utils.ts +37 -0
  85. package/src/stack/Stack.css +39 -0
  86. package/src/stack/Stack.tsx +173 -0
  87. package/src/stack/StackLayout.tsx +119 -0
  88. package/src/stack/index.ts +4 -0
  89. package/src/stack/stackTypes.ts +22 -0
  90. package/src/tabs/TabPanel.css +12 -0
  91. package/src/tabs/TabPanel.tsx +17 -0
  92. package/src/tabs/index.ts +1 -0
  93. package/src/tools/config-wrapper/ConfigWrapper.tsx +55 -0
  94. package/src/tools/config-wrapper/index.ts +1 -0
  95. package/src/tools/devtools-box/layout-configurator.css +112 -0
  96. package/src/tools/devtools-box/layout-configurator.jsx +369 -0
  97. package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
  98. package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
  99. package/src/tools/index.ts +4 -0
  100. package/src/use-persistent-state.ts +112 -0
  101. package/src/utils/index.ts +5 -0
  102. package/src/utils/pathUtils.ts +283 -0
  103. package/src/utils/propUtils.ts +26 -0
  104. package/src/utils/refUtils.ts +16 -0
  105. package/src/utils/styleUtils.ts +13 -0
  106. package/src/utils/typeOf.ts +25 -0
  107. package/tsconfig-emit-types.json +11 -0
  108. package/LICENSE +0 -201
  109. package/cjs/index.js +0 -20
  110. package/cjs/index.js.map +0 -7
  111. package/esm/index.js +0 -20
  112. package/esm/index.js.map +0 -7
  113. package/index.css +0 -2
  114. package/index.css.map +0 -7
@@ -0,0 +1,393 @@
1
+ import { rect, rectTuple } from "../common-types";
2
+ import { LayoutModel } from "../layout-reducer";
3
+ import { getProps, typeOf } from "../utils";
4
+ import {
5
+ BoxModel,
6
+ getPosition,
7
+ Measurements,
8
+ Position,
9
+ positionValues,
10
+ RelativeDropPosition,
11
+ } from "./BoxModel";
12
+ import { DragDropRect, DropPos, DropPosTab } from "./dragDropTypes";
13
+ import { DragState } from "./DragState";
14
+
15
+ export const isTabstrip = (dropTarget: DropTarget) =>
16
+ dropTarget.pos.tab &&
17
+ typeOf(dropTarget.component) === "Stack" &&
18
+ dropTarget.pos.position.Header;
19
+
20
+ const { north, south, east, west } = positionValues;
21
+ const eastwest = east + west;
22
+ const northsouth = north + south;
23
+
24
+ export interface DropTargetProps {
25
+ component: LayoutModel;
26
+ pos: DropPos;
27
+ clientRect: DragDropRect;
28
+ nextDropTarget: DropTarget | null;
29
+ }
30
+
31
+ export type GuideLine = [
32
+ number,
33
+ number,
34
+ number,
35
+ number,
36
+ number,
37
+ number,
38
+ number,
39
+ number
40
+ ];
41
+ export interface TargetDropOutline {
42
+ l: number;
43
+ r: number;
44
+ t: number;
45
+ b: number;
46
+ tabLeft?: number;
47
+ tabWidth?: number;
48
+ tabHeight?: number;
49
+ guideLines?: GuideLine;
50
+ }
51
+
52
+ export class DropTarget {
53
+ private active: boolean;
54
+
55
+ public box: unknown;
56
+ public clientRect: DragDropRect;
57
+ public component: LayoutModel;
58
+ public dropRect: rectTuple | undefined;
59
+ public nextDropTarget: DropTarget | null;
60
+ public pos: DropPos;
61
+
62
+ constructor({
63
+ component,
64
+ pos,
65
+ clientRect /*, closeToTheEdge*/,
66
+ nextDropTarget,
67
+ }: DropTargetProps) {
68
+ this.component = component;
69
+ this.pos = pos;
70
+ this.clientRect = clientRect;
71
+ this.nextDropTarget = nextDropTarget;
72
+ this.active = false;
73
+ this.dropRect = undefined;
74
+ }
75
+
76
+ targetTabPos(tab: DropPosTab) {
77
+ const { left: tabLeft, width: tabWidth, positionRelativeToTab } = tab;
78
+ return positionRelativeToTab === RelativeDropPosition.BEFORE
79
+ ? tabLeft
80
+ : tabLeft + tabWidth;
81
+ }
82
+
83
+ /**
84
+ * Determine what will be rendered by the dropTargetRenderer
85
+ *
86
+ * @param {*} lineWidth
87
+ * @param {*} dragState
88
+ * @returns {l, t, r, b, tabLeft, tabWidth, tabHeight}
89
+ */
90
+ getTargetDropOutline(
91
+ lineWidth: number,
92
+ dragState?: DragState
93
+ ): TargetDropOutline {
94
+ if (this.pos.tab) {
95
+ return this.getDropTabOutline(lineWidth, this.pos.tab);
96
+ } else if (dragState && dragState.hasIntrinsicSize()) {
97
+ return this.getIntrinsicDropRect(dragState);
98
+ } else {
99
+ const [l, t, r, b] = this.getDropRectOutline(
100
+ lineWidth,
101
+ dragState
102
+ ) as rectTuple;
103
+ return { l, t, r, b };
104
+ }
105
+ }
106
+
107
+ getDropTabOutline(lineWidth: number, tab: DropPosTab): TargetDropOutline {
108
+ const {
109
+ clientRect: { top, left, right, bottom, header },
110
+ } = this;
111
+
112
+ const inset = 0;
113
+ const gap = Math.round(lineWidth / 2) + inset;
114
+
115
+ const t = Math.round(top);
116
+ const l = Math.round(left + gap);
117
+ const r = Math.round(right - gap);
118
+ const b = Math.round(bottom - gap);
119
+ const tabLeft = this.targetTabPos(tab);
120
+ const tabWidth = 60; // should really measure text
121
+ const tabHeight = (header?.bottom ?? 0) - (header?.top ?? 0);
122
+ return { l, t, r, b, tabLeft, tabWidth, tabHeight };
123
+ }
124
+
125
+ getIntrinsicDropRect(dragState: DragState): TargetDropOutline {
126
+ const { pos, clientRect: rect } = this;
127
+
128
+ const { x, y } = dragState;
129
+
130
+ let height = dragState.intrinsicSize?.height ?? 0;
131
+ let width = dragState.intrinsicSize?.height ?? 0;
132
+
133
+ if (height && height > rect.height) {
134
+ console.log(`DropTarget: we're going to blow the gaff`);
135
+ height = rect.height;
136
+ } else if (width && width > rect.width) {
137
+ console.log(`DropTarget: we're going to blow the gaff`);
138
+ width = rect.width;
139
+ }
140
+
141
+ const left = Math.min(
142
+ rect.right - width,
143
+ Math.max(rect.left, Math.round(pos.x - x.mousePct * width))
144
+ );
145
+ const top = Math.min(
146
+ rect.bottom - height,
147
+ Math.max(rect.top, Math.round(pos.y - y.mousePct * height))
148
+ );
149
+ const [l, t, r, b] = (this.dropRect = [
150
+ left,
151
+ top,
152
+ left + width,
153
+ top + height,
154
+ ]);
155
+
156
+ const guideLines: GuideLine = pos.position.EastOrWest
157
+ ? [l, rect.top, l, rect.bottom, r, rect.top, r, rect.bottom]
158
+ : [rect.left, t, rect.right, t, rect.left, b, rect.right, b];
159
+
160
+ return { l, r, t, b, guideLines };
161
+ }
162
+
163
+ /**
164
+ * @returns [left, top, right, bottom]
165
+ */
166
+ getDropRectOutline(lineWidth: number, dragState?: DragState) {
167
+ const { pos, clientRect: rect } = this;
168
+ const { width: suggestedWidth, height: suggestedHeight, position } = pos;
169
+
170
+ const { width: intrinsicWidth, height: intrinsicHeight } =
171
+ dragState?.intrinsicSize ?? {};
172
+ const sizeHeight = intrinsicHeight ?? suggestedHeight ?? 0;
173
+ const sizeWidth = intrinsicWidth ?? suggestedWidth ?? 0;
174
+
175
+ this.dropRect = undefined;
176
+
177
+ const { top: t, left: l, right: r, bottom: b } = rect;
178
+
179
+ const inset = 0;
180
+ const gap = Math.round(lineWidth / 2) + inset;
181
+
182
+ switch (position) {
183
+ case Position.North:
184
+ case Position.Header: {
185
+ const halfHeight = Math.round((b - t) / 2);
186
+ const height = sizeHeight
187
+ ? Math.min(halfHeight, Math.round(sizeHeight))
188
+ : halfHeight;
189
+ return sizeWidth && l + sizeWidth < r
190
+ ? [l + gap, t + gap, l + sizeWidth - gap, t + gap + height] // need flex direction indicator
191
+ : [l + gap, t + gap, r - gap, t + gap + height];
192
+ }
193
+ case Position.West: {
194
+ const halfWidth = Math.round((r - l) / 2);
195
+ const width = sizeWidth
196
+ ? Math.min(halfWidth, Math.round(sizeWidth))
197
+ : halfWidth;
198
+ return sizeHeight && t + sizeHeight < b
199
+ ? [l + gap, t + gap, l - gap + width, t + sizeHeight + gap] // need flex direction indicator
200
+ : [l + gap, t + gap, l - gap + width, b - gap];
201
+ }
202
+ case Position.East: {
203
+ const halfWidth = Math.round((r - l) / 2);
204
+ const width = sizeWidth
205
+ ? Math.min(halfWidth, Math.round(sizeWidth))
206
+ : halfWidth;
207
+ return sizeHeight && t + sizeHeight < b
208
+ ? [r - gap - width, t + gap, r - gap, t + sizeHeight + gap] // need flex direction indicator
209
+ : [r - gap - width, t + gap, r - gap, b - gap];
210
+ }
211
+ case Position.South: {
212
+ const halfHeight = Math.round((b - t) / 2);
213
+ const height = sizeHeight
214
+ ? Math.min(halfHeight, Math.round(sizeHeight))
215
+ : halfHeight;
216
+
217
+ return sizeWidth && l + sizeWidth < r
218
+ ? [l + gap, b - gap - height, l + sizeWidth - gap, b - gap] // need flex direction indicator
219
+ : [l + gap, b - gap - height, r - gap, b - gap];
220
+ }
221
+ case Position.Centre: {
222
+ return [l + gap, t + gap, r - gap, b - gap];
223
+ }
224
+ default:
225
+ console.warn(`DropTarget does not recognize position ${position}`);
226
+ return null;
227
+ }
228
+ }
229
+
230
+ activate() {
231
+ this.active = true;
232
+ return this;
233
+ }
234
+
235
+ toArray(this: DropTarget) {
236
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
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
+ const 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
+ const 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
+
321
+ return dropTarget;
322
+ }
323
+
324
+ function getTargetPosition(
325
+ container: LayoutModel,
326
+ { closeToTheEdge, position }: DropPos,
327
+ box: rect,
328
+ measurements: Measurements,
329
+ x: number,
330
+ y: number
331
+ ): [DropPos, DragDropRect] | undefined {
332
+ if (!container || container.type === "DraggableLayout") {
333
+ return;
334
+ }
335
+
336
+ const containingBox = measurements[container.props.path];
337
+ const closeToTop = closeToTheEdge & positionValues.north;
338
+ const closeToRight = closeToTheEdge & positionValues.east;
339
+ const closeToBottom = closeToTheEdge & positionValues.south;
340
+ const closeToLeft = closeToTheEdge & positionValues.west;
341
+
342
+ const atTop =
343
+ (closeToTop || position.Header) &&
344
+ Math.round(box.top) === Math.round(containingBox.top);
345
+ const atRight =
346
+ closeToRight && Math.round(box.right) === Math.round(containingBox.right);
347
+ const atBottom =
348
+ closeToBottom &&
349
+ Math.round(box.bottom) === Math.round(containingBox.bottom);
350
+ const atLeft =
351
+ closeToLeft && Math.round(box.left) === Math.round(containingBox.left);
352
+
353
+ if (atTop || atRight || atBottom || atLeft) {
354
+ const { "data-path": dataPath, path = dataPath } = container.props;
355
+ const clientRect = measurements[path];
356
+ const containerPos = getPosition(x, y, clientRect);
357
+
358
+ // if its a VBox and we're close to left or right ...
359
+ if (
360
+ (isVBox(container) || isTabbedContainer(container)) &&
361
+ closeToTheEdge & eastwest
362
+ ) {
363
+ containerPos.width = 120;
364
+ return [containerPos, clientRect];
365
+ }
366
+ // if it's a HBox and we're close to top or bottom ...
367
+ else if (
368
+ (isHBox(container) || isTabbedContainer(container)) &&
369
+ (position.Header || closeToTheEdge & northsouth)
370
+ ) {
371
+ containerPos.height = 120;
372
+ return [containerPos, clientRect];
373
+ }
374
+ }
375
+ }
376
+
377
+ function isTabbedContainer(component: LayoutModel) {
378
+ return typeOf(component) === "Stack";
379
+ }
380
+
381
+ function isVBox(component: LayoutModel) {
382
+ return (
383
+ typeOf(component) === "Flexbox" &&
384
+ component.props.style.flexDirection === "column"
385
+ );
386
+ }
387
+
388
+ function isHBox(component: LayoutModel) {
389
+ return (
390
+ typeOf(component) === "Flexbox" &&
391
+ component.props.style.flexDirection === "row"
392
+ );
393
+ }
@@ -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
+ #vuu-drop-outline {
31
+ fill: rgba(0,0,255,.3);
32
+ stroke: none;
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,277 @@
1
+ import { PopupService } from "../../../vuu-popups/src/popup";
2
+ import { RelativeDropPosition } from "./BoxModel";
3
+ import { DragDropRect } from "./dragDropTypes";
4
+ import { DragState } from "./DragState";
5
+ import { computeMenuPosition, DropMenu } from "./DropMenu";
6
+ import { DropTarget, GuideLine } from "./DropTarget";
7
+
8
+ import "./DropTargetRenderer.css";
9
+
10
+ type Point = [number, number];
11
+ type TabMode = "full-view" | "tab-only";
12
+
13
+ let _multiDropOptions = false;
14
+ let _hoverDropTarget: DropTarget | null = null;
15
+ let _shiftedTab: HTMLElement | null = null;
16
+
17
+ const onHoverDropTarget = (dropTarget: DropTarget | null) =>
18
+ (_hoverDropTarget = dropTarget);
19
+
20
+ const start = ([x, y]: Point) => `M${x},${y}`;
21
+ const point = ([x, y]: Point) => `L${x},${y}`;
22
+ const pathFromPoints = ([p1, ...points]: Point[]) =>
23
+ `${start(p1)} ${points.map(point)}Z`;
24
+
25
+ const pathFromGuideLines = (guideLines?: GuideLine) => {
26
+ if (guideLines) {
27
+ const [x1, y1, x2, y2, x3, y3, x4, y4] = guideLines;
28
+ return `M${x1},${y1} L${x2},${y2} M${x3},${y3} L${x4},${y4}`;
29
+ } else {
30
+ return "";
31
+ }
32
+ };
33
+
34
+ function insertSVGRoot() {
35
+ if (document.getElementById("hw-drag-canvas") === null) {
36
+ const root = document.getElementById("root");
37
+ const container = document.createElement("div");
38
+ container.id = "hw-drag-canvas";
39
+ container.innerHTML = `
40
+ <svg width="100%" height="100%">
41
+ <path id="hw-drop-guides" />
42
+ <path
43
+ id="vuu-drop-outline"
44
+ d="M300,132 L380,132 L380,100 L460,100 L460,132, L550,132 L550,350 L300,350z">
45
+ <animate
46
+ attributeName="d"
47
+ id="hw-drop-outline-animate"
48
+ begin="indefinite"
49
+ dur="300ms"
50
+ fill="freeze"
51
+ to="M255,33 L255,33,L255,1,L315,1,L315,1,L794,1,L794,164,L255,164Z"
52
+ />
53
+ </path>
54
+ </svg>
55
+ `;
56
+ document.body.insertBefore(container, root);
57
+ }
58
+ }
59
+ export default class DropTargetCanvas {
60
+ private currentPath: string | null = null;
61
+ private tabMode: TabMode | null = null;
62
+
63
+ constructor() {
64
+ insertSVGRoot();
65
+ }
66
+
67
+ prepare(dragRect: DragDropRect, tabMode: TabMode = "full-view") {
68
+ // don't do this on body
69
+ document.body.classList.add("drawing");
70
+ this.currentPath = null;
71
+ this.tabMode = tabMode;
72
+
73
+ const points = this.getPoints(0, 0, 0, 0);
74
+ // const points = this.getPoints(left, top, width, height);
75
+ const d = pathFromPoints(points);
76
+
77
+ const dropOutlinePath = document.getElementById("vuu-drop-outline");
78
+ dropOutlinePath?.setAttribute("d", d);
79
+ this.currentPath = d;
80
+ }
81
+
82
+ clear() {
83
+ // don't do this on body
84
+ _hoverDropTarget = null;
85
+ clearShiftedTab();
86
+ document.body.classList.remove("drawing");
87
+ PopupService.hidePopup();
88
+ }
89
+
90
+ get hoverDropTarget() {
91
+ return _hoverDropTarget;
92
+ }
93
+
94
+ getPoints(
95
+ x: number,
96
+ y: number,
97
+ width: number,
98
+ height: number,
99
+ tabLeft = 0,
100
+ tabWidth = 0,
101
+ tabHeight = 0
102
+ ): Point[] {
103
+ const tabOnly = this.tabMode === "tab-only";
104
+ if (tabWidth === 0) {
105
+ return [
106
+ [x, y + tabHeight],
107
+ [x, y + tabHeight],
108
+ [x, y],
109
+ [x + tabWidth, y],
110
+ [x + tabWidth, y],
111
+ [x + width, y],
112
+ [x + width, y + height],
113
+ [x, y + height],
114
+ ];
115
+ } else if (tabOnly) {
116
+ const left = tabLeft;
117
+ return [
118
+ [left, y],
119
+ [left, y],
120
+ [left + tabWidth, y],
121
+ [left + tabWidth, y],
122
+ [left + tabWidth, y + tabHeight],
123
+ [left + tabWidth, y + tabHeight],
124
+ [left, y + tabHeight],
125
+ [left, y + tabHeight],
126
+ ];
127
+ } else if (tabLeft === 0) {
128
+ return [
129
+ [x, y + tabHeight],
130
+ [x, y + tabHeight],
131
+ [x, y],
132
+ [x + tabWidth, y],
133
+ [x + tabWidth, y + tabHeight],
134
+ [x + width, y + tabHeight],
135
+ [x + width, y + height],
136
+ [x, y + height],
137
+ ];
138
+ } else {
139
+ return [
140
+ [x, y + tabHeight],
141
+ [x + tabLeft, y + tabHeight],
142
+ [x + tabLeft, y],
143
+ [x + tabLeft, y],
144
+ [x + tabLeft, y + tabHeight],
145
+ [x + width, y + tabHeight],
146
+ [x + width, y + height],
147
+ [x, y + height],
148
+ ];
149
+ }
150
+ }
151
+
152
+ draw(dropTarget: DropTarget, dragState: DragState) {
153
+ const sameDropTarget = false;
154
+ const wasMultiDrop = _multiDropOptions;
155
+
156
+ if (_hoverDropTarget !== null) {
157
+ this.drawTarget(_hoverDropTarget);
158
+ } else {
159
+ if (sameDropTarget === false) {
160
+ _multiDropOptions = dropTarget.nextDropTarget != null;
161
+ if (dropTarget.pos.tab) {
162
+ moveExistingTabs(dropTarget);
163
+ } else if (_shiftedTab) {
164
+ clearShiftedTab();
165
+ }
166
+ this.drawTarget(dropTarget, dragState);
167
+ }
168
+
169
+ if (_multiDropOptions) {
170
+ const [left, top, orientation] = computeMenuPosition(dropTarget);
171
+ if (!wasMultiDrop || !sameDropTarget) {
172
+ const component = (
173
+ <DropMenu
174
+ dropTarget={dropTarget}
175
+ onHover={onHoverDropTarget}
176
+ orientation={orientation}
177
+ />
178
+ );
179
+ PopupService.showPopup({
180
+ left,
181
+ top,
182
+ component,
183
+ });
184
+ }
185
+ } else {
186
+ PopupService.hidePopup();
187
+ }
188
+ }
189
+ }
190
+
191
+ drawTarget(dropTarget: DropTarget, dragState?: DragState) {
192
+ const lineWidth = 6;
193
+
194
+ const targetDropOutline = dropTarget.getTargetDropOutline(
195
+ lineWidth,
196
+ dragState
197
+ );
198
+
199
+ if (targetDropOutline) {
200
+ const { l, t, r, b, tabLeft, tabWidth, tabHeight, guideLines } =
201
+ targetDropOutline;
202
+ const w = r - l;
203
+ const h = b - t;
204
+
205
+ if (this.currentPath) {
206
+ const path = document.getElementById("vuu-drop-outline");
207
+ path?.setAttribute("d", this.currentPath);
208
+ }
209
+
210
+ const points = this.getPoints(l, t, w, h, tabLeft, tabWidth, tabHeight);
211
+ const path = pathFromPoints(points);
212
+ const animation = document.getElementById(
213
+ "hw-drop-outline-animate"
214
+ ) as unknown as SVGAnimateElement;
215
+ animation?.setAttribute("to", path);
216
+ animation?.beginElement();
217
+ this.currentPath = path;
218
+
219
+ const dropGuidePath = document.getElementById("hw-drop-guides");
220
+ dropGuidePath?.setAttribute("d", pathFromGuideLines(guideLines));
221
+ }
222
+ }
223
+ }
224
+
225
+ const cssShiftRight = "transition:margin-left .4s ease-out;margin-left: 63px";
226
+ const cssShiftBack = "transition:margin-left .4s ease-out;margin-left: 0px";
227
+
228
+ function moveExistingTabs(dropTarget: DropTarget) {
229
+ const { AFTER, BEFORE } = RelativeDropPosition;
230
+ const {
231
+ clientRect: { Stack },
232
+ pos: {
233
+ // tab: { index: tabIndex, positionRelativeToTab }
234
+ tab,
235
+ },
236
+ } = dropTarget;
237
+
238
+ const { id } = dropTarget.component.props;
239
+ let tabEl = null;
240
+ // console.log(`tabPos = ${tabPos} (width=${tabWidth}) x=${x}`)
241
+ if (Stack && tab && tab.positionRelativeToTab !== AFTER) {
242
+ const tabOffset = tab.positionRelativeToTab === BEFORE ? 1 : 2;
243
+ const selector = `:scope .hwTabstrip > .hwTabstrip-inner > .hwTab:nth-child(${
244
+ tab.index + tabOffset
245
+ })`;
246
+ tabEl = document.getElementById(id)?.querySelector(selector) as HTMLElement;
247
+ if (tabEl) {
248
+ if (_shiftedTab === null || _shiftedTab !== tabEl) {
249
+ tabEl.style.cssText = cssShiftRight;
250
+ if (_shiftedTab) {
251
+ _shiftedTab.style.cssText = cssShiftBack;
252
+ }
253
+ _shiftedTab = tabEl;
254
+ }
255
+ } else {
256
+ clearShiftedTab();
257
+ }
258
+ } else if (tab?.positionRelativeToTab === BEFORE) {
259
+ if (_shiftedTab === null) {
260
+ const selector = ".vuuHeader-title";
261
+ tabEl = document
262
+ .getElementById(id)
263
+ ?.querySelector(selector) as HTMLElement;
264
+ tabEl.style.cssText = cssShiftRight;
265
+ _shiftedTab = tabEl;
266
+ }
267
+ } else {
268
+ clearShiftedTab();
269
+ }
270
+ }
271
+
272
+ function clearShiftedTab() {
273
+ if (_shiftedTab) {
274
+ _shiftedTab.style.cssText = cssShiftBack;
275
+ _shiftedTab = null;
276
+ }
277
+ }