@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.
Files changed (117) hide show
  1. package/README.md +1 -0
  2. package/package.json +30 -0
  3. package/src/Component.css +2 -0
  4. package/src/Component.tsx +20 -0
  5. package/src/DraggableLayout.css +18 -0
  6. package/src/DraggableLayout.tsx +29 -0
  7. package/src/__tests__/flexbox-utils.spec.js +90 -0
  8. package/src/action-buttons/action-buttons.css +12 -0
  9. package/src/action-buttons/action-buttons.tsx +30 -0
  10. package/src/action-buttons/index.ts +1 -0
  11. package/src/chest-of-drawers/Chest.css +36 -0
  12. package/src/chest-of-drawers/Chest.tsx +42 -0
  13. package/src/chest-of-drawers/Drawer.css +153 -0
  14. package/src/chest-of-drawers/Drawer.tsx +118 -0
  15. package/src/chest-of-drawers/index.ts +2 -0
  16. package/src/common-types.ts +9 -0
  17. package/src/debug.ts +16 -0
  18. package/src/dialog/Dialog.css +16 -0
  19. package/src/dialog/Dialog.tsx +59 -0
  20. package/src/dialog/index.ts +1 -0
  21. package/src/drag-drop/BoxModel.ts +546 -0
  22. package/src/drag-drop/DragState.ts +222 -0
  23. package/src/drag-drop/Draggable.ts +282 -0
  24. package/src/drag-drop/DropMenu.css +70 -0
  25. package/src/drag-drop/DropMenu.tsx +68 -0
  26. package/src/drag-drop/DropTarget.ts +392 -0
  27. package/src/drag-drop/DropTargetRenderer.css +40 -0
  28. package/src/drag-drop/DropTargetRenderer.tsx +284 -0
  29. package/src/drag-drop/dragDropTypes.ts +49 -0
  30. package/src/drag-drop/index.ts +4 -0
  31. package/src/editable-label/EditableLabel.css +28 -0
  32. package/src/editable-label/EditableLabel.tsx +99 -0
  33. package/src/editable-label/index.ts +1 -0
  34. package/src/flexbox/Flexbox.css +45 -0
  35. package/src/flexbox/Flexbox.tsx +70 -0
  36. package/src/flexbox/FlexboxLayout.jsx +26 -0
  37. package/src/flexbox/FluidGrid.css +134 -0
  38. package/src/flexbox/FluidGrid.tsx +84 -0
  39. package/src/flexbox/FluidGridLayout.tsx +10 -0
  40. package/src/flexbox/Splitter.css +140 -0
  41. package/src/flexbox/Splitter.tsx +135 -0
  42. package/src/flexbox/flexbox-utils.ts +128 -0
  43. package/src/flexbox/flexboxTypes.ts +63 -0
  44. package/src/flexbox/index.ts +4 -0
  45. package/src/flexbox/useResponsiveSizing.ts +85 -0
  46. package/src/flexbox/useSplitterResizing.ts +272 -0
  47. package/src/index.ts +20 -0
  48. package/src/layout-action.ts +21 -0
  49. package/src/layout-header/ActionButton.tsx +23 -0
  50. package/src/layout-header/Header.css +8 -0
  51. package/src/layout-header/Header.tsx +222 -0
  52. package/src/layout-header/index.ts +1 -0
  53. package/src/layout-provider/LayoutProvider.tsx +160 -0
  54. package/src/layout-provider/LayoutProviderContext.ts +17 -0
  55. package/src/layout-provider/index.ts +2 -0
  56. package/src/layout-provider/useLayoutDragDrop.ts +241 -0
  57. package/src/layout-reducer/flexUtils.ts +281 -0
  58. package/src/layout-reducer/index.ts +4 -0
  59. package/src/layout-reducer/insert-layout-element.ts +365 -0
  60. package/src/layout-reducer/layout-reducer.ts +255 -0
  61. package/src/layout-reducer/layoutTypes.ts +151 -0
  62. package/src/layout-reducer/layoutUtils.ts +302 -0
  63. package/src/layout-reducer/remove-layout-element.ts +240 -0
  64. package/src/layout-reducer/replace-layout-element.ts +118 -0
  65. package/src/layout-reducer/resize-flex-children.ts +56 -0
  66. package/src/layout-reducer/wrap-layout-element.ts +317 -0
  67. package/src/layout-view/View.css +58 -0
  68. package/src/layout-view/View.tsx +149 -0
  69. package/src/layout-view/ViewContext.ts +31 -0
  70. package/src/layout-view/index.ts +4 -0
  71. package/src/layout-view/useView.tsx +104 -0
  72. package/src/layout-view/useViewActionDispatcher.ts +133 -0
  73. package/src/layout-view/useViewResize.ts +53 -0
  74. package/src/layout-view/viewTypes.ts +37 -0
  75. package/src/palette/Palette.css +37 -0
  76. package/src/palette/Palette.tsx +140 -0
  77. package/src/palette/PaletteUitk.css +9 -0
  78. package/src/palette/PaletteUitk.tsx +79 -0
  79. package/src/palette/index.ts +2 -0
  80. package/src/placeholder/Placeholder.css +10 -0
  81. package/src/placeholder/Placeholder.tsx +39 -0
  82. package/src/placeholder/index.ts +1 -0
  83. package/src/registry/ComponentRegistry.ts +35 -0
  84. package/src/registry/index.ts +1 -0
  85. package/src/responsive/OverflowMenu.css +31 -0
  86. package/src/responsive/OverflowMenu.jsx +56 -0
  87. package/src/responsive/breakpoints.ts +48 -0
  88. package/src/responsive/index.ts +4 -0
  89. package/src/responsive/measureMinimumNodeSize.ts +23 -0
  90. package/src/responsive/overflowUtils.js +14 -0
  91. package/src/responsive/use-breakpoints.ts +100 -0
  92. package/src/responsive/useOverflowObserver.ts +606 -0
  93. package/src/responsive/useResizeObserver.ts +154 -0
  94. package/src/responsive/utils.ts +37 -0
  95. package/src/stack/Stack.css +39 -0
  96. package/src/stack/Stack.tsx +160 -0
  97. package/src/stack/StackLayout.tsx +137 -0
  98. package/src/stack/index.ts +3 -0
  99. package/src/stack/stackTypes.ts +19 -0
  100. package/src/tabs/TabPanel.css +12 -0
  101. package/src/tabs/TabPanel.tsx +17 -0
  102. package/src/tabs/index.ts +1 -0
  103. package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
  104. package/src/tools/config-wrapper/index.js +1 -0
  105. package/src/tools/devtools-box/layout-configurator.css +112 -0
  106. package/src/tools/devtools-box/layout-configurator.jsx +369 -0
  107. package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
  108. package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
  109. package/src/tools/index.js +3 -0
  110. package/src/use-persistent-state.ts +115 -0
  111. package/src/utils/componentFromLayout.tsx +30 -0
  112. package/src/utils/index.ts +6 -0
  113. package/src/utils/pathUtils.ts +294 -0
  114. package/src/utils/propUtils.ts +24 -0
  115. package/src/utils/refUtils.ts +16 -0
  116. package/src/utils/styleUtils.ts +14 -0
  117. 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,4 @@
1
+ export { default as Flexbox } from './Flexbox';
2
+ export * from './FlexboxLayout';
3
+ export * from './FluidGrid';
4
+ export * from './FluidGridLayout';
@@ -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";