@vuu-ui/vuu-layout 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +30 -0
- package/src/Component.css +2 -0
- package/src/Component.tsx +20 -0
- package/src/DraggableLayout.css +18 -0
- package/src/DraggableLayout.tsx +29 -0
- package/src/__tests__/flexbox-utils.spec.js +90 -0
- package/src/action-buttons/action-buttons.css +12 -0
- package/src/action-buttons/action-buttons.tsx +30 -0
- package/src/action-buttons/index.ts +1 -0
- package/src/chest-of-drawers/Chest.css +36 -0
- package/src/chest-of-drawers/Chest.tsx +42 -0
- package/src/chest-of-drawers/Drawer.css +153 -0
- package/src/chest-of-drawers/Drawer.tsx +118 -0
- package/src/chest-of-drawers/index.ts +2 -0
- package/src/common-types.ts +9 -0
- package/src/debug.ts +16 -0
- package/src/dialog/Dialog.css +16 -0
- package/src/dialog/Dialog.tsx +59 -0
- package/src/dialog/index.ts +1 -0
- package/src/drag-drop/BoxModel.ts +546 -0
- package/src/drag-drop/DragState.ts +222 -0
- package/src/drag-drop/Draggable.ts +282 -0
- package/src/drag-drop/DropMenu.css +70 -0
- package/src/drag-drop/DropMenu.tsx +68 -0
- package/src/drag-drop/DropTarget.ts +392 -0
- package/src/drag-drop/DropTargetRenderer.css +40 -0
- package/src/drag-drop/DropTargetRenderer.tsx +284 -0
- package/src/drag-drop/dragDropTypes.ts +49 -0
- package/src/drag-drop/index.ts +4 -0
- package/src/editable-label/EditableLabel.css +28 -0
- package/src/editable-label/EditableLabel.tsx +99 -0
- package/src/editable-label/index.ts +1 -0
- package/src/flexbox/Flexbox.css +45 -0
- package/src/flexbox/Flexbox.tsx +70 -0
- package/src/flexbox/FlexboxLayout.jsx +26 -0
- package/src/flexbox/FluidGrid.css +134 -0
- package/src/flexbox/FluidGrid.tsx +84 -0
- package/src/flexbox/FluidGridLayout.tsx +10 -0
- package/src/flexbox/Splitter.css +140 -0
- package/src/flexbox/Splitter.tsx +135 -0
- package/src/flexbox/flexbox-utils.ts +128 -0
- package/src/flexbox/flexboxTypes.ts +63 -0
- package/src/flexbox/index.ts +4 -0
- package/src/flexbox/useResponsiveSizing.ts +85 -0
- package/src/flexbox/useSplitterResizing.ts +272 -0
- package/src/index.ts +20 -0
- package/src/layout-action.ts +21 -0
- package/src/layout-header/ActionButton.tsx +23 -0
- package/src/layout-header/Header.css +8 -0
- package/src/layout-header/Header.tsx +222 -0
- package/src/layout-header/index.ts +1 -0
- package/src/layout-provider/LayoutProvider.tsx +160 -0
- package/src/layout-provider/LayoutProviderContext.ts +17 -0
- package/src/layout-provider/index.ts +2 -0
- package/src/layout-provider/useLayoutDragDrop.ts +241 -0
- package/src/layout-reducer/flexUtils.ts +281 -0
- package/src/layout-reducer/index.ts +4 -0
- package/src/layout-reducer/insert-layout-element.ts +365 -0
- package/src/layout-reducer/layout-reducer.ts +255 -0
- package/src/layout-reducer/layoutTypes.ts +151 -0
- package/src/layout-reducer/layoutUtils.ts +302 -0
- package/src/layout-reducer/remove-layout-element.ts +240 -0
- package/src/layout-reducer/replace-layout-element.ts +118 -0
- package/src/layout-reducer/resize-flex-children.ts +56 -0
- package/src/layout-reducer/wrap-layout-element.ts +317 -0
- package/src/layout-view/View.css +58 -0
- package/src/layout-view/View.tsx +149 -0
- package/src/layout-view/ViewContext.ts +31 -0
- package/src/layout-view/index.ts +4 -0
- package/src/layout-view/useView.tsx +104 -0
- package/src/layout-view/useViewActionDispatcher.ts +133 -0
- package/src/layout-view/useViewResize.ts +53 -0
- package/src/layout-view/viewTypes.ts +37 -0
- package/src/palette/Palette.css +37 -0
- package/src/palette/Palette.tsx +140 -0
- package/src/palette/PaletteUitk.css +9 -0
- package/src/palette/PaletteUitk.tsx +79 -0
- package/src/palette/index.ts +2 -0
- package/src/placeholder/Placeholder.css +10 -0
- package/src/placeholder/Placeholder.tsx +39 -0
- package/src/placeholder/index.ts +1 -0
- package/src/registry/ComponentRegistry.ts +35 -0
- package/src/registry/index.ts +1 -0
- package/src/responsive/OverflowMenu.css +31 -0
- package/src/responsive/OverflowMenu.jsx +56 -0
- package/src/responsive/breakpoints.ts +48 -0
- package/src/responsive/index.ts +4 -0
- package/src/responsive/measureMinimumNodeSize.ts +23 -0
- package/src/responsive/overflowUtils.js +14 -0
- package/src/responsive/use-breakpoints.ts +100 -0
- package/src/responsive/useOverflowObserver.ts +606 -0
- package/src/responsive/useResizeObserver.ts +154 -0
- package/src/responsive/utils.ts +37 -0
- package/src/stack/Stack.css +39 -0
- package/src/stack/Stack.tsx +160 -0
- package/src/stack/StackLayout.tsx +137 -0
- package/src/stack/index.ts +3 -0
- package/src/stack/stackTypes.ts +19 -0
- package/src/tabs/TabPanel.css +12 -0
- package/src/tabs/TabPanel.tsx +17 -0
- package/src/tabs/index.ts +1 -0
- package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
- package/src/tools/config-wrapper/index.js +1 -0
- package/src/tools/devtools-box/layout-configurator.css +112 -0
- package/src/tools/devtools-box/layout-configurator.jsx +369 -0
- package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
- package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
- package/src/tools/index.js +3 -0
- package/src/use-persistent-state.ts +115 -0
- package/src/utils/componentFromLayout.tsx +30 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/pathUtils.ts +294 -0
- package/src/utils/propUtils.ts +24 -0
- package/src/utils/refUtils.ts +16 -0
- package/src/utils/styleUtils.ts +14 -0
- package/src/utils/typeOf.ts +22 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax */
|
|
2
|
+
import { useCallback, useLayoutEffect, useRef, RefObject } from 'react';
|
|
3
|
+
export const WidthHeight = ['height', 'width'];
|
|
4
|
+
export const HeightOnly = ['height'];
|
|
5
|
+
export const WidthOnly = ['width'];
|
|
6
|
+
|
|
7
|
+
export type measurements<T = string | number> = {
|
|
8
|
+
height?: T;
|
|
9
|
+
scrollHeight?: T;
|
|
10
|
+
scrollWidth?: T;
|
|
11
|
+
width?: T;
|
|
12
|
+
};
|
|
13
|
+
type measuredDimension = keyof measurements<number>;
|
|
14
|
+
|
|
15
|
+
export type ResizeHandler = (measurements: measurements<number>) => void;
|
|
16
|
+
|
|
17
|
+
type observedDetails = {
|
|
18
|
+
onResize?: ResizeHandler;
|
|
19
|
+
measurements: measurements<number>;
|
|
20
|
+
};
|
|
21
|
+
const observedMap = new WeakMap<HTMLElement, observedDetails>();
|
|
22
|
+
|
|
23
|
+
const getTargetSize = (
|
|
24
|
+
element: HTMLElement,
|
|
25
|
+
contentRect: DOMRectReadOnly,
|
|
26
|
+
dimension: measuredDimension
|
|
27
|
+
): number => {
|
|
28
|
+
switch (dimension) {
|
|
29
|
+
case 'height':
|
|
30
|
+
return contentRect.height;
|
|
31
|
+
case 'scrollHeight':
|
|
32
|
+
return element.scrollHeight;
|
|
33
|
+
case 'scrollWidth':
|
|
34
|
+
return element.scrollWidth;
|
|
35
|
+
case 'width':
|
|
36
|
+
return contentRect.width;
|
|
37
|
+
default:
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const { target, contentRect } = entry;
|
|
45
|
+
const observedTarget = observedMap.get(target as HTMLElement);
|
|
46
|
+
if (observedTarget) {
|
|
47
|
+
const { onResize, measurements } = observedTarget;
|
|
48
|
+
let sizeChanged = false;
|
|
49
|
+
for (const [dimension, size] of Object.entries(measurements)) {
|
|
50
|
+
const newSize = getTargetSize(
|
|
51
|
+
target as HTMLElement,
|
|
52
|
+
contentRect,
|
|
53
|
+
dimension as measuredDimension
|
|
54
|
+
);
|
|
55
|
+
if (newSize !== size) {
|
|
56
|
+
sizeChanged = true;
|
|
57
|
+
measurements[dimension as measuredDimension] = newSize;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (sizeChanged) {
|
|
61
|
+
onResize && onResize(measurements);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// TODO use an optional lag (default to false) to ask to fire onResize
|
|
68
|
+
// with initial size
|
|
69
|
+
// Note asking for scrollHeight alone will not trigger onResize, this is only triggered by height,
|
|
70
|
+
// with scrollHeight returned as an auxilliary value
|
|
71
|
+
export function useResizeObserver(
|
|
72
|
+
ref: RefObject<Element | HTMLElement | null>,
|
|
73
|
+
dimensions: string[],
|
|
74
|
+
onResize: ResizeHandler,
|
|
75
|
+
reportInitialSize = false
|
|
76
|
+
): void {
|
|
77
|
+
const dimensionsRef = useRef(dimensions);
|
|
78
|
+
const measure = useCallback((target: HTMLElement): measurements<number> => {
|
|
79
|
+
const rect = target.getBoundingClientRect();
|
|
80
|
+
return dimensionsRef.current.reduce((map: { [key: string]: number }, dim) => {
|
|
81
|
+
map[dim] = getTargetSize(target, rect, dim as measuredDimension);
|
|
82
|
+
return map;
|
|
83
|
+
}, {});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
// TODO use ref to store resizeHandler here
|
|
87
|
+
// resize handler registered with REsizeObserver will never change
|
|
88
|
+
// use ref to store user onResize callback here
|
|
89
|
+
// resizeHandler will call user callback.current
|
|
90
|
+
|
|
91
|
+
// Keep this effect separate in case user inadvertently passes different
|
|
92
|
+
// dimensions or callback instance each time - we only ever want to
|
|
93
|
+
// initiate new observation when ref changes.
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
const target = ref.current as HTMLElement;
|
|
96
|
+
let cleanedUp = false;
|
|
97
|
+
|
|
98
|
+
async function registerObserver() {
|
|
99
|
+
// Create the map entry immediately. useEffect may fire below
|
|
100
|
+
// before fonts are ready and attempt to update entry
|
|
101
|
+
observedMap.set(target, { measurements: {} as measurements<number> });
|
|
102
|
+
cleanedUp = false;
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
104
|
+
const { fonts } = document as any;
|
|
105
|
+
if (fonts) {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
107
|
+
await fonts.ready;
|
|
108
|
+
}
|
|
109
|
+
if (!cleanedUp) {
|
|
110
|
+
const observedTarget = observedMap.get(target);
|
|
111
|
+
if (observedTarget) {
|
|
112
|
+
const measurements = measure(target);
|
|
113
|
+
observedTarget.measurements = measurements;
|
|
114
|
+
resizeObserver.observe(target);
|
|
115
|
+
if (reportInitialSize) {
|
|
116
|
+
onResize(measurements);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (target) {
|
|
123
|
+
// TODO might we want multiple callers to attach a listener to the same element ?
|
|
124
|
+
if (observedMap.has(target)) {
|
|
125
|
+
throw Error('useResizeObserver attemping to observe same element twice');
|
|
126
|
+
}
|
|
127
|
+
void registerObserver();
|
|
128
|
+
}
|
|
129
|
+
return () => {
|
|
130
|
+
if (target && observedMap.has(target)) {
|
|
131
|
+
resizeObserver.unobserve(target);
|
|
132
|
+
observedMap.delete(target);
|
|
133
|
+
cleanedUp = true;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [ref, measure]);
|
|
137
|
+
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
const target = ref.current as HTMLElement;
|
|
140
|
+
const record = observedMap.get(target);
|
|
141
|
+
if (record) {
|
|
142
|
+
if (dimensionsRef.current !== dimensions) {
|
|
143
|
+
dimensionsRef.current = dimensions;
|
|
144
|
+
const measurements = measure(target);
|
|
145
|
+
record.measurements = measurements;
|
|
146
|
+
}
|
|
147
|
+
// Might not have changed, but no harm ...
|
|
148
|
+
record.onResize = onResize;
|
|
149
|
+
}
|
|
150
|
+
}, [dimensions, measure, ref, onResize]);
|
|
151
|
+
|
|
152
|
+
// TODO might be a good idea to ref and return the current measurememnts. That way, derived hooks
|
|
153
|
+
// e.g useBreakpoints don't have to measure and client cn make onResize callback simpler
|
|
154
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const COLLAPSIBLE = 'data-collapsible';
|
|
2
|
+
|
|
3
|
+
const RESPONSIVE_ATTRIBUTE: { [key: string]: boolean } = {
|
|
4
|
+
[COLLAPSIBLE]: true,
|
|
5
|
+
'data-pad-start': true,
|
|
6
|
+
'data-pad-end': true
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const isResponsiveAttribute = (propName: string): boolean =>
|
|
10
|
+
RESPONSIVE_ATTRIBUTE[propName] ?? false;
|
|
11
|
+
|
|
12
|
+
const isCollapsible = (propName: string) => propName === COLLAPSIBLE;
|
|
13
|
+
|
|
14
|
+
const COLLAPSIBLE_VALUE: { [key: string]: string } = {
|
|
15
|
+
dynamic: 'dynamic',
|
|
16
|
+
instant: 'instant',
|
|
17
|
+
true: 'instant'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const collapsibleValue = (value: string) => COLLAPSIBLE_VALUE[value] ?? 'none';
|
|
21
|
+
|
|
22
|
+
type Props = { [key: string]: any };
|
|
23
|
+
export const extractResponsiveProps = (props: Props) => {
|
|
24
|
+
return Object.keys(props).reduce<[Props, Props]>(
|
|
25
|
+
(result, propName) => {
|
|
26
|
+
const [toolbarProps, rest] = result;
|
|
27
|
+
if (isResponsiveAttribute(propName)) {
|
|
28
|
+
const value = isCollapsible(propName) ? collapsibleValue(props[propName]) : props[propName];
|
|
29
|
+
|
|
30
|
+
toolbarProps[propName] = value;
|
|
31
|
+
rest[propName] = undefined;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
},
|
|
35
|
+
[{}, {}]
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.Tabs {
|
|
2
|
+
display: flex;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.Tabs .Toolbar:before {
|
|
8
|
+
left: 0;
|
|
9
|
+
width: 100%;
|
|
10
|
+
bottom: 0;
|
|
11
|
+
height: 1px;
|
|
12
|
+
content: '';
|
|
13
|
+
position: absolute;
|
|
14
|
+
background: var(--grey60);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.vuuTabHeader {
|
|
18
|
+
--uitkTabs-activationIndicator-background: transparent;
|
|
19
|
+
--uitkToolbarField-marginTop: calc(var(--uitk-size-unit) - 1px);
|
|
20
|
+
border-bottom: solid 1px var(--uitk-container-borderColor-medium);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.vuuTabHeader + .hwFlexbox,
|
|
24
|
+
.vuuTabHeader + * {
|
|
25
|
+
flex: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.vuuTabHeader + .vuuView > .vuuHeader {
|
|
29
|
+
height: 0;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* .Splitter.column + .Flexbox > .Tabs:first-child {
|
|
34
|
+
border-top: solid 1px lightgrey;
|
|
35
|
+
} */
|
|
36
|
+
|
|
37
|
+
.Layout-svg-button {
|
|
38
|
+
--spacing-medium: 5px;
|
|
39
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useIdMemo as useId } from "@heswell/uitk-core";
|
|
2
|
+
import { Tab, Tabstrip, Toolbar, ToolbarField } from "@heswell/uitk-lab";
|
|
3
|
+
import React, {
|
|
4
|
+
ForwardedRef,
|
|
5
|
+
forwardRef,
|
|
6
|
+
MouseEvent,
|
|
7
|
+
ReactElement,
|
|
8
|
+
ReactNode,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { StackProps } from "./stackTypes";
|
|
12
|
+
|
|
13
|
+
import "./Stack.css";
|
|
14
|
+
|
|
15
|
+
const getDefaultTabLabel = (component: ReactElement, tabIndex: number) =>
|
|
16
|
+
component.props?.title ?? `Tab ${tabIndex + 1}`;
|
|
17
|
+
|
|
18
|
+
const getChildElements = <T extends ReactElement = ReactElement>(
|
|
19
|
+
children: ReactNode
|
|
20
|
+
): T[] => {
|
|
21
|
+
const elements: T[] = [];
|
|
22
|
+
React.Children.forEach(children, (child) => {
|
|
23
|
+
if (React.isValidElement(child)) {
|
|
24
|
+
elements.push(child as T);
|
|
25
|
+
} else {
|
|
26
|
+
console.warn(`Stack has unexpected child element type`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return elements;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Stack = forwardRef(function Stack(
|
|
33
|
+
{
|
|
34
|
+
active = 0,
|
|
35
|
+
children,
|
|
36
|
+
enableAddTab,
|
|
37
|
+
enableCloseTabs,
|
|
38
|
+
getTabLabel = getDefaultTabLabel,
|
|
39
|
+
id: idProp,
|
|
40
|
+
keyBoardActivation = "manual",
|
|
41
|
+
onMouseDown,
|
|
42
|
+
onTabAdd,
|
|
43
|
+
onTabClose,
|
|
44
|
+
onTabEdit,
|
|
45
|
+
onTabSelectionChanged,
|
|
46
|
+
showTabs,
|
|
47
|
+
style,
|
|
48
|
+
}: StackProps,
|
|
49
|
+
ref: ForwardedRef<HTMLDivElement>
|
|
50
|
+
) {
|
|
51
|
+
const id = useId(idProp);
|
|
52
|
+
|
|
53
|
+
const handleTabSelection = (nextIdx: number) => {
|
|
54
|
+
// if uncontrolled, handle it internally
|
|
55
|
+
onTabSelectionChanged?.(nextIdx);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleTabClose = (tabIndex: number) => {
|
|
59
|
+
// if uncontrolled, handle it internally
|
|
60
|
+
onTabClose?.(tabIndex);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleAddTab = () => {
|
|
64
|
+
// if uncontrolled, handle it internally
|
|
65
|
+
onTabAdd?.(React.Children.count(children));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
|
69
|
+
// if uncontrolled, handle it internally
|
|
70
|
+
const target = e.target as HTMLElement;
|
|
71
|
+
const tabElement = target.closest('[role^="tab"]') as HTMLDivElement;
|
|
72
|
+
const role = tabElement?.getAttribute("role");
|
|
73
|
+
if (role === "tab") {
|
|
74
|
+
const tabIndex = parseInt(tabElement.dataset.idx ?? "-1");
|
|
75
|
+
if (tabIndex !== -1) {
|
|
76
|
+
onMouseDown?.(e, tabIndex);
|
|
77
|
+
} else {
|
|
78
|
+
throw Error("Stack: mousedown on tab with unknown index");
|
|
79
|
+
}
|
|
80
|
+
} else if (role === "tablist") {
|
|
81
|
+
console.log(`Stack mousedown on tabstrip`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleExitEditMode = useCallback(
|
|
86
|
+
(
|
|
87
|
+
_oldText: string,
|
|
88
|
+
newText: string,
|
|
89
|
+
_allowDeactivation: boolean,
|
|
90
|
+
tabIndex: number
|
|
91
|
+
) => {
|
|
92
|
+
onTabEdit?.(tabIndex, newText);
|
|
93
|
+
},
|
|
94
|
+
[onTabEdit]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const activeChild = () => {
|
|
98
|
+
if (React.isValidElement(children)) {
|
|
99
|
+
return children;
|
|
100
|
+
} else if (Array.isArray(children)) {
|
|
101
|
+
return children[active] ?? null;
|
|
102
|
+
} else {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const renderTabs = () =>
|
|
108
|
+
getChildElements(children).map((child, idx) => {
|
|
109
|
+
const rootId = `${id}-${idx}`;
|
|
110
|
+
const { closeable, id: childId } = child.props;
|
|
111
|
+
return (
|
|
112
|
+
<Tab
|
|
113
|
+
ariaControls={`${rootId}-tab`}
|
|
114
|
+
draggable
|
|
115
|
+
key={childId} // Important that we key by child identifier, not using index
|
|
116
|
+
id={rootId}
|
|
117
|
+
label={getTabLabel(child, idx)}
|
|
118
|
+
closeable={closeable}
|
|
119
|
+
editable={true}
|
|
120
|
+
// onEdit={handleTabEdit}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const child = activeChild();
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="Tabs" style={style} id={id} ref={ref}>
|
|
129
|
+
{showTabs ? (
|
|
130
|
+
<Toolbar
|
|
131
|
+
className="vuuTabHeader vuuHeader"
|
|
132
|
+
// onMouseDown={handleMouseDown}
|
|
133
|
+
>
|
|
134
|
+
<ToolbarField
|
|
135
|
+
disableFocusRing
|
|
136
|
+
data-collapsible="dynamic"
|
|
137
|
+
data-priority="3"
|
|
138
|
+
>
|
|
139
|
+
<Tabstrip
|
|
140
|
+
enableRenameTab
|
|
141
|
+
enableAddTab={enableAddTab}
|
|
142
|
+
enableCloseTab={enableCloseTabs}
|
|
143
|
+
keyBoardActivation={keyBoardActivation}
|
|
144
|
+
onActiveChange={handleTabSelection}
|
|
145
|
+
onAddTab={handleAddTab}
|
|
146
|
+
onCloseTab={handleTabClose}
|
|
147
|
+
onExitEditMode={handleExitEditMode}
|
|
148
|
+
onMouseDown={handleMouseDown}
|
|
149
|
+
activeTabIndex={active || (child === null ? -1 : 0)}
|
|
150
|
+
>
|
|
151
|
+
{renderTabs()}
|
|
152
|
+
</Tabstrip>
|
|
153
|
+
</ToolbarField>
|
|
154
|
+
</Toolbar>
|
|
155
|
+
) : null}
|
|
156
|
+
{child}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
Stack.displayName = "Stack";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useIdMemo as useId } from "@heswell/uitk-core";
|
|
2
|
+
import React, { ReactElement, useRef } from "react";
|
|
3
|
+
import { Stack } from "./Stack";
|
|
4
|
+
// import { Tooltray } from "../toolbar";
|
|
5
|
+
// import { CloseButton, MinimizeButton, MaximizeButton } from "../action-buttons";
|
|
6
|
+
import Component from "../Component";
|
|
7
|
+
import { useLayoutProviderDispatch } from "../layout-provider";
|
|
8
|
+
import { useViewActionDispatcher, View } from "../layout-view";
|
|
9
|
+
import { registerComponent } from "../registry/ComponentRegistry";
|
|
10
|
+
import { usePersistentState } from "../use-persistent-state";
|
|
11
|
+
import { StackProps } from "./stackTypes";
|
|
12
|
+
|
|
13
|
+
import "./Stack.css";
|
|
14
|
+
|
|
15
|
+
const defaultCreateNewChild = (index: number) => (
|
|
16
|
+
// Note make this width 100% and height 100% and we get a weird error where view continually resizes - growing
|
|
17
|
+
<View
|
|
18
|
+
resizeable
|
|
19
|
+
title={`Tab ${index}`}
|
|
20
|
+
style={{ flexGrow: 1, flexShrink: 0, flexBasis: 0 }}
|
|
21
|
+
header
|
|
22
|
+
closeable
|
|
23
|
+
>
|
|
24
|
+
<Component style={{ flex: 1 }} />
|
|
25
|
+
</View>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const StackLayout = (props: StackProps) => {
|
|
29
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
30
|
+
const dispatch = useLayoutProviderDispatch();
|
|
31
|
+
const { loadState, saveState } = usePersistentState();
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
createNewChild = defaultCreateNewChild,
|
|
35
|
+
id: idProp,
|
|
36
|
+
onTabSelectionChanged,
|
|
37
|
+
path,
|
|
38
|
+
...restProps
|
|
39
|
+
} = props;
|
|
40
|
+
|
|
41
|
+
const { children } = props;
|
|
42
|
+
|
|
43
|
+
const id = useId(idProp);
|
|
44
|
+
|
|
45
|
+
const [dispatchViewAction] = useViewActionDispatcher(id, ref, path);
|
|
46
|
+
|
|
47
|
+
const handleTabSelection = (nextIdx: number) => {
|
|
48
|
+
console.log(`StackLayout handleTabSelection nextTab = ${nextIdx}`);
|
|
49
|
+
if (path) {
|
|
50
|
+
dispatch({ type: "switch-tab", path, nextIdx });
|
|
51
|
+
onTabSelectionChanged?.(nextIdx);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleTabClose = (tabIndex: number) => {
|
|
56
|
+
if (Array.isArray(children)) {
|
|
57
|
+
const {
|
|
58
|
+
props: { "data-path": dataPath, path = dataPath },
|
|
59
|
+
} = children[tabIndex];
|
|
60
|
+
dispatch({ type: "remove", path });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleTabAdd = (e: any, tabIndex = React.Children.count(children)) => {
|
|
65
|
+
if (path) {
|
|
66
|
+
console.log(`[StackLayout] handleTabAdd`);
|
|
67
|
+
const component = createNewChild(tabIndex);
|
|
68
|
+
console.log({ component });
|
|
69
|
+
dispatch({
|
|
70
|
+
type: "add",
|
|
71
|
+
path,
|
|
72
|
+
component,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleMouseDown = async (e: any, index: number) => {
|
|
78
|
+
// If user drags the selected Tab, we need to select another Tab and re-render.
|
|
79
|
+
// This needs to be co-ordinated with drag Tab within Tabstrip, whcih can
|
|
80
|
+
// be handles within The Tabstrip until final release - much like Splitter
|
|
81
|
+
// dragging in Flexbox.
|
|
82
|
+
let readyToDrag: undefined | ((value: unknown) => void);
|
|
83
|
+
|
|
84
|
+
// Experimental
|
|
85
|
+
const preDragActivity = async () =>
|
|
86
|
+
new Promise((resolve) => {
|
|
87
|
+
console.log("preDragActivity: Ok, gonna release the drag");
|
|
88
|
+
readyToDrag = resolve;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const dragging = await dispatchViewAction(
|
|
92
|
+
{ type: "mousedown", index, preDragActivity },
|
|
93
|
+
e
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (dragging) {
|
|
97
|
+
readyToDrag?.(undefined);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleTabEdit = (tabIndex: number, text: string) => {
|
|
102
|
+
// Save into state on behalf of the associated View
|
|
103
|
+
// Do we need a mechanism to get this into the JSPOMN when we serialize ?
|
|
104
|
+
// const { id } = children[tabIndex].props;
|
|
105
|
+
// saveState(id, 'view-title', text);
|
|
106
|
+
dispatch({ type: "set-title", path: `${path}.${tabIndex}`, title: text });
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const getTabLabel = (component: ReactElement, idx: number) => {
|
|
110
|
+
const { id, title } = component.props;
|
|
111
|
+
return loadState(id, "view-title") || title || `Tab ${idx + 1}`;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Stack
|
|
116
|
+
{...restProps}
|
|
117
|
+
id={id}
|
|
118
|
+
getTabLabel={getTabLabel}
|
|
119
|
+
onMouseDown={handleMouseDown}
|
|
120
|
+
onTabAdd={handleTabAdd}
|
|
121
|
+
onTabClose={handleTabClose}
|
|
122
|
+
onTabEdit={handleTabEdit}
|
|
123
|
+
onTabSelectionChanged={handleTabSelection}
|
|
124
|
+
ref={ref}
|
|
125
|
+
// toolbarContent={
|
|
126
|
+
// <Tooltray data-align="right" className="layout-buttons">
|
|
127
|
+
// <MinimizeButton />
|
|
128
|
+
// <MaximizeButton />
|
|
129
|
+
// <CloseButton />
|
|
130
|
+
// </Tooltray>
|
|
131
|
+
// }
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
StackLayout.displayName = "Stack";
|
|
136
|
+
|
|
137
|
+
registerComponent("Stack", StackLayout, "container");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { HTMLAttributes, MouseEvent, ReactElement, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface StackProps
|
|
4
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "onMouseDown"> {
|
|
5
|
+
active?: number;
|
|
6
|
+
createNewChild?: (index: number) => ReactElement;
|
|
7
|
+
enableAddTab?: boolean;
|
|
8
|
+
enableCloseTabs?: boolean;
|
|
9
|
+
getTabLabel?: (component: ReactElement, index: number) => string;
|
|
10
|
+
keyBoardActivation?: "automatic" | "manual";
|
|
11
|
+
onMouseDown?: (e: MouseEvent, tabIndex: number) => void;
|
|
12
|
+
onTabAdd?: (tabIndex: number) => void;
|
|
13
|
+
onTabClose?: (tabIndex: number) => void;
|
|
14
|
+
onTabEdit?: (tabIndex: number, label: string) => void;
|
|
15
|
+
onTabSelectionChanged?: (nextIndex: number) => void;
|
|
16
|
+
path?: string;
|
|
17
|
+
showTabs?: boolean;
|
|
18
|
+
toolbarContent?: ReactNode;
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
import './TabPanel.css';
|
|
4
|
+
|
|
5
|
+
export interface TabPanelProps extends HTMLAttributes<HTMLDivElement>{
|
|
6
|
+
ariaLabelledBy: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TabPanel = ({ ariaLabelledBy, children, id }: TabPanelProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="TabPanel" id={id} role="tabpanel" aria-labelledby={ariaLabelledBy}>
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default TabPanel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as TabPanel } from './TabPanel';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { LayoutConfigurator, LayoutTreeViewer } from '..';
|
|
4
|
+
import { followPathToComponent } from '../..';
|
|
5
|
+
|
|
6
|
+
export const ConfigWrapper = ({ children }) => {
|
|
7
|
+
const designMode = false;
|
|
8
|
+
// const [designMode, setDesignMode] = useState(false);
|
|
9
|
+
const [layout, setLayout] = useState(children);
|
|
10
|
+
const [selectedComponent, setSelectedComponent] = useState(children);
|
|
11
|
+
|
|
12
|
+
const handleSelection = (selectedPath) => {
|
|
13
|
+
const targetComponent = followPathToComponent(layout, selectedPath);
|
|
14
|
+
setSelectedComponent(targetComponent);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const handleChange = (property, value) => {
|
|
18
|
+
console.log(`change ${property} -> ${value}`);
|
|
19
|
+
|
|
20
|
+
// 2) replace selectedComponent and set layout
|
|
21
|
+
const newComponent = React.cloneElement(selectedComponent, {
|
|
22
|
+
style: {
|
|
23
|
+
...selectedComponent.props.style,
|
|
24
|
+
[property]: value
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
setSelectedComponent(newComponent);
|
|
28
|
+
setLayout(React.cloneElement(layout, null, newComponent));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div data-design-mode={`${designMode}`}>
|
|
33
|
+
{layout}
|
|
34
|
+
<br />
|
|
35
|
+
<div style={{ display: 'flex' }}>
|
|
36
|
+
<LayoutConfigurator
|
|
37
|
+
height={300}
|
|
38
|
+
managedStyle={selectedComponent.props.style}
|
|
39
|
+
width={300}
|
|
40
|
+
onChange={handleChange}
|
|
41
|
+
/>
|
|
42
|
+
<LayoutTreeViewer
|
|
43
|
+
layout={layout}
|
|
44
|
+
onSelect={handleSelection}
|
|
45
|
+
style={{ width: 300, height: 300, backgroundColor: '#ccc' }}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
{/* <StateButton
|
|
49
|
+
defaultChecked={false}
|
|
50
|
+
onChange={(e, value) => setDesignMode(value)}>Design Mode</StateButton> */}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ConfigWrapper';
|