flexlayout-react 0.7.14 → 0.8.0

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 (252) hide show
  1. package/ChangeLog.txt +26 -0
  2. package/README.md +157 -330
  3. package/Screenshot_light.png +0 -0
  4. package/Screenshot_rounded.png +0 -0
  5. package/declarations/Attribute.d.ts +1 -1
  6. package/declarations/AttributeDefinitions.d.ts +1 -1
  7. package/declarations/DockLocation.d.ts +12 -12
  8. package/declarations/DropInfo.d.ts +12 -12
  9. package/declarations/I18nLabel.d.ts +12 -14
  10. package/declarations/Orientation.d.ts +7 -7
  11. package/declarations/PopupMenu.d.ts +1 -1
  12. package/declarations/Rect.d.ts +41 -28
  13. package/declarations/Types.d.ts +95 -79
  14. package/declarations/examples/demo/Utils.d.ts +4 -0
  15. package/declarations/index.d.ts +21 -22
  16. package/declarations/model/Action.d.ts +5 -5
  17. package/declarations/model/Actions.d.ts +127 -110
  18. package/declarations/model/BorderNode.d.ts +30 -34
  19. package/declarations/model/BorderSet.d.ts +3 -4
  20. package/declarations/model/ICloseType.d.ts +5 -5
  21. package/declarations/model/IDraggable.d.ts +2 -2
  22. package/declarations/model/IDropTarget.d.ts +2 -2
  23. package/declarations/model/IJsonModel.d.ts +811 -149
  24. package/declarations/model/LayoutWindow.d.ts +28 -0
  25. package/declarations/model/Model.d.ts +91 -86
  26. package/declarations/model/Node.d.ts +17 -17
  27. package/declarations/model/RowNode.d.ts +10 -11
  28. package/declarations/model/TabNode.d.ts +44 -37
  29. package/declarations/model/TabSetNode.d.ts +44 -41
  30. package/declarations/model/Utils.d.ts +1 -1
  31. package/declarations/model/WindowLayout.d.ts +24 -0
  32. package/declarations/src/Attribute.d.ts +1 -0
  33. package/declarations/src/AttributeDefinitions.d.ts +1 -0
  34. package/declarations/src/DockLocation.d.ts +12 -0
  35. package/declarations/src/DropInfo.d.ts +12 -0
  36. package/declarations/src/I18nLabel.d.ts +10 -0
  37. package/declarations/src/Orientation.d.ts +7 -0
  38. package/declarations/src/PopupMenu.d.ts +1 -0
  39. package/declarations/src/Rect.d.ts +31 -0
  40. package/declarations/src/Types.d.ts +92 -0
  41. package/declarations/src/index.d.ts +20 -0
  42. package/declarations/src/model/Action.d.ts +5 -0
  43. package/declarations/src/model/Actions.d.ts +110 -0
  44. package/declarations/src/model/BorderNode.d.ts +28 -0
  45. package/declarations/src/model/BorderSet.d.ts +3 -0
  46. package/declarations/src/model/ICloseType.d.ts +5 -0
  47. package/declarations/src/model/IDraggable.d.ts +2 -0
  48. package/declarations/src/model/IDropTarget.d.ts +2 -0
  49. package/declarations/src/model/IJsonModel.d.ts +153 -0
  50. package/declarations/src/model/Model.d.ts +98 -0
  51. package/declarations/src/model/Node.d.ts +16 -0
  52. package/declarations/src/model/RowNode.d.ts +11 -0
  53. package/declarations/src/model/TabNode.d.ts +36 -0
  54. package/declarations/src/model/TabSetNode.d.ts +37 -0
  55. package/declarations/src/model/Utils.d.ts +1 -0
  56. package/declarations/src/view/BorderButton.d.ts +1 -0
  57. package/declarations/src/view/BorderTab.d.ts +2 -0
  58. package/declarations/src/view/BorderTabSet.d.ts +1 -0
  59. package/declarations/src/view/DragContainer.d.ts +1 -0
  60. package/declarations/src/view/ErrorBoundary.d.ts +1 -0
  61. package/declarations/src/view/FloatingWindow.d.ts +1 -0
  62. package/declarations/src/view/Icons.d.ts +7 -0
  63. package/declarations/src/view/Layout.d.ts +113 -0
  64. package/declarations/src/view/Overlay.d.ts +1 -0
  65. package/declarations/src/view/PopupMenu.d.ts +1 -0
  66. package/declarations/src/view/Row.d.ts +1 -0
  67. package/declarations/src/view/Splitter.d.ts +1 -0
  68. package/declarations/src/view/Tab.d.ts +1 -0
  69. package/declarations/src/view/TabButton.d.ts +1 -0
  70. package/declarations/src/view/TabButtonStamp.d.ts +1 -0
  71. package/declarations/src/view/TabOverflowHook.d.ts +1 -0
  72. package/declarations/src/view/TabSet.d.ts +1 -0
  73. package/declarations/src/view/Utils.d.ts +4 -0
  74. package/declarations/view/BorderButton.d.ts +1 -1
  75. package/declarations/view/BorderTab.d.ts +2 -0
  76. package/declarations/view/BorderTabSet.d.ts +1 -1
  77. package/declarations/view/DragContainer.d.ts +1 -0
  78. package/declarations/view/ErrorBoundary.d.ts +1 -1
  79. package/declarations/view/ExtendedResizeObserver.d.ts +23 -0
  80. package/declarations/view/FloatingWindow.d.ts +1 -1
  81. package/declarations/view/Icons.d.ts +8 -6
  82. package/declarations/view/Layout.d.ts +139 -160
  83. package/declarations/view/Overlay.d.ts +1 -0
  84. package/declarations/view/PopoutWindow.d.ts +1 -0
  85. package/declarations/view/PopupMenu.d.ts +1 -0
  86. package/declarations/view/Row.d.ts +1 -0
  87. package/declarations/view/SizeTracker.d.ts +10 -0
  88. package/declarations/view/Splitter.d.ts +1 -1
  89. package/declarations/view/Tab.d.ts +1 -1
  90. package/declarations/view/TabButton.d.ts +1 -1
  91. package/declarations/view/TabButtonStamp.d.ts +1 -1
  92. package/declarations/view/TabOverflowHook.d.ts +1 -1
  93. package/declarations/view/TabSet.d.ts +1 -1
  94. package/declarations/view/Utils.d.ts +11 -1
  95. package/dist/bundles/demo.js +232052 -0
  96. package/dist/bundles/demo.js.map +1 -0
  97. package/dist/flexlayout.js +122 -92
  98. package/dist/flexlayout_min.js +1 -1
  99. package/lib/Attribute.js +42 -31
  100. package/lib/Attribute.js.map +1 -1
  101. package/lib/AttributeDefinitions.js +131 -108
  102. package/lib/AttributeDefinitions.js.map +1 -1
  103. package/lib/DockLocation.js +120 -124
  104. package/lib/DockLocation.js.map +1 -1
  105. package/lib/DropInfo.js +9 -13
  106. package/lib/DropInfo.js.map +1 -1
  107. package/lib/I18nLabel.js +13 -18
  108. package/lib/I18nLabel.js.map +1 -1
  109. package/lib/Orientation.js +22 -26
  110. package/lib/Orientation.js.map +1 -1
  111. package/lib/Rect.js +104 -72
  112. package/lib/Rect.js.map +1 -1
  113. package/lib/Types.js +96 -83
  114. package/lib/Types.js.map +1 -1
  115. package/lib/index.js +21 -38
  116. package/lib/index.js.map +1 -1
  117. package/lib/model/Action.js +6 -10
  118. package/lib/model/Action.js.map +1 -1
  119. package/lib/model/Actions.js +169 -155
  120. package/lib/model/Actions.js.map +1 -1
  121. package/lib/model/BorderNode.js +385 -406
  122. package/lib/model/BorderNode.js.map +1 -1
  123. package/lib/model/BorderSet.js +66 -121
  124. package/lib/model/BorderSet.js.map +1 -1
  125. package/lib/model/ICloseType.js +6 -9
  126. package/lib/model/ICloseType.js.map +1 -1
  127. package/lib/model/IDraggable.js +1 -2
  128. package/lib/model/IDropTarget.js +1 -2
  129. package/lib/model/IJsonModel.js +1 -2
  130. package/lib/model/LayoutWindow.js +83 -0
  131. package/lib/model/LayoutWindow.js.map +1 -0
  132. package/lib/model/Model.js +614 -496
  133. package/lib/model/Model.js.map +1 -1
  134. package/lib/model/Node.js +217 -228
  135. package/lib/model/Node.js.map +1 -1
  136. package/lib/model/RowNode.js +491 -504
  137. package/lib/model/RowNode.js.map +1 -1
  138. package/lib/model/TabNode.js +289 -184
  139. package/lib/model/TabNode.js.map +1 -1
  140. package/lib/model/TabSetNode.js +457 -446
  141. package/lib/model/TabSetNode.js.map +1 -1
  142. package/lib/model/Utils.js +47 -82
  143. package/lib/model/Utils.js.map +1 -1
  144. package/lib/view/BorderButton.js +124 -138
  145. package/lib/view/BorderButton.js.map +1 -1
  146. package/lib/view/BorderTab.js +47 -0
  147. package/lib/view/BorderTab.js.map +1 -0
  148. package/lib/view/BorderTabSet.js +134 -128
  149. package/lib/view/BorderTabSet.js.map +1 -1
  150. package/lib/view/DragContainer.js +16 -0
  151. package/lib/view/DragContainer.js.map +1 -0
  152. package/lib/view/ErrorBoundary.js +23 -27
  153. package/lib/view/ErrorBoundary.js.map +1 -1
  154. package/lib/view/Icons.js +40 -40
  155. package/lib/view/Icons.js.map +1 -1
  156. package/lib/view/Layout.js +918 -901
  157. package/lib/view/Layout.js.map +1 -1
  158. package/lib/view/Overlay.js +9 -0
  159. package/lib/view/Overlay.js.map +1 -0
  160. package/lib/view/PopoutWindow.js +129 -0
  161. package/lib/view/PopoutWindow.js.map +1 -0
  162. package/lib/view/PopupMenu.js +71 -0
  163. package/lib/view/PopupMenu.js.map +1 -0
  164. package/lib/view/Row.js +45 -0
  165. package/lib/view/Row.js.map +1 -0
  166. package/lib/view/SizeTracker.js +11 -0
  167. package/lib/view/SizeTracker.js.map +1 -0
  168. package/lib/view/Splitter.js +191 -147
  169. package/lib/view/Splitter.js.map +1 -1
  170. package/lib/view/Tab.js +86 -60
  171. package/lib/view/Tab.js.map +1 -1
  172. package/lib/view/TabButton.js +122 -135
  173. package/lib/view/TabButton.js.map +1 -1
  174. package/lib/view/TabButtonStamp.js +16 -21
  175. package/lib/view/TabButtonStamp.js.map +1 -1
  176. package/lib/view/TabOverflowHook.js +150 -149
  177. package/lib/view/TabOverflowHook.js.map +1 -1
  178. package/lib/view/TabSet.js +267 -234
  179. package/lib/view/TabSet.js.map +1 -1
  180. package/lib/view/Utils.js +126 -68
  181. package/lib/view/Utils.js.map +1 -1
  182. package/package.json +36 -30
  183. package/src/Attribute.ts +23 -0
  184. package/src/AttributeDefinitions.ts +38 -15
  185. package/src/DockLocation.ts +13 -13
  186. package/src/I18nLabel.ts +7 -9
  187. package/src/Rect.ts +53 -1
  188. package/src/Types.ts +16 -0
  189. package/src/index.ts +1 -2
  190. package/src/model/Actions.ts +49 -29
  191. package/src/model/BorderNode.ts +208 -214
  192. package/src/model/BorderSet.ts +42 -91
  193. package/src/model/IJsonModel.ts +883 -103
  194. package/src/model/LayoutWindow.ts +121 -0
  195. package/src/model/Model.ts +488 -366
  196. package/src/model/Node.ts +98 -111
  197. package/src/model/RowNode.ts +323 -319
  198. package/src/model/TabNode.ts +294 -110
  199. package/src/model/TabSetNode.ts +300 -242
  200. package/src/model/Utils.ts +6 -32
  201. package/src/view/BorderButton.tsx +32 -52
  202. package/src/view/BorderTab.tsx +70 -0
  203. package/src/view/BorderTabSet.tsx +64 -52
  204. package/src/view/DragContainer.tsx +32 -0
  205. package/src/view/Icons.tsx +13 -0
  206. package/src/view/Layout.tsx +1071 -1047
  207. package/src/view/Overlay.tsx +22 -0
  208. package/src/view/PopoutWindow.tsx +152 -0
  209. package/src/{PopupMenu.tsx → view/PopupMenu.tsx} +36 -31
  210. package/src/view/Row.tsx +68 -0
  211. package/src/view/SizeTracker.tsx +20 -0
  212. package/src/view/Splitter.tsx +167 -112
  213. package/src/view/Tab.tsx +76 -42
  214. package/src/view/TabButton.tsx +36 -55
  215. package/src/view/TabButtonStamp.tsx +5 -9
  216. package/src/view/TabOverflowHook.tsx +14 -9
  217. package/src/view/TabSet.tsx +217 -176
  218. package/src/view/Utils.tsx +119 -39
  219. package/style/_base.scss +143 -35
  220. package/style/dark.css +685 -577
  221. package/style/dark.css.map +1 -1
  222. package/style/dark.scss +4 -1
  223. package/style/gray.css +668 -560
  224. package/style/gray.css.map +1 -1
  225. package/style/gray.scss +4 -1
  226. package/style/light.css +669 -561
  227. package/style/light.css.map +1 -1
  228. package/style/light.scss +6 -3
  229. package/style/rounded.css +697 -0
  230. package/style/rounded.css.map +1 -0
  231. package/style/rounded.scss +194 -0
  232. package/style/underline.css +690 -582
  233. package/style/underline.css.map +1 -1
  234. package/style/underline.scss +4 -1
  235. package/cypress.config.ts +0 -16
  236. package/lib/DragDrop.js +0 -316
  237. package/lib/DragDrop.js.map +0 -1
  238. package/lib/PopupMenu.js +0 -68
  239. package/lib/PopupMenu.js.map +0 -1
  240. package/lib/model/SplitterNode.js +0 -72
  241. package/lib/model/SplitterNode.js.map +0 -1
  242. package/lib/view/FloatingWindow.js +0 -123
  243. package/lib/view/FloatingWindow.js.map +0 -1
  244. package/lib/view/FloatingWindowTab.js +0 -19
  245. package/lib/view/FloatingWindowTab.js.map +0 -1
  246. package/lib/view/TabFloating.js +0 -66
  247. package/lib/view/TabFloating.js.map +0 -1
  248. package/src/DragDrop.ts +0 -392
  249. package/src/model/SplitterNode.ts +0 -78
  250. package/src/view/FloatingWindow.tsx +0 -140
  251. package/src/view/FloatingWindowTab.tsx +0 -29
  252. package/src/view/TabFloating.tsx +0 -101
@@ -1,732 +1,878 @@
1
1
  import * as React from "react";
