fix(Map): Add new windows system and removed old

This commit is contained in:
achichenkov
2025-01-09 17:40:48 +03:00
parent 8aaa2e7add
commit fc36d51e24
6 changed files with 93 additions and 331 deletions

View File

@@ -1,6 +1,5 @@
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { WidgetGridItem, WidgetsGrid } from '@/hooks/Mapper/components/mapInterface/components';
import {
LocalCharacters,
RoutesWidget,
@@ -9,60 +8,10 @@ import {
} from '@/hooks/Mapper/components/mapInterface/widgets';
import { useState } from 'react';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { WindowManager, WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager';
// import { debounce } from 'lodash/debounce';
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
const DEFAULT_WINDOWS = [
{
name: 'info',
rightOffset: 5,
width: 5,
height: 4,
item: () => <SystemInfo />,
},
{
name: 'local',
rightOffset: 5,
topOffset: 4,
width: 5,
height: 4,
item: () => <LocalCharacters />,
},
{ name: 'signatures', width: 8, height: 4, topOffset: 8, rightOffset: 12, item: () => <SystemSignatures /> },
{
name: 'routes',
rightOffset: 0,
topOffset: 8,
width: 5,
height: 6,
item: () => <RoutesWidget />,
},
];
const saveWindowsToLS = (toSaveItems: WidgetGridItem[]) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const out = toSaveItems.map(({ item, ...rest }) => rest);
localStorage.setItem(SESSION_KEY.windows, JSON.stringify(out));
};
const restoreWindowsFromLS = (): WidgetGridItem[] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const raw = localStorage.getItem(SESSION_KEY.windows);
if (!raw) {
console.warn('No windows found in local storage!!');
return DEFAULT_WINDOWS;
}
// eslint-disable-next-line no-debugger
const out = (JSON.parse(raw) as Omit<WidgetGridItem, 'item'>[])
.filter(x => DEFAULT_WINDOWS.find(def => def.name === x.name))
.map(x => {
const windowItem = DEFAULT_WINDOWS.find(def => def.name === x.name)?.item;
return { ...x, item: windowItem! };
});
return out;
};
const CURRENT_WINDOWS_VERSION = 2;
const DEFAULT: WindowProps[] = [
{
@@ -95,18 +44,52 @@ const DEFAULT: WindowProps[] = [
},
];
export const MapInterface = () => {
return <WindowManager windows={DEFAULT} dragSelector=".react-grid-dragHandleExample" />;
// const [items, setItems] = useState<WidgetGridItem[]>(restoreWindowsFromLS);
//
// return (
// <WidgetsGrid
// items={items}
// onChange={x => {
// saveWindowsToLS(x);
// setItems(x);
// }}
// />
// );
type WindowsLS = {
windows: WindowProps[];
version: number;
};
const saveWindowsToLS = (toSaveItems: WindowProps[]) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const out = toSaveItems.map(({ content, ...rest }) => rest);
localStorage.setItem(SESSION_KEY.windows, JSON.stringify({ version: CURRENT_WINDOWS_VERSION, windows: out }));
};
const restoreWindowsFromLS = (): WindowProps[] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const raw = localStorage.getItem(SESSION_KEY.windows);
if (!raw) {
console.warn('No windows found in local storage!!');
return DEFAULT;
}
const { version, windows } = JSON.parse(raw) as WindowsLS;
if (!version || CURRENT_WINDOWS_VERSION > version) {
return DEFAULT;
}
// eslint-disable-next-line no-debugger
const out = (windows as Omit<WindowProps, 'content'>[])
.filter(x => DEFAULT.find(def => def.id === x.id))
.map(x => {
const content = DEFAULT.find(def => def.id === x.id)?.content;
return { ...x, content: content! };
});
return out;
};
export const MapInterface = () => {
const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
return (
<WindowManager
windows={items}
dragSelector=".react-grid-dragHandleExample"
onChange={x => {
saveWindowsToLS(x);
setItems(x);
}}
/>
);
};

View File

@@ -1,37 +0,0 @@
.GridLayoutWrapper {
width: 100%;
height: 100% !important;
}
.GridLayout {
width: 100%;
height: 100% !important;
pointer-events: none;
& > div {
pointer-events: initial;
}
:global {
.react-resizable-handle::after {
border-color: #696969 !important;
}
.react-grid-placeholder {
background-color: rgba(147, 147, 147, 0.3);
//filter: blur(5px);
border: 2px dashed #b6b6b6;
}
.react-grid-item {
transition-property: none !important;
}
.react-grid-item.cssTransforms {
transition-property: none !important;
}
}
}

View File

