From d16fb0e0ebfd988a16a6633ef8888f3c090eb3d3 Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sat, 6 Apr 2024 17:57:03 +0800 Subject: [PATCH] feat: add editMode --- package.json | 4 +- packages/studio-base/src/css/index.less | 1 + .../src/css/modules/page-tree.less | 96 ++++ packages/studio-base/src/css/theme/dark.less | 1 + packages/studio-base/src/css/theme/light.less | 1 + packages/studio-base/src/icons/index.ts | 2 + packages/studio-base/src/icons/layout.tsx | 15 + packages/studio-base/src/index.ts | 4 +- .../src/modules/element-tree/index.tsx | 3 +- .../src/modules/element-tree/wrap.tsx | 6 +- .../src/modules/page-tree/index.tsx | 102 +++++ .../src/modules/page-tree/tree-node.tsx | 230 ++++++++++ .../src/modules/page-tree/wrap.tsx | 54 +++ .../studio-base/src/tools/element-tree.ts | 17 +- packages/studio-base/src/tools/index.ts | 1 + packages/studio-base/src/tools/page-tree.ts | 24 + packages/studio-base/src/types/data.ts | 5 + packages/studio-base/src/types/index.ts | 1 + packages/studio-base/src/types/tree.ts | 17 + packages/studio/src/css/base.less | 1 + .../studio/src/css/modules/panel-page.less | 86 ++++ packages/studio/src/modules/_mod/child.tsx | 8 +- packages/studio/src/modules/_mod/index.tsx | 9 +- packages/studio/src/modules/context.ts | 26 +- .../studio/src/modules/dashboard/index.tsx | 59 ++- .../src/modules/export-image-file/index.tsx | 14 +- packages/studio/src/modules/header/index.tsx | 18 +- .../studio/src/modules/nav-menu/index.tsx | 10 +- .../studio/src/modules/panel-detail/index.tsx | 6 +- .../studio/src/modules/panel-layer/index.tsx | 47 +- .../studio/src/modules/panel-page/index.tsx | 415 ++++++++++++++++++ packages/studio/src/modules/sketch/index.tsx | 70 ++- packages/studio/src/shared/event.ts | 12 +- packages/studio/src/types/lib/context.ts | 10 +- packages/studio/src/types/lib/shared.ts | 2 +- packages/studio/src/types/lib/studio.ts | 15 +- pnpm-lock.yaml | 52 ++- 37 files changed, 1304 insertions(+), 140 deletions(-) create mode 100644 packages/studio-base/src/css/modules/page-tree.less create mode 100644 packages/studio-base/src/icons/layout.tsx create mode 100644 packages/studio-base/src/modules/page-tree/index.tsx create mode 100644 packages/studio-base/src/modules/page-tree/tree-node.tsx create mode 100644 packages/studio-base/src/modules/page-tree/wrap.tsx create mode 100644 packages/studio-base/src/tools/page-tree.ts create mode 100644 packages/studio-base/src/types/data.ts create mode 100644 packages/studio/src/css/modules/panel-page.less create mode 100644 packages/studio/src/modules/panel-page/index.tsx diff --git a/package.json b/package.json index ed82ad0..cc49d3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": false, - "version": "0.4.0-alpha.19", + "version": "0.4.0-alpha.20", "workspaces": [ "packages/*" ], @@ -19,7 +19,7 @@ "upgrade:version": "vite-node ./scripts/upgrade-version.ts && pnpm i" }, "dependencies": { - "idraw": "0.4.0-beta.19", + "idraw": "0.4.0-beta.20", "antd": "5.12.1" }, "devDependencies": { diff --git a/packages/studio-base/src/css/index.less b/packages/studio-base/src/css/index.less index e66e6de..d894053 100644 --- a/packages/studio-base/src/css/index.less +++ b/packages/studio-base/src/css/index.less @@ -7,6 +7,7 @@ @import './modules/theme-switch.less'; @import './modules/locale-selector.less'; @import './modules/scale-selector.less'; +@import './modules/page-tree.less'; @import './modules/element-tree.less'; @import './modules/element-detail.less'; @import './modules/layout-detail.less'; diff --git a/packages/studio-base/src/css/modules/page-tree.less b/packages/studio-base/src/css/modules/page-tree.less new file mode 100644 index 0000000..0944548 --- /dev/null +++ b/packages/studio-base/src/css/modules/page-tree.less @@ -0,0 +1,96 @@ +// @import "../variable.less"; + +@base-page-tree: ~'@{prefix}-base-page-tree'; + +.@{base-page-tree} { + // fix antd style + &.ant-tree { + .ant-tree-iconEle { + display: none; + } + + .ant-tree-title { + display: flex; + width: 100%; + } + + .ant-tree-switcher { + display: flex; + justify-content: center; + align-items: center; + } + } + + .@{base-page-tree}-node { + position: relative; + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; + } + + .@{base-page-tree}-node-title { + position: relative; + display: -webkit-box; + float: inline-start; + overflow: hidden; + height: 24px; + padding-left: 24px; + white-space: initial; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + } + + .@{base-page-tree}-node-title-input { + background: ~'var(--@{prefix}-bg-color)'; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: inline-flex; + // display: none; + float: inline-start; + // overflow: hidden; + // white-space: nowrap; + // text-overflow: ellipsis; + + // &.@{base-page-tree}-node-title-input-active { + // display: flex; + // } + } + + .@{base-page-tree}-node-title-icon { + position: absolute; + top: 6px; + left: 6px; + font-size: 14px; + display: inline-flex; + width: 14px; + margin-right: 6px; + } + + .@{base-page-tree}-node-action { + position: absolute; + top: 0; + bottom: 0; + right: 0; + display: inline-flex; + float: inline-end; + padding: 0 8px; + background: ~'var(--@{prefix}-bg-color)'; + } + + .@{base-page-tree}-node-icon { + font-size: 12px; + margin-left: 4px; + cursor: pointer; + } + + .@{base-page-tree}-node-selected { + .@{base-page-tree}-node-action { + background: ~'var(--@{prefix}-common-primary-color)'; + } + } +} diff --git a/packages/studio-base/src/css/theme/dark.less b/packages/studio-base/src/css/theme/dark.less index 70eeb4a..74482d3 100644 --- a/packages/studio-base/src/css/theme/dark.less +++ b/packages/studio-base/src/css/theme/dark.less @@ -6,6 +6,7 @@ --@{prefix}-text-color: @gray-4; --@{prefix}-text-hover-color: @gray-1; --@{prefix}-border-color: @gray-6; + --@{prefix}-shadow-color: @gray-7; --@{prefix}-border-outline-color: @gray-2; --@{prefix}-primary-color: @blue-6; diff --git a/packages/studio-base/src/css/theme/light.less b/packages/studio-base/src/css/theme/light.less index b7828bb..3705720 100644 --- a/packages/studio-base/src/css/theme/light.less +++ b/packages/studio-base/src/css/theme/light.less @@ -6,6 +6,7 @@ --@{prefix}-text-color: @gray-6; --@{prefix}-text-hover-color: @gray-10; --@{prefix}-border-color: @gray-4; + --@{prefix}-shadow-color: @gray-3; --@{prefix}-border-outline-color: @gray-8; --@{prefix}-primary-color: @blue-5; diff --git a/packages/studio-base/src/icons/index.ts b/packages/studio-base/src/icons/index.ts index 5742b53..c1ceaa5 100644 --- a/packages/studio-base/src/icons/index.ts +++ b/packages/studio-base/src/icons/index.ts @@ -43,6 +43,7 @@ import IconIndent from './indent'; import IconImage from './image'; import IconInvisible from './invisible'; import IconLayer from './layer'; +import IconLayout from './layout'; import IconLeft from './left'; import IconLight from './light'; import IconLike from './like'; @@ -118,6 +119,7 @@ export { IconImage, IconInvisible, IconLayer, + IconLayout, IconLeft, IconLight, IconLike, diff --git a/packages/studio-base/src/icons/layout.tsx b/packages/studio-base/src/icons/layout.tsx new file mode 100644 index 0000000..f2ebef3 --- /dev/null +++ b/packages/studio-base/src/icons/layout.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { IconWrapper } from './util'; +import type { IconWrapperProps } from './util'; + +const Icon = (props: IconWrapperProps) => { + return ( + + + + + + ); +}; + +export default Icon; diff --git a/packages/studio-base/src/index.ts b/packages/studio-base/src/index.ts index 7e970cd..5fa03ae 100644 --- a/packages/studio-base/src/index.ts +++ b/packages/studio-base/src/index.ts @@ -6,10 +6,11 @@ export { ThemeSwitch, type ThemeSwitchProps } from './modules/theme-switch'; export { LocaleSelector, type LocaleSelectorProps } from './modules/locale-selector'; export { ScaleSelector, type ScaleSelectorProps } from './modules/scale-selector'; export { ElementTree, type ElementTreeProps } from './modules/element-tree'; +export { PageTree, type PageTreeProps } from './modules/page-tree'; export { ElementDetail, type ElementDetailProps } from './modules/element-detail'; export { LayoutDetail, type LayoutDetailProps } from './modules/layout-detail'; -export { getElementTree } from './tools'; +export { getElementTree, getPageTree } from './tools'; export { setClassNameTopPrefix, generateClassName } from './css/index'; export { useThemeClassName } from './hooks/theme'; export * from './locale/types'; @@ -60,6 +61,7 @@ export { IconImage, IconInvisible, IconLayer, + IconLayout, IconLeft, IconLight, IconLike, diff --git a/packages/studio-base/src/modules/element-tree/index.tsx b/packages/studio-base/src/modules/element-tree/index.tsx index b12bdb6..ce33357 100644 --- a/packages/studio-base/src/modules/element-tree/index.tsx +++ b/packages/studio-base/src/modules/element-tree/index.tsx @@ -52,6 +52,7 @@ export const ElementTree = React.forwardRef((props: ElementTreeProps, ref: any) onExpand, onGoToGroup } = props; + const onSelectNode: TreeProps['onSelect'] = (selectedKeys, info) => { const pos = treePosToElementPosition(info.node.pos); const positions: ElementPosition[] = [pos]; @@ -83,7 +84,7 @@ export const ElementTree = React.forwardRef((props: ElementTreeProps, ref: any) className={classnames(generateClassName(modName), className)} showLine blockNode - multiple + // multiple selectedKeys={selectedKeys} switcherIcon={} // icon={(props: any) => { diff --git a/packages/studio-base/src/modules/element-tree/wrap.tsx b/packages/studio-base/src/modules/element-tree/wrap.tsx index 257ee85..30646e1 100644 --- a/packages/studio-base/src/modules/element-tree/wrap.tsx +++ b/packages/studio-base/src/modules/element-tree/wrap.tsx @@ -12,12 +12,12 @@ type WrapOptions = Pick { + elementTree.forEach((node, i) => { pos.push(i); tree.push(wrapTreeViewNode(node, { ...opts, ...{ position: [...pos] } })); pos.pop(); diff --git a/packages/studio-base/src/modules/page-tree/index.tsx b/packages/studio-base/src/modules/page-tree/index.tsx new file mode 100644 index 0000000..c39e12d --- /dev/null +++ b/packages/studio-base/src/modules/page-tree/index.tsx @@ -0,0 +1,102 @@ +import React, { useMemo } from 'react'; +import classnames from 'classnames'; +import { Tree } from 'antd'; +import type { CSSProperties } from 'react'; +import type { TreeProps } from 'antd'; +import type { ElementPosition } from 'idraw'; +import { wrapTreeViewData } from './wrap'; +import type { TreeNodeProps } from './tree-node'; +import type { PageTreeData } from '../../types'; +import { IconDown } from '../../icons'; +import { generateClassName } from '../../css'; + +const { DirectoryTree } = Tree; +const modName = 'base-page-tree'; + +export type PageTreeProps = Pick & { + height: number; + className?: string; + style?: CSSProperties; + treeData?: PageTreeData; + selectedKeys?: string[]; + defaultExpandedKeys?: string[]; + expandedKeys?: string[]; + onSelect?: (e: { uuids: string[]; positions: ElementPosition[] }) => void; + onDrop?: (e: { from: ElementPosition; to: ElementPosition }) => void; + onDelete?: (e: { uuid: string }) => void; +}; + +const treePosToElementPosition = (pos: string) => { + const elemPos: ElementPosition = pos.split('-').map((i) => parseInt(i)); + elemPos.shift(); + return elemPos; +}; + +export const PageTree = React.forwardRef((props: PageTreeProps, ref: any) => { + const { height, className, style, treeData, onTitleChange, onOperationToggle, onSelect, selectedKeys, onDrop, defaultExpandedKeys, expandedKeys, onDelete } = + props; + const onSelectNode: TreeProps['onSelect'] = (selectedKeys, info) => { + const pos = treePosToElementPosition(info.node.pos); + const positions: ElementPosition[] = [pos]; + const uuids = [selectedKeys[0]] as string[]; + onSelect?.({ uuids, positions }); + }; + + const onElementDelete: PageTreeProps['onDelete'] = ({ uuid }) => { + onDelete?.({ uuid }); + }; + + return useMemo(() => { + const wrappedTreeData = wrapTreeViewData(treeData || [], { + parentModName: modName, + generateClassName, + onTitleChange, + onOperationToggle, + onDelete: onElementDelete, + position: [], + selectedKeys: selectedKeys || [] + }); + + return ( + } + // icon={(props: any) => { + // const type: ElementType | undefined = props?.data?.title?.props?.type; + // return getIcon(type); + // }} + icon={null} + onSelect={onSelectNode} + treeData={wrappedTreeData} + defaultExpandedKeys={defaultExpandedKeys} + expandedKeys={expandedKeys} + draggable={{ + icon: false, + nodeDraggable: () => true + }} + onDrop={(info) => { + const { dragNode, node, dropToGap, dropPosition } = info; + const from: ElementPosition = treePosToElementPosition(dragNode.pos); + const to: ElementPosition = treePosToElementPosition(node.pos); + if (dropToGap === true && dropPosition >= 0) { + to[to.length - 1] = dropPosition; + } else if (node.dragOverGapBottom === true) { + to[to.length - 1] = to[to.length - 1] + 1; + } else if (node.dragOverGapTop === true) { + to[to.length - 1] = Math.max(0, to[to.length - 1] - 1); + } else if (node.dragOver === true) { + to.push(0); + } + onDrop?.({ from, to }); + }} + /> + ); + }, [className, style, onSelectNode, treeData, selectedKeys]); +}); diff --git a/packages/studio-base/src/modules/page-tree/tree-node.tsx b/packages/studio-base/src/modules/page-tree/tree-node.tsx new file mode 100644 index 0000000..e7fbdf8 --- /dev/null +++ b/packages/studio-base/src/modules/page-tree/tree-node.tsx @@ -0,0 +1,230 @@ +import React, { useMemo, useState, useRef, useEffect } from 'react'; +import type { CSSProperties } from 'react'; +import classnames from 'classnames'; +import type { ElementType, ElementOperations, ElementPosition } from 'idraw'; +import { Input } from 'antd'; +import type { InputRef } from 'antd'; +import IconVisible from '../../icons/visible'; +import IconInvisible from '../../icons/invisible'; +import IconFile from '../../icons/file'; + +import IconCloseCircle from '../../icons/close-circle'; + +const modName = 'node'; + +export interface TreeNodeProps { + uuid: string; + nodeKey: string; + title: string; + operations: ElementOperations; + position: ElementPosition; + className?: string; + type?: ElementType; + style?: CSSProperties; + parentModName: string; + generateClassName: (...args: string[]) => string; + onTitleChange?: (e: { uuid: string; value: string }) => void; + onOperationToggle?: (e: { uuid: string; operations: ElementOperations }) => void; + onDelete?: (e: { uuid: string }) => void; + onSelect?: (e: { uuids: string[]; positions: ElementPosition[] }) => void; + isSelected: boolean; +} + +export const TreeNode = (props: TreeNodeProps) => { + const { + className, + style, + type, + uuid, + nodeKey, + title, + position, + parentModName, + generateClassName, + onTitleChange, + onOperationToggle, + onDelete, + onSelect, + operations, + isSelected + } = props; + const [isEdit, setIsEdit] = useState(false); + const [showAction, setShowAction] = useState(false); + const refTitle = useRef(title); + + const rootClassName = generateClassName(parentModName, modName); + const iconClassName = generateClassName(parentModName, modName, 'icon'); + const titleClassName = generateClassName(parentModName, modName, 'title'); + const titleInputClassName = generateClassName(parentModName, modName, 'title', 'input'); + const titleIconClassName = generateClassName(parentModName, modName, 'title', 'icon'); + const actionClassName = generateClassName(parentModName, modName, 'action'); + const selectedClassName = generateClassName(parentModName, modName, 'selected'); + const clickTime = useRef(0); + const refInput = useRef(null); + + // useEffect(() => { + // refTitle.current = title; + // }, [title]); + + // const refClickTitleTime = useRef(0); + + // const onTitleClick = (e: React.MouseEvent) => { + // // e.stopPropagation(); + // // e.preventDefault(); + // const nowTime = Date.now(); + // const timeDiff = nowTime - refClickTitleTime.current; + // refClickTitleTime.current = nowTime; + // if (!(timeDiff >= 0 && timeDiff <= 500)) { + // return; + // } + // e.stopPropagation(); + // if (isEdit === true) { + // return; + // } + // setIsEdit(true); + // }; + + useEffect(() => { + if (isEdit === true) { + refInput.current?.focus(); + } + }, [isEdit]); + + const onTitleInputBlur = (e: React.FocusEvent) => { + setIsEdit(false); + onTitleChange?.({ uuid: nodeKey, value: e.target.value || '' }); + }; + // const onTitleInputOk = (e: any) => { + // e.stopPropagation(); + // setIsEdit(false); + // onTitleChange?.({ uuid: nodeKey, value: refTitle.current || '' }); + // }; + const onTitleInputChange = (e: React.ChangeEvent) => { + refTitle.current = e.target.value || ''; + }; + + const onTitleInputKeyDown = (e: React.KeyboardEvent) => { + if (e.code === 'Enter') { + setIsEdit(false); + onTitleChange?.({ uuid: nodeKey, value: (e?.target as any)?.value || '' }); + } + }; + + const onTitleInputClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + const onNodeMouseOver = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowAction(true); + }; + const onNodeMouseLeave = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowAction(false); + }; + + // const onClickToEdit = (e: React.MouseEvent) => { + // e.stopPropagation(); + // e.preventDefault(); + // setIsEdit(true); + // }; + + const onClickTitle = (e: React.MouseEvent) => { + const nowTime = Date.now(); + const countTime = nowTime - clickTime.current; + clickTime.current = nowTime; + + onSelect?.({ + uuids: [uuid], + positions: [position] + }); + if (countTime <= 300 && countTime > 0) { + e.stopPropagation(); + e.preventDefault(); + setIsEdit(true); + } + }; + + const onClickToDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onDelete?.({ + uuid: nodeKey + }); + }; + + const onClickToggleVisible = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onOperationToggle?.({ + uuid: nodeKey, + operations: { + invisible: !operations.invisible + } + }); + }; + + // const onClickToggleLock = (e: React.MouseEvent) => { + // e.stopPropagation(); + // e.preventDefault(); + // onOperationToggle?.({ + // uuid: nodeKey, + // operations: { + // lock: !operations.lock + // } + // }); + // }; + + return useMemo(() => { + refTitle.current = title; + + return ( + + + + {title} + + {showAction && ( + + {operations.invisible ? ( + + ) : ( + + )} + + {/* {operations.lock ? ( + + ) : ( + + )} */} + {/* */} + + + )} + + {isEdit && ( + + } + /> + + )} + + ); + }, [nodeKey, title, isEdit, type, showAction, operations, isSelected]); +}; diff --git a/packages/studio-base/src/modules/page-tree/wrap.tsx b/packages/studio-base/src/modules/page-tree/wrap.tsx new file mode 100644 index 0000000..6f9f01f --- /dev/null +++ b/packages/studio-base/src/modules/page-tree/wrap.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import type { ElementPosition } from 'idraw'; +import type { PageTreeNode, PageTreeData, PageTreeViewNode, PageTreeViewData } from '../../types'; +import { TreeNode } from './tree-node'; +import type { TreeNodeProps } from './tree-node'; + +type WrapOptions = Pick & { + parentModName: string; + generateClassName: (...args: string[]) => string; + position: ElementPosition; + onSelect?: (e: { uuids: string[]; positions: ElementPosition[] }) => void; + selectedKeys: string[]; +}; + +export function wrapTreeViewData(elementTree: PageTreeData, opts: WrapOptions): PageTreeViewData { + const tree: PageTreeViewData = []; + const { position } = opts; + if (Array.isArray(elementTree)) { + const pos = [...position]; + elementTree.forEach((node, i) => { + pos.push(i); + tree.push(wrapTreeViewNode(node, { ...opts, ...{ position: [...pos] } })); + pos.pop(); + }); + } + return tree; +} + +const wrapTreeViewNode = (treeNode: PageTreeNode, opts: WrapOptions) => { + const { parentModName, onTitleChange, onOperationToggle, onDelete, onSelect, position, selectedKeys } = opts; + const node: PageTreeViewNode = { + key: treeNode.key, + // title: treeNode.title, + title: ( + + ) + }; + + return node; +}; diff --git a/packages/studio-base/src/tools/element-tree.ts b/packages/studio-base/src/tools/element-tree.ts index 8f40741..8d1ddad 100644 --- a/packages/studio-base/src/tools/element-tree.ts +++ b/packages/studio-base/src/tools/element-tree.ts @@ -5,19 +5,12 @@ const getElementTreeNode = (elem: Element) => { const node: ElementTreeNode = { uuid: elem.uuid, key: elem.uuid, - title: - elem.name || - (elem as Element<'text'>).detail.text || - elem.type || - 'unamed', + title: elem.name || (elem as Element<'text'>).detail.text || elem.type || 'unamed', type: elem.type, children: [], operations: elem.operations || {} }; - if ( - elem.type === 'group' && - Array.isArray((elem as Element<'group'>)?.detail?.children) - ) { + if (elem.type === 'group' && Array.isArray((elem as Element<'group'>)?.detail?.children)) { (elem as Element<'group'>).detail.children.forEach((child) => { node.children.push(getElementTreeNode(child)); }); @@ -26,10 +19,10 @@ const getElementTreeNode = (elem: Element) => { }; export function getElementTree(data: Data): ElementTreeData { - const treeData: ElementTreeData = []; + const elementTree: ElementTreeData = []; data.elements.forEach((elem) => { const node = getElementTreeNode(elem); - treeData.push(node); + elementTree.push(node); }); - return treeData; + return elementTree; } diff --git a/packages/studio-base/src/tools/index.ts b/packages/studio-base/src/tools/index.ts index 8d0af7d..47a0fa5 100644 --- a/packages/studio-base/src/tools/index.ts +++ b/packages/studio-base/src/tools/index.ts @@ -1 +1,2 @@ export { getElementTree } from './element-tree'; +export { getPageTree } from './page-tree'; diff --git a/packages/studio-base/src/tools/page-tree.ts b/packages/studio-base/src/tools/page-tree.ts new file mode 100644 index 0000000..ce8bab0 --- /dev/null +++ b/packages/studio-base/src/tools/page-tree.ts @@ -0,0 +1,24 @@ +import type { Element, ElementType } from 'idraw'; +import type { PageTreeData, PageTreeNode, StudioData } from '../types'; + +const getPageTreeNode = (elem: Element) => { + const node: PageTreeNode = { + uuid: elem.uuid, + key: elem.uuid, + title: elem.name || (elem as Element<'text'>).detail.text || elem.type || 'unamed', + type: elem.type, + operations: elem.operations || {} + }; + return node; +}; + +export function getPageTree(data: StudioData): PageTreeData { + const elementTree: PageTreeData = []; + data.elements.forEach((elem) => { + if (elem.type === 'group' && elem.extends?.isPage === true) { + const node = getPageTreeNode(elem); + elementTree.push(node); + } + }); + return elementTree; +} diff --git a/packages/studio-base/src/types/data.ts b/packages/studio-base/src/types/data.ts new file mode 100644 index 0000000..a0465bb --- /dev/null +++ b/packages/studio-base/src/types/data.ts @@ -0,0 +1,5 @@ +import type { Data } from '@idraw/types'; + +export type StudioData = Data<{ + isPage?: boolean; +}>; diff --git a/packages/studio-base/src/types/index.ts b/packages/studio-base/src/types/index.ts index a1748a2..d0cc0fb 100644 --- a/packages/studio-base/src/types/index.ts +++ b/packages/studio-base/src/types/index.ts @@ -1,2 +1,3 @@ export * from './tree'; +export * from './data'; export type ThemeMode = 'dark' | 'light'; diff --git a/packages/studio-base/src/types/tree.ts b/packages/studio-base/src/types/tree.ts index e6ae15b..134ed65 100644 --- a/packages/studio-base/src/types/tree.ts +++ b/packages/studio-base/src/types/tree.ts @@ -19,3 +19,20 @@ export type ElementTreeViewNode = { }; export type ElementTreeViewData = ElementTreeViewNode[]; + +export type PageTreeNode = { + uuid: string; + key: string; + title: string; + type?: ElementType; + operations: ElementOperations; +}; + +export type PageTreeData = PageTreeNode[]; + +export type PageTreeViewNode = { + key: string; + title: React.ReactNode; +}; + +export type PageTreeViewData = PageTreeViewNode[]; diff --git a/packages/studio/src/css/base.less b/packages/studio/src/css/base.less index b6ada53..56ec6e7 100644 --- a/packages/studio/src/css/base.less +++ b/packages/studio/src/css/base.less @@ -9,5 +9,6 @@ @import './modules/toolbar.less'; @import './modules/nav-menu.less'; @import './modules/panel-layer.less'; +@import './modules/panel-page.less'; @import './modules/panel-detail.less'; @import './modules/export-image-file.less'; diff --git a/packages/studio/src/css/modules/panel-page.less b/packages/studio/src/css/modules/panel-page.less new file mode 100644 index 0000000..dd2b0ca --- /dev/null +++ b/packages/studio/src/css/modules/panel-page.less @@ -0,0 +1,86 @@ +// @import '../variable.less'; + +@mod-panel-page: ~'@{prefix}-mod-panel-page'; + +.@{mod-panel-page} { + height: 100%; + width: 100%; + display: flex; + flex-flow: column; + + .@{mod-panel-page}-header { + height: 32px; + box-sizing: border-box; + padding: 0 12px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .@{mod-panel-page}-header-title { + font-size: 12px; + position: relative; + display: -webkit-box; + float: inline-start; + overflow: hidden; + height: 32px; + line-height: 32px; + white-space: initial; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + } + + .@{mod-panel-page}-header-btn { + margin-right: 4px; + } + + .@{mod-panel-page}-content { + flex: 1; + // display: flex; + width: 100%; + overflow: auto; + box-sizing: border-box; + padding: 0; + margin: 0; + } + + .@{mod-panel-page}-footer { + height: 32px; + box-sizing: border-box; + border-top: 1px solid; + border-color: ~'var(--@{prefix}-border-color)'; + padding: 0 20px; + } + + .ant-collapse { + .ant-collapse-item { + box-shadow: 0px 1px 1px ~'var(--@{prefix}-border-color)'; + border-radius: 0; + &:first-child { + .ant-collapse-header { + border-top: none; + } + } + .ant-collapse-header { + box-sizing: border-box; + padding: 0; + display: flex; + align-items: center; + border-top: 1px solid; + border-color: ~'var(--@{prefix}-border-color)'; + border-radius: 0; + box-shadow: 0px 1px 4px ~'var(--@{prefix}-shadow-color)'; + .ant-collapse-expand-icon { + padding: 0 8px; + } + } + + .ant-collapse-content { + .ant-collapse-content-box { + padding: 0; + } + } + } + } +} diff --git a/packages/studio/src/modules/_mod/child.tsx b/packages/studio/src/modules/_mod/child.tsx index ea36e7f..1b491d8 100644 --- a/packages/studio/src/modules/_mod/child.tsx +++ b/packages/studio/src/modules/_mod/child.tsx @@ -1,7 +1,7 @@ -import React, { useMemo, useContext } from 'react'; +import React, { useMemo } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ConfigContext } from '@idraw/studio-base'; +import { generateClassName } from '@idraw/studio-base'; const modName = 'mod-xxxxxx'; @@ -12,9 +12,7 @@ export interface ChildProps { export const Child = (props: ChildProps) => { const { className, style } = props; - const { createPrefixName } = useContext(ConfigContext); - const generateClassName = createPrefixName(modName); - const rootClassName = generateClassName(); + const rootClassName = generateClassName(modName); return useMemo(() => { return ( diff --git a/packages/studio/src/modules/_mod/index.tsx b/packages/studio/src/modules/_mod/index.tsx index eb3ad0b..8c796d7 100644 --- a/packages/studio/src/modules/_mod/index.tsx +++ b/packages/studio/src/modules/_mod/index.tsx @@ -1,7 +1,7 @@ -import React, { useContext, useMemo } from 'react'; +import React, { useMemo } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ConfigContext } from '@idraw/studio-base'; +import { generateClassName } from '@idraw/studio-base'; const modName = 'mod-xxx'; @@ -12,12 +12,11 @@ export interface ModProps { export const Mod = (props: ModProps) => { const { className, style } = props; - const { createPrefixName } = useContext(ConfigContext); - const generateClassName = createPrefixName(modName); + const rootClassName = generateClassName(modName); return useMemo(() => { return ( -
+
Mod
); diff --git a/packages/studio/src/modules/context.ts b/packages/studio/src/modules/context.ts index 3c34521..d420755 100644 --- a/packages/studio/src/modules/context.ts +++ b/packages/studio/src/modules/context.ts @@ -1,12 +1,13 @@ import { createContext } from 'react'; import { ElementPosition, getElementPositionFromList } from 'idraw'; -import { getElementTree } from '@idraw/studio-base'; +import { getElementTree, getPageTree } from '@idraw/studio-base'; import type { Data } from 'idraw'; import { StudioState, StudioAction, StudioContext, StudioProps } from '../types'; import { cloneEditingDataByPosition, updateEditingDataLayoutToTargetGroup } from '../util/data'; const defaultThemeMode = 'dark'; const defaultLocale = 'en-US'; +const defaultEditMode = 'data'; export function createStudioContextStateByProps(props?: StudioProps): StudioState { const data = { @@ -15,19 +16,30 @@ export function createStudioContextStateByProps(props?: StudioProps): StudioStat }; let editingDataPosition: ElementPosition = []; let editingData: Data = data; + + const pageTree = getPageTree(data); + if (props?.defaultEditingGroupUUID) { editingDataPosition = getElementPositionFromList(props.defaultEditingGroupUUID, data.elements); } + + if (props?.defaultEditMode === 'page' && editingDataPosition.length !== 1 && pageTree.length > 0) { + editingDataPosition = [0]; + } + editingData = cloneEditingDataByPosition(editingDataPosition, data); - const treeData = getElementTree(editingData); + + const elementTree = getElementTree(editingData); return { localeCode: props?.defaultLocale || defaultLocale, themeMode: props?.defaultThemeMode || defaultThemeMode, + editMode: props?.defaultEditMode || defaultEditMode, data, editingData, editingDataPosition, - treeData, + elementTree, + pageTree, selectedUUIDs: props?.defaultSelectedElementUUIDs || [], scaleInfo: { scale: 1, @@ -44,15 +56,17 @@ export function createStudioContextState(opts?: Partial): StudioSta ...(opts?.data || {}) }; return { - localeCode: 'en-US', - themeMode: opts?.themeMode || 'light', + localeCode: defaultLocale, + themeMode: opts?.themeMode || defaultThemeMode, + editMode: opts?.editMode || defaultEditMode, data: { elements: [], ...(opts?.data || {}) }, editingData: cloneEditingDataByPosition([], data), editingDataPosition: [], - treeData: [], + elementTree: [], + pageTree: [], selectedUUIDs: [], scaleInfo: { scale: 1, diff --git a/packages/studio/src/modules/dashboard/index.tsx b/packages/studio/src/modules/dashboard/index.tsx index a4fe589..c3856d9 100644 --- a/packages/studio/src/modules/dashboard/index.tsx +++ b/packages/studio/src/modules/dashboard/index.tsx @@ -1,21 +1,21 @@ import React, { useEffect, useState, useContext, forwardRef, useMemo } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ConfigContext, SplitPane } from '@idraw/studio-base'; +import { SplitPane, generateClassName } from '@idraw/studio-base'; import { PanelLayer } from '../panel-layer'; +import { PanelPage } from '../panel-page'; import { PanelDetail } from '../panel-detail'; import { Header } from '../header'; import { Sketch } from '../sketch'; // import SplitPane from '../split-pane'; import type { SharedEvent, SharedStore, HookUseContextMenuOptions } from '../../types'; +import { Context } from '../context'; const modName = 'mod-dashboard'; const leftSiderDefaultWidth = 240; const rightSiderDefaultWidth = 240; const headerHeight = 36; -// const prefixName = createPrefixName(modName); - export interface DashboardProps { className?: string; style?: CSSProperties; @@ -52,8 +52,8 @@ export const Dashboard = forwardRef((props: Dash useContextMenuOptions, handleKeyboard } = props; - const { createPrefixName } = useContext(ConfigContext); - const prefixName = createPrefixName(modName); + const { state } = useContext(Context); + const { editMode } = state; const [openLeftSider, setOpenLeftSider] = useState(true); const [openRightSider, setOpenRightSider] = useState(true); @@ -92,12 +92,19 @@ export const Dashboard = forwardRef((props: Dash }); }, [height, width, openLeftSider, openRightSider]); + const rootClassName = generateClassName(modName); + const headerClassName = generateClassName(modName, 'header'); + const contentClassName = generateClassName(modName, 'content'); + const leftClassName = generateClassName(modName, 'left'); + const rightClassName = generateClassName(modName, 'right'); + const centerClassName = generateClassName(modName, 'right'); + return useMemo(() => { const { leftWidth, rightWidth, centerWidth } = layout; return ( -
-
+
+
((props: Dash }} />
-
+
((props: Dash }} >
- {openLeftSider && ( - - )} + {openLeftSider && + (editMode === 'page' ? ( + + ) : ( + + ))}
-
+
@@ -192,5 +209,5 @@ export const Dashboard = forwardRef((props: Dash
); - }, [className, openLeftSider, openRightSider, layout, height]); + }, [className, editMode, openLeftSider, openRightSider, layout, height]); }); diff --git a/packages/studio/src/modules/export-image-file/index.tsx b/packages/studio/src/modules/export-image-file/index.tsx index 4d6d605..146ec9d 100644 --- a/packages/studio/src/modules/export-image-file/index.tsx +++ b/packages/studio/src/modules/export-image-file/index.tsx @@ -1,7 +1,7 @@ -import React, { useContext, useMemo, useEffect, useState, useCallback } from 'react'; +import React, { useMemo, useEffect, useState, useCallback } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ConfigContext } from '@idraw/studio-base'; +import { generateClassName } from '@idraw/studio-base'; import { Button, Spin, Form, Input, Select, Divider } from 'antd'; import type { SharedEvent, SharedStore } from '../../types'; @@ -35,12 +35,10 @@ const defaultFileOptions = { export const ExportFile = (props: ExportFileProps) => { const { className, style, sharedStore } = props; - const { createPrefixName } = useContext(ConfigContext); - const generateClassName = createPrefixName(modName); - const rootClassName = generateClassName(); - const previewClassName = generateClassName('preview'); - const optionsClassName = generateClassName('options'); - const canvasClassName = generateClassName('canvas'); + const rootClassName = generateClassName(modName); + const previewClassName = generateClassName(modName, 'preview'); + const optionsClassName = generateClassName(modName, 'options'); + const canvasClassName = generateClassName(modName, 'canvas'); const [fileInfo, setFileInfo] = useState(null); const [isLoading, setIsLoading] = useState(true); const [imageSrc, setImageSrc] = useState(null); diff --git a/packages/studio/src/modules/header/index.tsx b/packages/studio/src/modules/header/index.tsx index 4d2b563..14ec4af 100644 --- a/packages/studio/src/modules/header/index.tsx +++ b/packages/studio/src/modules/header/index.tsx @@ -1,7 +1,7 @@ import React, { useContext, useMemo } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ThemeSwitch, LocaleSelector, ScaleSelector, ConfigContext } from '@idraw/studio-base'; +import { ThemeSwitch, LocaleSelector, ScaleSelector, generateClassName } from '@idraw/studio-base'; import { formatNumber } from 'idraw'; import { Context } from '../context'; import { NavMenu } from '../nav-menu'; @@ -36,16 +36,14 @@ export const Header = (props: ModProps) => { sharedEvent } = props; const { state, dispatch } = useContext(Context); - const { createPrefixName } = useContext(ConfigContext); - const generateClassName = createPrefixName(modName); - const rootClassName = generateClassName(); - const leftClassName = generateClassName('left'); - const rightClassName = generateClassName('right'); - const centerClassName = generateClassName('center'); + const rootClassName = generateClassName(modName); + const leftClassName = generateClassName(modName, 'left'); + const rightClassName = generateClassName(modName, 'right'); + const centerClassName = generateClassName(modName, 'center'); - const localeClassName = generateClassName('locale'); - const scaleClassName = generateClassName('scale'); - const switchClassName = generateClassName('switch'); + const localeClassName = generateClassName(modName, 'locale'); + const scaleClassName = generateClassName(modName, 'scale'); + const switchClassName = generateClassName(modName, 'switch'); const { localeCode, scaleInfo } = state; return useMemo(() => { diff --git a/packages/studio/src/modules/nav-menu/index.tsx b/packages/studio/src/modules/nav-menu/index.tsx index 4f0b59b..3e1bc92 100644 --- a/packages/studio/src/modules/nav-menu/index.tsx +++ b/packages/studio/src/modules/nav-menu/index.tsx @@ -1,7 +1,7 @@ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ConfigContext, IconRect, IconCircle, IconText, IconStar, IconGroup, IconImage, IconHTML } from '@idraw/studio-base'; +import { generateClassName, IconRect, IconCircle, IconText, IconStar, IconGroup, IconImage, IconHTML } from '@idraw/studio-base'; import { Dropdown, Button, Space, Modal } from 'antd'; import type { MenuProps, MenuItemProps, ButtonProps } from 'antd'; import { downloadFileFromText } from 'idraw'; @@ -28,10 +28,8 @@ export interface NavMenuProps { export const NavMenu = (props: NavMenuProps) => { const { className, style, sharedStore, sharedEvent } = props; const [modal, contextHolder] = Modal.useModal(); - const { createPrefixName } = useContext(ConfigContext); - const generateClassName = createPrefixName(modName); - const rootClassName = generateClassName(); - const dropdownClassName = generateClassName('dropdown'); + const rootClassName = generateClassName(modName); + const dropdownClassName = generateClassName(modName, 'dropdown'); const [selectedKeys, setSelectedKeys] = useState([]); const moduleLocale = useModuleLocale(); const clickToCreateElement: MenuItemProps['onClick'] = ({ key, domEvent }) => { diff --git a/packages/studio/src/modules/panel-detail/index.tsx b/packages/studio/src/modules/panel-detail/index.tsx index 2bd69f0..fb64dfa 100644 --- a/packages/studio/src/modules/panel-detail/index.tsx +++ b/packages/studio/src/modules/panel-detail/index.tsx @@ -3,7 +3,7 @@ import type { CSSProperties } from 'react'; import classnames from 'classnames'; import { findElementFromList, updateElementInList, isAssetId, createAssetId } from 'idraw'; import type { Element, DataLayout, ElementAssetsItem, RecursivePartial, Data } from 'idraw'; -import { ConfigContext, ElementDetail, LayoutDetail } from '@idraw/studio-base'; +import { generateClassName, ElementDetail, LayoutDetail } from '@idraw/studio-base'; import { Context } from '../context'; const modName = 'mod-panel-detail'; @@ -15,11 +15,9 @@ export interface PanelDetailProps { export const PanelDetail = (props: PanelDetailProps) => { const { className, style } = props; - const { createPrefixName } = useContext(ConfigContext); const { state, dispatch } = useContext(Context); - const generateClassName = createPrefixName(modName); const { selectedUUIDs, editingData, editingDataPosition } = state; - const modClassName = generateClassName(); + const modClassName = generateClassName(modName); const refEditingData = useRef(editingData); useEffect(() => { diff --git a/packages/studio/src/modules/panel-layer/index.tsx b/packages/studio/src/modules/panel-layer/index.tsx index 174be2e..f0e2ace 100644 --- a/packages/studio/src/modules/panel-layer/index.tsx +++ b/packages/studio/src/modules/panel-layer/index.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useMemo, useState, useRef } from 'react'; import type { CSSProperties } from 'react'; import classnames from 'classnames'; -import { ConfigContext, ElementTree, getElementTree, IconDoubleLeft, IconLeft } from '@idraw/studio-base'; +import { generateClassName, ElementTree, getElementTree, IconDoubleLeft, IconLeft } from '@idraw/studio-base'; import { updateElementInList, moveElementPosition, getGroupQueueFromList, findElementFromListByPosition } from 'idraw'; import type { ElementPosition } from 'idraw'; import { Dropdown, Button } from 'antd'; @@ -23,19 +23,18 @@ export interface PanelLayerProps { export const PanelLayer = (props: PanelLayerProps) => { const { className, style, height, defaultSelectedElementUUIDs = [], sharedStore, sharedEvent, useContextMenuOptions } = props; const { state, dispatch } = useContext(Context); - const { createPrefixName } = useContext(ConfigContext); - const generateClassName = createPrefixName(modName); - const { treeData, selectedUUIDs, editingData } = state; + const { elementTree, selectedUUIDs, editingData } = state; + const refTree = useRef<{ scrollTo: (e: { key: string | number; align?: 'top' | 'bottom' | 'auto'; offset?: number }) => void; } | null>(null); const [expandedKeys, setExpandedKeys] = useState(defaultSelectedElementUUIDs); - const rootClassName = generateClassName(); - const contentClassName = generateClassName('content'); - const headerClassName = generateClassName('header'); - const headerTitleClassName = generateClassName('header', 'title'); - const headerBtnClassName = generateClassName('header', 'btn'); - // const footerClassName = generateClassName('footer'); + const rootClassName = generateClassName(modName); + const contentClassName = generateClassName(modName, 'content'); + const headerClassName = generateClassName(modName, 'header'); + const headerTitleClassName = generateClassName(modName, 'header', 'title'); + const headerBtnClassName = generateClassName(modName, 'header', 'btn'); + // const footerClassName = generateClassName(modName, 'footer'); const [contextMenuOptions] = useContextMenuOptions({ sharedEvent, sharedStore }); const getCurrentName = () => { @@ -92,8 +91,11 @@ export const PanelLayer = (props: PanelLayerProps) => { sharedEvent.trigger('resetEditingView', { type: 'back-one', position: null }); }; + const headerHeight = 32; + const elementsHeight = height - headerHeight; + return useMemo(() => { - if (!(Array.isArray(treeData) && treeData.length > 0)) { + if (!(Array.isArray(elementTree) && elementTree.length > 0)) { return (
{
); } + return (
{ e.preventDefault(); }} > -
+
+ ), + children: ( +
+ { + updateElementInList(uuid, { name: value }, data.elements); + + const pageTree = getPageTree(data); + const payload: Partial = { + pageTree, + data: { ...data } + }; + if (editingDataPosition.length === 0) { + payload.editingData = { ...data }; + const elementTree = getElementTree(data); + payload.elementTree = elementTree; + } + dispatch({ + type: 'update', + payload + }); + }} + onOperationToggle={({}) => { + // updateElementInList(uuid, { operations }, state.editingData.elements); + // const elementTree = getElementTree(editingData); + // dispatch({ + // type: 'update', + // payload: { editingData: { ...editingData }, elementTree } + // }); + }} + onSelect={(e) => { + if (e?.positions.length === 1) { + if (inPageOverview) { + if (!selectedPageUUIDs?.includes(e.uuids[0])) { + selectElementsByPositions(e.positions); + setSelectedPageUUIDs([e.uuids[0]]); + } + return; + } + + sharedEvent.trigger('resetEditingView', { type: 'go-to-page', position: e.positions[0] }); + + const keys: string[] = []; + const elem = findElementFromListByPosition(e.positions[0], state.data.elements); + if (elem?.uuid) { + keys.push(elem.uuid); + setSelectedPageUUIDs(keys); + } + } + }} + onDrop={(e) => { + if (!(e.from.length === 1 && e.to.length === 1)) { + return; + } + const { elements } = moveElementPosition(data.elements, { + from: e.from, + to: e.to + }); + + const pageTree = getPageTree(data); + const payload: Partial = { + pageTree, + data: { ...data, ...{ elements } } + }; + if (editingDataPosition.length === 0) { + payload.editingData = { ...data, ...{ elements } }; + const elementTree = getElementTree(data); + payload.elementTree = elementTree; + } + dispatch({ + type: 'update', + payload + }); + }} + onDelete={({}) => { + // sharedEvent.trigger('deleteElement', { uuid }); + }} + /> +
+ ) + }, + { + key: elementTreeKey, + label: ( +
+
+
+ {getCurrentName()} +
+ ), + collapsible: inPageOverview ? 'disabled' : undefined, + children: inPageOverview ? null : ( + +
+ { + updateElementInList(uuid, { name: value }, state.editingData.elements); + const elementTree = getElementTree(editingData); + dispatch({ + type: 'update', + payload: { editingData: { ...editingData }, elementTree } + }); + }} + onOperationToggle={({ uuid, operations }) => { + updateElementInList(uuid, { operations }, state.editingData.elements); + const elementTree = getElementTree(editingData); + dispatch({ + type: 'update', + payload: { editingData: { ...editingData }, elementTree } + }); + }} + onSelect={(e) => { + if (!selectedUUIDs?.includes(e.uuids[0])) { + selectElementsByPositions(e.positions); + } + }} + onDrop={(e) => { + const { elements } = moveElementPosition(editingData.elements, { + from: e.from, + to: e.to + }); + + const targetElem = findElementFromListByPosition(e.to, editingData.elements); + if (targetElem) { + targetElem.x = 0; + targetElem.y = 0; + } + + const elementTree = getElementTree(editingData); + dispatch({ + type: 'update', + payload: { editingData: { ...editingData, ...{ elements: [...elements] } }, elementTree } + }); + }} + onDelete={({ uuid }) => { + sharedEvent.trigger('deleteElement', { uuid }); + }} + onGoToGroup={(e) => { + sharedEvent.trigger('resetEditingView', { type: 'go-to-next-group', position: e.position }); + }} + onExpand={(keys, { node }) => { + const currentKey = node.key as string; + if (currentKey) { + let newKeys = [...expandedElementKeys]; + if (expandedElementKeys.includes(currentKey)) { + newKeys.splice(newKeys.indexOf(currentKey), 1); + } else { + newKeys = [...newKeys, ...[currentKey]]; + } + setExpandedElementKeys(newKeys); + } + }} + /> +
+
+ ) + } + ]; + + return useMemo(() => { + if (!(Array.isArray(elementTree) && elementTree.length > 0)) { + return ( +
{ + e.preventDefault(); + }} + > +
...
+
+
Empty
+
+ {/*
...
*/} +
+ ); + } + + return ( +
{ + e.preventDefault(); + }} + > + { + if (Array.isArray(e)) { + refContentActiveKeys.current = [...e]; + } + resetContentHeight(); + }} + /> +
+ ); + }, [ + height, + pageTreeHeight, + elementTreeHeight, + elementTree, + selectedUUIDs, + expandedElementKeys, + data.elements, + editingData.elements, + editingDataPosition, + contextMenuOptions, + resetContentHeight, + inPageOverview, + selectedPageUUIDs + ]); +}; diff --git a/packages/studio/src/modules/sketch/index.tsx b/packages/studio/src/modules/sketch/index.tsx index 232dcc3..a4941c7 100644 --- a/packages/studio/src/modules/sketch/index.tsx +++ b/packages/studio/src/modules/sketch/index.tsx @@ -64,6 +64,10 @@ export const Sketch = (props: SketchProps) => { const listenMiddlewareEventSelect = (e: { uuids: string[]; positions: ElementPosition[] }) => { const editingData = refEditingData.current; let { uuids } = e; + if (uuids?.length === 1 && refSelectedUUIDs.current?.length === 1 && uuids[0] === refSelectedUUIDs.current[0]) { + return; + } + const { positions } = e; if (positions && Array.isArray(positions)) { const elems = findElementsFromListByPositions(positions, editingData.elements); @@ -84,7 +88,7 @@ export const Sketch = (props: SketchProps) => { if (['addElement', 'updateElement', 'deleteElement', 'moveElement', 'dragElement', 'resizeElement'].includes(type)) { const payload: Partial = { editingData: { ...data } }; if (['addElement', 'deleteElement', 'moveElement'].includes(type)) { - payload.treeData = getElementTree(editingData); + payload.elementTree = getElementTree(editingData); } dispatch({ type: 'update', @@ -129,7 +133,7 @@ export const Sketch = (props: SketchProps) => { type: 'update', payload: { editingData: { ...newEditingData }, - treeData: newTreeData + elementTree: newTreeData } }); }; @@ -168,7 +172,7 @@ export const Sketch = (props: SketchProps) => { type: 'update', payload: { editingData: { ...newEditingData }, - treeData: newTreeData + elementTree: newTreeData } }); idraw.selectElements([elem.uuid]); @@ -208,7 +212,7 @@ export const Sketch = (props: SketchProps) => { type: 'update', payload: { editingData: { ...newEditingData }, - treeData: newTreeData + elementTree: newTreeData } }); idraw.selectElements([element.uuid]); @@ -219,10 +223,10 @@ export const Sketch = (props: SketchProps) => { idraw?.deleteElement(uuid); const editingData = idraw?.getData(); if (editingData) { - const treeData = getElementTree(editingData); + const elementTree = getElementTree(editingData); dispatch({ type: 'update', - payload: { editingData: { ...editingData }, treeData } + payload: { editingData: { ...editingData }, elementTree } }); idraw.trigger(eventKeys.clearSelect, {}); } @@ -244,9 +248,9 @@ export const Sketch = (props: SketchProps) => { updateEditingDataChildrenToData(editingDataPosition, editingData, data); } - if (type === 'go-to-group' && position) { + if (type === 'go-to-page' && position) { // update new editing data - const newEditingDataPosition = [...editingDataPosition, ...position]; + const newEditingDataPosition = [...position]; const newEditingData = cloneEditingDataByPosition(newEditingDataPosition, data); const newTreeData = getElementTree(newEditingData); @@ -256,16 +260,44 @@ export const Sketch = (props: SketchProps) => { data: { ...data }, editingData: { ...newEditingData }, editingDataPosition: newEditingDataPosition, - treeData: newTreeData + elementTree: newTreeData } }); - // idraw.setViewScale({ - // scale: 1, - // offsetX: 0, - // offsetY: 0 - // }); idraw.centerContent({ data: newEditingData }); + idraw.trigger(eventKeys.clearSelect, {}); + } else if (type === 'go-to-group' && position) { + // update new editing data + const newEditingDataPosition = [...position]; + const newEditingData = cloneEditingDataByPosition(newEditingDataPosition, data); + const newTreeData = getElementTree(newEditingData); + dispatch({ + type: 'update', + payload: { + data: { ...data }, + editingData: { ...newEditingData }, + editingDataPosition: newEditingDataPosition, + elementTree: newTreeData + } + }); + idraw.centerContent({ data: newEditingData }); + idraw.trigger(eventKeys.clearSelect, {}); + } else if (type === 'go-to-next-group' && position) { + // update new editing data + const newEditingDataPosition = [...editingDataPosition, ...position]; + const newEditingData = cloneEditingDataByPosition(newEditingDataPosition, data); + const newTreeData = getElementTree(newEditingData); + + dispatch({ + type: 'update', + payload: { + data: { ...data }, + editingData: { ...newEditingData }, + editingDataPosition: newEditingDataPosition, + elementTree: newTreeData + } + }); + idraw.centerContent({ data: newEditingData }); idraw.trigger(eventKeys.clearSelect, {}); } else if (type === 'back-one' && editingDataPosition.length > 0) { const newEditingDataPosition = [...editingDataPosition]; @@ -279,7 +311,7 @@ export const Sketch = (props: SketchProps) => { data: { ...data }, editingData: { ...newEditingData }, editingDataPosition: [...newEditingDataPosition], - treeData: newTreeData + elementTree: newTreeData } }); // idraw.setViewScale({ @@ -300,7 +332,7 @@ export const Sketch = (props: SketchProps) => { data: { ...data }, editingData: newEditingData, editingDataPosition: newEditingDataPosition, - treeData: newTreeData + elementTree: newTreeData } }); // idraw.setViewScale({ @@ -325,7 +357,7 @@ export const Sketch = (props: SketchProps) => { data: { ...data }, editingData: { ...newEditingData }, editingDataPosition: newEditingDataPosition, - treeData: newTreeData + elementTree: newTreeData } }); idraw.setViewScale({ @@ -343,7 +375,7 @@ export const Sketch = (props: SketchProps) => { type: 'update', payload: { editingData: { ...editingData }, - treeData: newTreeData + elementTree: newTreeData } }); idraw.trigger(eventKeys.clearSelect, {}); @@ -370,6 +402,8 @@ export const Sketch = (props: SketchProps) => { offsetX, offsetY }); + } else { + idraw.centerContent({ data: state.editingData }); } } refHasFirstDraw.current = true; diff --git a/packages/studio/src/shared/event.ts b/packages/studio/src/shared/event.ts index 3abfcb9..cdda6fe 100644 --- a/packages/studio/src/shared/event.ts +++ b/packages/studio/src/shared/event.ts @@ -57,13 +57,13 @@ export function initActionEvent(opts: { sharedEvent: SharedEvent; sharedStore: S idraw.addElement(elem, { position }); }); const editingData = idraw?.getData() as Data; - const treeData = getElementTree(editingData); + const elementTree = getElementTree(editingData); sharedEvent.trigger('dispatch', { type: 'update', payload: { editingData, - treeData + elementTree } }); idraw.selectElement(targetElements[0].uuid); @@ -97,13 +97,13 @@ export function initActionEvent(opts: { sharedEvent: SharedEvent; sharedStore: S } const editingData = idraw?.getData() as Data; - const treeData = getElementTree(editingData); + const elementTree = getElementTree(editingData); sharedEvent.trigger('dispatch', { type: 'update', payload: { editingData, - treeData + elementTree } }); idraw?.trigger(eventKeys.clearSelect); @@ -120,12 +120,12 @@ export function initActionEvent(opts: { sharedEvent: SharedEvent; sharedStore: S idraw?.deleteElement(elements[i].uuid); } const editingData = idraw?.getData() as Data; - const treeData = getElementTree(editingData); + const elementTree = getElementTree(editingData); sharedEvent.trigger('dispatch', { type: 'update', payload: { editingData, - treeData + elementTree } }); idraw?.trigger(eventKeys.clearSelect); diff --git a/packages/studio/src/types/lib/context.ts b/packages/studio/src/types/lib/context.ts index 33f6abe..337f159 100644 --- a/packages/studio/src/types/lib/context.ts +++ b/packages/studio/src/types/lib/context.ts @@ -1,16 +1,22 @@ import type { Dispatch } from 'react'; import type { Data, ElementPosition } from 'idraw'; -import type { ElementTreeData, LocaleCode } from '@idraw/studio-base'; +import type { ElementTreeData, PageTreeData, LocaleCode } from '@idraw/studio-base'; export type StudioThemeMode = 'light' | 'dark'; +export type StudioEditMode = 'data' | 'page'; + export interface StudioState { localeCode: LocaleCode; themeMode: StudioThemeMode; + editMode: StudioEditMode; + data: Data; editingData: Data; editingDataPosition: ElementPosition; - treeData: ElementTreeData; + elementTree: ElementTreeData; + pageTree: PageTreeData; + selectedUUIDs: string[]; scaleInfo: { scale: number; diff --git a/packages/studio/src/types/lib/shared.ts b/packages/studio/src/types/lib/shared.ts index d163872..0dd5a77 100644 --- a/packages/studio/src/types/lib/shared.ts +++ b/packages/studio/src/types/lib/shared.ts @@ -34,7 +34,7 @@ export interface SharedEventMap { uuid: string; }; resetEditingView: { - type: 'go-to-group' | 'back-one' | 'back-root'; + type: 'go-to-page' | 'go-to-group' | 'go-to-next-group' | 'back-one' | 'back-root'; position: ElementPosition | null; }; resetData: { diff --git a/packages/studio/src/types/lib/studio.ts b/packages/studio/src/types/lib/studio.ts index e519294..de8e789 100644 --- a/packages/studio/src/types/lib/studio.ts +++ b/packages/studio/src/types/lib/studio.ts @@ -1,23 +1,28 @@ import type { CSSProperties } from 'react'; -import type { Data, ElementPosition } from 'idraw'; -import type { LocaleCode } from '@idraw/studio-base'; +import type { ElementPosition } from 'idraw'; +import type { LocaleCode, StudioData } from '@idraw/studio-base'; import type { DashboardProps } from '../../modules'; import type { SharedEvent, SharedStore } from './shared'; -import type { StudioActionType, StudioState } from './context'; +import type { StudioActionType, StudioState, StudioThemeMode, StudioEditMode } from './context'; export type StudioProps = Omit & Pick, 'useContextMenuOptions' | 'handleKeyboard'> & { className?: string; style?: CSSProperties; - data: Data; + prefiexName?: string; defaultLocale?: LocaleCode; - defaultThemeMode?: 'light' | 'dark'; + defaultThemeMode?: StudioThemeMode; + defaultEditMode?: StudioEditMode; + defaultScaleInfo?: { scale: number; offsetX: number; offsetY: number; }; + + // data + data?: StudioData; defaultEditingGroupUUID?: string; onEditGroupElement?: (e: { uuid?: string; position: ElementPosition }) => void; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82f98fe..551bbfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: 5.12.1 version: 5.12.1(react-dom@18.2.0)(react@18.2.0) idraw: - specifier: 0.4.0-beta.19 - version: 0.4.0-beta.19 + specifier: 0.4.0-beta.20 + version: 0.4.0-beta.20 devDependencies: '@babel/core': specifier: ^7.22.19 @@ -2041,6 +2041,16 @@ packages: '@idraw/util': 0.4.0-beta.19 dev: false + /@idraw/board@0.4.0-beta.20(@idraw/renderer@0.4.0-beta.20)(@idraw/util@0.4.0-beta.20): + resolution: {integrity: sha512-l109binq3vvd9b1ArXrqrNO9e1y7ACuCYSp70ImAh0+1IMartpCoS6CmIxpzarlMPWoWjg7tmBccjvKlXOBa/w==} + peerDependencies: + '@idraw/renderer': ^0.4.0-beta.20 + '@idraw/util': ^0.4.0-beta.20 + dependencies: + '@idraw/renderer': 0.4.0-beta.20(@idraw/util@0.4.0-beta.20) + '@idraw/util': 0.4.0-beta.20 + dev: false + /@idraw/core@0.4.0-beta.19(@idraw/board@0.4.0-beta.19)(@idraw/renderer@0.4.0-beta.19)(@idraw/util@0.4.0-beta.19): resolution: {integrity: sha512-l+SGzTA5/8bq9TuRDsoe0UloAQ5HfAOigpcRZeQNrYj5ryiRQWJRUJkxZy31rHGqj7BvaetWFRGEkR/vi8ediw==} peerDependencies: @@ -2053,6 +2063,18 @@ packages: '@idraw/util': 0.4.0-beta.19 dev: false + /@idraw/core@0.4.0-beta.20(@idraw/board@0.4.0-beta.20)(@idraw/renderer@0.4.0-beta.20)(@idraw/util@0.4.0-beta.20): + resolution: {integrity: sha512-OK7n7n7b5jYbl+50lB2vpY97OMrdThEzM2wIeLUgLF0gOWsr5hXrtfiIG0MwBgFid+xKSsyOKLNn2gzn5McyVg==} + peerDependencies: + '@idraw/board': ^0.4.0-beta.20 + '@idraw/renderer': ^0.4.0-beta.20 + '@idraw/util': ^0.4.0-beta.20 + dependencies: + '@idraw/board': 0.4.0-beta.20(@idraw/renderer@0.4.0-beta.20)(@idraw/util@0.4.0-beta.20) + '@idraw/renderer': 0.4.0-beta.20(@idraw/util@0.4.0-beta.20) + '@idraw/util': 0.4.0-beta.20 + dev: false + /@idraw/renderer@0.4.0-beta.19(@idraw/util@0.4.0-beta.19): resolution: {integrity: sha512-W4vNsOfjWmLMcEPOdCTJNNsoVMupsBfU/BMgAHjUZYnBeoYzVZUcX5wVm+Iie3Kl2SoFctfUU7EJm9rZ1+G6YA==} peerDependencies: @@ -2061,14 +2083,30 @@ packages: '@idraw/util': 0.4.0-beta.19 dev: false + /@idraw/renderer@0.4.0-beta.20(@idraw/util@0.4.0-beta.20): + resolution: {integrity: sha512-iJ6zo1Zcoua+XapMTjHx5c/1mhrOjjXXfQX2iSTbzk1JpNEfv5qCbAF3t+ydRV8QJL76/h5k6M/SxBPI47tUTQ==} + peerDependencies: + '@idraw/util': ^0.4.0-beta.20 + dependencies: + '@idraw/util': 0.4.0-beta.20 + dev: false + /@idraw/types@0.4.0-beta.19: resolution: {integrity: sha512-gVbXGFyV53NL4cTaKRFAp95Vgu+AuRAZbt2znc5dS3729h29yYEgXv401eEKrXFGUICM3o50R0It8jpREoddaw==} dev: false + /@idraw/types@0.4.0-beta.20: + resolution: {integrity: sha512-ZM9/QqbPkOS77Y6Fogab3a/Pa114oB02UHSFK1CSXC2fvBJK8bXWOixJ2M77IKiaZxG4z5JI/OMe/yn1OKc3yg==} + dev: false + /@idraw/util@0.4.0-beta.19: resolution: {integrity: sha512-neZ/BihVFB0M5CDUOvtjVWp5NtjqAoQ8Poc7eAZRtXdaVGFXLApJ6UogUt0Aoti0hVluA+s8Ki/w8y3yg3FVkQ==} dev: false + /@idraw/util@0.4.0-beta.20: + resolution: {integrity: sha512-6CrQD4qB1AeTu8dhMQi0ku6oA46VxWr8cQw8kWbPd9TC9B7/AjAQkSwp+MvxANGNYdAmrglJ909xNKuIjIVHvQ==} + dev: false + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -4921,6 +4959,16 @@ packages: '@idraw/util': 0.4.0-beta.19 dev: false + /idraw@0.4.0-beta.20: + resolution: {integrity: sha512-G/ugOUjoPcribojSSXrXNmlIqrC+jjYg/hH+Lsf5VhYv2nIjxoaARgt2xSI//C+tnthW62J5Y4ZBzwgY1LV8Pg==} + dependencies: + '@idraw/board': 0.4.0-beta.20(@idraw/renderer@0.4.0-beta.20)(@idraw/util@0.4.0-beta.20) + '@idraw/core': 0.4.0-beta.20(@idraw/board@0.4.0-beta.20)(@idraw/renderer@0.4.0-beta.20)(@idraw/util@0.4.0-beta.20) + '@idraw/renderer': 0.4.0-beta.20(@idraw/util@0.4.0-beta.20) + '@idraw/types': 0.4.0-beta.20 + '@idraw/util': 0.4.0-beta.20 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true