From c60cebecff1be64cb6cb5915f058a806a15c93d9 Mon Sep 17 00:00:00 2001 From: kyusho Date: Thu, 24 Nov 2022 12:33:49 +0800 Subject: [PATCH 1/4] feat(client): routing pages through hotkey --- .../rath-client/src/components/appNav.tsx | 53 ++++++++++++++-- packages/rath-client/src/hooks/use-hotkey.ts | 61 +++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 packages/rath-client/src/hooks/use-hotkey.ts diff --git a/packages/rath-client/src/components/appNav.tsx b/packages/rath-client/src/components/appNav.tsx index 7356a94c..2965b4a0 100644 --- a/packages/rath-client/src/components/appNav.tsx +++ b/packages/rath-client/src/components/appNav.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Nav, INavLinkGroup } from '@fluentui/react'; import { observer } from 'mobx-react-lite'; import intl from 'react-intl-universal'; @@ -6,6 +6,7 @@ import styled from 'styled-components'; import { PIVOT_KEYS } from '../constants'; import { useGlobalStore } from '../store'; +import useHotKey from '../hooks/use-hotkey'; import UserSetting from './userSettings'; const NavContainer = styled.div` @@ -57,6 +58,17 @@ const IconMap = { [key: string]: string; }; +const HotKeyMap = { + D: PIVOT_KEYS.dataSource, + M: PIVOT_KEYS.editor, + S: PIVOT_KEYS.semiAuto, + A: PIVOT_KEYS.megaAuto, + P: PIVOT_KEYS.painter, + L: PIVOT_KEYS.collection, + B: PIVOT_KEYS.dashboard, + C: PIVOT_KEYS.causal, +} as const; + function getIcon(k: string): string { return IconMap[k] || 'Settings'; } @@ -67,13 +79,20 @@ const AppNav: React.FC = (props) => { const { appKey, navMode } = commonStore; + const [altKeyPressed, setAltKeyPressed] = useState(false); + const getLinks = useCallback( (pivotKeys: string[]) => { return pivotKeys.map((p) => { + const hotkeyAccess = altKeyPressed ? Object.entries(HotKeyMap).find( + ([, key]) => key === p + )?.[0] ?? null : null; return { url: `#${p}`, key: p, - name: navMode === 'text' ? intl.get(`menu.${p}`) : '', + name: `${navMode === 'text' ? intl.get(`menu.${p}`) : ''}${ + hotkeyAccess ? ` (${hotkeyAccess})` : '' + }`, forceAnchor: true, iconProps: { iconName: getIcon(p) }, // iconProps: navMode === 'icon' ? {iconName: getIcon(p) } : undefined, @@ -84,9 +103,34 @@ const AppNav: React.FC = (props) => { }; }); }, - [commonStore, navMode] + [commonStore, navMode, altKeyPressed] ); + useEffect(() => { + const handleKeyDown = (ev: KeyboardEvent) => { + if (ev.key === 'Alt') { + setAltKeyPressed(true); + } + }; + const handleKeyUp = (ev: KeyboardEvent) => { + if (ev.key === 'Alt' || !ev.altKey) { + setAltKeyPressed(false); + } + }; + document.body.addEventListener('keydown', handleKeyDown); + document.body.addEventListener('keyup', handleKeyUp); + return () => { + document.body.removeEventListener('keydown', handleKeyDown); + document.body.removeEventListener('keyup', handleKeyUp); + }; + }, []); + + const HotKeyActions = useMemo(() => Object.fromEntries(Object.entries(HotKeyMap).map(([k, appKey]) => [ + `Alt+${k}`, () => commonStore.setAppKey(appKey) + ])), [commonStore]); + + useHotKey(HotKeyActions); + const groups: INavLinkGroup[] = [ { links: [ @@ -103,7 +147,7 @@ const AppNav: React.FC = (props) => { url: '#dev-mode', key: intl.get('menu.devCollection'), name: navMode === 'text' ? intl.get('menu.devCollection') : '', - isExpanded: false, + isExpanded: altKeyPressed, forceAnchor: true, onClick(e: any) { e.preventDefault(); @@ -139,6 +183,7 @@ const AppNav: React.FC = (props) => { ], }, ]; + return ( diff --git a/packages/rath-client/src/hooks/use-hotkey.ts b/packages/rath-client/src/hooks/use-hotkey.ts new file mode 100644 index 00000000..a10ab76f --- /dev/null +++ b/packages/rath-client/src/hooks/use-hotkey.ts @@ -0,0 +1,61 @@ +import { useEffect, useRef } from "react"; + +export type HotKeyLeadingKey = ( + | 'Meta' // MacOS: command Windows: Windows + | 'Control' // MacOS: control^ Windows: Ctrl + | 'Shift' // MacOS: shift Windows: Shift + | 'Alt' // MacOS: option Windows: Alt +); + +export type HotKeyMainKey = ( + // | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' + | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' + | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' + | 'W' | 'X' | 'Y' | 'Z' +) + +export type HotKeyTrigger = `${ + `${HotKeyLeadingKey}+` | '' +}${ + `${HotKeyLeadingKey}+` | '' +}${ + HotKeyMainKey +}`; + +export type HotKeyCallback = (e: KeyboardEvent) => void; + +export type HotKeyActions = { + [key in HotKeyTrigger]?: HotKeyCallback; +}; + + +const useHotKey = (actions: HotKeyActions) => { + const actionsRef = useRef(actions); + actionsRef.current = actions; + + useEffect(() => { + const cb = (e: KeyboardEvent) => { + const mainKey = /^Key(?[A-Z])$/.exec(e.code)?.groups?.['key'] ?? null; + if (mainKey) { + const totalLeadingKey = [ + e.metaKey ? 'Meta\\+' : '', + e.ctrlKey ? 'Control\\+' : '', + e.shiftKey ? 'Shift\\+' : '', + e.altKey ? 'Alt\\+' : '', + ].filter(Boolean); + const keyPattern = new RegExp(`^(${totalLeadingKey.join('|')})+${mainKey}$`); + const matched = Object.entries(actionsRef.current).find(([k]) => keyPattern.test(k)); + matched?.[1](e); + } + }; + + document.body.addEventListener('keydown', cb); + + return () => { + document.body.removeEventListener('keydown', cb); + }; + }, []); +}; + + +export default useHotKey From 295766d99c1ea265172dfb6a6c4352887cb6f0fb Mon Sep 17 00:00:00 2001 From: kyusho Date: Thu, 24 Nov 2022 14:39:25 +0800 Subject: [PATCH 2/4] fix(dashboard): bad auto layout policy --- .../src/pages/dashboard/dashboard-panel.tsx | 146 +++++++----------- .../src/pages/dashboard/source-panel.tsx | 4 +- .../rath-client/src/store/dashboardStore.ts | 2 +- 3 files changed, 57 insertions(+), 95 deletions(-) diff --git a/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx b/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx index 52f2ee30..58863cb8 100644 --- a/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx +++ b/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx @@ -1,4 +1,4 @@ -import { ActionButton, ChoiceGroup, IChoiceGroupOption, Stack, Text, TextField } from "@fluentui/react"; +import { ActionButton, ChoiceGroup, DefaultButton, IChoiceGroupOption, Pivot, PivotItem, Stack, Text, TextField } from "@fluentui/react"; import { observer } from "mobx-react-lite"; import { FC, useCallback, useState } from "react"; import styled from "styled-components"; @@ -19,52 +19,10 @@ const Panel = styled.div` padding: 1em; overflow: auto; box-shadow: - -9px 1.6px 6.4px 0 rgb(0 0 0 / 5%), -1px 0.3px 0.9px 0 rgb(0 0 0 / 11%), - -9px -1.6px 6.4px 0 rgb(0 0 0 / 5%), -1px -0.3px 0.9px 0 rgb(0 0 0 / 11%); + -9px 1.6px 6.4px 0 rgb(0 0 0 / 1.5%), -1px 0.7px 0.9px 0 rgb(0 0 0 / 5%), + -9px -1.6px 6.4px 0 rgb(0 0 0 / 1.5%), -1px -0.7px 0.9px 0 rgb(0 0 0 / 5%); + z-index: 100; - & *[role=tablist] { - display: flex; - flex-direction: row; - --corner-radius: 0.5em; - --border-color: #444; - --bgColor: #fff; - - & *[role=tab] { - border: 1px solid var(--border-color); - border-left: none; - user-select: none; - line-height: 1.2em; - padding: 0.2em calc(1.25em + var(--corner-radius)) 0.4em 0.6em; - border-radius: var(--corner-radius) var(--corner-radius) 0 0; - position: relative; - background-color: var(--bgColor); - - &:first-child, &[aria-selected=true] { - border-left: 1px solid var(--border-color); - } - &:not(:first-child) { - margin-left: calc(-2 * var(--corner-radius)); - padding: 0.2em calc(1.25em + var(--corner-radius)) 0.4em calc(0.6em + var(--corner-radius)); - } - &[aria-selected=false] { - cursor: pointer; - } - &[aria-disabled=true] { - opacity: 0.6; - } - &[aria-selected=true] { - border-bottom-color: var(--bgColor); - cursor: default; - } - } - ::after { - content: ""; - display: block; - flex-grow: 1; - flex-shrink: 1; - border-bottom: 1px solid var(--border-color); - } - } & *[role=tabpanel] { flex-grow: 1; flex-shrink: 1; @@ -146,36 +104,37 @@ const DashboardPanel: FC = ({ page, card, operators, sample }, })), [page, dashboardStore]); - const layoutOptions = useCallback((mode: 'single' | 'global') => CardAlignTypes.map(alg => ({ - key: CardAlignName[alg], - text: CardAlignName[alg], - onRenderField: (option, origin) => { - const applyToAll = () => { - const key = { - Auto: DashboardCardInsetLayout.Auto, - Column: DashboardCardInsetLayout.Column, - Row: DashboardCardInsetLayout.Row, - }[option?.key ?? '']; - if (typeof key === 'number') { - dashboardStore.runInAction(() => { - page.cards.forEach(c => c.config.align = key); - }); - } - }; - return option ? mode === 'single' ? ( - - {origin?.(option)} - - Apply to all - - - ) : ( - - {option.text} - - ) : null; - }, - })), [page, dashboardStore]); + // Temporarily use column layout only + // const layoutOptions = useCallback((mode: 'single' | 'global') => CardAlignTypes.map(alg => ({ + // key: CardAlignName[alg], + // text: CardAlignName[alg], + // onRenderField: (option, origin) => { + // const applyToAll = () => { + // const key = { + // Auto: DashboardCardInsetLayout.Auto, + // Column: DashboardCardInsetLayout.Column, + // Row: DashboardCardInsetLayout.Row, + // }[option?.key ?? '']; + // if (typeof key === 'number') { + // dashboardStore.runInAction(() => { + // page.cards.forEach(c => c.config.align = key); + // }); + // } + // }; + // return option ? mode === 'single' ? ( + // + // {origin?.(option)} + // + // Apply to all + // + // + // ) : ( + // + // {option.text} + // + // ) : null; + // }, + // })), [page, dashboardStore]); return ( e.stopPropagation()} onKeyDown={e => e.stopPropagation()}> @@ -213,7 +172,7 @@ const DashboardPanel: FC = ({ page, card, operators, sample } })} /> - = ({ page, card, operators, sample card.config.align = key; } })} - /> + /> */} Chart -
- {SupportedTabs.map((key, i) => ( -
key !== tab && setTab(key)} - style={{ zIndex: key === tab ? SupportedTabs.length + 1 : SupportedTabs.length - i }} - > - {key} -
+ dashboardStore.runInAction(() => { + card.content.chart = undefined; + })} + /> + { + item && setTab(item.props.itemKey as typeof tab); + }} + > + {SupportedTabs.map(key => ( + ))} -
+
{({ collection: , @@ -261,10 +223,10 @@ const DashboardPanel: FC = ({ page, card, operators, sample label="Theme" options={themeOptions('global')} /> - + /> */} Filters diff --git a/packages/rath-client/src/pages/dashboard/source-panel.tsx b/packages/rath-client/src/pages/dashboard/source-panel.tsx index 063c31fc..81a3fb94 100644 --- a/packages/rath-client/src/pages/dashboard/source-panel.tsx +++ b/packages/rath-client/src/pages/dashboard/source-panel.tsx @@ -50,8 +50,8 @@ const SourcePanel: FC = ({ page, card, sampleSize }) => { selectors: [], highlighter: [], }; - card.content.title = data.title; - card.content.text = data.desc; + card.content.title = data.title || card.content.title; + card.content.text = data.desc || card.content.text; }); } }, [card, dashboardStore]); diff --git a/packages/rath-client/src/store/dashboardStore.ts b/packages/rath-client/src/store/dashboardStore.ts index 17b07cff..0e4f2968 100644 --- a/packages/rath-client/src/store/dashboardStore.ts +++ b/packages/rath-client/src/store/dashboardStore.ts @@ -200,7 +200,7 @@ export default class DashboardStore { content: {}, config: { appearance: DashboardCardAppearance.Transparent, - align: DashboardCardInsetLayout.Auto, + align: DashboardCardInsetLayout.Column, }, }); } From fb41f4369334726373f0d21a5ca790731bebecb5 Mon Sep 17 00:00:00 2001 From: kyusho Date: Thu, 24 Nov 2022 17:31:17 +0800 Subject: [PATCH 3/4] feat(dashboard): card move / resize ref line --- .../src/pages/dashboard/dashboard-draft.tsx | 78 +++++ .../src/pages/dashboard/dashboard-panel.tsx | 22 +- .../pages/dashboard/renderer/card-editor.tsx | 295 ++++++++++++++---- .../src/pages/dashboard/renderer/card.tsx | 20 +- .../renderer/components/resize-handler.tsx | 44 ++- .../src/pages/dashboard/renderer/constant.ts | 2 +- 6 files changed, 371 insertions(+), 90 deletions(-) diff --git a/packages/rath-client/src/pages/dashboard/dashboard-draft.tsx b/packages/rath-client/src/pages/dashboard/dashboard-draft.tsx index cd9bf7ad..e8efc03d 100644 --- a/packages/rath-client/src/pages/dashboard/dashboard-draft.tsx +++ b/packages/rath-client/src/pages/dashboard/dashboard-draft.tsx @@ -5,6 +5,7 @@ import { useGlobalStore } from '../../store'; import { DashboardCard } from '../../store/dashboardStore'; import DashboardPanel from './dashboard-panel'; import DashboardRenderer, { transformCoord } from './renderer'; +import type { RefLine } from './renderer/card'; import { MIN_CARD_SIZE } from './renderer/constant'; @@ -359,6 +360,82 @@ const DashboardDraft: FC = ({ cursor, mode, ratio: r, sampl } }, [focus, page.cards]); + const getRefLinesCache = useRef<[number, RefLine[]]>(); + + const getRefLines = useCallback((selfIdx: number): RefLine[] => { + const cache = getRefLinesCache.current; + if (cache?.[0] === selfIdx) { + return cache[1]; + } + const lines: RefLine[] = [{ + direction: 'x', + position: 0, + reason: ['canvas-limit'], + score: 1, + }, { + direction: 'y', + position: 0, + reason: ['canvas-limit'], + score: 1, + }, { + direction: 'x', + position: page.config.size.w, + reason: ['canvas-limit'], + score: 1, + }, { + direction: 'y', + position: page.config.size.h, + reason: ['canvas-limit'], + score: 1, + }]; + const cards = page.cards.filter((_, i) => i !== selfIdx); + for (const card of cards) { + lines.push({ + direction: 'x', + position: card.layout.x, + reason: ['align-other-card'], + score: 1, + }, { + direction: 'y', + position: card.layout.y, + reason: ['align-other-card'], + score: 1, + }, { + direction: 'x', + position: card.layout.x + card.layout.w, + reason: ['align-other-card'], + score: 1, + }, { + direction: 'y', + position: card.layout.y + card.layout.h, + reason: ['align-other-card'], + score: 1, + }); + } + const res = lines.reduce((list, line) => { + const same = list.find(which => which.direction === line.direction && which.position === line.position); + if (same) { + for (const reason of line.reason) { + if (!same.reason.includes(reason)) { + same.reason.push(reason); + } + } + same.score += line.score; + } else { + list.push(line); + } + return list; + }, []).sort((a, b) => b.score - a.score); + const cacheData: [number, RefLine[]] = [selfIdx, res]; + getRefLinesCache.current = cacheData; + setTimeout(() => { + if (getRefLinesCache.current === cacheData) { + getRefLinesCache.current = undefined; + } + }, 1_000); + return res; + }, [page]); + return (
@@ -381,6 +458,7 @@ const DashboardDraft: FC = ({ cursor, mode, ratio: r, sampl operators: { ...operators, adjustCardSize: adjustCardSize.bind({}, index), + getRefLines, }, })) : undefined} > diff --git a/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx b/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx index 58863cb8..ddba871f 100644 --- a/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx +++ b/packages/rath-client/src/pages/dashboard/dashboard-panel.tsx @@ -2,7 +2,7 @@ import { ActionButton, ChoiceGroup, DefaultButton, IChoiceGroupOption, Pivot, Pi import { observer } from "mobx-react-lite"; import { FC, useCallback, useState } from "react"; import styled from "styled-components"; -import { DashboardCardAppearance, DashboardCardInsetLayout, DashboardCardState, DashboardDocument, DashboardDocumentOperators } from "../../store/dashboardStore"; +import { DashboardCardAppearance/*, DashboardCardInsetLayout*/, DashboardCardState, DashboardDocument, DashboardDocumentOperators } from "../../store/dashboardStore"; import { useGlobalStore } from "../../store"; import SourcePanel from "./source-panel"; import FilterList from './filter-list'; @@ -61,17 +61,17 @@ const CardThemes: readonly DashboardCardAppearance[] = [ DashboardCardAppearance.Neumorphism, ]; -const CardAlignTypes: readonly DashboardCardInsetLayout[] = [ - DashboardCardInsetLayout.Auto, - DashboardCardInsetLayout.Column, - DashboardCardInsetLayout.Row, -]; +// const CardAlignTypes: readonly DashboardCardInsetLayout[] = [ +// DashboardCardInsetLayout.Auto, +// DashboardCardInsetLayout.Column, +// DashboardCardInsetLayout.Row, +// ]; -const CardAlignName: Readonly> = { - [DashboardCardInsetLayout.Auto]: 'Auto', - [DashboardCardInsetLayout.Column]: 'Column', - [DashboardCardInsetLayout.Row]: 'Row', -}; +// const CardAlignName: Readonly> = { +// [DashboardCardInsetLayout.Auto]: 'Auto', +// [DashboardCardInsetLayout.Column]: 'Column', +// [DashboardCardInsetLayout.Row]: 'Row', +// }; const DashboardPanel: FC = ({ page, card, operators, sampleSize }) => { const { dashboardStore } = useGlobalStore(); diff --git a/packages/rath-client/src/pages/dashboard/renderer/card-editor.tsx b/packages/rath-client/src/pages/dashboard/renderer/card-editor.tsx index 4b5bf813..e865e03c 100644 --- a/packages/rath-client/src/pages/dashboard/renderer/card-editor.tsx +++ b/packages/rath-client/src/pages/dashboard/renderer/card-editor.tsx @@ -4,7 +4,7 @@ import { createPortal } from "react-dom"; import styled from "styled-components"; import type { IFilter } from "../../../interfaces"; import { useGlobalStore } from "../../../store"; -import { CardProviderProps } from "./card"; +import { CardProviderProps, RefLine } from "./card"; import MoveHandler from "./components/move-handler"; import ResizeHandler from "./components/resize-handler"; import DeleteButton from "./components/delete-button"; @@ -16,6 +16,7 @@ const DragBox = styled.div<{ canDrop: boolean }>` --c: ${({ canDrop }) => canDrop ? '#13a10e' : '#da3b01'}; border: 1px solid var(--c); cursor: move; + z-index: 999; ::after { content: ""; display: block; @@ -29,6 +30,24 @@ const DragBox = styled.div<{ canDrop: boolean }>` } `; +const RefLineOutline = styled.div<{ direction: 'x' | 'y'; matched: boolean }>` + pointer-events: none; + position: absolute; + width: ${({ direction, matched }) => direction === 'x' ? `${ + matched ? 3 : 1 + }px` : '100%'}; + height: ${({ direction, matched }) => direction === 'y' ? `${ + matched ? 3 : 1 + }px` : '100%'}; + left: 0; + top: 0; + background-color: ${({ matched }) => matched ? '#13a10e' : '#ebc310'}; + opacity: 0.5; + z-index: 998; +`; + +const AUTO_ALIGN_THRESHOLD = 4; + const CardEditor: FC = ({ item, index, children, transformCoord, draftRef, canDrop, isSizeValid, operators, onFocus, focused, ratio, }) => { @@ -53,15 +72,15 @@ const CardEditor: FC = ({ const handleClick = useCallback(() => { onFocus?.(); }, [onFocus]); - const handleDoubleClick = useCallback(() => { - const adjustSize = operators.adjustCardSize; - if (adjustSize) { - adjustSize('n'); - adjustSize('e'); - adjustSize('s'); - adjustSize('w'); - } - }, [operators.adjustCardSize]); + // const handleDoubleClick = useCallback(() => { + // const adjustSize = operators.adjustCardSize; + // if (adjustSize) { + // adjustSize('n'); + // adjustSize('e'); + // adjustSize('s'); + // adjustSize('w'); + // } + // }, [operators.adjustCardSize]); useEffect(() => { operators.fireUpdate?.(); @@ -74,6 +93,8 @@ const CardEditor: FC = ({ item.content.chart?.size.w, item.content.chart?.size.h, ]); + const [refLines, setRefLines] = useState([]); + // Move const [dragging, setDragging] = useState(null); const dragDest = useMemo(() => dragging ? { @@ -93,32 +114,20 @@ const CardEditor: FC = ({ if (!dragging) { return; } + const refLines = operators.getRefLines?.(index); + if (refLines) { + setRefLines(refLines); + } setDragging({ from: dragging.from, to: { x, y }, }); - }, [dragging]); - const handleDragEnd = useCallback(() => { - if (!dragging) { - return; - } - const dest = { - x: dragging.to.x, - y: dragging.to.y, - w: item.layout.w, - h: item.layout.h, - }; - const readyToDrop = canDrop(dest, index); - if (readyToDrop) { - moveCard?.(index, dest.x, dest.y); - } - setDragging(null); - }, [moveCard, dragging, item.layout, index, canDrop]); + }, [dragging, operators, index]); const handleDragCancel = useCallback(() => { setDragging(null); }, []); - // Move + // Resize const [resizing, setResizing] = useState(null); const resizeDest = useMemo(() => resizing ? { x: item.layout.x, @@ -139,24 +148,12 @@ const CardEditor: FC = ({ if (!resizing) { return; } - setResizing({ w, h }); - }, [resizing]); - const handleResizeEnd = useCallback(() => { - if (!resizing) { - return; - } - const dest = { - x: item.layout.x, - y: item.layout.y, - w: resizing.w, - h: resizing.h, - }; - const readyToResize = canDrop(dest, index) && isSizeValid(dest.w, dest.h); - if (readyToResize) { - resizeCard?.(index, dest.w, dest.h); + const refLines = operators.getRefLines?.(index); + if (refLines) { + setRefLines(refLines); } - setResizing(null); - }, [resizeCard, canDrop, isSizeValid, resizing, item.layout, index]); + setResizing({ w, h }); + }, [resizing, index, operators]); const handleResizeCancel = useCallback(() => { setResizing(null); }, []); @@ -169,6 +166,11 @@ const CardEditor: FC = ({ } }, [chart, dashboardStore]); + const isDraggingOrResizing = Boolean(dragging) || Boolean(resizing); + useEffect(() => { + setRefLines([]); + }, [isDraggingOrResizing]) + const { removeCard } = operators; useEffect(() => { @@ -183,6 +185,161 @@ const CardEditor: FC = ({ } }, [focused, removeCard, index]); + const curPositionToPut = useMemo(() => { + const realPos = dragDest ?? resizeDest ?? null; + if (realPos) { + if (!canDrop(realPos, index) || !isSizeValid(realPos.w, realPos.h)) { + return realPos; + } + const availableLines = refLines.reduce<{ + target: keyof typeof item.layout; + value: number; + distance: number; + score: number; + }[]>((list, line) => { + if (line.direction === 'x') { + const x1 = realPos.x; + const x2 = realPos.x + realPos.w; + const diffX1 = Math.abs(line.position - x1); + if (diffX1 <= AUTO_ALIGN_THRESHOLD) { + list.push({ + target: 'x', + value: line.position, + distance: diffX1, + score: line.score, + }); + } + const diffX2 = Math.abs(line.position - x2); + if (diffX2 <= AUTO_ALIGN_THRESHOLD) { + list.push(dragDest ? { + target: 'x', + value: line.position - item.layout.w, + distance: diffX2, + score: line.score, + } : { + target: 'w', + value: line.position - item.layout.x, + distance: diffX2, + score: line.score, + }); + } + } else { + const y1 = realPos.y; + const y2 = realPos.y + realPos.h; + const diffY1 = Math.abs(line.position - y1); + if (diffY1 <= AUTO_ALIGN_THRESHOLD) { + list.push({ + target: 'y', + value: line.position, + distance: diffY1, + score: line.score, + }); + } + const diffY2 = Math.abs(line.position - y2); + if (diffY2 <= AUTO_ALIGN_THRESHOLD) { + list.push(dragDest ? { + target: 'y', + value: line.position - item.layout.h, + distance: diffY2, + score: line.score, + } : { + target: 'h', + value: line.position - item.layout.y, + distance: diffY2, + score: line.score, + }); + } + } + return list; + }, []).filter(link => { + const target = { + ...item.layout, + [link.target]: link.value, + }; + return canDrop(target, index); + }); + let target = { ...realPos }; + let xAppliedDist = Infinity; + let yAppliedDist = Infinity; + for (const line of availableLines) { + switch (line.target) { + case 'x': { + if (dragDest && line.distance < xAppliedDist) { + const next = { ...target, x: line.value }; + if (canDrop(next, index)) { + xAppliedDist = line.distance; + target = next; + } + } + break; + } + case 'y': { + if (dragDest && line.distance < yAppliedDist) { + const next = { ...target, y: line.value }; + if (canDrop(next, index)) { + yAppliedDist = line.distance; + target = next; + } + } + break; + } + case 'w': { + if (resizeDest && line.distance < xAppliedDist) { + const next = { ...target, w: line.value }; + if (canDrop(next, index) && isSizeValid(next.w, next.h)) { + xAppliedDist = line.distance; + target = next; + } + } + break; + } + case 'h': { + if (resizeDest && line.distance < yAppliedDist) { + const next = { ...target, h: line.value }; + if (canDrop(next, index) && isSizeValid(next.w, next.h)) { + yAppliedDist = line.distance; + target = next; + } + } + break; + } + } + } + return target; + } + return null; + }, [dragDest, resizeDest, refLines, item, canDrop, isSizeValid, index]); + + const readyToPut = dragDest ? readyToDrop : readyToResize; + + const handleResizeEnd = useCallback(() => { + setResizing(null); + if (!curPositionToPut || !readyToPut) { + return; + } + const dest = { + x: item.layout.x, + y: item.layout.y, + w: curPositionToPut.w, + h: curPositionToPut.h, + }; + resizeCard?.(index, dest.w, dest.h); + }, [curPositionToPut, readyToPut, resizeCard, item.layout, index]); + + const handleDragEnd = useCallback(() => { + setDragging(null); + if (!curPositionToPut || !readyToPut) { + return; + } + const dest = { + x: curPositionToPut.x, + y: curPositionToPut.y, + w: item.layout.w, + h: item.layout.h, + }; + moveCard?.(index, dest.x, dest.y); + }, [curPositionToPut, readyToPut, moveCard, item.layout, index]); + return children({ content: focused ? ( <> @@ -194,18 +351,6 @@ const CardEditor: FC = ({ onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} /> - {dragDest && draftRef.current && createPortal( - , - draftRef.current - )} = ({ remove={() => removeCard(index)} /> )} - {resizeDest && draftRef.current && createPortal( + {curPositionToPut && draftRef.current && createPortal( , draftRef.current )} ) : null, - onDoubleClick: handleDoubleClick, + canvasContent: ( + <> + {curPositionToPut && refLines.map((line, i) => ( + + ))} + + ), + // onDoubleClick: handleDoubleClick, // onRootMouseDown(x, y) { // // console.log({x,y}) // }, diff --git a/packages/rath-client/src/pages/dashboard/renderer/card.tsx b/packages/rath-client/src/pages/dashboard/renderer/card.tsx index 2a2c06fd..e71c072f 100644 --- a/packages/rath-client/src/pages/dashboard/renderer/card.tsx +++ b/packages/rath-client/src/pages/dashboard/renderer/card.tsx @@ -23,6 +23,7 @@ export interface CardProps { export interface CardProvider { content: JSX.Element | null; + canvasContent?: JSX.Element | null | undefined; onRootMouseDown: (x: number, y: number) => void; onDoubleClick: () => void; onClick: () => void; @@ -44,6 +45,19 @@ export const layoutOption = { }, } as const; +export type RefLine = { + direction: 'x' | 'y'; + position: number; + reason: ( + | 'canvas-limit' + | 'align-other-card' + | 'other-card-size' // TODO: 还没有实现,而且需要额外考虑当前卡片位置 + | 'card-padding' // TODO: 还没有实现 + | 'canvas-padding' // TODO: 还没有实现 + )[]; + score: number; +}; + export interface CardProviderProps { children: (provider: Partial) => JSX.Element; transformCoord: (ev: { clientX: number; clientY: number }) => { x: number; y: number }; @@ -56,6 +70,7 @@ export interface CardProviderProps { isSizeValid: (w: number, h: number) => boolean; operators: Partial void; + getRefLines: (selfIdx: number) => RefLine[]; }>; ratio: number; } @@ -173,7 +188,7 @@ const Card: FC = ({ globalFilters, cards, card, editor, transformCoor index={index} ratio={ratio} > - {provider => ( + {provider => (<> { e.stopPropagation(); @@ -215,7 +230,8 @@ const Card: FC = ({ globalFilters, cards, card, editor, transformCoor )} {provider.content} - )} + {provider.canvasContent} + )} ); }; diff --git a/packages/rath-client/src/pages/dashboard/renderer/components/resize-handler.tsx b/packages/rath-client/src/pages/dashboard/renderer/components/resize-handler.tsx index 1d08dca5..2415b799 100644 --- a/packages/rath-client/src/pages/dashboard/renderer/components/resize-handler.tsx +++ b/packages/rath-client/src/pages/dashboard/renderer/components/resize-handler.tsx @@ -19,15 +19,25 @@ const Resizer = styled.div` background-color: #ffffff; `; -const Adjuster = styled.div` +// const Adjuster = styled.div` +// position: absolute; +// width: 8px; +// height: 8px; +// display: flex; +// align-items: center; +// justify-content: center; +// overflow: hidden; +// background-color: #888; +// `; + +const Outline = styled.div` position: absolute; - width: 8px; - height: 8px; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - background-color: #888; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + border: 1.5px solid #8888; `; export interface ResizeHandlerProps { @@ -37,7 +47,7 @@ export interface ResizeHandlerProps { onResize: (x: number, y: number) => void; onResizeEnd: (x: number, y: number) => void; onResizeCancel: () => void; - adjustCardSize?: ( dir: 'n' | 'e' | 's' | 'w') => void; + adjustCardSize?: (dir: 'n' | 'e' | 's' | 'w') => void; } const ResizeHandler: FC = ({ @@ -91,8 +101,18 @@ const ResizeHandler: FC = ({ return ( <> - - {adjustCardSize && ( + + { + e.stopPropagation(); + adjustCardSize?.('e'); + requestAnimationFrame(() => { + adjustCardSize?.('s'); + }); + }} + /> + {/* {adjustCardSize && ( <> = ({ }} /> - )} + )} */} ); }; diff --git a/packages/rath-client/src/pages/dashboard/renderer/constant.ts b/packages/rath-client/src/pages/dashboard/renderer/constant.ts index 9fc8e239..cb72ac5d 100644 --- a/packages/rath-client/src/pages/dashboard/renderer/constant.ts +++ b/packages/rath-client/src/pages/dashboard/renderer/constant.ts @@ -1,2 +1,2 @@ -export const MIN_CARD_SIZE = 64; +export const MIN_CARD_SIZE = 32; export const scaleRatio = Math.min(window.innerWidth, window.innerHeight) / 256; From d717b95fd5169001218634d161c4a5cf3622497f Mon Sep 17 00:00:00 2001 From: kyusho Date: Thu, 24 Nov 2022 18:01:22 +0800 Subject: [PATCH 4/4] fix(dashboard): zh plain text --- packages/rath-client/src/pages/dashboard/source-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rath-client/src/pages/dashboard/source-panel.tsx b/packages/rath-client/src/pages/dashboard/source-panel.tsx index 81a3fb94..1966dd91 100644 --- a/packages/rath-client/src/pages/dashboard/source-panel.tsx +++ b/packages/rath-client/src/pages/dashboard/source-panel.tsx @@ -77,7 +77,7 @@ const SourcePanel: FC = ({ page, card, sampleSize }) => { return ( - {collectionList.length === 0 && '你的收藏夹是空的'} + {collectionList.length === 0 && 'collection is empty'} {collectionList.map(item => (