flexlayout-react 0.7.15 → 0.8.1

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