2
2
  import { createPortal } from "react-dom";
3
+ import { createRoot } from "react-dom/client";
3
4
  import { DockLocation } from "../DockLocation";
4
- import { DragDrop } from "../DragDrop";
5
5
  import { DropInfo } from "../DropInfo";
6
6
  import { I18nLabel } from "../I18nLabel";
7
+ import { Orientation } from "../Orientation";
8
+ import { Rect } from "../Rect";
9
+ import { CLASSES } from "../Types";
7
10
  import { Action } from "../model/Action";
8
11
  import { Actions } from "../model/Actions";
9
12
  import { BorderNode } from "../model/BorderNode";
10
- import { BorderSet } from "../model/BorderSet";
11
13
  import { IDraggable } from "../model/IDraggable";
12
- import { Model, ILayoutMetrics } from "../model/Model";
14
+ import { IJsonTabNode } from "../model/IJsonModel";
15
+ import { Model } from "../model/Model";
13
16
  import { Node } from "../model/Node";
14
- import { RowNode } from "../model/RowNode";
15
- import { SplitterNode } from "../model/SplitterNode";
16
17
  import { TabNode } from "../model/TabNode";
17
18
  import { TabSetNode } from "../model/TabSetNode";
18
- import { Rect } from "../Rect";
19
- import { CLASSES } from "../Types";
19
+ import { BorderTab } from "./BorderTab";
20
20
  import { BorderTabSet } from "./BorderTabSet";
21
- import { Splitter } from "./Splitter";
21
+ import { DragContainer } from "./DragContainer";
22
+ import { ErrorBoundary } from "./ErrorBoundary";
23
+ import { PopoutWindow } from "./PopoutWindow";
24
+ import { AsterickIcon, CloseIcon, EdgeIcon, MaximizeIcon, OverflowIcon, PopoutIcon, RestoreIcon } from "./Icons";
25
+ import { Overlay } from "./Overlay";
26
+ import { Row } from "./Row";
22
27
  import { Tab } from "./Tab";
23
- import { TabSet } from "./TabSet";
24
- import { FloatingWindow } from "./FloatingWindow";
25
- import { FloatingWindowTab } from "./FloatingWindowTab";
26
- import { TabFloating } from "./TabFloating";
27
- import { IJsonTabNode } from "../model/IJsonModel";
28
- import { Orientation } from "../Orientation";
29
- import { CloseIcon, MaximizeIcon, OverflowIcon, PopoutIcon, RestoreIcon } from "./Icons";
28
+ import { copyInlineStyles, enablePointerOnIFrames, isDesktop, isSafari } from "./Utils";
29
+ import { LayoutWindow } from "../model/LayoutWindow";
30
30
  import { TabButtonStamp } from "./TabButtonStamp";
