@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,135 @@
|
|
|
1
|
+
import React, { HTMLAttributes, KeyboardEvent, useCallback, useRef, useState } from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
|
|
4
|
+
import './Splitter.css';
|
|
5
|
+
|
|
6
|
+
export type SplitterDragStartHandler = (index: number) => void;
|
|
7
|
+
export type SplitterDragHandler = (index: number, distance: number) => void;
|
|
8
|
+
export type SplitterDragEndHandler = () => void;
|
|
9
|
+
|
|
10
|
+
export interface SplitterProps
|
|
11
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'onDrag' | 'onDragStart'> {
|
|
12
|
+
column: boolean;
|
|
13
|
+
index: number;
|
|
14
|
+
onDragStart: SplitterDragStartHandler;
|
|
15
|
+
onDrag: SplitterDragHandler;
|
|
16
|
+
onDragEnd: SplitterDragEndHandler;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Splitter = React.memo(function Splitter({
|
|
20
|
+
column,
|
|
21
|
+
index,
|
|
22
|
+
onDrag,
|
|
23
|
+
onDragEnd,
|
|
24
|
+
onDragStart,
|
|
25
|
+
style
|
|
26
|
+
}: SplitterProps) {
|
|
27
|
+
const ignoreClick = useRef<boolean>();
|
|
28
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
const lastPos = useRef<number>(0);
|
|
30
|
+
|
|
31
|
+
const [active, setActive] = useState(false);
|
|
32
|
+
|
|
33
|
+
const handleKeyDownDrag = useCallback(
|
|
34
|
+
({ key, shiftKey }) => {
|
|
35
|
+
// TODO calc max distance
|
|
36
|
+
const distance = shiftKey ? 10 : 1;
|
|
37
|
+
if (column && key === 'ArrowDown') {
|
|
38
|
+
onDrag(index, distance);
|
|
39
|
+
} else if (column && key === 'ArrowUp') {
|
|
40
|
+
onDrag(index, -distance);
|
|
41
|
+
} else if (!column && key === 'ArrowLeft') {
|
|
42
|
+
onDrag(index, -distance);
|
|
43
|
+
} else if (!column && key === 'ArrowRight') {
|
|
44
|
+
onDrag(index, distance);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
[column, index, onDrag]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleKeyDownInitDrag = useCallback(
|
|
51
|
+
(evt) => {
|
|
52
|
+
const { key } = evt;
|
|
53
|
+
const horizontalMove = key === 'ArrowLeft' || key === 'ArrowRIght';
|
|
54
|
+
const verticalMove = key === 'ArrowUp' || key === 'ArrowDown';
|
|
55
|
+
if ((column && verticalMove) || (!column && horizontalMove)) {
|
|
56
|
+
onDragStart(index);
|
|
57
|
+
handleKeyDownDrag(evt);
|
|
58
|
+
keyDownHandlerRef.current = handleKeyDownDrag;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[column, handleKeyDownDrag, index, onDragStart]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const keyDownHandlerRef = useRef(handleKeyDownInitDrag);
|
|
65
|
+
const handleKeyDown = (evt: KeyboardEvent) => keyDownHandlerRef.current(evt);
|
|
66
|
+
|
|
67
|
+
const handleMouseMove = useCallback(
|
|
68
|
+
(e) => {
|
|
69
|
+
ignoreClick.current = true;
|
|
70
|
+
const pos = e[column ? 'clientY' : 'clientX'];
|
|
71
|
+
const diff = pos - lastPos.current;
|
|
72
|
+
// we seem to get a final value of zero
|
|
73
|
+
if (pos && pos !== lastPos.current) {
|
|
74
|
+
onDrag(index, diff);
|
|
75
|
+
}
|
|
76
|
+
lastPos.current = pos;
|
|
77
|
+
},
|
|
78
|
+
[column, index, onDrag]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const handleMouseUp = useCallback(() => {
|
|
82
|
+
window.removeEventListener('mousemove', handleMouseMove, false);
|
|
83
|
+
window.removeEventListener('mouseup', handleMouseUp, false);
|
|
84
|
+
onDragEnd();
|
|
85
|
+
setActive(false);
|
|
86
|
+
rootRef.current?.focus();
|
|
87
|
+
}, [handleMouseMove, onDragEnd, setActive]);
|
|
88
|
+
|
|
89
|
+
const handleMouseDown = useCallback(
|
|
90
|
+
(e) => {
|
|
91
|
+
lastPos.current = column ? e.clientY : e.clientX;
|
|
92
|
+
onDragStart(index);
|
|
93
|
+
window.addEventListener('mousemove', handleMouseMove, false);
|
|
94
|
+
window.addEventListener('mouseup', handleMouseUp, false);
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setActive(true);
|
|
97
|
+
},
|
|
98
|
+
[column, handleMouseMove, handleMouseUp, index, onDragStart, setActive]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const handleFocus = () => {
|
|
102
|
+
// TODO
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleClick = () => {
|
|
106
|
+
if (ignoreClick.current) {
|
|
107
|
+
ignoreClick.current = false;
|
|
108
|
+
} else {
|
|
109
|
+
rootRef.current?.focus();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleBlur = () => {
|
|
114
|
+
// TODO
|
|
115
|
+
keyDownHandlerRef.current = handleKeyDownInitDrag;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const className = cx('Splitter', 'focusable', { active, column });
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
className={className}
|
|
122
|
+
data-splitter
|
|
123
|
+
ref={rootRef}
|
|
124
|
+
role="separator"
|
|
125
|
+
style={style}
|
|
126
|
+
onBlur={handleBlur}
|
|
127
|
+
onClick={handleClick}
|
|
128
|
+
onFocus={handleFocus}
|
|
129
|
+
onKeyDown={handleKeyDown}
|
|
130
|
+
onMouseDown={handleMouseDown}
|
|
131
|
+
tabIndex={0}>
|
|
132
|
+
<div className="grab-zone" />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getProp } from '../utils';
|
|
2
|
+
import { getIntrinsicSize, hasUnboundedFlexStyle } from '../layout-reducer/flexUtils';
|
|
3
|
+
import { ReactElement } from 'react';
|
|
4
|
+
import type { BreakPoint, ContentMeta } from './flexboxTypes';
|
|
5
|
+
|
|
6
|
+
const NO_INTRINSIC_SIZE: {
|
|
7
|
+
height?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
} = {};
|
|
10
|
+
|
|
11
|
+
export const SPLITTER = 1;
|
|
12
|
+
export const PLACEHOLDER = 2;
|
|
13
|
+
|
|
14
|
+
const isIntrinsicallySized = (item: ContentMeta) => typeof item.intrinsicSize === 'number';
|
|
15
|
+
|
|
16
|
+
const getBreakPointValues = (breakPoints: BreakPoint[], component: ReactElement) => {
|
|
17
|
+
const values: { [key: string]: number | undefined } = {};
|
|
18
|
+
breakPoints.forEach((breakPoint) => {
|
|
19
|
+
values[breakPoint] = getProp(component, breakPoint);
|
|
20
|
+
});
|
|
21
|
+
return values;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const gatherChildMeta = (
|
|
25
|
+
children: ReactElement[],
|
|
26
|
+
dimension: 'width' | 'height',
|
|
27
|
+
breakPoints?: BreakPoint[]
|
|
28
|
+
) => {
|
|
29
|
+
return children.map((child, index) => {
|
|
30
|
+
const resizeable = getProp(child, 'resizeable');
|
|
31
|
+
const { [dimension]: intrinsicSize } = getIntrinsicSize(child) ?? NO_INTRINSIC_SIZE;
|
|
32
|
+
const flexOpen = hasUnboundedFlexStyle(child);
|
|
33
|
+
if (breakPoints) {
|
|
34
|
+
return {
|
|
35
|
+
index,
|
|
36
|
+
flexOpen,
|
|
37
|
+
intrinsicSize,
|
|
38
|
+
resizeable,
|
|
39
|
+
...getBreakPointValues(breakPoints, child)
|
|
40
|
+
};
|
|
41
|
+
} else {
|
|
42
|
+
return { index, flexOpen, intrinsicSize, resizeable };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Splitters are inserted AFTER the associated index, so
|
|
48
|
+
// never a splitter in last position.
|
|
49
|
+
// Placeholder goes before (first) OR after(last) index
|
|
50
|
+
export const findSplitterAndPlaceholderPositions = (childMeta: ContentMeta[]) => {
|
|
51
|
+
const count = childMeta.length;
|
|
52
|
+
const allIntrinsic = childMeta.every(isIntrinsicallySized);
|
|
53
|
+
const splitterPositions = Array(count).fill(0);
|
|
54
|
+
if (allIntrinsic) {
|
|
55
|
+
splitterPositions[0] = PLACEHOLDER;
|
|
56
|
+
splitterPositions[count - 1] = PLACEHOLDER;
|
|
57
|
+
}
|
|
58
|
+
if (count < 2) {
|
|
59
|
+
return splitterPositions;
|
|
60
|
+
} else {
|
|
61
|
+
// 1) From the left, check each item.
|
|
62
|
+
// Once we hit a resizable item, set this index and all subsequent indices,
|
|
63
|
+
// except for last, to SPLITTER
|
|
64
|
+
for (let i = 0, resizeablesLeft = 0; i < count - 1; i++) {
|
|
65
|
+
if (childMeta[i].resizeable && !resizeablesLeft) {
|
|
66
|
+
resizeablesLeft = SPLITTER;
|
|
67
|
+
}
|
|
68
|
+
splitterPositions[i] += resizeablesLeft;
|
|
69
|
+
}
|
|
70
|
+
// 2) Now check from the right. Undo splitter insertion until we reach a point
|
|
71
|
+
// where there is a resizeable to our right.
|
|
72
|
+
for (let i = count - 1; i > 0; i--) {
|
|
73
|
+
if (splitterPositions[i] & SPLITTER) {
|
|
74
|
+
splitterPositions[i] -= SPLITTER;
|
|
75
|
+
}
|
|
76
|
+
if (childMeta[i].resizeable) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return splitterPositions;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const identifyResizeParties = (contentMeta: ContentMeta[], idx: number) => {
|
|
85
|
+
const idx1 = getLeadingResizeablePos(contentMeta, idx);
|
|
86
|
+
const idx2 = getTrailingResizeablePos(contentMeta, idx);
|
|
87
|
+
const participants = idx1 !== -1 && idx2 !== -1 ? [idx1, idx2] : undefined;
|
|
88
|
+
const bystanders = identifyResizeBystanders(contentMeta, participants);
|
|
89
|
+
return [participants, bystanders];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function identifyResizeBystanders(contentMeta: ContentMeta[], participants?: number[]) {
|
|
93
|
+
if (participants) {
|
|
94
|
+
let bystanders = [];
|
|
95
|
+
for (let i = 0; i < contentMeta.length; i++) {
|
|
96
|
+
if (contentMeta[i].flexOpen && !participants.includes(i)) {
|
|
97
|
+
bystanders.push(i);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return bystanders;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getLeadingResizeablePos(contentMeta: ContentMeta[], idx: number) {
|
|
105
|
+
let pos = idx,
|
|
106
|
+
resizeable = false;
|
|
107
|
+
while (pos >= 1 && !resizeable) {
|
|
108
|
+
pos = pos - 1;
|
|
109
|
+
resizeable = isResizeable(contentMeta, pos);
|
|
110
|
+
}
|
|
111
|
+
return pos;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getTrailingResizeablePos(contentMeta: ContentMeta[], idx: number) {
|
|
115
|
+
let pos = idx,
|
|
116
|
+
resizeable = false,
|
|
117
|
+
count = contentMeta.length;
|
|
118
|
+
while (pos < count && !resizeable) {
|
|
119
|
+
pos = pos + 1;
|
|
120
|
+
resizeable = isResizeable(contentMeta, pos);
|
|
121
|
+
}
|
|
122
|
+
return pos === count ? -1 : pos;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isResizeable(contentMeta: ContentMeta[], idx: number): boolean {
|
|
126
|
+
const { placeholder, splitter, resizeable, intrinsicSize } = contentMeta[idx];
|
|
127
|
+
return Boolean(!splitter && !intrinsicSize && (placeholder || resizeable));
|
|
128
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
HTMLAttributes,
|
|
4
|
+
MutableRefObject,
|
|
5
|
+
ReactElement,
|
|
6
|
+
ReactNode,
|
|
7
|
+
RefObject
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { SplitterProps } from './Splitter';
|
|
10
|
+
|
|
11
|
+
export interface LayoutContainerProps {
|
|
12
|
+
resizeable?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FlexboxProps extends LayoutContainerProps, HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
breakPoints?: BreakPointsProp;
|
|
17
|
+
children: ReactElement[];
|
|
18
|
+
cols?: number;
|
|
19
|
+
column?: true;
|
|
20
|
+
fullPage?: number;
|
|
21
|
+
flexFill?: boolean;
|
|
22
|
+
gap?: number;
|
|
23
|
+
onSplitterMoved?: (content: ContentMeta[]) => void;
|
|
24
|
+
row?: true;
|
|
25
|
+
spacing?: number;
|
|
26
|
+
splitterSize?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SplitterHookProps {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
onSplitterMoved?: (content: ContentMeta[]) => void;
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SplitterHookResult {
|
|
36
|
+
content: ReactElement[];
|
|
37
|
+
rootRef: MutableRefObject<HTMLDivElement | undefined>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type SplitterFactory = (index: number) => ReactElement<SplitterProps>;
|
|
41
|
+
|
|
42
|
+
export type ContentMeta = {
|
|
43
|
+
currentSize?: number;
|
|
44
|
+
flexOpen?: boolean;
|
|
45
|
+
flexBasis?: number;
|
|
46
|
+
intrinsicSize?: number;
|
|
47
|
+
minSize?: number;
|
|
48
|
+
placeholder?: boolean;
|
|
49
|
+
resizeable?: boolean;
|
|
50
|
+
shim?: boolean;
|
|
51
|
+
splitter?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type FlexSize = {
|
|
55
|
+
size: number;
|
|
56
|
+
minSize: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type BreakPoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
60
|
+
export type BreakPoints = BreakPoint[];
|
|
61
|
+
export type BreakPointsProp = {
|
|
62
|
+
[keys in BreakPoint]?: number;
|
|
63
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
CSSProperties,
|
|
4
|
+
isValidElement,
|
|
5
|
+
ReactElement,
|
|
6
|
+
useCallback,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { getUniqueId } from "@vuu-ui/vuu-utils";
|
|
11
|
+
import { gatherChildMeta } from "./flexbox-utils";
|
|
12
|
+
import { BreakPoint } from "./flexboxTypes";
|
|
13
|
+
|
|
14
|
+
const breakPoints: BreakPoint[] = ["xs", "sm", "md", "lg", "xl"];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_COLS = 12;
|
|
17
|
+
|
|
18
|
+
export const useResponsiveSizing = ({
|
|
19
|
+
children: childrenProp,
|
|
20
|
+
cols: colsProp,
|
|
21
|
+
style,
|
|
22
|
+
}: {
|
|
23
|
+
children: ReactElement[];
|
|
24
|
+
cols?: number;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
}) => {
|
|
27
|
+
const rootRef = useRef(null);
|
|
28
|
+
const metaRef = useRef(null);
|
|
29
|
+
const contentRef = useRef<ReactElement[]>();
|
|
30
|
+
const cols = colsProp ?? DEFAULT_COLS;
|
|
31
|
+
|
|
32
|
+
const isColumn = style?.flexDirection === "column";
|
|
33
|
+
const dimension = isColumn ? "height" : "width";
|
|
34
|
+
|
|
35
|
+
const children = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
Array.isArray(childrenProp)
|
|
38
|
+
? childrenProp
|
|
39
|
+
: isValidElement(childrenProp)
|
|
40
|
+
? [childrenProp]
|
|
41
|
+
: [],
|
|
42
|
+
[childrenProp]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const buildContent = useCallback(
|
|
46
|
+
(children, dimension): [ReactElement[], any] => {
|
|
47
|
+
const childMeta = gatherChildMeta(children, dimension, breakPoints);
|
|
48
|
+
const content = [];
|
|
49
|
+
const meta = [];
|
|
50
|
+
for (let i = 0; i < children.length; i++) {
|
|
51
|
+
const child = children[i];
|
|
52
|
+
const {
|
|
53
|
+
style: { flex, ...rest },
|
|
54
|
+
} = child.props;
|
|
55
|
+
// TODO do we always need to clone ?
|
|
56
|
+
// TODO emit the --col-span based on media query
|
|
57
|
+
content.push(
|
|
58
|
+
cloneElement(child, {
|
|
59
|
+
key: getUniqueId(), // need to store these
|
|
60
|
+
style: {
|
|
61
|
+
...rest,
|
|
62
|
+
"--parent-col-count": cols,
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
meta.push(childMeta[i]);
|
|
67
|
+
}
|
|
68
|
+
return [content, meta];
|
|
69
|
+
},
|
|
70
|
+
[cols]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
useMemo(() => {
|
|
74
|
+
// console.log(`useMemo<initialCotent>`, children)
|
|
75
|
+
const [content, meta] = buildContent(children, dimension);
|
|
76
|
+
metaRef.current = meta;
|
|
77
|
+
contentRef.current = content;
|
|
78
|
+
}, [buildContent, children, dimension]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
cols,
|
|
82
|
+
content: contentRef.current,
|
|
83
|
+
rootRef,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ReactElement,
|
|
3
|
+
useCallback,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { getUniqueId } from "@vuu-ui/vuu-utils";
|
|
9
|
+
import { Splitter } from "./Splitter";
|
|
10
|
+
import { Placeholder } from "../placeholder";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
findSplitterAndPlaceholderPositions,
|
|
14
|
+
gatherChildMeta,
|
|
15
|
+
identifyResizeParties,
|
|
16
|
+
PLACEHOLDER,
|
|
17
|
+
SPLITTER,
|
|
18
|
+
} from "./flexbox-utils";
|
|
19
|
+
import {
|
|
20
|
+
ContentMeta,
|
|
21
|
+
FlexSize,
|
|
22
|
+
SplitterFactory,
|
|
23
|
+
SplitterHookProps,
|
|
24
|
+
SplitterHookResult,
|
|
25
|
+
} from "./flexboxTypes";
|
|
26
|
+
|
|
27
|
+
const originalContentOnly = (meta: ContentMeta) =>
|
|
28
|
+
!meta.splitter && !meta.placeholder;
|
|
29
|
+
|
|
30
|
+
export const useSplitterResizing = ({
|
|
31
|
+
children: childrenProp,
|
|
32
|
+
onSplitterMoved,
|
|
33
|
+
style,
|
|
34
|
+
}: SplitterHookProps): SplitterHookResult => {
|
|
35
|
+
const rootRef = useRef<HTMLDivElement>();
|
|
36
|
+
const metaRef = useRef<ContentMeta[]>();
|
|
37
|
+
const contentRef = useRef<ReactElement[]>();
|
|
38
|
+
const assignedKeys = useRef([]);
|
|
39
|
+
const [, forceUpdate] = useState({});
|
|
40
|
+
|
|
41
|
+
const setContent = (content: ReactElement[]) => {
|
|
42
|
+
contentRef.current = content;
|
|
43
|
+
forceUpdate({});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const isColumn = style?.flexDirection === "column";
|
|
47
|
+
const dimension = isColumn ? "height" : "width";
|
|
48
|
+
const children = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
Array.isArray(childrenProp)
|
|
51
|
+
? childrenProp
|
|
52
|
+
: React.isValidElement(childrenProp)
|
|
53
|
+
? [childrenProp]
|
|
54
|
+
: [],
|
|
55
|
+
[childrenProp]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const handleDragStart = useCallback(
|
|
59
|
+
(index) => {
|
|
60
|
+
const { current: contentMeta } = metaRef;
|
|
61
|
+
if (contentMeta) {
|
|
62
|
+
const [participants, bystanders] = identifyResizeParties(
|
|
63
|
+
contentMeta,
|
|
64
|
+
index
|
|
65
|
+
);
|
|
66
|
+
if (participants) {
|
|
67
|
+
participants.forEach((index) => {
|
|
68
|
+
const el = rootRef.current?.childNodes[index] as HTMLElement;
|
|
69
|
+
if (el) {
|
|
70
|
+
const { size, minSize } = measureElement(el, dimension);
|
|
71
|
+
contentMeta[index].currentSize = size;
|
|
72
|
+
contentMeta[index].minSize = minSize;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (bystanders) {
|
|
76
|
+
bystanders.forEach((index) => {
|
|
77
|
+
const el = rootRef.current?.childNodes[index] as HTMLElement;
|
|
78
|
+
if (el) {
|
|
79
|
+
const { [dimension]: size } = el.getBoundingClientRect();
|
|
80
|
+
contentMeta[index].flexBasis = size;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[dimension]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const handleDrag = useCallback(
|
|
91
|
+
(idx, distance) => {
|
|
92
|
+
if (contentRef.current && metaRef.current) {
|
|
93
|
+
setContent(
|
|
94
|
+
resizeContent(
|
|
95
|
+
contentRef.current,
|
|
96
|
+
metaRef.current,
|
|
97
|
+
distance,
|
|
98
|
+
dimension
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[dimension]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const handleDragEnd = useCallback(() => {
|
|
107
|
+
const contentMeta = metaRef.current;
|
|
108
|
+
if (contentMeta) {
|
|
109
|
+
onSplitterMoved?.(contentMeta.filter(originalContentOnly));
|
|
110
|
+
}
|
|
111
|
+
contentMeta?.forEach((meta) => {
|
|
112
|
+
meta.currentSize = undefined;
|
|
113
|
+
meta.flexBasis = undefined;
|
|
114
|
+
meta.flexOpen = false;
|
|
115
|
+
});
|
|
116
|
+
}, [onSplitterMoved]);
|
|
117
|
+
|
|
118
|
+
const createSplitter: SplitterFactory = useCallback(
|
|
119
|
+
(i) => {
|
|
120
|
+
return React.createElement(Splitter, {
|
|
121
|
+
column: isColumn,
|
|
122
|
+
index: i,
|
|
123
|
+
key: `splitter-${i}`,
|
|
124
|
+
onDrag: handleDrag,
|
|
125
|
+
onDragEnd: handleDragEnd,
|
|
126
|
+
onDragStart: handleDragStart,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
[handleDrag, handleDragEnd, handleDragStart, isColumn]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
useMemo(() => {
|
|
133
|
+
// This will always fire when Flexbox has rendered, but nor during splitter resize
|
|
134
|
+
const [content, meta] = buildContent(
|
|
135
|
+
children,
|
|
136
|
+
dimension,
|
|
137
|
+
createSplitter,
|
|
138
|
+
assignedKeys.current
|
|
139
|
+
);
|
|
140
|
+
metaRef.current = meta;
|
|
141
|
+
contentRef.current = content;
|
|
142
|
+
}, [children, createSplitter, dimension]);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content: contentRef.current || [],
|
|
146
|
+
rootRef,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function buildContent(
|
|
151
|
+
children: ReactElement[],
|
|
152
|
+
dimension: "width" | "height",
|
|
153
|
+
createSplitter: SplitterFactory,
|
|
154
|
+
keys: any[]
|
|
155
|
+
): [any[], ContentMeta[]] {
|
|
156
|
+
const childMeta = gatherChildMeta(children, dimension);
|
|
157
|
+
const splitterAndPlaceholderPositions =
|
|
158
|
+
findSplitterAndPlaceholderPositions(childMeta);
|
|
159
|
+
const content = [];
|
|
160
|
+
const meta: ContentMeta[] = [];
|
|
161
|
+
for (let i = 0; i < children.length; i++) {
|
|
162
|
+
const child = children[i];
|
|
163
|
+
if (i === 0 && splitterAndPlaceholderPositions[i] & PLACEHOLDER) {
|
|
164
|
+
//TODO need to assign an id to placeholder
|
|
165
|
+
content.push(createPlaceholder(i));
|
|
166
|
+
meta.push({ placeholder: true, shim: true });
|
|
167
|
+
}
|
|
168
|
+
if (child.key == null) {
|
|
169
|
+
const key = keys[i] || (keys[i] = getUniqueId());
|
|
170
|
+
content.push(React.cloneElement(child, { key }));
|
|
171
|
+
} else {
|
|
172
|
+
content.push(child);
|
|
173
|
+
}
|
|
174
|
+
meta.push(childMeta[i]);
|
|
175
|
+
|
|
176
|
+
if (i > 0 && splitterAndPlaceholderPositions[i] & PLACEHOLDER) {
|
|
177
|
+
content.push(createPlaceholder(i));
|
|
178
|
+
meta.push({ placeholder: true });
|
|
179
|
+
} else if (splitterAndPlaceholderPositions[i] & SPLITTER) {
|
|
180
|
+
content.push(createSplitter(content.length));
|
|
181
|
+
meta.push({ splitter: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [content, meta];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resizeContent(
|
|
188
|
+
content: ReactElement[],
|
|
189
|
+
contentMeta: ContentMeta[],
|
|
190
|
+
distance: number,
|
|
191
|
+
dimension: "width" | "height"
|
|
192
|
+
) {
|
|
193
|
+
const metaUpdated = updateMeta(contentMeta, distance);
|
|
194
|
+
if (!metaUpdated) {
|
|
195
|
+
return content;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return content.map((child, idx) => {
|
|
199
|
+
const meta = contentMeta[idx];
|
|
200
|
+
let { currentSize, flexOpen, flexBasis } = meta;
|
|
201
|
+
const hasCurrentSize = currentSize !== undefined;
|
|
202
|
+
if (hasCurrentSize || flexOpen) {
|
|
203
|
+
const { flexBasis: actualFlexBasis } = child.props.style || {};
|
|
204
|
+
const size = hasCurrentSize ? meta.currentSize : flexBasis;
|
|
205
|
+
if (size !== actualFlexBasis) {
|
|
206
|
+
return React.cloneElement(child, {
|
|
207
|
+
style: {
|
|
208
|
+
...child.props.style,
|
|
209
|
+
flexBasis: size,
|
|
210
|
+
[dimension]: "auto",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
return child;
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
return child;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//TODO detect cursor move beyond drag limit and suspend further resize until cursoe re-engages with splitter
|
|
223
|
+
function updateMeta(contentMeta: ContentMeta[], distance: number) {
|
|
224
|
+
const resizeTargets: number[] = [];
|
|
225
|
+
|
|
226
|
+
contentMeta.forEach((meta, idx) => {
|
|
227
|
+
if (meta.currentSize !== undefined) {
|
|
228
|
+
resizeTargets.push(idx);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// we want the target being reduced first, this may limit the distance we can apply
|
|
233
|
+
let target1 = distance < 0 ? resizeTargets[0] : resizeTargets[1];
|
|
234
|
+
|
|
235
|
+
const { currentSize = 0, minSize = 0 } = contentMeta[target1];
|
|
236
|
+
if (currentSize === minSize) {
|
|
237
|
+
// size is already 0, we cannot go further
|
|
238
|
+
return false;
|
|
239
|
+
} else if (Math.abs(distance) > currentSize - minSize) {
|
|
240
|
+
// reduce to 0
|
|
241
|
+
const multiplier = distance < 0 ? -1 : 1;
|
|
242
|
+
distance = Math.max(0, currentSize - minSize) * multiplier;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const leadingItem = contentMeta[resizeTargets[0]] as ContentMeta;
|
|
246
|
+
const { currentSize: leadingSize = 0 } = leadingItem;
|
|
247
|
+
leadingItem.currentSize = leadingSize + distance;
|
|
248
|
+
|
|
249
|
+
const trailingItem = contentMeta[resizeTargets[1]] as ContentMeta;
|
|
250
|
+
const { currentSize: trailingSize = 0 } = trailingItem;
|
|
251
|
+
trailingItem.currentSize = trailingSize - distance;
|
|
252
|
+
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function createPlaceholder(index: number) {
|
|
257
|
+
return React.createElement(Placeholder, {
|
|
258
|
+
shim: index === 0,
|
|
259
|
+
key: `placeholder-${index}`,
|
|
260
|
+
} as any);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function measureElement(
|
|
264
|
+
el: HTMLElement,
|
|
265
|
+
dimension: "width" | "height"
|
|
266
|
+
): FlexSize {
|
|
267
|
+
const { [dimension]: size } = el.getBoundingClientRect();
|
|
268
|
+
const style = getComputedStyle(el);
|
|
269
|
+
const minSizeVal = style.getPropertyValue(`min-${dimension}`);
|
|
270
|
+
const minSize = minSizeVal.endsWith("px") ? parseInt(minSizeVal, 10) : 0;
|
|
271
|
+
return { size, minSize };
|
|
272
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export * from "./action-buttons";
|
|
2
|
+
export * from "./chest-of-drawers";
|
|
3
|
+
export { default as Component } from "./Component";
|
|
4
|
+
export * from "./common-types";
|
|
5
|
+
export * from "./dialog";
|
|
6
|
+
export * from "./DraggableLayout";
|
|
7
|
+
export * from "./drag-drop";
|
|
8
|
+
export * from "./flexbox";
|
|
9
|
+
export { Action } from "./layout-action";
|
|
10
|
+
export * from "./layout-header";
|
|
11
|
+
export * from "./layout-provider";
|
|
12
|
+
export * from "./palette";
|
|
13
|
+
export * from "./placeholder";
|
|
14
|
+
export * from "./registry";
|
|
15
|
+
export * from "./responsive";
|
|
16
|
+
export * from "./stack";
|
|
17
|
+
export * from "./tools";
|
|
18
|
+
export * from "./use-persistent-state";
|
|
19
|
+
export * from "./utils";
|
|
20
|
+
export * from "./layout-view";
|