flexlayout-react 0.8.4 → 0.8.6

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 (56) hide show
  1. package/ChangeLog.txt +9 -0
  2. package/README.md +29 -2
  3. package/declarations/Types.d.ts +2 -6
  4. package/declarations/view/Layout.d.ts +3 -1
  5. package/dist/flexlayout.js +7 -7
  6. package/dist/flexlayout_min.js +1 -1
  7. package/lib/Types.js +2 -6
  8. package/lib/Types.js.map +1 -1
  9. package/lib/model/TabSetNode.js +9 -4
  10. package/lib/model/TabSetNode.js.map +1 -1
  11. package/lib/view/BorderTabSet.js +11 -5
  12. package/lib/view/BorderTabSet.js.map +1 -1
  13. package/lib/view/Layout.js +3 -2
  14. package/lib/view/Layout.js.map +1 -1
  15. package/lib/view/PopupMenu.js +11 -5
  16. package/lib/view/PopupMenu.js.map +1 -1
  17. package/lib/view/TabOverflowHook.js +104 -21
  18. package/lib/view/TabOverflowHook.js.map +1 -1
  19. package/lib/view/TabSet.js +11 -5
  20. package/lib/view/TabSet.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/Types.ts +3 -8
  23. package/src/model/TabSetNode.ts +8 -4
  24. package/src/view/BorderTabSet.tsx +38 -24
  25. package/src/view/Layout.tsx +9 -5
  26. package/src/view/PopupMenu.tsx +31 -19
  27. package/src/view/TabOverflowHook.tsx +112 -21
  28. package/src/view/TabSet.tsx +36 -22
  29. package/style/_base.scss +569 -595
  30. package/style/_themes.scss +649 -0
  31. package/style/combined.css +1055 -0
  32. package/style/combined.css.map +1 -0
  33. package/style/combined.scss +46 -0
  34. package/style/dark.css +704 -701
  35. package/style/dark.css.map +1 -1
  36. package/style/dark.scss +5 -181
  37. package/style/gray.css +707 -684
  38. package/style/gray.css.map +1 -1
  39. package/style/gray.scss +5 -180
  40. package/style/light.css +681 -685
  41. package/style/light.css.map +1 -1
  42. package/style/light.scss +5 -163
  43. package/style/rounded.css +723 -730
  44. package/style/rounded.css.map +1 -1
  45. package/style/rounded.scss +5 -210
  46. package/style/underline.css +710 -705
  47. package/style/underline.css.map +1 -1
  48. package/style/underline.scss +5 -186
  49. package/.editorconfig +0 -8
  50. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -104
  51. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  52. package/.prettierrc.json +0 -3
  53. package/Screenshot_light.png +0 -0
  54. package/Screenshot_rounded.png +0 -0
  55. package/dist/bundles/demo.js +0 -232052
  56. package/dist/bundles/demo.js.map +0 -1
@@ -3,10 +3,13 @@ import { TabNode } from "../model/TabNode";
3
3
  import { CLASSES } from "../Types";
4
4
  import { LayoutInternal } from "./Layout";
5
5
  import { TabButtonStamp } from "./TabButtonStamp";
6
+ import { TabSetNode } from "../model/TabSetNode";
7
+ import { BorderNode } from "../model/BorderNode";
6
8
 
7
9
  /** @internal */