31
-
32
- export type CustomDragCallback = (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation) => void;
33
- export type DragRectRenderCallback = (content: React.ReactElement | undefined, node?: Node, json?: IJsonTabNode) => React.ReactElement | undefined;
34
- export type FloatingTabPlaceholderRenderCallback = (dockPopout: () => void, showPopout: () => void) => React.ReactElement | undefined;
35
- export type NodeMouseEvent = (node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
36
- export type ShowOverflowMenuCallback = (
37
- node: TabSetNode | BorderNode,
38
- mouseEvent: React.MouseEvent<HTMLElement, MouseEvent>,
39
- items: { index: number; node: TabNode }[],
40
- onSelect: (item: { index: number; node: TabNode }) => void,
41
- ) => void;
42
- export type TabSetPlaceHolderCallback = (node: TabSetNode) => React.ReactNode;
43
- export type IconFactory = (node: TabNode) => React.ReactNode;
44
- export type TitleFactory = (node: TabNode) => ITitleObject | React.ReactNode;
31
+ import { SizeTracker } from "./SizeTracker";
45
32
 
46
33
  export interface ILayoutProps {
34
+ /** the model for this layout */
47
35
  model: Model;
36
+ /** factory function for creating the tab components */
48
37
  factory: (node: TabNode) => React.ReactNode;
49
- font?: IFontValues;
50
- fontFamily?: string;
51
- iconFactory?: IconFactory;
52
- titleFactory?: TitleFactory;
38
+ /** object mapping keys among close, maximize, restore, more, popout to React nodes to use in place of the default icons, can alternatively return functions for creating the React nodes */
53
39
  icons?: IIcons;
40
+ /** function called whenever the layout generates an action to update the model (allows for intercepting actions before they are dispatched to the model, for example, asking the user to confirm a tab close.) Returning undefined from the function will halt the action, otherwise return the action to continue */
54
41
  onAction?: (action: Action) => Action | undefined;
42
+ /** function called when rendering a tab, allows leading (icon), content section, buttons and name used in overflow menu to be customized */
55
43
  onRenderTab?: (
56
44
  node: TabNode,
57
45
  renderValues: ITabRenderValues, // change the values in this object as required
58
46
  ) => void;
47
+ /** function called when rendering a tabset, allows header and buttons to be customized */
59
48
  onRenderTabSet?: (
60
49
  tabSetNode: TabSetNode | BorderNode,
61
50
  renderValues: ITabSetRenderValues, // change the values in this object as required
62
51
  ) => void;
52
+ /** function called when model has changed */
63
53
  onModelChange?: (model: Model, action: Action) => void;
64
- onExternalDrag?: (event: React.DragEvent<HTMLDivElement>) => undefined | {
65
- dragText: string,
54
+ /** function called when an external object (not a tab) gets dragged onto the layout, with a single dragenter argument. Should return either undefined to reject the drag/drop or an object with keys dragText, jsonDrop, to create a tab via drag (similar to a call to addTabToTabSet). Function onDropis passed the added tabNodeand thedrop DragEvent`, unless the drag was canceled. */
55
+ onExternalDrag?: (event: React.DragEvent<HTMLElement>) => undefined | {
66
56
  json: any,
67
- onDrop?: (node?: Node, event?: Event) => void
57
+ onDrop?: (node?: Node, event?: React.DragEvent<HTMLElement>) => void
68
58
  };
59
+ /** function called with default css class name, return value is class name that will be used. Mainly for use with css modules. */
69
60
  classNameMapper?: (defaultClassName: string) => string;
61
+ /** function called for each I18nLabel to allow user translation, currently used for tab and tabset move messages, return undefined to use default values */
70
62
  i18nMapper?: (id: I18nLabel, param?: string) => string | undefined;
63
+ /** if left undefined will do simple check based on userAgent */
71
64
  supportsPopout?: boolean | undefined;
65
+ /** URL of popout window relative to origin, defaults to popout.html */
72
66
  popoutURL?: string | undefined;
67
+ /** boolean value, defaults to false, resize tabs as splitters are dragged. Warning: this can cause resizing to become choppy when tabs are slow to draw */
73
68
  realtimeResize?: boolean | undefined;
74
- onTabDrag?: (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation, refresh: () => void) => undefined | {
75
- x: number,
76
- y: number,
77
- width: number,
78
- height: number,
79
- callback: CustomDragCallback,
80
- // Called once when `callback` is not going to be called anymore (user canceled the drag, moved mouse and you returned a different callback, etc)
81
- invalidated?: () => void,
82
- cursor?: string | undefined
83
- };
69
+ /** callback for rendering the drag rectangles */
84
70
  onRenderDragRect?: DragRectRenderCallback;
85
- onRenderFloatingTabPlaceholder?: FloatingTabPlaceholderRenderCallback;
71
+ /** callback for handling context actions on tabs and tabsets */
86
72
  onContextMenu?: NodeMouseEvent;
73
+ /** callback for handling mouse clicks on tabs and tabsets with alt, meta, shift keys, also handles center mouse clicks */
87
74
  onAuxMouseClick?: NodeMouseEvent;
75
+ /** callback for handling the display of the tab overflow menu */
88
76
  onShowOverflowMenu?: ShowOverflowMenuCallback;
77
+ /** callback for rendering a placeholder when a tabset is empty */
89
78
  onTabSetPlaceHolder?: TabSetPlaceHolderCallback;
90
- }
91
- export interface IFontValues {
92
- size?: string;
93
- family?: string;
94
- style?: string;
95
- weight?: string;
79
+ /** Name given to popout windows, defaults to 'Popout Window' */
80
+ popoutWindowName?: string;
96
81
  }
97
82
 
98
- export interface ITabSetRenderValues {
99
- headerContent?: React.ReactNode;
100
- centerContent?: React.ReactNode;
101
- stickyButtons: React.ReactNode[];
102
- buttons: React.ReactNode[];
103
- headerButtons: React.ReactNode[];
104
- // position to insert overflow button within [...stickyButtons, ...buttons]
105
- // if left undefined position will be after the sticky buttons (if any)
106
- overflowPosition: number | undefined;
107
- }
83
+ /**
84
+ * A React component that hosts a multi-tabbed layout
85
+ */
86
+ export class Layout extends React.Component<ILayoutProps> {
87
+ /** @internal */
88
+ private selfRef: React.RefObject<LayoutInternal>;
89
+ /** @internal */
90
+ private revision: number; // so LayoutInternal knows this is a parent render (used for optimization)
108
91
 
109
- export interface ITabRenderValues {
110
- leading: React.ReactNode;
111
- content: React.ReactNode;
112
- name: string;
113
- buttons: React.ReactNode[];
114
- }
92
+ /** @internal */
93
+ constructor(props: ILayoutProps) {
94
+ super(props);
95
+ this.selfRef = React.createRef<LayoutInternal>();
96
+ this.revision = 0;
97
+ }
115
98
 
116
- export interface ITitleObject {
117
- titleContent: React.ReactNode;
118
- name: string;
119
- }
99
+ /** re-render the layout */
100
+ redraw() {
101
+ this.selfRef.current!.redraw("parent " + this.revision);
102
+ }
120
103
 
121
- export interface ILayoutState {
122
- rect: Rect;
123
- calculatedHeaderBarSize: number;
124
- calculatedTabBarSize: number;
125
- calculatedBorderBarSize: number;
126
- editingTab?: TabNode;
127
- showHiddenBorder: DockLocation;
128
- portal?: React.ReactPortal;
129
- showEdges?: boolean;
130
- }
104
+ /**
105
+ * Adds a new tab to the given tabset
106
+ * @param tabsetId the id of the tabset where the new tab will be added
107
+ * @param json the json for the new tab node
108
+ * @returns the added tab node or undefined
109
+ */
110
+ addTabToTabSet(tabsetId: string, json: IJsonTabNode): TabNode | undefined {
111
+ return this.selfRef.current!.addTabToTabSet(tabsetId, json);
112
+ }
131
113
 
132
- export interface IIcons {
133
- close?: (React.ReactNode | ((tabNode: TabNode) => React.ReactNode));
134
- closeTabset?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
135
- popout?: (React.ReactNode | ((tabNode: TabNode) => React.ReactNode));
136
- maximize?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
137
- restore?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
138
- more?: (React.ReactNode | ((tabSetNode: (TabSetNode | BorderNode), hiddenTabs: { node: TabNode; index: number }[]) => React.ReactNode));
139
- }
114
+ /**
115
+ * Adds a new tab by dragging an item to the drop location, must be called from within an HTML
116
+ * drag start handler. You can use the setDragComponent() method to set the drag image before calling this
117
+ * method.
118
+ * @param event the drag start event
119
+ * @param json the json for the new tab node
120
+ * @param onDrop a callback to call when the drag is complete
121
+ */
122
+ addTabWithDragAndDrop(event: DragEvent, json: IJsonTabNode, onDrop?: (node?: Node, event?: React.DragEvent<HTMLElement>) => void) {
123
+ this.selfRef.current!.addTabWithDragAndDrop(event, json, onDrop);
124
+ }
140
125
 
141
- const defaultIcons = {
142
- close: <CloseIcon />,
143
- closeTabset: <CloseIcon />,
144
- popout: <PopoutIcon />,
145
- maximize: <MaximizeIcon />,
146
- restore: <RestoreIcon />,
147
- more: <OverflowIcon />,
148
- };
126
+ /**
127
+ * Move a tab/tabset using drag and drop, must be called from within an HTML
128
+ * drag start handler
129
+ * @param event the drag start event
130
+ * @param node the tab or tabset to drag
131
+ */
132
+ moveTabWithDragAndDrop(event: DragEvent, node: (TabNode | TabSetNode)) {
133
+ this.selfRef.current!.moveTabWithDragAndDrop(event, node);
134
+ }
149
135
 
150
- export interface ICustomDropDestination {
151
- rect: Rect;
152
- callback: CustomDragCallback;
153
- invalidated: (() => void) | undefined;
154
- dragging: TabNode | IJsonTabNode;
155
- over: TabNode;
156
- x: number;
157
- y: number;
158
- location: DockLocation;
159
- cursor: string | undefined;
136
+ /**
137
+ * Adds a new tab to the active tabset (if there is one)
138
+ * @param json the json for the new tab node
139
+ * @returns the added tab node or undefined
140
+ */
141
+ addTabToActiveTabSet(json: IJsonTabNode): TabNode | undefined {
142
+ return this.selfRef.current!.addTabToActiveTabSet(json);
143
+ }
144
+
145
+ /**
146
+ * Sets the drag image from a react component for a drag event
147
+ * @param event the drag event
148
+ * @param component the react component to be used for the drag image
149
+ * @param x the x position of the drag cursor on the image
150
+ * @param y the x position of the drag cursor on the image
151
+ */
152
+ setDragComponent(event: DragEvent, component: React.ReactNode, x: number, y: number) {
153
+ this.selfRef.current!.setDragComponent(event, component, x, y);
154
+ }
155
+
156
+ /** Get the root div element of the layout */
157
+ getRootDiv() {
158
+ return this.selfRef.current!.getRootDiv();
159
+ }
160
+
161
+ /** @internal */
162
+ render() {
163
+ return (<LayoutInternal ref={this.selfRef} {...this.props} renderRevision={this.revision++} />)
164
+ }
160
165
  }
161
166
 
162
167
  /** @internal */
163
- export interface ILayoutCallbacks {
164
- i18nName(id: I18nLabel, param?: string): string;
165
- maximize(tabsetNode: TabSetNode): void;
166
- getPopoutURL(): string;
167
- isSupportsPopout(): boolean;
168
- isRealtimeResize(): boolean;
169
- getCurrentDocument(): Document | undefined;
170
- getClassName(defaultClassName: string): string;
171
- doAction(action: Action): Node | undefined;
172
- getDomRect(): DOMRect | undefined;
173
- getRootDiv(): HTMLDivElement | null;
174
- dragStart(
175
- event: Event | React.MouseEvent<HTMLDivElement, MouseEvent> | React.TouchEvent<HTMLDivElement> | React.DragEvent<HTMLDivElement> | undefined,
176
- dragDivText: string | undefined,
177
- node: Node & IDraggable,
178
- allowDrag: boolean,
179
- onClick?: (event: Event) => void,
180
- onDoubleClick?: (event: Event) => void
181
- ): void;
182
- customizeTab(
183
- tabNode: TabNode,
184
- renderValues: ITabRenderValues,
185
- ): void;
186
- customizeTabSet(
187
- tabSetNode: TabSetNode | BorderNode,
188
- renderValues: ITabSetRenderValues,
189
- ): void;
190
- styleFont: (style: Record<string, string>) => Record<string, string>;
191
- setEditingTab(tabNode?: TabNode): void;
192
- getEditingTab(): TabNode | undefined;
193
- getOnRenderFloatingTabPlaceholder(): FloatingTabPlaceholderRenderCallback | undefined;
194
- showContextMenu(node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>): void;
195
- auxMouseClick(node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>): void;
196
- showPortal: (portal: React.ReactNode, portalDiv: HTMLDivElement) => void;
197
- hidePortal: () => void;
198
- getShowOverflowMenu(): ShowOverflowMenuCallback | undefined;
199
- getTabSetPlaceHolderCallback(): TabSetPlaceHolderCallback | undefined;
168
+ interface ILayoutInternalProps extends ILayoutProps {
169
+ renderRevision: number;
170
+
171
+ // used only for popout windows:
172
+ windowId?: string;
173
+ mainLayout?: LayoutInternal;
200
174
  }
201
175
 
202
- // Popout windows work in latest browsers based on webkit (Chrome, Opera, Safari, latest Edge) and Firefox. They do
203
- // not work on any version if IE or the original Edge browser
204
- // Assume any recent desktop browser not IE or original Edge will work
205
176
  /** @internal */
206
- // @ts-ignore
207
- const isIEorEdge = typeof window !== "undefined" && (window.document.documentMode || /Edge\//.test(window.navigator.userAgent));
208
- /** @internal */
209
- const isDesktop = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
210
- /** @internal */
211
- const defaultSupportsPopout: boolean = isDesktop && !isIEorEdge;
177
+ interface ILayoutInternalState {
178
+ rect: Rect;
179
+ editingTab?: TabNode;
180
+ portal?: React.ReactPortal;
181
+ showEdges: boolean;
182
+ showOverlay: boolean;
183
+ calculatedBorderBarSize: number;
184
+ layoutRevision: number;
185
+ forceRevision: number;
186
+ showHiddenBorder: DockLocation;
187
+ }
212
188
 
213
- /**
214
- * A React component that hosts a multi-tabbed layout
215
- */
216
- export class Layout extends React.Component<ILayoutProps, ILayoutState> {
189
+ /** @internal */
190
+ export class LayoutInternal extends React.Component<ILayoutInternalProps, ILayoutInternalState> {
191
+ public static dragState: DragState | undefined = undefined;
217
192
 
218
- /** @internal */
219
193
  private selfRef: React.RefObject<HTMLDivElement>;
220
- /** @internal */
221
- private findHeaderBarSizeRef: React.RefObject<HTMLDivElement>;
222
- /** @internal */
223
- private findTabBarSizeRef: React.RefObject<HTMLDivElement>;
224
- /** @internal */
194
+ private moveablesRef: React.RefObject<HTMLDivElement>;
225
195
  private findBorderBarSizeRef: React.RefObject<HTMLDivElement>;
226
- /** @internal */
196
+ private mainRef: React.RefObject<HTMLDivElement>;
227
197
  private previousModel?: Model;
228
- /** @internal */
229
- private centerRect?: Rect;
230
-
231
- /** @internal */
232
- // private start: number = 0;
233
- /** @internal */
234
- // private layoutTime: number = 0;
235
-
236
- /** @internal */
237
- private tabIds: string[];
238
- /** @internal */
239
- private newTabJson: IJsonTabNode | undefined;
240
- /** @internal */
241
- private firstMove: boolean = false;
242
- /** @internal */
243
- private dragNode?: Node & IDraggable;
244
- /** @internal */
245
- private dragDiv?: HTMLDivElement;
246
- /** @internal */
247
- private dragRectRendered: boolean = true;
248
- /** @internal */
249
- private dragDivText: string | undefined = undefined;
250
- /** @internal */
198
+ private orderedIds: string[];
199
+ private moveableElementMap = new Map<string, HTMLElement>();
251
200
  private dropInfo: DropInfo | undefined;
252
- /** @internal */
253
- private customDrop: ICustomDropDestination | undefined;
254
- /** @internal */
255
- private outlineDiv?: HTMLDivElement;
256
- /** @internal */
257
- private edgeRectLength = 100;
258
- /** @internal */
259
- private edgeRectWidth = 10;
260
- /** @internal */
261
- private fnNewNodeDropped?: (node?: Node, event?: Event) => void;
262
- /** @internal */
201
+ private outlineDiv?: HTMLElement;
263
202
  private currentDocument?: Document;
264
- /** @internal */
265
203
  private currentWindow?: Window;
266
- /** @internal */
267
204
  private supportsPopout: boolean;
268
- /** @internal */
269
205
  private popoutURL: string;
270
- /** @internal */
271
206
  private icons: IIcons;
272
- /** @internal */
273
207
  private resizeObserver?: ResizeObserver;
274
208
 
275
- constructor(props: ILayoutProps) {
209
+ private dragEnterCount: number = 0;
210
+ private dragging: boolean = false;
211
+ private windowId: string;
212
+ private layoutWindow: LayoutWindow;
213
+ private mainLayout: LayoutInternal;
214
+ private isMainWindow: boolean;
215
+ private isDraggingOverWindow: boolean;
216
+ private styleObserver: MutationObserver | undefined;
217
+ private popoutWindowName: string;
218
+ // private renderCount: any;
219
+
220
+ constructor(props: ILayoutInternalProps) {
276
221
  super(props);
277
- this.props.model._setChangeListener(this.onModelChange);
278
- this.tabIds = [];
222
+
223
+ this.orderedIds = [];
279
224
  this.selfRef = React.createRef<HTMLDivElement>();
280
- this.findHeaderBarSizeRef = React.createRef<HTMLDivElement>();
281
- this.findTabBarSizeRef = React.createRef<HTMLDivElement>();
225
+ this.moveablesRef = React.createRef<HTMLDivElement>();
226
+ this.mainRef = React.createRef<HTMLDivElement>();
282
227
  this.findBorderBarSizeRef = React.createRef<HTMLDivElement>();
228
+
283
229
  this.supportsPopout = props.supportsPopout !== undefined ? props.supportsPopout : defaultSupportsPopout;
284
230
  this.popoutURL = props.popoutURL ? props.popoutURL : "popout.html";
285
231
  this.icons = { ...defaultIcons, ...props.icons };
232
+ this.windowId = props.windowId ? props.windowId : Model.MAIN_WINDOW_ID;
233
+ this.mainLayout = this.props.mainLayout ? this.props.mainLayout : this;
234
+ this.isDraggingOverWindow = false;
235
+ this.layoutWindow = this.props.model.getwindowsMap().get(this.windowId)!;
236
+ this.layoutWindow.layout = this;
237
+ this.popoutWindowName = this.props.popoutWindowName || "Popout Window";
238
+ // this.renderCount = 0;
286
239
 
287
240
  this.state = {
288
- rect: new Rect(0, 0, 0, 0),
289
- calculatedHeaderBarSize: 25,
290
- calculatedTabBarSize: 26,
291
- calculatedBorderBarSize: 30,
241
+ rect: Rect.empty(),
292
242
  editingTab: undefined,
293
- showHiddenBorder: DockLocation.CENTER,
294
243
  showEdges: false,
244
+ showOverlay: false,
245
+ calculatedBorderBarSize: 29,
246
+ layoutRevision: 0,
247
+ forceRevision: 0,
248
+ showHiddenBorder: DockLocation.CENTER
295
249
  };
296
250
 
297
- this.onDragEnter = this.onDragEnter.bind(this);
298
- }
299
-
300
- /** @internal */
301
- styleFont(style: Record<string, string>): Record<string, string> {
302
- if (this.props.font) {
303
- if (this.selfRef.current) {
304
- if (this.props.font.size) {
305
- this.selfRef.current.style.setProperty("--font-size", this.props.font.size);
306
- }
307
- if (this.props.font.family) {
308
- this.selfRef.current.style.setProperty("--font-family", this.props.font.family);
309
- }
310
- }
311
- if (this.props.font.style) {
312
- style.fontStyle = this.props.font.style;
313
- }
314
- if (this.props.font.weight) {
315
- style.fontWeight = this.props.font.weight;
316
- }
317
- }
318
- return style;
319
- }
320
-
321
- /** @internal */
322
- onModelChange = (action: Action) => {
323
- this.forceUpdate();
324
- if (this.props.onModelChange) {
325
- this.props.onModelChange(this.props.model, action);
326
- }
327
- };
328
-
329
- /** @internal */
330
- doAction(action: Action): Node | undefined {
331
- if (this.props.onAction !== undefined) {
332
- const outcome = this.props.onAction(action);
333
- if (outcome !== undefined) {
334
- return this.props.model.doAction(outcome);
335
- }
336
- return undefined;
337
- } else {
338
- return this.props.model.doAction(action);
339
- }
251
+ this.isMainWindow = this.windowId === Model.MAIN_WINDOW_ID;
340
252
  }
341
253
 
342
- /** @internal */
343
254
  componentDidMount() {
344
255
  this.updateRect();
345
- this.updateLayoutMetrics();
346
256
 
347
- // need to re-render if size changes
348
- this.currentDocument = (this.selfRef.current as HTMLDivElement).ownerDocument;
257
+ this.currentDocument = (this.selfRef.current as HTMLElement).ownerDocument;
349
258
  this.currentWindow = this.currentDocument.defaultView!;
259
+
260
+ this.layoutWindow.window = this.currentWindow;
261
+ this.layoutWindow.toScreenRectFunction = (r) => this.getScreenRect(r);
262
+
350
263
  this.resizeObserver = new ResizeObserver(entries => {
351
- this.updateRect(entries[0].contentRect);
264
+ requestAnimationFrame(() => {
265
+ this.updateRect();
266
+ });
352
267
  });
353
- const selfRefCurr = this.selfRef.current;
354
- if (selfRefCurr) {
355
- this.resizeObserver.observe(selfRefCurr);
268
+ if (this.selfRef.current) {
269
+ this.resizeObserver.observe(this.selfRef.current);
356
270
  }
271
+
272
+ if (this.isMainWindow) {
273
+ this.props.model.addChangeListener(this.onModelChange);
274
+ this.updateLayoutMetrics();
275
+ } else {
276
+ // since resizeObserver doesn't always work as expected when observing element in another document
277
+ this.currentWindow.addEventListener("resize", () => {
278
+ this.updateRect();
279
+ });
280
+
281
+ const sourceElement = this.props.mainLayout!.getRootDiv()!;
282
+ const targetElement = this.selfRef.current!;
283
+
284
+ copyInlineStyles(sourceElement, targetElement);
285
+
286
+ this.styleObserver = new MutationObserver(() => {
287
+ const changed = copyInlineStyles(sourceElement, targetElement);
288
+ if (changed) {
289
+ this.redraw("mutation observer");
290
+ }
291
+ });
292
+
293
+ // Observe changes to the source element's style attribute
294
+ this.styleObserver.observe(sourceElement, { attributeFilter: ['style'] });
295
+ }
296
+
297
+ // allow tabs to overlay when hidden
298
+ document.addEventListener('visibilitychange', () => {
299
+ for (const [_, layoutWindow] of this.props.model.getwindowsMap()) {
300
+ const layout = layoutWindow.layout;
301
+ if (layout) {
302
+ this.redraw("visibility change");
303
+ }
304
+ }
305
+ });
357
306
  }
358
307
 
359
- /** @internal */
360
308
  componentDidUpdate() {
361
- this.updateLayoutMetrics();
362
- if (this.props.model !== this.previousModel) {
363
- if (this.previousModel !== undefined) {
364
- this.previousModel._setChangeListener(undefined); // stop listening to old model
309
+ this.currentDocument = (this.selfRef.current as HTMLElement).ownerDocument;
310
+ this.currentWindow = this.currentDocument.defaultView!;
311
+ if (this.isMainWindow) {
312
+ if (this.props.model !== this.previousModel) {
313
+ if (this.previousModel !== undefined) {
314
+ this.previousModel.removeChangeListener(this.onModelChange); // stop listening to old model
315
+ }
316
+ this.props.model.getwindowsMap().get(this.windowId)!.layout = this;
317
+ this.props.model.addChangeListener(this.onModelChange);
318
+ this.layoutWindow = this.props.model.getwindowsMap().get(this.windowId)!;
319
+ this.layoutWindow.layout = this;
320
+ this.layoutWindow.toScreenRectFunction = (r) => this.getScreenRect(r);
321
+ this.previousModel = this.props.model;
322
+ this.tidyMoveablesMap();
365
323
  }
366
- this.props.model._setChangeListener(this.onModelChange);
367
- this.previousModel = this.props.model;
324
+
325
+ this.updateLayoutMetrics();
368
326
  }
369
- // console.log("Layout time: " + this.layoutTime + "ms Render time: " + (Date.now() - this.start) + "ms");
370
327
  }
371
328
 
372
- /** @internal */
373
- updateRect = (domRect?: DOMRectReadOnly) => {
374
- if (!domRect) {
375
- domRect = this.getDomRect();
376
- }
377
- if (!domRect) {
378
- // no dom rect available, return.
379
- return;
329
+ componentWillUnmount() {
330
+ if (this.selfRef.current) {
331
+ this.resizeObserver?.unobserve(this.selfRef.current);
380
332
  }
381
- const rect = new Rect(0, 0, domRect.width, domRect.height);
382
- if (!rect.equals(this.state.rect) && rect.width !== 0 && rect.height !== 0) {
383
- this.setState({ rect });
333
+ this.styleObserver?.disconnect();
334
+ }
335
+
336
+ render() {
337
+ // console.log("render", this.windowId, this.state.revision, this.renderCount++);
338
+ // first render will be used to find the size (via selfRef)
339
+ if (!this.selfRef.current) {
340
+ return (
341
+ <div ref={this.selfRef} className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT)}>
342
+ <div ref={this.moveablesRef} key="__moveables__" className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_MOVEABLES)}></div>
343
+ {this.renderMetricsElements()}
344
+ </div>
345
+ );
384
346
  }
385
- };
386
347
 
387
- /** @internal */
388
- updateLayoutMetrics = () => {
389
- if (this.findHeaderBarSizeRef.current) {
390
- const headerBarSize = this.findHeaderBarSizeRef.current.getBoundingClientRect().height;
391
- if (headerBarSize !== this.state.calculatedHeaderBarSize) {
392
- this.setState({ calculatedHeaderBarSize: headerBarSize });
393
- }
348
+ const model = this.props.model;
349
+ model.getRoot(this.windowId).calcMinMaxSize();
350
+ model.getRoot(this.windowId).setPaths("");
351
+ model.getBorderSet().setPaths();
352
+
353
+ const inner = this.renderLayout();
354
+ const outer = this.renderBorders(inner);
355
+
356
+ const tabs = this.renderTabs();
357
+ const reorderedTabs = this.reorderComponents(tabs, this.orderedIds);
358
+
359
+ let floatingWindows = null;
360
+ let tabMoveables = null;
361
+ let tabStamps = null;
362
+ let metricElements = null;
363
+
364
+ if (this.isMainWindow) {
365
+ floatingWindows = this.renderWindows();
366
+ metricElements = this.renderMetricsElements();
367
+ tabMoveables = this.renderTabMoveables();
368
+ tabStamps = <div key="__tabStamps__" className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_TAB_STAMPS)}>
369
+ {this.renderTabStamps()}
370
+ </div>;
394
371
  }
395
- if (this.findTabBarSizeRef.current) {
396
- const tabBarSize = this.findTabBarSizeRef.current.getBoundingClientRect().height;
397
- if (tabBarSize !== this.state.calculatedTabBarSize) {
398
- this.setState({ calculatedTabBarSize: tabBarSize });
372
+
373
+ return (
374
+ <div
375
+ ref={this.selfRef}
376
+ className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT)}
377
+ onDragEnter={this.onDragEnterRaw}
378
+ onDragLeave={this.onDragLeaveRaw}
379
+ onDragOver={this.onDragOver}
380
+ onDrop={this.onDrop}
381
+ >
382
+ <div ref={this.moveablesRef} key="__moveables__" className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_MOVEABLES)}></div>
383
+ {metricElements}
384
+ <Overlay key="__overlay__" layout={this} show={this.state.showOverlay} />
385
+ {outer}
386
+ {reorderedTabs}
387
+ {tabMoveables}
388
+ {tabStamps}
389
+ {this.state.portal}
390
+ {floatingWindows}
391
+ </div>
392
+ );
393
+ }
394
+
395
+ renderBorders(
396
+ inner: React.ReactNode
397
+ ) {
398
+ const classMain = this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_MAIN);
399
+ const borders = this.props.model.getBorderSet().getBorderMap()
400
+ if (this.isMainWindow && borders.size > 0) {
401
+ inner = (
402
+ <div className={classMain} ref={this.mainRef}>
403
+ {inner}
404
+ </div>);
405
+ const borderSetComponents = new Map<DockLocation, React.ReactNode>();
406
+ const borderSetContentComponents = new Map<DockLocation, React.ReactNode>();
407
+ for (const [_, location] of DockLocation.values) {
408
+ const border = borders.get(location);
409
+ const showBorder = border && (
410
+ !border.isAutoHide() ||
411
+ (border.isAutoHide() && (border.getChildren().length > 0 || this.state.showHiddenBorder === location)));
412
+ if (showBorder) {
413
+ borderSetComponents.set(location, <BorderTabSet layout={this} border={border} size={this.state.calculatedBorderBarSize} />);
414
+ borderSetContentComponents.set(location, <BorderTab layout={this} border={border} show={border.getSelected() !== -1} />);
415
+ }
399
416
  }
400
- }
401
- if (this.findBorderBarSizeRef.current) {
402
- const borderBarSize = this.findBorderBarSizeRef.current.getBoundingClientRect().height;
403
- if (borderBarSize !== this.state.calculatedBorderBarSize) {
404
- this.setState({ calculatedBorderBarSize: borderBarSize });
417
+
418
+ const classBorderOuter = this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_BORDER_CONTAINER);
419
+ const classBorderInner = this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT_BORDER_CONTAINER_INNER);
420
+
421
+ if (this.props.model.getBorderSet().getLayoutHorizontal()) {
422
+ const innerWithBorderTabs = (
423
+ <div className={classBorderInner} style={{ flexDirection: "column" }}>
424
+ {borderSetContentComponents.get(DockLocation.TOP)}
425
+ <div className={classBorderInner} style={{ flexDirection: "row" }}>
426
+ {borderSetContentComponents.get(DockLocation.LEFT)}
427
+ {inner}
428
+ {borderSetContentComponents.get(DockLocation.RIGHT)}
429
+ </div>
430
+ {borderSetContentComponents.get(DockLocation.BOTTOM)}
431
+ </div>
432
+ );
433
+ return (
434
+ <div className={classBorderOuter} style={{ flexDirection: "column" }}>
435
+ {borderSetComponents.get(DockLocation.TOP)}
436
+ <div className={classBorderInner} style={{ flexDirection: "row" }}>
437
+ {borderSetComponents.get(DockLocation.LEFT)}
438
+ {innerWithBorderTabs}
439
+ {borderSetComponents.get(DockLocation.RIGHT)}
440
+ </div>
441
+ {borderSetComponents.get(DockLocation.BOTTOM)}
442
+ </div>
443
+ );
444
+ } else {
445
+ const innerWithBorderTabs = (
446
+ <div className={classBorderInner} style={{ flexDirection: "row" }}>
447
+ {borderSetContentComponents.get(DockLocation.LEFT)}
448
+ <div className={classBorderInner} style={{ flexDirection: "column" }}>
449
+ {borderSetContentComponents.get(DockLocation.TOP)}
450
+ {inner}
451
+ {borderSetContentComponents.get(DockLocation.BOTTOM)}
452
+ </div>
453
+ {borderSetContentComponents.get(DockLocation.RIGHT)}
454
+ </div>
455
+ );
456
+
457
+ return (
458
+ <div className={classBorderOuter} style={{ flexDirection: "row" }}>
459
+ {borderSetComponents.get(DockLocation.LEFT)}
460
+ <div className={classBorderInner} style={{ flexDirection: "column" }}>
461
+ {borderSetComponents.get(DockLocation.TOP)}
462
+ {innerWithBorderTabs}
463
+ {borderSetComponents.get(DockLocation.BOTTOM)}
464
+ </div>
465
+ {borderSetComponents.get(DockLocation.RIGHT)}
466
+ </div>
467
+ );
405
468
  }
406
- }
407
- };
408
469
 
409
- /** @internal */
410
- getClassName = (defaultClassName: string) => {
411
- if (this.props.classNameMapper === undefined) {
412
- return defaultClassName;
413
- } else {
414
- return this.props.classNameMapper(defaultClassName);
470
+ } else { // no borders
471
+ return (
472
+ <div className={classMain} ref={this.mainRef} style={{ position: "absolute", top: 0, left: 0, bottom: 0, right: 0, display: "flex" }}>
473
+ {inner}
474
+ </div>
475
+ );
415
476
  }
416
- };
417
-
418
- /** @internal */
419
- getCurrentDocument() {
420
- return this.currentDocument;
421
477
  }
422
478
 
423
- /** @internal */
424
- getDomRect() {
425
- return this.selfRef.current?.getBoundingClientRect();
479
+ renderLayout() {
480
+ return (
481
+ <>
482
+ <Row key="__row__" layout={this} node={this.props.model.getRoot(this.windowId)} />
483
+ {this.renderEdgeIndicators()}
484
+ </>
485
+ );
426
486
  }
427
487
 
428
- /** @internal */
429
- getRootDiv() {
430
- return this.selfRef.current;
431
- }
488
+ renderEdgeIndicators() {
489
+ const edges: React.ReactNode[] = [];
490
+ const arrowIcon = this.icons.edgeArrow;
491
+ if (this.state.showEdges) {
492
+ const r = this.props.model.getRoot(this.windowId).getRect();
493
+ const length = edgeRectLength;
494
+ const width = edgeRectWidth;
495
+ const offset = edgeRectLength / 2;
496
+ const className = this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT);
497
+ const radius = 50;
498
+ edges.push(<div key="North" style={{ top: 0, left: r.width / 2 - offset, width: length, height: width, borderBottomLeftRadius: radius, borderBottomRightRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_TOP)}>
499
+ <div style={{ transform: "rotate(180deg)" }}>
500
+ {arrowIcon}
501
+ </div>
502
+ </div>);
503
+ edges.push(<div key="West" style={{ top: r.height / 2 - offset, left: 0, width: width, height: length, borderTopRightRadius: radius, borderBottomRightRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_LEFT)}>
504
+ <div style={{ transform: "rotate(90deg)" }}>
505
+ {arrowIcon}
506
+ </div>
507
+ </div>);
508
+ edges.push(<div key="South" style={{ top: r.height - width, left: r.width / 2 - offset, width: length, height: width, borderTopLeftRadius: radius, borderTopRightRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_BOTTOM)}>
509
+ <div>
510
+ {arrowIcon}
511
+ </div>
512
+ </div>);
513
+ edges.push(<div key="East" style={{ top: r.height / 2 - offset, left: r.width - width, width: width, height: length, borderTopLeftRadius: radius, borderBottomLeftRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_RIGHT)}>
514
+ <div style={{ transform: "rotate(-90deg)" }}>
515
+ {arrowIcon}
516
+ </div>
517
+ </div>);
518
+ }
432
519
 
433
- /** @internal */
434
- isSupportsPopout() {
435
- return this.supportsPopout;
520
+ return edges;
436
521
  }
437
522
 
438
- /** @internal */
439
- isRealtimeResize() {
440
- return this.props.realtimeResize ?? false;
441
- }
523
+ renderWindows() {
524
+ const floatingWindows: React.ReactNode[] = [];
525
+ if (this.supportsPopout) {
526
+ const windows = this.props.model.getwindowsMap();
527
+ let i = 1;
528
+ for (const [windowId, layoutWindow] of windows) {
442
529
 
443
- /** @internal */
444
- onTabDrag(...args: Parameters<Required<ILayoutProps>['onTabDrag']>) {
445
- return this.props.onTabDrag?.(...args);
530
+ if (windowId !== Model.MAIN_WINDOW_ID) {
531
+ floatingWindows.push(
532
+ <PopoutWindow
533
+ key={windowId}
534
+ layout={this}
535
+ title={this.popoutWindowName + " " + i}
536
+ layoutWindow={layoutWindow}
537
+ url={this.popoutURL + "?id=" + windowId}
538
+ onSetWindow={this.onSetWindow}
539
+ onCloseWindow={this.onCloseWindow}
540
+ >
541
+ <LayoutInternal {...this.props} windowId={windowId} mainLayout={this} />
542
+ </PopoutWindow>
543
+ );
544
+ i++;
545
+ }
546
+ }
547
+ }
548
+ return floatingWindows;
446
549
  }
447
550
 
448
- /** @internal */
449
- getPopoutURL() {
450
- return this.popoutURL;
551
+ renderTabMoveables() {
552
+ const tabMoveables: React.ReactNode[] = [];
553
+
554
+ this.props.model.visitNodes((node) => {
555
+ if (node instanceof TabNode) {
556
+ const child = node as TabNode;
557
+ const element = this.getMoveableElement(child.getId());
558
+ child.setMoveableElement(element);
559
+ const selected = child.isSelected();
560
+ const rect = (child.getParent() as BorderNode | TabSetNode).getContentRect();
561
+
562
+ // only render first time if size >0
563
+ const renderTab = child.isRendered() ||
564
+ ((selected || !child.isEnableRenderOnDemand()) && (rect.width > 0 && rect.height > 0));
565
+
566
+ if (renderTab) {
567
+ // console.log("rendertab", child.getName(), this.props.renderRevision);
568
+ const key = child.getId() + (child.isEnableWindowReMount() ? child.getWindowId() : "");
569
+ tabMoveables.push(createPortal(
570
+ <SizeTracker rect={rect} selected={child.isSelected()} forceRevision={this.state.forceRevision} tabsRevision={this.props.renderRevision} key={key}>
571
+ <ErrorBoundary message={this.i18nName(I18nLabel.Error_rendering_component)}>
572
+ {this.props.factory(child)}
573
+ </ErrorBoundary>
574
+ </SizeTracker>
575
+ , element, key));
576
+
577
+ child.setRendered(renderTab);
578
+ }
579
+ }
580
+ });
581
+
582
+ return tabMoveables;
451
583
  }
452
584
 
453
- /** @internal */
454
- componentWillUnmount() {
455
- const selfRefCurr = this.selfRef.current;
456
- if (selfRefCurr) {
457
- this.resizeObserver?.unobserve(selfRefCurr);
458
- }
585
+ renderTabStamps() {
586
+ const tabStamps: React.ReactNode[] = [];
587
+
588
+ this.props.model.visitNodes((node) => {
589
+ if (node instanceof TabNode) {
590
+ const child = node as TabNode;
591
+
592
+ // what the tab should look like when dragged (since images need to have been loaded before drag image can be taken)
593
+ tabStamps.push(<DragContainer key={child.getId()} layout={this} node={child} />)
594
+ }
595
+ });
596
+
597
+ return tabStamps;
459
598
  }
460
599
 
461
- /** @internal */
462
- setEditingTab(tabNode?: TabNode) {
463
- this.setState({ editingTab: tabNode });
600
+ renderTabs() {
601
+ const tabs = new Map<string, React.ReactNode>();
602
+ this.props.model.visitWindowNodes(this.windowId, (node) => {
603
+ if (node instanceof TabNode) {
604
+ const child = node as TabNode;
605
+ const selected = child.isSelected();
606
+ const path = child.getPath();
607
+
608
+ const renderTab = child.isRendered() || selected || !child.isEnableRenderOnDemand();
609
+
610
+ if (renderTab) {
611
+ // const rect = (child.getParent() as BorderNode | TabSetNode).getContentRect();
612
+ // const key = child.getId();
613
+
614
+ tabs.set(child.getId(), (
615
+ // <SizeTracker rect={rect} forceRevision={this.state.forceRevision} key={key}>
616
+ <Tab
617
+ key={child.getId()}
618
+ layout={this}
619
+ path={path}
620
+ node={child}
621
+ selected={selected} />
622
+ // </SizeTracker>
623
+ ));
624
+ }
625
+ }
626
+ });
627
+ return tabs;
464
628
  }
465
629
 
466
- /** @internal */
467
- getEditingTab() {
468
- return this.state.editingTab;
630
+ renderMetricsElements() {
631
+ return (
632
+ <div key="findBorderBarSize" ref={this.findBorderBarSizeRef} className={this.getClassName(CLASSES.FLEXLAYOUT__BORDER_SIZER)}>
633
+ FindBorderBarSize
634
+ </div>
635
+ );
469
636
  }
470
637
 
471
- /** @internal */
472
- render() {
473
- // first render will be used to find the size (via selfRef)
474
- if (!this.selfRef.current) {
475
- return (
476
- <div ref={this.selfRef} className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT)}>
477
- {this.metricsElements()}
478
- </div>
479
- );
638
+ checkForBorderToShow(x: number, y: number) {
639
+ const r = this.getBoundingClientRect(this.mainRef.current!);
640
+ const c = r.getCenter();
641
+ const margin = edgeRectWidth;
642
+ const offset = edgeRectLength / 2;
643
+
644
+ let overEdge = false;
645
+ if (this.props.model.isEnableEdgeDock() && this.state.showHiddenBorder === DockLocation.CENTER) {
646
+ if ((y > c.y - offset && y < c.y + offset) ||
647
+ (x > c.x - offset && x < c.x + offset)) {
648
+ overEdge = true;
649
+ }
480
650
  }
481
651
 
482
- this.props.model._setPointerFine(window && window.matchMedia && window.matchMedia("(pointer: fine)").matches);
483
- // this.start = Date.now();
484
- const borderComponents: React.ReactNode[] = [];
485
- const tabSetComponents: React.ReactNode[] = [];
486
- const floatingWindows: React.ReactNode[] = [];
487
- const tabComponents: Record<string, React.ReactNode> = {};
488
- const splitterComponents: React.ReactNode[] = [];
652
+ let location = DockLocation.CENTER;
653
+ if (!overEdge) {
654
+ if (x <= r.x + margin) {
655
+ location = DockLocation.LEFT;
656
+ } else if (x >= r.getRight() - margin) {
657
+ location = DockLocation.RIGHT;
658
+ } else if (y <= r.y + margin) {
659
+ location = DockLocation.TOP;
660
+ } else if (y >= r.getBottom() - margin) {
661
+ location = DockLocation.BOTTOM;
662
+ }
663
+ }
489
664
 
490
- const metrics: ILayoutMetrics = {
491
- headerBarSize: this.state.calculatedHeaderBarSize,
492
- tabBarSize: this.state.calculatedTabBarSize,
493
- borderBarSize: this.state.calculatedBorderBarSize
494
- };
495
- this.props.model._setShowHiddenBorder(this.state.showHiddenBorder);
665
+ if (location !== this.state.showHiddenBorder) {
666
+ this.setState({ showHiddenBorder: location });
667
+ }
668
+ }
496
669
 
497
- this.centerRect = this.props.model._layout(this.state.rect, metrics);
670
+ updateLayoutMetrics = () => {
671
+ if (this.findBorderBarSizeRef.current) {
672
+ const borderBarSize = this.findBorderBarSizeRef.current.getBoundingClientRect().height;
673
+ if (borderBarSize !== this.state.calculatedBorderBarSize) {
674
+ this.setState({ calculatedBorderBarSize: borderBarSize });
675
+ }
676
+ }
677
+ };
498
678
 
499
- this.renderBorder(this.props.model.getBorderSet(), borderComponents, tabComponents, floatingWindows, splitterComponents);
500
- this.renderChildren("", this.props.model.getRoot(), tabSetComponents, tabComponents, floatingWindows, splitterComponents);
679
+ tidyMoveablesMap() {
680
+ // console.log("tidyMoveablesMap");
681
+ const tabs = new Map<string, TabNode>();
682
+ this.props.model.visitNodes((node, _) => {
683
+ if (node instanceof TabNode) {
684
+ tabs.set(node.getId(), node);
685
+ }
686
+ });
501
687
 
502
- const nextTopIds: string[] = [];
503
- const nextTopIdsMap: Record<string, string> = {};
688
+ for (const [nodeId, element] of this.moveableElementMap) {
689
+ if (!tabs.has(nodeId)) {
690
+ // console.log("delete", nodeId);
691
+ element.remove(); // remove from dom
692
+ this.moveableElementMap.delete(nodeId); // remove map entry
693
+ }
694
+ }
695
+ }
696
+
697
+ reorderComponents(components: Map<string, React.ReactNode>, ids: string[]) {
698
+ const nextIds: string[] = [];
699
+ const nextIdsSet = new Set<string>();
504
700
 
701
+ let reordered: React.ReactNode[] = [];
505
702
  // Keep any previous tabs in the same DOM order as before, removing any that have been deleted
506
- for (const t of this.tabIds) {
507
- if (tabComponents[t]) {
508
- nextTopIds.push(t);
509
- nextTopIdsMap[t] = t;
703
+ for (const id of ids) {
704
+ if (components.get(id)) {
705
+ nextIds.push(id);
706
+ nextIdsSet.add(id);
510
707
  }
511
708
  }
512
- this.tabIds = nextTopIds;
709
+ ids.splice(0, ids.length, ...nextIds);
513
710
 
514
711
  // Add tabs that have been added to the DOM
515
- for (const t of Object.keys(tabComponents)) {
516
- if (!nextTopIdsMap[t]) {
517
- this.tabIds.push(t);
712
+ for (const [id, _] of components) {
713
+ if (!nextIdsSet.has(id)) {
714
+ ids.push(id);
518
715
  }
519
716
  }
520
717
 
521
- const edges: React.ReactNode[] = [];
522
- if (this.state.showEdges) {
523
- const r = this.centerRect;
524
- const length = this.edgeRectLength;
525
- const width = this.edgeRectWidth;
526
- const offset = this.edgeRectLength / 2;
527
- const className = this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT);
528
- const radius = 50;
529
- edges.push(<div key="North" style={{ top: r.y, left: r.x + r.width / 2 - offset, width: length, height: width, borderBottomLeftRadius: radius, borderBottomRightRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_TOP)}></div>);
530
- edges.push(<div key="West" style={{ top: r.y + r.height / 2 - offset, left: r.x, width: width, height: length, borderTopRightRadius: radius, borderBottomRightRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_LEFT)}></div>);
531
- edges.push(<div key="South" style={{ top: r.y + r.height - width, left: r.x + r.width / 2 - offset, width: length, height: width, borderTopLeftRadius: radius, borderTopRightRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_BOTTOM)}></div>);
532
- edges.push(<div key="East" style={{ top: r.y + r.height / 2 - offset, left: r.x + r.width - width, width: width, height: length, borderTopLeftRadius: radius, borderBottomLeftRadius: radius }} className={className + " " + this.getClassName(CLASSES.FLEXLAYOUT__EDGE_RECT_RIGHT)}></div>);
718
+ reordered = ids.map((id) => {
719
+ return components.get(id);
720
+ });
721
+
722
+ return reordered;
723
+ }
724
+
725
+ onModelChange = (action: Action) => {
726
+ this.redrawInternal("model change");
727
+ if (this.props.onModelChange) {
728
+ this.props.onModelChange(this.props.model, action);
533
729
  }
730
+ };
534
731
 
535
- // this.layoutTime = (Date.now() - this.start);
732
+ redraw(type?: string) {
733
+ // console.log("redraw", this.windowId, type);
734
+ this.mainLayout.setState((state, props) => { return { forceRevision: state.forceRevision + 1 } });
735
+ }
536
736
 
537
- return (
538
- <div ref={this.selfRef} className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT)} onDragEnter={this.props.onExternalDrag ? this.onDragEnter : undefined}>
539
- {tabSetComponents}
540
- {this.tabIds.map((t) => {
541
- return tabComponents[t];
542
- })}
543
- {borderComponents}
544
- {splitterComponents}
545
- {edges}
546
- {floatingWindows}
547
- {this.metricsElements()}
548
- {this.state.portal}
549
- </div>
550
- );
737
+ redrawInternal(type: string) {
738
+ // console.log("redrawInternal", this.windowId, type);
739
+ this.mainLayout.setState((state, props) => { return { layoutRevision: state.layoutRevision + 1 } });
551
740
  }
552
741
 
553
- /** @internal */
554
- metricsElements() {
555
- // used to measure the tab and border tab sizes
556
- const fontStyle = this.styleFont({ visibility: "hidden" });
557
- return (
558
- <React.Fragment>
559
- <div key="findHeaderBarSize" ref={this.findHeaderBarSizeRef} style={fontStyle} className={this.getClassName(CLASSES.FLEXLAYOUT__TABSET_HEADER_SIZER)}>
560
- FindHeaderBarSize
561
- </div>
562
- <div key="findTabBarSize" ref={this.findTabBarSizeRef} style={fontStyle} className={this.getClassName(CLASSES.FLEXLAYOUT__TABSET_SIZER)}>
563
- FindTabBarSize
564
- </div>
565
- <div key="findBorderBarSize" ref={this.findBorderBarSizeRef} style={fontStyle} className={this.getClassName(CLASSES.FLEXLAYOUT__BORDER_SIZER)}>
566
- FindBorderBarSize
567
- </div>
568
- </React.Fragment>
569
- );
742
+ doAction(action: Action): Node | undefined {
743
+ if (this.props.onAction !== undefined) {
744
+ const outcome = this.props.onAction(action);
745
+ if (outcome !== undefined) {
746
+ return this.props.model.doAction(outcome);
747
+ }
748
+ return undefined;
749
+ } else {
750
+ return this.props.model.doAction(action);
751
+ }
570
752
  }
571
753
 
572
- /** @internal */
573
- onCloseWindow = (id: string) => {
574
- this.doAction(Actions.unFloatTab(id));
575
- try {
576
- (this.props.model.getNodeById(id) as TabNode)._setWindow(undefined);
577
- } catch (e) {
578
- // catch incase it was a model change
754
+ updateRect = () => {
755
+ const rect = this.getDomRect()
756
+ if (!rect.equals(this.state.rect) && rect.width !== 0 && rect.height !== 0) {
757
+ // console.log("updateRect", rect.floor());
758
+ this.setState({ rect });
759
+ if (this.windowId !== Model.MAIN_WINDOW_ID) {
760
+ this.redrawInternal("rect updated");
761
+ }
579
762
  }
580
763
  };
581
764
 
582
- /** @internal */
583
- onSetWindow = (id: string, window: Window) => {
584
- (this.props.model.getNodeById(id) as TabNode)._setWindow(window);
585
- };
765
+ getBoundingClientRect(div: HTMLElement): Rect {
766
+ const layoutRect = this.getDomRect();
767
+ if (layoutRect) {
768
+ return Rect.getBoundingClientRect(div).relativeTo(layoutRect);
769
+ }
770
+ return Rect.empty();
771
+ }
586
772
 
587
- /** @internal */
588
- renderBorder(borderSet: BorderSet, borderComponents: React.ReactNode[], tabComponents: Record<string, React.ReactNode>, floatingWindows: React.ReactNode[], splitterComponents: React.ReactNode[]) {
589
- for (const border of borderSet.getBorders()) {
590
- const borderPath = `/border/${border.getLocation().getName()}`;
591
- if (border.isShowing()) {
592
- borderComponents.push(
593
- <BorderTabSet
594
- key={`border_${border.getLocation().getName()}`}
595
- path={borderPath}
596
- border={border}
597
- layout={this}
598
- iconFactory={this.props.iconFactory}
599
- titleFactory={this.props.titleFactory}
600
- icons={this.icons}
601
- />
602
- );
603
- const drawChildren = border._getDrawChildren();
604
- let i = 0;
605
- let tabCount = 0;
606
- for (const child of drawChildren) {
607
- if (child instanceof SplitterNode) {
608
- let path = borderPath + "/s";
609
- splitterComponents.push(<Splitter key={child.getId()} layout={this} node={child} path={path} />);
610
- } else if (child instanceof TabNode) {
611
- let path = borderPath + "/t" + tabCount++;
612
- if (this.supportsPopout && child.isFloating()) {
613
- const rect = this._getScreenRect(child);
614
-
615
- const tabBorderWidth = child._getAttr("borderWidth");
616
- const tabBorderHeight = child._getAttr("borderHeight");
617
- if (rect) {
618
- if (tabBorderWidth !== -1 && border.getLocation().getOrientation() === Orientation.HORZ) {
619
- rect.width = tabBorderWidth;
620
- } else if (tabBorderHeight !== -1 && border.getLocation().getOrientation() === Orientation.VERT) {
621
- rect.height = tabBorderHeight;
622
- }
623
- }
624
-
625
- floatingWindows.push(
626
- <FloatingWindow
627
- key={child.getId()}
628
- url={this.popoutURL}
629
- rect={rect}
630
- title={child.getName()}
631
- id={child.getId()}
632
- onSetWindow={this.onSetWindow}
633
- onCloseWindow={this.onCloseWindow}
634
- >
635
- <FloatingWindowTab layout={this} node={child} factory={this.props.factory} />
636
- </FloatingWindow>
637
- );
638
- tabComponents[child.getId()] = <TabFloating key={child.getId()}
639
- layout={this}
640
- path={path}
641
- node={child}
642
- selected={i === border.getSelected()
643
- } />;
644
- } else {
645
- tabComponents[child.getId()] = <Tab key={child.getId()}
646
- layout={this}
647
- path={path}
648
- node={child}
649
- selected={i === border.getSelected()}
650
- factory={this.props.factory} />;
651
- }
652
- }
653
- i++;
654
- }
655
- }
773
+ getMoveableContainer() {
774
+ return this.moveablesRef.current;
775
+ }
776
+
777
+ getMoveableElement(id: string) {
778
+ let moveableElement = this.moveableElementMap.get(id);
779
+ if (moveableElement === undefined) {
780
+ moveableElement = document.createElement("div");
781
+ this.moveablesRef.current!.appendChild(moveableElement);
782
+ moveableElement.className = CLASSES.FLEXLAYOUT__TAB_MOVEABLE;
783
+ this.moveableElementMap.set(id, moveableElement);
656
784
  }
785
+ return moveableElement;
657
786
  }
658
787
 
659
- /** @internal */
660
- renderChildren(path: string, node: RowNode | TabSetNode, tabSetComponents: React.ReactNode[], tabComponents: Record<string, React.ReactNode>, floatingWindows: React.ReactNode[], splitterComponents: React.ReactNode[]) {
661
- const drawChildren = node._getDrawChildren();
662
- let splitterCount = 0;
663
- let tabCount = 0;
664
- let rowCount = 0;
665
-
666
- for (const child of drawChildren!) {
667
- if (child instanceof SplitterNode) {
668
- const newPath = path + "/s" + (splitterCount++);
669
- splitterComponents.push(<Splitter key={child.getId()} layout={this} path={newPath} node={child} />);
670
- } else if (child instanceof TabSetNode) {
671
- const newPath = path + "/ts" + (rowCount++);
672
- tabSetComponents.push(<TabSet key={child.getId()} layout={this} path={newPath} node={child} iconFactory={this.props.iconFactory} titleFactory={this.props.titleFactory} icons={this.icons} />);
673
- this.renderChildren(newPath, child, tabSetComponents, tabComponents, floatingWindows, splitterComponents);
674
- } else if (child instanceof TabNode) {
675
- const newPath = path + "/t" + (tabCount++);
676
- const selectedTab = child.getParent()!.getChildren()[(child.getParent() as TabSetNode).getSelected()];
677
- if (selectedTab === undefined) {
678
- // this should not happen!
679
- console.warn("undefined selectedTab should not happen");
680
- }
681
- if (this.supportsPopout && child.isFloating()) {
682
- const rect = this._getScreenRect(child);
683
- floatingWindows.push(
684
- <FloatingWindow
685
- key={child.getId()}
686
- url={this.popoutURL}
687
- rect={rect}
688
- title={child.getName()}
689
- id={child.getId()}
690
- onSetWindow={this.onSetWindow}
691
- onCloseWindow={this.onCloseWindow}
692
- >
693
- <FloatingWindowTab layout={this} node={child} factory={this.props.factory} />
694
- </FloatingWindow>
695
- );
696
- tabComponents[child.getId()] = <TabFloating key={child.getId()} layout={this} path={newPath} node={child} selected={child === selectedTab} />;
697
- } else {
698
- tabComponents[child.getId()] = <Tab key={child.getId()} layout={this} path={newPath} node={child} selected={child === selectedTab} factory={this.props.factory} />;
699
- }
700
- } else {
701
- // is row
702
- const newPath = path + ((child.getOrientation() === Orientation.HORZ) ? "/r" : "/c") + (rowCount++);
703
- this.renderChildren(newPath, child as RowNode, tabSetComponents, tabComponents, floatingWindows, splitterComponents);
704
- }
788
+ getMainLayout() {
789
+ return this.mainLayout;
790
+ }
791
+
792
+ getClassName = (defaultClassName: string) => {
793
+ if (this.props.classNameMapper === undefined) {
794
+ return defaultClassName;
795
+ } else {
796
+ return this.props.classNameMapper(defaultClassName);
705
797
  }
798
+ };
799
+
800
+ getCurrentDocument() {
801
+ return this.currentDocument;
706
802
  }
707
803
 
708
- /** @internal */
709
- _getScreenRect(node: TabNode) {
710
- const rect = node!.getRect()!.clone();
711
- const bodyRect: DOMRect | undefined =
712
- this.selfRef.current?.getBoundingClientRect();
713
- if (!bodyRect) {
714
- return null;
804
+ getDomRect() {
805
+ if (this.selfRef.current) {
806
+ return Rect.fromDomRect(this.selfRef.current.getBoundingClientRect());
807
+ } else {
808
+ return Rect.empty();
715
809
  }
716
- const navHeight = Math.min(80, this.currentWindow!.outerHeight - this.currentWindow!.innerHeight);
717
- const navWidth = Math.min(80, this.currentWindow!.outerWidth - this.currentWindow!.innerWidth);
718
- rect.x = rect.x + bodyRect.x + this.currentWindow!.screenX + navWidth;
719
- rect.y = rect.y + bodyRect.y + this.currentWindow!.screenY + navHeight;
810
+ }
811
+
812
+ getWindowId() {
813
+ return this.windowId;
814
+ }
815
+
816
+ getRootDiv() {
817
+ return this.selfRef.current;
818
+ }
819
+
820
+ getMainElement() {
821
+ return this.mainRef.current;
822
+ }
823
+
824
+ getFactory() {
825
+ return this.props.factory;
826
+ }
827
+
828
+ isSupportsPopout() {
829
+ return this.supportsPopout;
830
+ }
831
+
832
+ isRealtimeResize() {
833
+ return this.props.realtimeResize ?? false;
834
+ }
835
+
836
+ getPopoutURL() {
837
+ return this.popoutURL;
838
+ }
839
+
840
+ setEditingTab(tabNode?: TabNode) {
841
+ this.setState({ editingTab: tabNode });
842
+ }
843
+
844
+ getEditingTab() {
845
+ return this.state.editingTab;
846
+ }
847
+
848
+ getModel() {
849
+ return this.props.model;
850
+ }
851
+
852
+ onCloseWindow = (windowLayout: LayoutWindow) => {
853
+ this.doAction(Actions.closeWindow(windowLayout.windowId));
854
+ };
855
+
856
+ onSetWindow = (windowLayout: LayoutWindow, window: Window) => {
857
+ };
858
+
859
+ getScreenRect(inRect: Rect) {
860
+ const rect = inRect.clone();
861
+ const layoutRect = this.getDomRect();
862
+ // Note: outerHeight can be less than innerHeight when window is zoomed, so cannot use
863
+ // const navHeight = Math.min(65, this.currentWindow!.outerHeight - this.currentWindow!.innerHeight);
864
+ // const navWidth = Math.min(65, this.currentWindow!.outerWidth - this.currentWindow!.innerWidth);
865
+ const navHeight = 60;
866
+ const navWidth = 2;
867
+ // console.log(rect.y, this.currentWindow!.screenX,layoutRect.y);
868
+ rect.x = this.currentWindow!.screenX + this.currentWindow!.scrollX + navWidth / 2 + layoutRect.x + rect.x;
869
+ rect.y = this.currentWindow!.screenY + this.currentWindow!.scrollY + (navHeight - navWidth / 2) + layoutRect.y + rect.y;
870
+ rect.height += navHeight;
871
+ rect.width += navWidth;
720
872
  return rect;
721
873
  }
722
874
 
723
- /**
724
- * Adds a new tab to the given tabset
725
- * @param tabsetId the id of the tabset where the new tab will be added
726
- * @param json the json for the new tab node
727
- * @returns the added tab node or undefined
728
- */
729
- addTabToTabSet(tabsetId: string, json: IJsonTabNode) : TabNode | undefined {
875
+ addTabToTabSet(tabsetId: string, json: IJsonTabNode): TabNode | undefined {
730
876
  const tabsetNode = this.props.model.getNodeById(tabsetId);
731
877
  if (tabsetNode !== undefined) {
732
878
  const node = this.doAction(Actions.addNode(json, tabsetId, DockLocation.CENTER, -1));
@@ -735,13 +881,8 @@ export class Layout extends React.Component<ILayoutProps, ILayoutState> {
735
881
  return undefined;
736
882
  }
737
883
 
738
- /**
739
- * Adds a new tab to the active tabset (if there is one)
740
- * @param json the json for the new tab node
741
- * @returns the added tab node or undefined
742
- */
743
- addTabToActiveTabSet(json: IJsonTabNode) : TabNode | undefined {
744
- const tabsetNode = this.props.model.getActiveTabset();
884
+ addTabToActiveTabSet(json: IJsonTabNode): TabNode | undefined {
885
+ const tabsetNode = this.props.model.getActiveTabset(this.windowId);
745
886
  if (tabsetNode !== undefined) {
746
887
  const node = this.doAction(Actions.addNode(json, tabsetNode.getId(), DockLocation.CENTER, -1));
747
888
  return node as TabNode;
@@ -749,552 +890,435 @@ export class Layout extends React.Component<ILayoutProps, ILayoutState> {
749
890
  return undefined;
750
891
  }
751
892
 
752
- /**
753
- * Adds a new tab by dragging a labeled panel to the drop location, dragging starts immediatelly
754
- * @param dragText the text to show on the drag panel
755
- * @param json the json for the new tab node
756
- * @param onDrop a callback to call when the drag is complete (node and event will be undefined if the drag was cancelled)
757
- */
758
- addTabWithDragAndDrop(dragText: string | undefined, json: IJsonTabNode, onDrop?: (node?: Node, event?: Event) => void) {
759
- this.fnNewNodeDropped = onDrop;
760
- this.newTabJson = json;
761
- this.dragStart(undefined, dragText, TabNode._fromJson(json, this.props.model, false), true, undefined, undefined);
893
+ showControlInPortal = (control: React.ReactNode, element: HTMLElement) => {
894
+ const portal = createPortal(control, element) as React.ReactPortal;
895
+ this.setState({ portal });
896
+ };
897
+
898
+ hideControlInPortal = () => {
899
+ this.setState({ portal: undefined });
900
+ };
901
+
902
+ getIcons = () => {
903
+ return this.icons;
904
+ };
905
+
906
+ maximize(tabsetNode: TabSetNode) {
907
+ this.doAction(Actions.maximizeToggle(tabsetNode.getId(), this.getWindowId()));
908
+ }
909
+
910
+ customizeTab(
911
+ tabNode: TabNode,
912
+ renderValues: ITabRenderValues,
913
+ ) {
914
+ if (this.props.onRenderTab) {
915
+ this.props.onRenderTab(tabNode, renderValues);
916
+ }
917
+ }
918
+
919
+ customizeTabSet(
920
+ tabSetNode: TabSetNode | BorderNode,
921
+ renderValues: ITabSetRenderValues,
922
+ ) {
923
+ if (this.props.onRenderTabSet) {
924
+ this.props.onRenderTabSet(tabSetNode, renderValues);
925
+ }
926
+ }
927
+
928
+ i18nName(id: I18nLabel, param?: string) {
929
+ let message;
930
+ if (this.props.i18nMapper) {
931
+ message = this.props.i18nMapper(id, param);
932
+ }
933
+ if (message === undefined) {
934
+ message = id + (param === undefined ? "" : param);
935
+ }
936
+ return message;
937
+ }
938
+
939
+ getShowOverflowMenu() {
940
+ return this.props.onShowOverflowMenu;
941
+ }
942
+
943
+ getTabSetPlaceHolderCallback() {
944
+ return this.props.onTabSetPlaceHolder;
762
945
  }
763
946
 
764
- /**
765
- * Move a tab/tabset using drag and drop
766
- * @param node the tab or tabset to drag
767
- * @param dragText the text to show on the drag panel
768
- */
769
- moveTabWithDragAndDrop(node: (TabNode | TabSetNode), dragText?: string) {
770
- this.dragStart(undefined, dragText, node, true, undefined, undefined);
947
+ showContextMenu(node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>) {
948
+ if (this.props.onContextMenu) {
949
+ this.props.onContextMenu(node, event);
950
+ }
771
951
  }
772
952
 
773
- /**
774
- * Adds a new tab by dragging a labeled panel to the drop location, dragging starts when you
775
- * mouse down on the panel
776
- *
777
- * @param dragText the text to show on the drag panel
778
- * @param json the json for the new tab node
779
- * @param onDrop a callback to call when the drag is complete (node and event will be undefined if the drag was cancelled)
780
- */
781
- addTabWithDragAndDropIndirect(dragText: string | undefined, json: IJsonTabNode, onDrop?: (node?: Node, event?: Event) => void) {
782
- this.fnNewNodeDropped = onDrop;
783
- this.newTabJson = json;
784
-
785
- DragDrop.instance.addGlass(this.onCancelAdd);
786
-
787
- this.dragDivText = dragText;
788
- this.dragDiv = this.currentDocument!.createElement("div");
789
- this.dragDiv.className = this.getClassName(CLASSES.FLEXLAYOUT__DRAG_RECT);
790
- this.dragDiv.addEventListener("mousedown", this.onDragDivMouseDown);
791
- this.dragDiv.addEventListener("touchstart", this.onDragDivMouseDown, { passive: false });
792
-
793
- this.dragRectRender(this.dragDivText, undefined, this.newTabJson, () => {
794
- if (this.dragDiv) {
795
- // now it's been rendered into the dom it can be centered
796
- this.dragDiv.style.visibility = "visible";
797
- const domRect = this.dragDiv.getBoundingClientRect();
798
- const r = new Rect(0, 0, domRect?.width, domRect?.height);
799
- r.centerInRect(this.state.rect);
800
- this.dragDiv.setAttribute("data-layout-path", "/drag-rectangle");
801
- this.dragDiv.style.left = r.x + "px";
802
- this.dragDiv.style.top = r.y + "px";
803
- }
804
- });
953
+ auxMouseClick(node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>) {
954
+ if (this.props.onAuxMouseClick) {
955
+ this.props.onAuxMouseClick(node, event);
956
+ }
957
+ }
805
958
 
806
- const rootdiv = this.selfRef.current;
807
- rootdiv!.appendChild(this.dragDiv);
959
+ public showOverlay(show: boolean) {
960
+ this.setState({ showOverlay: show });
961
+ enablePointerOnIFrames(!show, this.currentDocument!);
808
962
  }
809
963
 
810
- /** @internal */
811
- onCancelAdd = () => {
812
- const rootdiv = this.selfRef.current;
813
- if (rootdiv && this.dragDiv) {
814
- rootdiv.removeChild(this.dragDiv);
815
- }
816
- this.dragDiv = undefined;
817
- this.hidePortal();
818
- if (this.fnNewNodeDropped != null) {
819
- this.fnNewNodeDropped();
820
- this.fnNewNodeDropped = undefined;
821
- }
822
964
 
823
- try {
824
- this.customDrop?.invalidated?.()
825
- } catch (e) {
826
- console.error(e)
827
- }
828
965
 
829
- DragDrop.instance.hideGlass();
830
- this.newTabJson = undefined;
831
- this.customDrop = undefined;
832
- };
966
+ // *************************** Start Drag Drop *************************************
833
967
 
834
- /** @internal */
835
- onCancelDrag = (wasDragging: boolean) => {
836
- if (wasDragging) {
837
- const rootdiv = this.selfRef.current;
968
+ addTabWithDragAndDrop(event: DragEvent, json: IJsonTabNode, onDrop?: (node?: Node, event?: React.DragEvent<HTMLElement>) => void) {
969
+ const tempNode = TabNode.fromJson(json, this.props.model, false);
970
+ LayoutInternal.dragState = new DragState(this.mainLayout, DragSource.Add, tempNode, json, onDrop);
971
+ }
838
972
 
839
- const outlineDiv = this.outlineDiv;
840
- if (rootdiv && outlineDiv) {
841
- try {
842
- rootdiv.removeChild(outlineDiv);
843
- } catch (e) {}
844
- }
973
+ moveTabWithDragAndDrop(event: DragEvent, node: (TabNode | TabSetNode)) {
974
+ this.setDragNode(event, node);
975
+ }
845
976
 
846
- const dragDiv = this.dragDiv;
847
- if (rootdiv && dragDiv) {
848
- try {
849
- rootdiv.removeChild(dragDiv);
850
- } catch (e) {}
977
+ public setDragNode = (event: DragEvent, node: Node & IDraggable) => {
978
+ LayoutInternal.dragState = new DragState(this.mainLayout, DragSource.Internal, node, undefined, undefined);
979
+ // Note: can only set (very) limited types on android! so cannot set json
980
+ // Note: must set text/plain for android to allow drag,
981
+ // so just set a simple message indicating its a flexlayout drag (this is not used anywhere else)
982
+ event.dataTransfer!.setData('text/plain', "--flexlayout--");
983
+ event.dataTransfer!.effectAllowed = "copyMove";
984
+ event.dataTransfer!.dropEffect = "move";
985
+
986
+ this.dragEnterCount = 0;
987
+
988
+ if (node instanceof TabSetNode) {
989
+ let rendered = false;
990
+ let content = this.i18nName(I18nLabel.Move_Tabset);
991
+ if (node.getChildren().length > 0) {
992
+ content = this.i18nName(I18nLabel.Move_Tabs).replace("?", String(node.getChildren().length));
851
993
  }
852
-
853
- this.dragDiv = undefined;
854
- this.hidePortal();
855
- this.setState({ showEdges: false });
856
- if (this.fnNewNodeDropped != null) {
857
- this.fnNewNodeDropped();
858
- this.fnNewNodeDropped = undefined;
994
+ if (this.props.onRenderDragRect) {
995
+ const dragComponent = this.props.onRenderDragRect(content, node, undefined);
996
+ if (dragComponent) {
997
+ this.setDragComponent(event, dragComponent, 10, 10);
998
+ rendered = true;
999
+ }
859
1000
  }
860
-
861
- try {
862
- this.customDrop?.invalidated?.()
863
- } catch (e) {
864
- console.error(e)
1001
+ if (!rendered) {
1002
+ this.setDragComponent(event, content, 10, 10);
1003
+ }
1004
+ } else {
1005
+ const element = event.target as HTMLElement;
1006
+ const rect = element.getBoundingClientRect();
1007
+ const offsetX = event.clientX - rect.left;
1008
+ const offsetY = event.clientY - rect.top;
1009
+ const parentNode = node?.getParent();
1010
+ const isInVerticalBorder = parentNode instanceof BorderNode && (parentNode as BorderNode).getOrientation() === Orientation.HORZ;
1011
+ const x = isInVerticalBorder ? 10 : offsetX;
1012
+ const y = isInVerticalBorder ? 10 : offsetY;
1013
+
1014
+ let rendered = false;
1015
+ if (this.props.onRenderDragRect) {
1016
+ const content = <TabButtonStamp key={node.getId()} layout={this} node={node as TabNode} />;
1017
+ const dragComponent = this.props.onRenderDragRect(content, node, undefined);
1018
+ if (dragComponent) {
1019
+ this.setDragComponent(event, dragComponent, x, y);
1020
+ rendered = true;
1021
+ }
1022
+ }
1023
+ if (!rendered) {
1024
+ if (isSafari()) { // safari doesnt render the offscreen tabstamps
1025
+ this.setDragComponent(event, <TabButtonStamp node={node as TabNode} layout={this}/>, x,y);
1026
+ } else {
1027
+ event.dataTransfer!.setDragImage((node as TabNode).getTabStamp()!, x, y);
1028
+ }
865
1029
  }
866
-
867
- DragDrop.instance.hideGlass();
868
- this.newTabJson = undefined;
869
- this.customDrop = undefined;
870
1030
  }
871
- this.setState({ showHiddenBorder: DockLocation.CENTER });
872
-
873
1031
  };
874
1032
 
875
- /** @internal */
876
- onDragDivMouseDown = (event: Event) => {
877
- event.preventDefault();
878
- this.dragStart(event, this.dragDivText, TabNode._fromJson(this.newTabJson, this.props.model, false), true, undefined, undefined);
879
- };
880
1033
 
881
- /** @internal */
882
- dragStart = (
883
- event: Event | React.MouseEvent<HTMLDivElement, MouseEvent> | React.TouchEvent<HTMLDivElement> | React.DragEvent<HTMLDivElement> | undefined,
884
- dragDivText: string | undefined,
885
- node: Node & IDraggable,
886
- allowDrag: boolean,
887
- onClick?: (event: Event) => void,
888
- onDoubleClick?: (event: Event) => void
889
- ) => {
890
- if (!allowDrag) {
891
- DragDrop.instance.startDrag(
892
- event,
893
- undefined,
894
- undefined,
895
- undefined,
896
- undefined,
897
- onClick,
898
- onDoubleClick,
899
- this.currentDocument,
900
- this.selfRef.current ?? undefined
901
- );
902
- } else {
903
- this.dragNode = node;
904
- this.dragDivText = dragDivText;
905
- DragDrop.instance.startDrag(
906
- event,
907
- this.onDragStart,
908
- this.onDragMove,
909
- this.onDragEnd,
910
- this.onCancelDrag,
911
- onClick,
912
- onDoubleClick,
913
- this.currentDocument,
914
- this.selfRef.current ?? undefined
915
- );
916
- }
917
- };
918
1034
 
919
- /** @internal */
920
- dragRectRender = (text: String | undefined, node?: Node, json?: IJsonTabNode, onRendered?: () => void) => {
921
- let content: React.ReactElement | undefined;
1035
+ public setDragComponent(event: DragEvent, component: React.ReactNode, x: number, y: number) {
1036
+ let dragElement: JSX.Element = (
1037
+ <div style={{ position: "unset" }}
1038
+ className={this.getClassName(CLASSES.FLEXLAYOUT__LAYOUT) + " " + this.getClassName(CLASSES.FLEXLAYOUT__DRAG_RECT)}>
1039
+ {component}
1040
+ </div>
1041
+ );
922
1042
 
923
- if (text !== undefined) {
924
- content = <div style={{ whiteSpace: "pre" }}>{text.replace("<br>", "\n")}</div>;
925
- } else {
926
- if (node && node instanceof TabNode) {
927
- content = (<TabButtonStamp
928
- node={node}
929
- layout={this}
930
- iconFactory={this.props.iconFactory}
931
- titleFactory={this.props.titleFactory}
932
- />);
1043
+ const tempDiv = this.currentDocument!.createElement('div');
1044
+ tempDiv.setAttribute("data-layout-path", "/drag-rectangle");
1045
+ tempDiv.style.position = "absolute";
1046
+ tempDiv.style.left = "-10000px";
1047
+ tempDiv.style.top = "-10000px";
1048
+ this.currentDocument!.body.appendChild(tempDiv);
1049
+ createRoot(tempDiv).render(dragElement);
1050
+
1051
+ event.dataTransfer!.setDragImage(tempDiv, x, y);
1052
+ setTimeout(() => {
1053
+ this.currentDocument!.body.removeChild(tempDiv!);
1054
+ }, 0);
1055
+ }
1056
+
1057
+ setDraggingOverWindow(overWindow: boolean) {
1058
+ // console.log("setDraggingOverWindow", overWindow);
1059
+ if (this.isDraggingOverWindow !== overWindow) {
1060
+ if (this.outlineDiv) {
1061
+ this.outlineDiv!.style.visibility = overWindow ? "hidden" : "visible";
933
1062
  }
934
- }
935
1063
 
936
- if (this.props.onRenderDragRect !== undefined) {
937
- const customContent = this.props.onRenderDragRect(content, node, json);
938
- if (customContent !== undefined) {
939
- content = customContent;
1064
+ if (overWindow) {
1065
+ this.setState({ showEdges: false });
1066
+ } else {
1067
+ // add edge indicators
1068
+ if (this.props.model.getMaximizedTabset(this.windowId) === undefined) {
1069
+ this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
1070
+ }
940
1071
  }
941
- }
942
1072
 
943
- // hide div until the render is complete
944
- this.dragRectRendered = false;
945
- const dragDiv = this.dragDiv;
946
- if (dragDiv) {
947
- dragDiv.style.visibility = "hidden";
948
- this.showPortal(
949
- <DragRectRenderWrapper
950
- // wait for it to be rendered
951
- onRendered={() => {
952
- this.dragRectRendered = true;
953
- onRendered?.();
954
- }}>
955
- {content}
956
- </DragRectRenderWrapper>,
957
- dragDiv,
958
- );
1073
+ this.isDraggingOverWindow = overWindow;
959
1074
  }
960
- };
961
-
962
- /** @internal */
963
- showPortal = (control: React.ReactNode, element: HTMLElement) => {
964
- const portal = createPortal(control, element) as React.ReactPortal;
965
- this.setState({ portal });
966
- };
967
-
968
- /** @internal */
969
- hidePortal = () => {
970
- this.setState({ portal: undefined });
971
- };
1075
+ }
972
1076
 
973
- /** @internal */
974
- onDragStart = () => {
975
- this.dropInfo = undefined;
976
- this.customDrop = undefined;
977
- const rootdiv = this.selfRef.current;
978
- this.outlineDiv = this.currentDocument!.createElement("div");
979
- this.outlineDiv.className = this.getClassName(CLASSES.FLEXLAYOUT__OUTLINE_RECT);
980
- this.outlineDiv.style.visibility = "hidden";
981
- if (rootdiv) {
982
- rootdiv.appendChild(this.outlineDiv);
1077
+ onDragEnterRaw = (event: React.DragEvent<HTMLElement>) => {
1078
+ this.dragEnterCount++;
1079
+ if (this.dragEnterCount === 1) {
1080
+ this.onDragEnter(event);
983
1081
  }
1082
+ }
984
1083
 
985
- if (this.dragDiv == null) {
986
- this.dragDiv = this.currentDocument!.createElement("div");
987
- this.dragDiv.className = this.getClassName(CLASSES.FLEXLAYOUT__DRAG_RECT);
988
- this.dragDiv.setAttribute("data-layout-path", "/drag-rectangle");
989
- this.dragRectRender(this.dragDivText, this.dragNode, this.newTabJson);
1084
+ onDragLeaveRaw = (event: React.DragEvent<HTMLElement>) => {
1085
+ this.dragEnterCount--;
1086
+ if (this.dragEnterCount === 0) {
1087
+ this.onDragLeave(event);
1088
+ }
1089
+ }
990
1090
 
991
- if (rootdiv) {
992
- rootdiv.appendChild(this.dragDiv);
993
- }
1091
+ clearDragMain() {
1092
+ // console.log("clear drag main");
1093
+ LayoutInternal.dragState = undefined;
1094
+ if (this.windowId === Model.MAIN_WINDOW_ID) {
1095
+ this.isDraggingOverWindow = false;
994
1096
  }
995
- // add edge indicators
996
- if (this.props.model.getMaximizedTabset() === undefined) {
997
- this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
1097
+ for (const [, layoutWindow] of this.props.model.getwindowsMap()) {
1098
+ // console.log(layoutWindow);
1099
+ layoutWindow.layout!.clearDragLocal();
998
1100
  }
1101
+ }
999
1102
 
1000
- if (this.dragNode && this.outlineDiv && this.dragNode instanceof TabNode && this.dragNode.getTabRect() !== undefined) {
1001
- this.dragNode.getTabRect()?.positionElement(this.outlineDiv);
1103
+ clearDragLocal() {
1104
+ // console.log("clear drag local", this.windowId);
1105
+ this.setState({ showEdges: false });
1106
+ this.showOverlay(false);
1107
+ this.dragEnterCount = 0;
1108
+ this.dragging = false;
1109
+ if (this.outlineDiv) {
1110
+ this.selfRef.current!.removeChild(this.outlineDiv);
1111
+ this.outlineDiv = undefined;
1002
1112
  }
1003
- this.firstMove = true;
1113
+ }
1004
1114
 
1005
- return true;
1006
- };
1115
+ onDragEnter = (event: React.DragEvent<HTMLElement>) => {
1116
+ // console.log("onDragEnter", this.windowId, this.dragEnterCount);
1007
1117
 
1008
- /** @internal */
1009
- onDragMove = (event: React.MouseEvent<Element>) => {
1010
- if (this.firstMove === false) {
1011
- const speed = this.props.model._getAttribute("tabDragSpeed") as number;
1012
- if (this.outlineDiv) {
1013
- this.outlineDiv.style.transition = `top ${speed}s, left ${speed}s, width ${speed}s, height ${speed}s`;
1118
+ if (!LayoutInternal.dragState && this.props.onExternalDrag) { // not internal dragging
1119
+ const externalDrag = this.props.onExternalDrag!(event);
1120
+ if (externalDrag) {
1121
+ const tempNode = TabNode.fromJson(externalDrag.json, this.props.model, false);
1122
+ LayoutInternal.dragState = new DragState(this.mainLayout, DragSource.External, tempNode, externalDrag.json, externalDrag.onDrop);
1014
1123
  }
1015
1124
  }
1016
- this.firstMove = false;
1017
- const clientRect = this.selfRef.current?.getBoundingClientRect();
1018
- const pos = {
1019
- x: event.clientX - (clientRect?.left ?? 0),
1020
- y: event.clientY - (clientRect?.top ?? 0),
1021
- };
1022
-
1023
- this.checkForBorderToShow(pos.x, pos.y);
1024
1125
 
1025
- // keep it between left & right
1026
- const dragRect = this.dragDiv?.getBoundingClientRect() ?? new DOMRect(0, 0, 100, 100);
1027
- let newLeft = pos.x - dragRect.width / 2;
1028
- if (newLeft + dragRect.width > (clientRect?.width ?? 0)) {
1029
- newLeft = (clientRect?.width ?? 0) - dragRect.width;
1030
- }
1031
- newLeft = Math.max(0, newLeft);
1032
-
1033
- if (this.dragDiv) {
1034
- this.dragDiv.style.left = newLeft + "px";
1035
- this.dragDiv.style.top = pos.y + 5 + "px";
1036
- if (this.dragRectRendered && this.dragDiv.style.visibility === "hidden") {
1037
- // make visible once the drag rect has been rendered
1038
- this.dragDiv.style.visibility = "visible";
1126
+ if (LayoutInternal.dragState) {
1127
+ if (this.windowId !== Model.MAIN_WINDOW_ID && LayoutInternal.dragState.mainLayout === this.mainLayout) {
1128
+ LayoutInternal.dragState.mainLayout.setDraggingOverWindow(true);
1039
1129
  }
1040
- }
1041
1130
 
1042
- let dropInfo = this.props.model._findDropTargetNode(this.dragNode!, pos.x, pos.y);
1043
- if (dropInfo) {
1044
- if (this.props.onTabDrag) {
1045
- this.handleCustomTabDrag(dropInfo, pos, event);
1046
- } else {
1047
- this.dropInfo = dropInfo;
1048
- if (this.outlineDiv) {
1049
- this.outlineDiv.className = this.getClassName(dropInfo.className);
1050
- dropInfo.rect.positionElement(this.outlineDiv);
1051
- this.outlineDiv.style.visibility = "visible";
1052
- }
1131
+ if (LayoutInternal.dragState.mainLayout !== this.mainLayout) {
1132
+ return; // drag not by this layout or its popouts
1053
1133
  }
1054
- }
1055
- };
1056
1134
 
1057
- /** @internal */
1058
- onDragEnd = (event: Event) => {
1059
- const rootdiv = this.selfRef.current;
1060
- if (rootdiv) {
1061
- if (this.outlineDiv) {
1062
- rootdiv.removeChild(this.outlineDiv);
1063
- }
1064
- if (this.dragDiv) {
1065
- rootdiv.removeChild(this.dragDiv);
1135
+ event.preventDefault();
1136
+
1137
+ this.dropInfo = undefined;
1138
+ const rootdiv = this.selfRef.current;
1139
+ this.outlineDiv = this.currentDocument!.createElement("div");
1140
+ this.outlineDiv.className = this.getClassName(CLASSES.FLEXLAYOUT__OUTLINE_RECT);
1141
+ this.outlineDiv.style.visibility = "hidden";
1142
+ const speed = this.props.model.getAttribute("tabDragSpeed") as number;
1143
+ this.outlineDiv.style.transition = `top ${speed}s, left ${speed}s, width ${speed}s, height ${speed}s`;
1144
+
1145
+ rootdiv!.appendChild(this.outlineDiv);
1146
+
1147
+ this.dragging = true;
1148
+ this.showOverlay(true);
1149
+ // add edge indicators
1150
+ if (!this.isDraggingOverWindow && this.props.model.getMaximizedTabset(this.windowId) === undefined) {
1151
+ this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
1066
1152
  }
1153
+
1154
+ const clientRect = this.selfRef.current?.getBoundingClientRect()!;
1155
+ const r = new Rect(
1156
+ event.clientX - (clientRect.left),
1157
+ event.clientY - (clientRect.top),
1158
+ 1, 1
1159
+ );
1160
+ r.positionElement(this.outlineDiv);
1067
1161
  }
1068
- this.dragDiv = undefined;
1069
- this.hidePortal();
1162
+ }
1070
1163
 
1071
- this.setState({ showEdges: false });
1072
- DragDrop.instance.hideGlass();
1073
-
1074
- if (this.dropInfo) {
1075
- if (this.customDrop) {
1076
- this.newTabJson = undefined;
1077
-
1078
- try {
1079
- const { callback, dragging, over, x, y, location } = this.customDrop;
1080
- callback(dragging, over, x, y, location);
1081
- if (this.fnNewNodeDropped != null) {
1082
- this.fnNewNodeDropped();
1083
- this.fnNewNodeDropped = undefined;
1084
- }
1085
- } catch (e) {
1086
- console.error(e)
1087
- }
1088
- } else if (this.newTabJson !== undefined) {
1089
- const newNode = this.doAction(Actions.addNode(this.newTabJson, this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index));
1164
+ onDragOver = (event: React.DragEvent<HTMLElement>) => {
1165
+ if (this.dragging && !this.isDraggingOverWindow) {
1166
+ // console.log("onDragOver");
1090
1167
 
1091
- if (this.fnNewNodeDropped != null) {
1092
- this.fnNewNodeDropped(newNode, event);
1093
- this.fnNewNodeDropped = undefined;
1094
- }
1095
- this.newTabJson = undefined;
1096
- } else if (this.dragNode !== undefined) {
1097
- this.doAction(Actions.moveNode(this.dragNode.getId(), this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index));
1098
- }
1099
- }
1100
- this.setState({ showHiddenBorder: DockLocation.CENTER });
1101
- };
1168
+ event.preventDefault();
1169
+ const clientRect = this.selfRef.current?.getBoundingClientRect();
1170
+ const pos = {
1171
+ x: event.clientX - (clientRect?.left ?? 0),
1172
+ y: event.clientY - (clientRect?.top ?? 0),
1173
+ };
1102
1174
 
1103
- /** @internal */
1104
- private handleCustomTabDrag(dropInfo: DropInfo, pos: { x: number; y: number; }, event: React.MouseEvent<Element, MouseEvent>) {
1105
- let invalidated = this.customDrop?.invalidated;
1106
- const currentCallback = this.customDrop?.callback;
1107
- this.customDrop = undefined;
1108
-
1109
- const dragging = this.newTabJson || (this.dragNode instanceof TabNode ? this.dragNode : undefined);
1110
- if (dragging && (dropInfo.node instanceof TabSetNode || dropInfo.node instanceof BorderNode) && dropInfo.index === -1) {
1111
- const selected = dropInfo.node.getSelectedNode() as TabNode | undefined;
1112
- const tabRect = selected?.getRect();
1113
-
1114
- if (selected && tabRect?.contains(pos.x, pos.y)) {
1115
- let customDrop: ICustomDropDestination | undefined = undefined;
1116
-
1117
- try {
1118
- const dest = this.onTabDrag(dragging, selected, pos.x - tabRect.x, pos.y - tabRect.y, dropInfo.location, () => this.onDragMove(event));
1119
-
1120
- if (dest) {
1121
- customDrop = {
1122
- rect: new Rect(dest.x + tabRect.x, dest.y + tabRect.y, dest.width, dest.height),
1123
- callback: dest.callback,
1124
- invalidated: dest.invalidated,
1125
- dragging: dragging,
1126
- over: selected,
1127
- x: pos.x - tabRect.x,
1128
- y: pos.y - tabRect.y,
1129
- location: dropInfo.location,
1130
- cursor: dest.cursor
1131
- };
1132
- }
1133
- } catch (e) {
1134
- console.error(e);
1135
- }
1175
+ this.checkForBorderToShow(pos.x, pos.y);
1136
1176
 
1137
- if (customDrop?.callback === currentCallback) {
1138
- invalidated = undefined;
1177
+ let dropInfo = this.props.model.findDropTargetNode(this.windowId, LayoutInternal.dragState!.dragNode!, pos.x, pos.y);
1178
+ if (dropInfo) {
1179
+ this.dropInfo = dropInfo;
1180
+ if (this.outlineDiv) {
1181
+ this.outlineDiv.className = this.getClassName(dropInfo.className);
1182
+ dropInfo.rect.positionElement(this.outlineDiv);
1183
+ this.outlineDiv.style.visibility = "visible";
1139
1184
  }
1140
-
1141
- this.customDrop = customDrop;
1142
1185
  }
1143
1186
  }
1187
+ }
1144
1188
 
1145
- this.dropInfo = dropInfo;
1146
- if (this.outlineDiv) {
1147
- this.outlineDiv.className = this.getClassName(this.customDrop ? CLASSES.FLEXLAYOUT__OUTLINE_RECT : dropInfo.className);
1148
- if (this.customDrop) {
1149
- this.customDrop.rect.positionElement(this.outlineDiv);
1150
- } else {
1151
- dropInfo.rect.positionElement(this.outlineDiv);
1189
+ onDragLeave = (event: React.DragEvent<HTMLElement>) => {
1190
+ // console.log("onDragLeave", this.windowId, this.dragging);
1191
+ if (this.dragging) {
1192
+ if (this.windowId !== Model.MAIN_WINDOW_ID) {
1193
+ LayoutInternal.dragState!.mainLayout.setDraggingOverWindow(false);
1152
1194
  }
1153
- }
1154
-
1155
- DragDrop.instance.setGlassCursorOverride(this.customDrop?.cursor);
1156
- if (this.outlineDiv) {
1157
- this.outlineDiv.style.visibility = "visible";
1158
- }
1159
1195
 
1160
- try {
1161
- invalidated?.();
1162
- } catch (e) {
1163
- console.error(e);
1164
- }
1165
- }
1166
-
1167
- /** @internal */
1168
- onDragEnter(event: React.DragEvent<HTMLDivElement>) {
1169
- // DragDrop keeps track of number of dragenters minus the number of
1170
- // dragleaves. Only start a new drag if there isn't one already.
1171
- if (DragDrop.instance.isDragging())
1172
- return;
1173
- const drag = this.props.onExternalDrag!(event);
1174
- if (drag) {
1175
- // Mimic addTabWithDragAndDrop, but pass in DragEvent
1176
- this.fnNewNodeDropped = drag.onDrop;
1177
- this.newTabJson = drag.json;
1178
- this.dragStart(event, drag.dragText, TabNode._fromJson(drag.json, this.props.model, false), true, undefined, undefined);
1196
+ this.clearDragLocal();
1179
1197
  }
1180
1198
  }
1181
1199
 
1200
+ onDrop = (event: React.DragEvent<HTMLElement>) => {
1201
+ // console.log("ondrop", this.windowId, this.dragging, Layout.dragState);
1182
1202
 
1183
- /** @internal */
1184
- checkForBorderToShow(x: number, y: number) {
1185
- const r = this.props.model._getOuterInnerRects().outer;
1186
- const c = r.getCenter();
1187
- const margin = this.edgeRectWidth;
1188
- const offset = this.edgeRectLength / 2;
1203
+ if (this.dragging) {
1204
+ event.preventDefault();
1189
1205
 
1190
- let overEdge = false;
1191
- if (this.props.model.isEnableEdgeDock() && this.state.showHiddenBorder === DockLocation.CENTER) {
1192
- if ((y > c.y - offset && y < c.y + offset) ||
1193
- (x > c.x - offset && x < c.x + offset)) {
1194
- overEdge = true;
1195
- }
1196
- }
1206
+ const dragState = LayoutInternal.dragState!;
1207
+ if (this.dropInfo) {
1208
+ if (dragState.dragJson !== undefined) {
1209
+ const newNode = this.doAction(Actions.addNode(dragState.dragJson, this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index));
1197
1210
 
1198
- let location = DockLocation.CENTER;
1199
- if (!overEdge) {
1200
- if (x <= r.x + margin) {
1201
- location = DockLocation.LEFT;
1202
- } else if (x >= r.getRight() - margin) {
1203
- location = DockLocation.RIGHT;
1204
- } else if (y <= r.y + margin) {
1205
- location = DockLocation.TOP;
1206
- } else if (y >= r.getBottom() - margin) {
1207
- location = DockLocation.BOTTOM;
1211
+ if (dragState.fnNewNodeDropped !== undefined) {
1212
+ dragState.fnNewNodeDropped(newNode, event);
1213
+ }
1214
+ } else if (dragState.dragNode !== undefined) {
1215
+ this.doAction(Actions.moveNode(dragState.dragNode.getId(), this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index));
1216
+ }
1208
1217
  }
1209
- }
1210
1218
 
1211
- if (location !== this.state.showHiddenBorder) {
1212
- this.setState({ showHiddenBorder: location });
1219
+ this.mainLayout.clearDragMain();
1213
1220
  }
1221
+ this.dragEnterCount = 0; // must set to zero here ref sublayouts
1214
1222
  }
1215
1223
 
1216
- /** @internal */
1217
- maximize(tabsetNode: TabSetNode) {
1218
- this.doAction(Actions.maximizeToggle(tabsetNode.getId()));
1219
- }
1224
+ // *************************** End Drag Drop *************************************
1225
+ }
1220
1226
 
1221
- /** @internal */
1222
- customizeTab(
1223
- tabNode: TabNode,
1224
- renderValues: ITabRenderValues,
1225
- ) {
1226
- if (this.props.onRenderTab) {
1227
- this.props.onRenderTab(tabNode, renderValues);
1228
- }
1229
- }
1227
+ export type DragRectRenderCallback = (
1228
+ content: React.ReactNode | undefined,
1229
+ node?: Node,
1230
+ json?: IJsonTabNode
1231
+ ) => React.ReactNode | undefined;
1230
1232
 
1231
- /** @internal */
1232
- customizeTabSet(
1233
- tabSetNode: TabSetNode | BorderNode,
1234
- renderValues: ITabSetRenderValues,
1235
- ) {
1236
- if (this.props.onRenderTabSet) {
1237
- this.props.onRenderTabSet(tabSetNode, renderValues);
1238
- }
1239
- }
1233
+ export type NodeMouseEvent = (
1234
+ node: TabNode | TabSetNode | BorderNode,
1235
+ event: React.MouseEvent<HTMLElement, MouseEvent>
1236
+ ) => void;
1240
1237
 
1241
- /** @internal */
1242
- i18nName(id: I18nLabel, param?: string) {
1243
- let message;
1244
- if (this.props.i18nMapper) {
1245
- message = this.props.i18nMapper(id, param);
1246
- }
1247
- if (message === undefined) {
1248
- message = id + (param === undefined ? "" : param);
1249
- }
1250
- return message;
1251
- }
1238
+ export type ShowOverflowMenuCallback = (
1239
+ node: TabSetNode | BorderNode,
1240
+ mouseEvent: React.MouseEvent<HTMLElement, MouseEvent>,
1241
+ items: { index: number; node: TabNode }[],
1242
+ onSelect: (item: { index: number; node: TabNode }) => void,
1243
+ ) => void;
1252
1244
 
1253
- /** @internal */
1254
- getOnRenderFloatingTabPlaceholder() {
1255
- return this.props.onRenderFloatingTabPlaceholder;
1256
- }
1245
+ export type TabSetPlaceHolderCallback = (node: TabSetNode) => React.ReactNode;
1257
1246
 
1258
- /** @internal */
1259
- getShowOverflowMenu() {
1260
- return this.props.onShowOverflowMenu;
1261
- }
1247
+ export interface ITabSetRenderValues {
1248
+ /** components that will be added after the tabs */
1249
+ stickyButtons: React.ReactNode[];
1250
+ /** components that will be added at the end of the tabset */
1251
+ buttons: React.ReactNode[];
1252
+ /** position to insert overflow button within [...stickyButtons, ...buttons]
1253
+ * if left undefined position will be after the sticky buttons (if any)
1254
+ */
1255
+ overflowPosition: number | undefined;
1256
+ }
1262
1257
 
1263
- /** @internal */
1264
- getTabSetPlaceHolderCallback() {
1265
- return this.props.onTabSetPlaceHolder;
1266
- }
1267
- /** @internal */
1268
- showContextMenu(node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>) {
1269
- if (this.props.onContextMenu) {
1270
- this.props.onContextMenu(node, event);
1271
- }
1272
- }
1258
+ export interface ITabRenderValues {
1259
+ /** the icon or other leading component */
1260
+ leading: React.ReactNode;
1261
+ /** the main tab text/component */
1262
+ content: React.ReactNode;
1263
+ /** a set of react components to add to the tab after the content */
1264
+ buttons: React.ReactNode[];
1265
+ }
1273
1266
 
1274
- /** @internal */
1275
- auxMouseClick(node: TabNode | TabSetNode | BorderNode, event: React.MouseEvent<HTMLElement, MouseEvent>) {
1276
- if (this.props.onAuxMouseClick) {
1277
- this.props.onAuxMouseClick(node, event);
1278
- }
1279
- }
1267
+ export interface IIcons {
1268
+ close?: (React.ReactNode | ((tabNode: TabNode) => React.ReactNode));
1269
+ closeTabset?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
1270
+ popout?: (React.ReactNode | ((tabNode: TabNode) => React.ReactNode));
1271
+ maximize?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
1272
+ restore?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
1273
+ more?: (React.ReactNode | ((tabSetNode: (TabSetNode | BorderNode), hiddenTabs: { node: TabNode; index: number }[]) => React.ReactNode));
1274
+ edgeArrow?: React.ReactNode;
1275
+ activeTabset?: (React.ReactNode | ((tabSetNode: TabSetNode) => React.ReactNode));
1280
1276
  }
1281
1277
 
1282
- // wrapper round the drag rect renderer that can call
1283
- // a method once the rendering is written to the dom
1278
+ const defaultIcons = {
1279
+ close: <CloseIcon />,
1280
+ closeTabset: <CloseIcon />,
1281
+ popout: <PopoutIcon />,
1282
+ maximize: <MaximizeIcon />,
1283
+ restore: <RestoreIcon />,
1284
+ more: <OverflowIcon />,
1285
+ edgeArrow: <EdgeIcon />,
1286
+ activeTabset: <AsterickIcon />
1287
+ };
1288
+
1289
+ enum DragSource {
1290
+ Internal = "internal",
1291
+ External = "external",
1292
+ Add = "add"
1293
+ }
1284
1294
 
1285
1295
  /** @internal */
1286
- interface IDragRectRenderWrapper {
1287
- onRendered?: () => void;
1288
- children: React.ReactNode;
1289
- }
1296
+ const defaultSupportsPopout: boolean = isDesktop();
1290
1297
 
1291
1298
  /** @internal */
1292
- const DragRectRenderWrapper = (props: IDragRectRenderWrapper) => {
1293
- React.useEffect(() => {
1294
- props.onRendered?.();
1295
- }, [props]);
1296
-
1297
- return (<React.Fragment>
1298
- {props.children}
1299
- </React.Fragment>)
1300
- }
1299
+ const edgeRectLength = 100;
1300
+ /** @internal */
1301
+ const edgeRectWidth = 10;
1302
+
1303
+ // global layout drag state
1304
+ class DragState {
1305
+ public readonly mainLayout: LayoutInternal;
1306
+ public readonly dragSource: DragSource;
1307
+ public readonly dragNode: Node & IDraggable | undefined;
1308
+ public readonly dragJson: IJsonTabNode | undefined;
1309
+ public readonly fnNewNodeDropped: ((node?: Node, event?: React.DragEvent<HTMLElement>) => void) | undefined;
1310
+
1311
+ public constructor(
1312
+ mainLayout: LayoutInternal,
1313
+ dragSource: DragSource,
1314
+ dragNode: Node & IDraggable | undefined,
1315
+ dragJson: IJsonTabNode | undefined,
1316
+ fnNewNodeDropped: ((node?: Node, event?: React.DragEvent<HTMLElement>) => void) | undefined
1317
+ ) {
1318
+ this.mainLayout = mainLayout;
1319
+ this.dragSource = dragSource;
1320
+ this.dragNode = dragNode;
1321
+ this.dragJson = dragJson;
1322
+ this.fnNewNodeDropped = fnNewNodeDropped;
1323
+ }
1324
+ }