@@ -1,196 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import classes from './WidgetsGrid.module.scss';
import { ItemCallback, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
import clsx from 'clsx';
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
const ResponsiveGridLayout = WidthProvider(Responsive);
const colSize = 50;
const initState = { breakpoints: 100, cols: 2 };
export type WidgetGridItem = {
rightOffset?: number;
leftOffset?: number;
topOffset?: number;
width: number;
height: number;
name: string;
item: () => React.ReactNode;
};
export interface WidgetsGridProps {
items: WidgetGridItem[];
onChange: (items: WidgetGridItem[]) => void;
}
export const WidgetsGrid = ({ items, onChange }: WidgetsGridProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const [, setKey] = useState(0);
const [callRerenderOfGrid, setCallRerenderOfGrid] = useState(0);
const isTabVisible = usePageVisibility();
const refAll = useRef({
isReady: false,
layouts: {
lg: [
// { i: 'a', w: 4, h: 16, x: 22, y: 0 },
// { i: 'b', w: 5, h: 10, x: 17, y: 0 },
],
} as Layouts,
breakpoints: { lg: 100, md: 0, sm: 0, xs: 0, xxs: 0 },
cols: { lg: 26, md: 0, sm: 0, xs: 0, xxs: 0 },
containerWidth: 0,
colsPrev: 26,
needPostProcess: false,
items: [...items],
});
// TODO
// 1. onLayoutChange (original) not calling when we change x of any widget
// 2. setKey need no call rerender for update props
const onLayoutChange: ItemCallback = (newItems, _, newItem) => {
const updatedItems = newItems.map(item => {
const toLeft = (item.x + item.w / 2) / refAll.current.cols.lg <= 0.5;
const original = refAll.current.items.find(x => x.name === item.i)!;
return {
...original,
width: item.w,
height: item.h,
leftOffset: toLeft ? item.x : undefined,
rightOffset: !toLeft ? refAll.current.cols.lg - (item.x + item.w) : undefined,
topOffset: item.y,
};
});
const sortedItems = [
...updatedItems.filter(x => x.name !== newItem.i),
updatedItems.find(x => x.name === newItem.i)!,
];
refAll.current.layouts = {
lg: [...newItems.filter(x => x.i !== newItem.i), newItem],
};
onChange(sortedItems);
setKey(x => x + 1);
};
useEffect(() => {
refAll.current.items = [...items];
setKey(x => x + 1);
}, [items]);
// TODO
// 1. Unknown why but if we set layout and cols both instantly it not help...
// 1.2 it means that we should make report... until we will send new key on window resize
useEffect(() => {
const updateItems = () => {
if (!containerRef.current) {
return;
}
const { width } = containerRef.current.getBoundingClientRect();
const newColsCount = (width - (width % colSize)) / colSize;
refAll.current.layouts = {
lg: refAll.current.items.map(({ name, width, height, rightOffset, leftOffset, topOffset = 0 }) => {
return {
i: name,
x: rightOffset != null ? newColsCount - width - rightOffset : leftOffset ?? 0,
y: topOffset,
w: width,
h: height,
};
}),
};
refAll.current.cols = { lg: newColsCount, md: 0, sm: 0, xs: 0, xxs: 0 };
};
const updateContainerWidth = () => {
if (!containerRef.current) {
return;
}
const { width } = containerRef.current.getBoundingClientRect();
refAll.current.containerWidth = width;
const newColsCount = (width - (width % colSize)) / colSize;
if (width <= 100 || refAll.current.cols.lg === newColsCount) {
return false;
}
if (!refAll.current.isReady) {
updateItems();
setCallRerenderOfGrid(x => x + 1);
refAll.current.isReady = true;
return;
}
refAll.current.layouts = {
lg: refAll.current.layouts.lg.map(lgEl => {
const toLeft = (lgEl.x + lgEl.w / 2) / refAll.current.cols.lg <= 0.5;
const next = {
...lgEl,
x: toLeft ? lgEl.x : newColsCount - (refAll.current.cols.lg - lgEl.x),
};
return next;
}),
};
refAll.current.cols = { lg: newColsCount, md: 0, sm: 0, xs: 0, xxs: 0 };
setCallRerenderOfGrid(x => x + 1);
};
setTimeout(() => updateContainerWidth(), 100);
const withRerender = () => {
updateContainerWidth();
setCallRerenderOfGrid(x => x + 1);
};
window.addEventListener('resize', withRerender);
return () => {
window.removeEventListener('resize', withRerender);
};
}, []);
const isNotSet = initState.cols === refAll.current.cols.lg;
return (
<div ref={containerRef} className={clsx(classes.GridLayoutWrapper, 'relative p-4')}>
{!isNotSet && isTabVisible && (
<ResponsiveGridLayout
key={callRerenderOfGrid}
className={classes.GridLayout}
layouts={refAll.current.layouts}
breakpoints={refAll.current.breakpoints}
cols={refAll.current.cols}
rowHeight={30}
width={refAll.current.containerWidth}
preventCollision={true}
compactType={null}
allowOverlap
onDragStop={onLayoutChange}
onResizeStop={onLayoutChange}
// onResizeStart={onLayoutChange}
// onDragStart={onLayoutChange}
isBounded
containerPadding={[0, 0]}
resizeHandles={['sw', 'se']}
draggableHandle=".react-grid-dragHandleExample"
>
{refAll.current.items.map(x => (
<div key={x.name} className="grid-item">
{x.item()}
</div>
))}
</ResponsiveGridLayout>
)}
</div>
);
};

View File

@@ -1 +0,0 @@
export * from './WidgetsGrid';

View File

@@ -1,5 +1,7 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import styles from './WindowManager.module.scss';
import debounce from 'lodash.debounce';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
const MIN_WINDOW_SIZE = 100;
const SNAP_THRESHOLD = 10;
@@ -17,14 +19,6 @@ export const DefaultWindowState = {
height: 0,
};
export type WindowProps = {
id: number | string;
content: (w: WindowProps) => React.ReactNode;
position: { x: number; y: number };
size: { width: number; height: number };
zIndex: number;
};
function getWindowsBySides(windows: WindowProps[], containerWidth: number, containerHeight: number) {
const centerX = containerWidth / 2;
const centerY = containerHeight / 2;
@@ -86,9 +80,10 @@ export const WindowWrapper = ({ onResize, onDrag, ...window }: WindowWrapperProp
type WindowManagerProps = {
windows: WindowProps[];
dragSelector?: string;
onChange?(windows: WindowProps[]): void;
};
export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWindows, dragSelector }) => {
export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWindows, dragSelector, onChange }) => {
const [windows, setWindows] = useState(
initialWindows.map((window, index) => ({
...window,
@@ -102,11 +97,17 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
const startMousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const startWindowStateRef = useRef<{ x: number; y: number; width: number; height: number }>(DefaultWindowState);
const ref = useRef({ windows });
ref.current = { windows };
const ref = useRef({ windows, onChange });
ref.current = { windows, onChange };
const refPrevSize = useRef({ w: 0, h: 0 });
const onDebouncedChange = useMemo(() => {
return debounce(() => {
ref.current.onChange?.(ref.current.windows);
}, 20);
}, []);
const handleMouseDown = (
e: React.MouseEvent,
windowId: string | number,
@@ -313,17 +314,20 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
return window;
}),
);
onDebouncedChange();
}
};
const handleMouseUp = () => {
const handleMouseUp = useCallback(() => {
activeWindowIdRef.current = null;
actionTypeRef.current = null;
resizeDirectionRef.current = null;
onDebouncedChange();
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, []);
// Handle resize of the container and reposition windows
useEffect(() => {
@@ -345,40 +349,49 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
setWindows(w => {
return w.map(x => {
let next = { ...x };
if (right.some(r => r.id === x.id)) {
return {
...x,
next = {
...next,
position: {
...x.position,
x: x.position.x + deltaX,
...next.position,
x: next.position.x + deltaX,
},
};
}
return x;
});
});
setWindows(w => {
return w.map(x => {
if (bottom.some(r => r.id === x.id)) {
return {
...x,
next = {
...next,
position: {
...x.position,
y: x.position.y + deltaY,
...next.position,
y: next.position.y + deltaY,
},
};
}
return x;
if (next.position.x + next.size.width > container.clientWidth - SNAP_GAP) {
next.position.x = container.clientWidth - next.size.width - SNAP_GAP;
}
if (next.position.y + next.size.height > container.clientHeight - SNAP_GAP) {
next.position.y = container.clientHeight - next.size.height - SNAP_GAP;
}
return next;
});
});
onDebouncedChange();
refPrevSize.current = { w: container.clientWidth, h: container.clientHeight };
};
const tid = setTimeout(handleResize, 10);
window.addEventListener('resize', handleResize);
return () => {
clearTimeout(tid);
window.removeEventListener('resize', handleResize);
};
}, []);

View File

@@ -1,7 +1,7 @@
import React from 'react';
export type WindowProps = {
id: number;
id: string | number;
content: (w: WindowProps) => React.ReactNode;
position: { x: number; y: number };
size: { width: number; height: number };