diff --git a/assets/js/hooks/Mapper/components/mapInterface/MapInterface.tsx b/assets/js/hooks/Mapper/components/mapInterface/MapInterface.tsx index 52a59459..0c8faa8e 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/MapInterface.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/MapInterface.tsx @@ -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: () => , - }, - { - name: 'local', - rightOffset: 5, - topOffset: 4, - width: 5, - height: 4, - item: () => , - }, - { name: 'signatures', width: 8, height: 4, topOffset: 8, rightOffset: 12, item: () => }, - { - name: 'routes', - rightOffset: 0, - topOffset: 8, - width: 5, - height: 6, - item: () => , - }, -]; - -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[]) - .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 ; - - // const [items, setItems] = useState(restoreWindowsFromLS); - // - // return ( - // { - // 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[]) + .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(restoreWindowsFromLS); + + return ( + { + saveWindowsToLS(x); + setItems(x); + }} + /> + ); }; diff --git a/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/WidgetsGrid.module.scss b/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/WidgetsGrid.module.scss deleted file mode 100644 index 8a9252c3..00000000 --- a/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/WidgetsGrid.module.scss +++ /dev/null @@ -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; - } - } -} - - - diff --git a/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/WidgetsGrid.tsx b/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/WidgetsGrid.tsx deleted file mode 100644 index 8ae9db8f..00000000 --- a/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/WidgetsGrid.tsx +++ /dev/null @@ -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(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 ( -
- {!isNotSet && isTabVisible && ( - - {refAll.current.items.map(x => ( -
- {x.item()} -
- ))} -
- )} -
- ); -}; diff --git a/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/index.ts b/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/index.ts deleted file mode 100644 index 7a73e228..00000000 --- a/assets/js/hooks/Mapper/components/mapInterface/components/WidgetsGrid/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './WidgetsGrid'; diff --git a/assets/js/hooks/Mapper/components/ui-kit/WindowManager/WindowManager.tsx b/assets/js/hooks/Mapper/components/ui-kit/WindowManager/WindowManager.tsx index bc86fd8f..b8082151 100644 --- a/assets/js/hooks/Mapper/components/ui-kit/WindowManager/WindowManager.tsx +++ b/assets/js/hooks/Mapper/components/ui-kit/WindowManager/WindowManager.tsx @@ -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 = ({ windows: initialWindows, dragSelector }) => { +export const WindowManager: React.FC = ({ windows: initialWindows, dragSelector, onChange }) => { const [windows, setWindows] = useState( initialWindows.map((window, index) => ({ ...window, @@ -102,11 +97,17 @@ export const WindowManager: React.FC = ({ 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 = ({ 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 = ({ 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); }; }, []); diff --git a/assets/js/hooks/Mapper/components/ui-kit/WindowManager/types.ts b/assets/js/hooks/Mapper/components/ui-kit/WindowManager/types.ts index b9d64922..fbdde57c 100644 --- a/assets/js/hooks/Mapper/components/ui-kit/WindowManager/types.ts +++ b/assets/js/hooks/Mapper/components/ui-kit/WindowManager/types.ts @@ -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 };