8
10
  export function showPopup(
9
11
  triggerElement: Element,
12
+ parentNode: TabSetNode | BorderNode,
10
13
  items: { index: number; node: TabNode }[],
11
14
  onSelect: (item: { index: number; node: TabNode }) => void,
12
15
  layout: LayoutInternal,
@@ -60,6 +63,7 @@ export function showPopup(
60
63
 
61
64
  layout.showControlInPortal(<PopupMenu
62
65
  currentDocument={currentDocument}
66
+ parentNode={parentNode}
63
67
  onSelect={onSelect}
64
68
  onHide={onHide}
65
69
  items={items}
@@ -70,6 +74,7 @@ export function showPopup(
70
74
 
71
75
  /** @internal */
72
76
  interface IPopupMenuProps {
77
+ parentNode: TabSetNode | BorderNode;
73
78
  items: { index: number; node: TabNode }[];
74
79
  currentDocument: Document;
75
80
  onHide: () => void;
@@ -80,7 +85,7 @@ interface IPopupMenuProps {
80
85
 
81
86
  /** @internal */
82
87
  const PopupMenu = (props: IPopupMenuProps) => {
83
- const { items, onHide, onSelect, classNameMapper, layout} = props;
88
+ const { parentNode, items, onHide, onSelect, classNameMapper, layout } = props;
84
89
 
85
90
  const onItemClick = (item: { index: number; node: TabNode }, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
86
91
  onSelect(item);
@@ -88,38 +93,45 @@ const PopupMenu = (props: IPopupMenuProps) => {
88
93
  event.stopPropagation();
89
94
  };
90
95
 
91
- const onDragStart = (event: React.DragEvent<HTMLElement>, node:TabNode) => {
96
+ const onDragStart = (event: React.DragEvent<HTMLElement>, node: TabNode) => {
92
97
  event.stopPropagation(); // prevent starting a tabset drag as well
93
98
  layout.setDragNode(event.nativeEvent, node as TabNode);
94
99
  setTimeout(() => {
95
100
  onHide();
96
101
  }, 0);
97
-
102
+
98
103
  };
99
104
 
100
105
  const onDragEnd = (event: React.DragEvent<HTMLElement>) => {
101
106
  layout.clearDragMain();
102
107
  };
103
108
 
104
- const itemElements = items.map((item, i) => (
105
- <div key={item.index}
106
- className={classNameMapper(CLASSES.FLEXLAYOUT__POPUP_MENU_ITEM)}
107
- data-layout-path={"/popup-menu/tb" + i}
108
- onClick={(event) => onItemClick(item, event)}
109
- draggable={true}
110
- onDragStart={(e) => onDragStart(e, item.node)}
111
- onDragEnd={onDragEnd}
112
- title={item.node.getHelpText()} >
113
- <TabButtonStamp
114
- node={item.node}
115
- layout={layout}
116
- />
117
- </div>
118
- ));
109
+ const itemElements = items.map((item, i) => {
110
+ let classes = classNameMapper(CLASSES.FLEXLAYOUT__POPUP_MENU_ITEM);
111
+ if (parentNode.getSelected() === item.index) {
112
+ classes += " " + classNameMapper(CLASSES.FLEXLAYOUT__POPUP_MENU_ITEM__SELECTED);
113
+ }
114
+ return (
115
+ <div key={item.index}
116
+ className={classes}
117
+ data-layout-path={"/popup-menu/tb" + i}
118
+ onClick={(event) => onItemClick(item, event)}
119
+ draggable={true}
120
+ onDragStart={(e) => onDragStart(e, item.node)}
121
+ onDragEnd={onDragEnd}
122
+ title={item.node.getHelpText()} >
123
+ <TabButtonStamp
124
+ node={item.node}
125
+ layout={layout}
126
+ />
127
+ </div>
128
+ )
129
+ }
130
+ );
119
131
 
120
132
  return (
121
133
  <div className={classNameMapper(CLASSES.FLEXLAYOUT__POPUP_MENU)}
122
- data-layout-path="/popup-menu"
134
+ data-layout-path="/popup-menu"
123
135
  >
124
136
  {itemElements}
125
137
  </div>);
@@ -4,6 +4,7 @@ import { BorderNode } from "../model/BorderNode";
4
4
  import { Orientation } from "../Orientation";
5
5
  import { LayoutInternal } from "./Layout";
6
6
  import { TabNode } from "../model/TabNode";
7
+ import { startDrag } from "./Utils";
7
8
 
8
9
  /** @internal */
9
10
  export const useTabOverflow = (
@@ -11,6 +12,7 @@ export const useTabOverflow = (
11
12
  node: TabSetNode | BorderNode,
12
13
  orientation: Orientation,
13
14
  tabStripRef: React.RefObject<HTMLElement | null>,
15
+ miniScrollRef: React.RefObject<HTMLElement | null>,
14
16
  tabClassName: string
15
17
  ) => {
16
18
  const [hiddenTabs, setHiddenTabs] = React.useState<number[]>([]);
@@ -20,8 +22,16 @@ export const useTabOverflow = (
20
22
  const userControlledPositionRef = React.useRef<boolean>(false);
21
23
  const updateHiddenTabsTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
22
24
  const hiddenTabsRef = React.useRef<number[]>([]);
25
+ const thumbInternalPos = React.useRef<number>(0);
23
26
  hiddenTabsRef.current = hiddenTabs;
24
27
 
28
+ // if node changes (new model) then reset scroll to 0
29
+ React.useEffect(() => {
30
+ if (tabStripRef.current) {
31
+ setScrollPosition(0);
32
+ }
33
+ }, [node]);
34
+
25
35
  // if selected node or tabset/border rectangle change then unset usercontrolled (so selected tab will be kept in view)
26
36
  React.useEffect(() => {
27
37
  userControlledPositionRef.current = false;
@@ -37,9 +47,47 @@ export const useTabOverflow = (
37
47
  }
38
48
  }
39
49
 
40
- updateHiddenTabs();
50
+ updateHiddenTabs();
51
+ updateScrollMetrics();
41
52
  });
42
53
 
54
+ const updateScrollMetrics = () => {
55
+ if (tabStripRef.current && miniScrollRef.current) {
56
+ const t = tabStripRef.current;
57
+ const s = miniScrollRef.current;
58
+
59
+ const size = getSize(t);
60
+ const scrollSize = getScrollSize(t);
61
+ const position = getScrollPosition(t);
62
+
63
+ if (scrollSize > size && scrollSize > 0) {
64
+ let thumbSize = size * size / scrollSize;
65
+ let adjust = 0;
66
+ if (thumbSize < 20) {
67
+ adjust = 20 - thumbSize;
68
+ thumbSize = 20;
69
+ }
70
+ const thumbPos = position * (size - adjust) / scrollSize;
71
+ if (orientation === Orientation.HORZ) {
72
+ s.style.width = thumbSize + "px";
73
+ s.style.left = thumbPos + "px";
74
+ } else {
75
+ s.style.height = thumbSize + "px";
76
+ s.style.top = thumbPos + "px";
77
+ }
78
+ s.style.display = "block";
79
+ } else {
80
+ s.style.display = "none";
81
+ }
82
+
83
+ if (orientation === Orientation.HORZ) {
84
+ s.style.bottom = "0px";
85
+ } else {
86
+ s.style.right = "0px";
87
+ }
88
+ }
89
+ }
90
+
43
91
  const updateHiddenTabs = () => {
44
92
  if (updateHiddenTabsTimerRef.current === undefined) {
45
93
  // throttle updates to prevent Maximum update depth exceeded error
@@ -71,11 +119,55 @@ export const useTabOverflow = (
71
119
  }
72
120
 
73
121
  const onScroll = () => {
122
+ userControlledPositionRef.current = true;
74
123
  updateTabRects();
124
+ updateScrollMetrics()
75
125
  updateHiddenTabs();
76
- userControlledPositionRef.current = true;
77
126
  };
78
127
 
128
+ const onScrollPointerDown = (event: React.PointerEvent<HTMLElement>) => {
129
+ event.stopPropagation();
130
+ miniScrollRef.current!.setPointerCapture(event.pointerId)
131
+ const r = miniScrollRef.current?.getBoundingClientRect()!;
132
+ if (orientation === Orientation.HORZ) {
133
+ thumbInternalPos.current = event.clientX - r.x;
134
+ } else {
135
+ thumbInternalPos.current = event.clientY - r.y;
136
+ }
137
+ userControlledPositionRef.current = true;
138
+ startDrag(event.currentTarget.ownerDocument, event, onDragMove, onDragEnd, onDragCancel);
139
+ }
140
+
141
+ const onDragMove = (x: number, y: number) => {
142
+ if (tabStripRef.current && miniScrollRef.current) {
143
+ const t = tabStripRef.current;
144
+ const s = miniScrollRef.current;
145
+ const size = getSize(t);
146
+ const scrollSize = getScrollSize(t);
147
+ const thumbSize = getSize(s);
148
+
149
+ const r = t.getBoundingClientRect()!;
150
+ let thumb = 0;
151
+ if (orientation === Orientation.HORZ) {
152
+ thumb = x - r.x - thumbInternalPos.current;
153
+ } else {
154
+ thumb = y - r.y - thumbInternalPos.current
155
+ }
156
+
157
+ thumb = Math.max(0, Math.min(scrollSize - thumbSize, thumb));
158
+ if (size > 0) {
159
+ const scrollPos = thumb * scrollSize / size;
160
+ setScrollPosition(scrollPos);
161
+ }
162
+ }
163
+ }
164
+
165
+ const onDragEnd = () => {
166
+ }
167
+
168
+ const onDragCancel = () => {
169
+ }
170
+
79
171
  const findSelectedTab: () => Element | undefined = () => {
80
172
  let found: Element | undefined = undefined;
81
173
  if (tabStripRef.current) {
@@ -132,24 +224,23 @@ export const useTabOverflow = (
132
224
  const onMouseWheel = (event: React.WheelEvent<HTMLElement>) => {
133
225
  if (tabStripRef.current) {
134
226
  if (node.getChildren().length === 0) return;
227
+
135
228
  let delta = 0;
136
- if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
137
- delta = -event.deltaX;
138
- } else {
229
+ if (Math.abs(event.deltaY) > 0) {
139
230
  delta = -event.deltaY;
231
+ if (event.deltaMode === 1) {
232
+ // DOM_DELTA_LINE 0x01 The delta values are specified in lines.
233
+ delta *= 40;
234
+ }
235
+ const newPos = getScrollPosition(tabStripRef.current) - delta;
236
+ const maxScroll = getScrollSize(tabStripRef.current) - getSize(tabStripRef.current);
237
+ const p = Math.max(0, Math.min(maxScroll, newPos));
238
+ setScrollPosition(p);
239
+ updateTabRects();
240
+ updateHiddenTabs();
241
+ userControlledPositionRef.current = true;
242
+ event.stopPropagation();
140
243
  }
141
- if (event.deltaMode === 1) {
142
- // DOM_DELTA_LINE 0x01 The delta values are specified in lines.
143
- delta *= 40;
144
- }
145
- const newPos = getScrollPosition() - delta;
146
- const maxScroll = getScrollSize(tabStripRef.current) - getSize(tabStripRef.current);
147
- const p = Math.max(0, Math.min(maxScroll, newPos));
148
- userControlledPositionRef.current = true;
149
- setScrollPosition(p);
150
- updateTabRects();
151
- updateHiddenTabs();
152
- event.stopPropagation();
153
244
  }
154
245
  };
155
246
 
@@ -195,15 +286,15 @@ export const useTabOverflow = (
195
286
  }
196
287
  }
197
288
 
198
- const getScrollPosition = () => {
289
+ const getScrollPosition = (elm: Element) => {
199
290
  if (orientation === Orientation.HORZ) {
200
- return tabStripRef.current!.scrollLeft;
291
+ return elm.scrollLeft;
201
292
  } else {
202
- return tabStripRef.current!.scrollTop;
293
+ return elm.scrollTop;
203
294
  }
204
295
  }
205
296
 
206
- return { selfRef, userControlledPositionRef, onScroll, hiddenTabs, onMouseWheel, isTabOverflow };
297
+ return { selfRef, userControlledPositionRef, onScroll, onScrollPointerDown, hiddenTabs, onMouseWheel, isTabOverflow };
207
298
  };
208
299
 
209
300
  function arraysEqual(arr1: number[], arr2: number[]) {
@@ -24,6 +24,7 @@ export const TabSet = (props: ITabSetProps) => {
24
24
  const { node, layout } = props;
25
25
 
26
26
  const tabStripRef = React.useRef<HTMLDivElement | null>(null);
27
+ const miniScrollRef = React.useRef<HTMLDivElement | null>(null);
27
28
  const tabStripInnerRef = React.useRef<HTMLDivElement | null>(null);
28
29
  const contentRef = React.useRef<HTMLDivElement | null>(null);
29
30
  const buttonBarRef = React.useRef<HTMLDivElement | null>(null);
@@ -43,26 +44,27 @@ export const TabSet = (props: ITabSetProps) => {
43
44
  const newContentRect = Rect.getContentRect(contentRef.current!).relativeTo(layout.getDomRect()!);
44
45
  if (!node.getContentRect().equals(newContentRect)) {
45
46
  node.setContentRect(newContentRect);
46
- setTimeout(()=> { //prevent Maximum update depth exceeded error
47
+ setTimeout(() => { //prevent Maximum update depth exceeded error
47
48
  layout.redrawInternal("tabset content rect " + newContentRect);
48
- },0);
49
+ }, 0);
49
50
  }
50
51
  });
51
52
 
52
53
  // this must be after the useEffect, so the node rect is already set (else window popin will not position tabs correctly)
53
- const { selfRef, userControlledPositionRef, onScroll, hiddenTabs, onMouseWheel, isTabOverflow } =
54
- useTabOverflow(layout, node, Orientation.HORZ, tabStripInnerRef,
54
+ const { selfRef, userControlledPositionRef, onScroll, onScrollPointerDown, hiddenTabs, onMouseWheel, isTabOverflow } =
55
+ useTabOverflow(layout, node, Orientation.HORZ, tabStripInnerRef, miniScrollRef,
55
56
  layout.getClassName(CLASSES.FLEXLAYOUT__TAB_BUTTON));
56
57
 
57
58
  const onOverflowClick = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
58
59
  const callback = layout.getShowOverflowMenu();
59
- const items = hiddenTabs.map(h=> {return {index: h, node: (node.getChildren()[h] as TabNode)};});
60
+ const items = hiddenTabs.map(h => { return { index: h, node: (node.getChildren()[h] as TabNode) }; });
60
61
  if (callback !== undefined) {
61
62
  callback(node, event, items, onOverflowItemSelect);
62
63
  } else {
63
64
  const element = overflowbuttonRef.current!;
64
65
  showPopup(
65
66
  element,
67
+ node,
66
68
  items,
67
69
  onOverflowItemSelect,
68
70
  layout
@@ -200,11 +202,11 @@ export const TabSet = (props: ITabSetProps) => {
200
202
  }
201
203
 
202
204
  if (!node.isEnableTabWrap()) {
203
- if (hiddenTabs.length > 0) {
205
+ if (hiddenTabs.length > 0) {
204
206
  const overflowTitle = layout.i18nName(I18nLabel.Overflow_Menu_Tooltip);
205
207
  let overflowContent;
206
208
  if (typeof icons.more === "function") {
207
- const items = hiddenTabs.map(h=> {return {index: h, node: (node.getChildren()[h] as TabNode)};});
209
+ const items = hiddenTabs.map(h => { return { index: h, node: (node.getChildren()[h] as TabNode) }; });
208
210
  overflowContent = icons.more(node, items);
209
211
  } else {
210
212
  overflowContent = (<>
@@ -229,10 +231,10 @@ export const TabSet = (props: ITabSetProps) => {
229
231
  }
230
232
  }
231
233
 
232
- if (selectedTabNode !== undefined &&
233
- layout.isSupportsPopout() &&
234
- selectedTabNode.isEnablePopout() &&
235
- selectedTabNode.isEnablePopoutIcon() ) {
234
+ if (selectedTabNode !== undefined &&
235
+ layout.isSupportsPopout() &&
236
+ selectedTabNode.isEnablePopout() &&
237
+ selectedTabNode.isEnablePopoutIcon()) {
236
238
 
237
239
  const popoutTitle = layout.i18nName(I18nLabel.Popout_Tab);
238
240
  buttons.push(
@@ -291,7 +293,7 @@ export const TabSet = (props: ITabSetProps) => {
291
293
  key="active"
292
294
  data-layout-path={path + "/button/active"}
293
295
  title={title}
294
- className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_ICON) }
296
+ className={cm(CLASSES.FLEXLAYOUT__TAB_TOOLBAR_ICON)}
295
297
  >
296
298
  {(typeof icons.activeTabset === "function") ? icons.activeTabset(node) : icons.activeTabset}
297
299
  </div>
@@ -335,7 +337,7 @@ export const TabSet = (props: ITabSetProps) => {
335
337
  if (node.isEnableTabStrip()) {
336
338
  tabStrip = (
337
339
  <div className={tabStripClasses}
338
- style={{ flexWrap: "wrap", gap:"1px", marginTop:"2px" }}
340
+ style={{ flexWrap: "wrap", gap: "1px", marginTop: "2px" }}
339
341
  ref={tabStripRef}
340
342
  data-layout-path={path + "/tabstrip"}
341
343
  onPointerDown={onPointerDown}
@@ -354,7 +356,15 @@ export const TabSet = (props: ITabSetProps) => {
354
356
  }
355
357
  } else {
356
358
  if (node.isEnableTabStrip()) {
357
- const miniScrollbarClasses = cm(CLASSES.FLEXLAYOUT__MINI_SCROLLBAR) + (node.isEnableTabScrollbar()?"":" " + cm(CLASSES.FLEXLAYOUT__MINI_SCROLLBAR_HIDDEN));
359
+ let miniScrollbar = undefined;
360
+ if (node.isEnableTabScrollbar()) {
361
+ miniScrollbar = (
362
+ <div ref={miniScrollRef}
363
+ className={cm(CLASSES.FLEXLAYOUT__MINI_SCROLLBAR)}
364
+ onPointerDown={onScrollPointerDown}
365
+ />
366
+ );
367
+ }
358
368
  tabStrip = (
359
369
  <div className={tabStripClasses}
360
370
  ref={tabStripRef}
@@ -368,16 +378,20 @@ export const TabSet = (props: ITabSetProps) => {
368
378
  onWheel={onMouseWheel}
369
379
  onDragStart={onDragStart}
370
380
  >
371
- <div ref={tabStripInnerRef}
372
- className={cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER) + " " + cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_ + node.getTabLocation()) + " " + miniScrollbarClasses}
373
- style={{overflowX: 'auto', overflowY:"hidden"}}
374
- onScroll={onScroll} >
375
- <div
376
- style={{width: (isTabStretch ? "100%" : "none") }}
377
- className={cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_TAB_CONTAINER) + " " + cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_TAB_CONTAINER_ + node.getTabLocation())}
381
+ <div className={cm(CLASSES.FLEXLAYOUT__MINI_SCROLLBAR_CONTAINER)}>
382
+ <div ref={tabStripInnerRef}
383
+ className={cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER) + " " + cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_ + node.getTabLocation())}
384
+ style={{ overflowX: 'auto', overflowY: "hidden" }}
385
+ onScroll={onScroll}
378
386
  >
379
- {tabs}
387
+ <div
388
+ style={{ width: (isTabStretch ? "100%" : "none") }}
389
+ className={cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_TAB_CONTAINER) + " " + cm(CLASSES.FLEXLAYOUT__TABSET_TABBAR_INNER_TAB_CONTAINER_ + node.getTabLocation())}
390
+ >
391
+ {tabs}
392
+ </div>
380
393
  </div>
394
+ {miniScrollbar}
381
395
  </div>
382
396
  {buttonbar}
383
397
  </div>