From 9cbe416c2ccd120a45b01e0151f2595145531686 Mon Sep 17 00:00:00 2001 From: akumatus Date: Thu, 5 Sep 2024 15:24:36 +0000 Subject: [PATCH] feat(core): shape editor settings (#8122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit
🎥 Video uploaded on Graphite:
--- .../editor/edgeless/connector.tsx | 9 +- .../editor/edgeless/docs/flow.json | 470 ++++++++++++++++++ .../editor/edgeless/docs/index.ts | 2 + .../editor/edgeless/mind-map.tsx | 12 +- .../general-setting/editor/edgeless/note.tsx | 7 +- .../general-setting/editor/edgeless/pen.tsx | 10 +- .../general-setting/editor/edgeless/shape.tsx | 437 +++++++++++++--- .../editor/edgeless/snapshot.tsx | 92 ++-- .../general-setting/editor/edgeless/text.tsx | 8 +- .../general-setting/editor/edgeless/utils.ts | 13 + .../general-setting/editor/style.css.ts | 1 + .../src/modules/editor-settting/schema.ts | 17 +- packages/frontend/i18n/src/resources/en.json | 2 + .../frontend/i18n/src/resources/zh-Hans.json | 2 + 14 files changed, 939 insertions(+), 143 deletions(-) create mode 100644 packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/docs/flow.json create mode 100644 packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/utils.ts diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx index d566174399..0f7942252e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/connector.tsx @@ -15,6 +15,7 @@ import { PointStyle, StrokeStyle, } from '@blocksuite/blocks'; +import type { Doc } from '@blocksuite/store'; import { useFramework, useLiveData } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; @@ -23,6 +24,7 @@ import { menuTrigger, settingWrapper } from '../style.css'; import { useColor } from '../utils'; import { Point } from './point'; import { EdgelessSnapshot } from './snapshot'; +import { getSurfaceBlock } from './utils'; enum ConnecterStyle { General = 'general', @@ -189,13 +191,18 @@ export const ConnectorSettings = () => { }); }, [editorSetting, settings]); + const getElements = useCallback((doc: Doc) => { + const surface = getSurfaceBlock(doc); + return surface?.getElementsByType('connector') || []; + }, []); + return ( <> { const t = useI18n(); @@ -43,13 +45,19 @@ export const MindMapSettings = () => { ], [t] ); + + const getElements = useCallback((doc: Doc) => { + const surface = getSurfaceBlock(doc); + return surface?.getElementsByType('mindmap') || []; + }, []); + return ( <> { return getColorFromMap(background, NoteBackgroundColorMap); }, [getColorFromMap, settings]); + const getElements = useCallback((doc: Doc) => { + return doc.getBlocksByFlavour('affine:note') || []; + }, []); + return ( <> { const t = useI18n(); @@ -52,13 +54,19 @@ export const PenSettings = () => { }, [editorSetting] ); + + const getElements = useCallback((doc: Doc) => { + const surface = getSurfaceBlock(doc); + return surface?.getElementsByType('brush') || []; + }, []); + return ( <> { const t = useI18n(); - const [currentShape, setCurrentShape] = useState('square'); - const [shapeStyle, setShapeStyle] = useState<'general' | 'scribbled'>( - 'general' - ); - const [borderStyle, setBorderStyle] = useState<'solid' | 'dash' | 'none'>( - 'solid' - ); - const [borderThickness, setBorderThickness] = useState([3]); - const [textAlignment, setTextAlignment] = useState< - 'left' | 'center' | 'right' - >('left'); + const framework = useFramework(); + const { editorSetting } = framework.get(EditorSettingService); + const settings = useLiveData(editorSetting.settings$); + const getColorFromMap = useColor(); + + const [currentShape, setCurrentShape] = useState(ShapeType.Rect); const shapeStyleItems = useMemo( () => [ { - value: 'general', + value: ShapeStyle.General, label: t['com.affine.settings.editorSettings.edgeless.style.general'](), }, { - value: 'scribbled', + value: ShapeStyle.Scribbled, label: t['com.affine.settings.editorSettings.edgeless.style.scribbled'](), }, @@ -49,20 +75,30 @@ export const ShapeSettings = () => { [t] ); + const { shapeStyle } = settings[`shape:${currentShape}`]; + const setShapeStyle = useCallback( + (value: ShapeStyle) => { + editorSetting.set(`shape:${currentShape}`, { + shapeStyle: value, + }); + }, + [editorSetting, currentShape] + ); + const borderStyleItems = useMemo( () => [ { - value: 'solid', + value: StrokeStyle.Solid, label: t['com.affine.settings.editorSettings.edgeless.note.border.solid'](), }, { - value: 'dash', + value: StrokeStyle.Dash, label: t['com.affine.settings.editorSettings.edgeless.note.border.dash'](), }, { - value: 'none', + value: StrokeStyle.None, label: t['com.affine.settings.editorSettings.edgeless.note.border.none'](), }, @@ -70,24 +106,34 @@ export const ShapeSettings = () => { [t] ); + const borderStyle = settings[`shape:${currentShape}`].strokeStyle; + const setBorderStyle = useCallback( + (value: StrokeStyle) => { + editorSetting.set(`shape:${currentShape}`, { + strokeStyle: value, + }); + }, + [editorSetting, currentShape] + ); + const alignItems = useMemo( () => [ { - value: 'left', + value: TextAlign.Left, label: t[ 'com.affine.settings.editorSettings.edgeless.text.alignment.left' ](), }, { - value: 'center', + value: TextAlign.Center, label: t[ 'com.affine.settings.editorSettings.edgeless.text.alignment.center' ](), }, { - value: 'right', + value: TextAlign.Right, label: t[ 'com.affine.settings.editorSettings.edgeless.text.alignment.right' @@ -97,27 +143,37 @@ export const ShapeSettings = () => { [t] ); + const textAlignment = settings[`shape:${currentShape}`].textAlign; + const setTextAlignment = useCallback( + (value: TextAlign) => { + editorSetting.set(`shape:${currentShape}`, { + textAlign: value, + }); + }, + [editorSetting, currentShape] + ); + const shapes = useMemo( () => [ { - value: 'square', + value: ShapeType.Rect, label: t['com.affine.settings.editorSettings.edgeless.shape.square'](), }, { - value: 'ellipse', + value: ShapeType.Ellipse, label: t['com.affine.settings.editorSettings.edgeless.shape.ellipse'](), }, { - value: 'diamond', + value: ShapeType.Diamond, label: t['com.affine.settings.editorSettings.edgeless.shape.diamond'](), }, { - value: 'triangle', + value: ShapeType.Triangle, label: t['com.affine.settings.editorSettings.edgeless.shape.triangle'](), }, { - value: 'rounded-rectangle', + value: 'roundedRect', label: t[ 'com.affine.settings.editorSettings.edgeless.shape.rounded-rectangle' @@ -127,14 +183,212 @@ export const ShapeSettings = () => { [t] ); + const docs = useMemo( + () => [ + { + value: 'shape', + label: t['com.affine.settings.editorSettings.edgeless.shape.list'](), + }, + { + value: 'flow', + label: t['com.affine.settings.editorSettings.edgeless.shape.flow'](), + }, + ], + [t] + ); + const [currentDoc, setCurrentDoc] = useState('shape'); + + const fillColorItems = useMemo(() => { + const { fillColor } = settings[`shape:${currentShape}`]; + return Object.entries(ShapeFillColor).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { fillColor: value }); + }; + const isSelected = fillColor === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const borderColorItems = useMemo(() => { + const { strokeColor } = settings[`shape:${currentShape}`]; + return Object.entries(LineColor).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { strokeColor: value }); + }; + const isSelected = strokeColor === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const borderThickness = settings[`shape:${currentShape}`].strokeWidth; + const setBorderThickness = useCallback( + (value: number[]) => { + editorSetting.set(`shape:${currentShape}`, { + strokeWidth: value[0], + }); + }, + [editorSetting, currentShape] + ); + + const fontFamilyItems = useMemo(() => { + const { fontFamily } = settings[`shape:${currentShape}`]; + return Object.entries(FontFamily).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { fontFamily: value }); + }; + const isSelected = fontFamily === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const fontStyleItems = useMemo(() => { + const { fontStyle } = settings[`shape:${currentShape}`]; + return Object.entries(FontStyle).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { fontStyle: value }); + }; + const isSelected = fontStyle === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const fontWeightItems = useMemo(() => { + const { fontWeight } = settings[`shape:${currentShape}`]; + return Object.entries(FontWeight).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { fontWeight: value }); + }; + const isSelected = fontWeight === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const fontSizeItems = useMemo(() => { + const { fontSize } = settings[`shape:${currentShape}`]; + return Object.entries(ShapeTextFontSize).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { fontSize: Number(value) }); + }; + const isSelected = fontSize === Number(value); + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const textColorItems = useMemo(() => { + const { color } = settings[`shape:${currentShape}`]; + return Object.entries(LineColor).map(([name, value]) => { + const handler = () => { + editorSetting.set(`shape:${currentShape}`, { color: value }); + }; + const isSelected = color === value; + return ( + + {name} + + ); + }); + }, [editorSetting, settings, currentShape]); + + const getElements = useCallback( + (doc: Doc) => { + const surface = getSurfaceBlock(doc); + if (!surface) return []; + return surface.getElementsByType('shape').filter(node => { + const shape = node as ShapeElementModel; + const { shapeType, radius } = shape; + const shapeName = getShapeName(shapeType, radius); + return shapeName === currentShape; + }); + }, + [currentShape] + ); + + const firstUpdate = useCallback( + (doc: Doc, editorHost: EditorHost) => { + const edgelessService = editorHost.std.getService( + 'affine:page' + ) as EdgelessRootService; + const surface = getSurfaceBlock(doc); + if (!surface) return; + surface.getElementsByType('shape').forEach(node => { + const shape = node as ShapeElementModel; + const { shapeType, radius } = shape; + const shapeName = getShapeName(shapeType, radius); + const props = editorSetting.get(`shape:${shapeName}`); + edgelessService.updateElement(shape.id, props); + }); + }, + [editorSetting] + ); + + const fillColor = useMemo(() => { + const color = settings[`shape:${currentShape}`].fillColor; + return getColorFromMap(color, ShapeFillColorMap); + }, [currentShape, getColorFromMap, settings]); + + const borderColor = useMemo(() => { + const color = settings[`shape:${currentShape}`].strokeColor; + return getColorFromMap(color, LineColorMap); + }, [currentShape, getColorFromMap, settings]); + + const textColor = useMemo(() => { + const color = settings[`shape:${currentShape}`].color; + return getColorFromMap(color, LineColorMap); + }, [currentShape, getColorFromMap, settings]); + + const height = currentDoc === 'flow' ? 456 : 180; return ( <> + docName={currentDoc} + keyName={`shape:${currentShape}`} + height={height} + getElements={getElements} + firstUpdate={firstUpdate} + > + + { ]()} desc={''} > - Yellow} - trigger={ - - Yellow - - } - /> + {fillColor ? ( + } + > + {fillColor.key} + + } + /> + ) : null} { ]()} desc={''} > - Yellow} - trigger={ - - Yellow - - } - /> + {borderColor ? ( + } + > + {borderColor.key} + + } + /> + ) : null} { desc={''} > { ]()} desc={''} > - Yellow} - trigger={ - - Yellow - - } - /> + {textColor ? ( + } + > + {textColor.key} + + } + /> + ) : null} - {' '} Inter} + items={fontFamilyItems} trigger={ - - Inter + + {FontFamilyMap[settings[`shape:${currentShape}`].fontFamily]} } /> @@ -254,25 +524,40 @@ export const ShapeSettings = () => { desc={''} > 15px} + items={fontSizeItems} trigger={ - - 15px + + {settings[`shape:${currentShape}`].fontSize + 'px'} } /> Regular} + items={fontStyleItems} trigger={ - - Regular + + {String(settings[`shape:${currentShape}`].fontStyle)} + + } + /> + + + + {FontWeightMap[settings[`shape:${currentShape}`].fontWeight]} } /> diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/snapshot.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/snapshot.tsx index 0b6034299e..0d8407190d 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/snapshot.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/snapshot.tsx @@ -2,11 +2,11 @@ import type { EditorSettingSchema } from '@affine/core/modules/editor-settting'; import { EditorSettingService } from '@affine/core/modules/editor-settting'; import type { EditorHost } from '@blocksuite/block-std'; import { BlockStdScope } from '@blocksuite/block-std'; -import type { SurfaceBlockModel } from '@blocksuite/block-std/gfx'; -import type { EdgelessRootService, FrameBlockModel } from '@blocksuite/blocks'; +import type { GfxPrimitiveElementModel } from '@blocksuite/block-std/gfx'; +import type { EdgelessRootService } from '@blocksuite/blocks'; import { SpecProvider } from '@blocksuite/blocks'; import { Bound } from '@blocksuite/global/utils'; -import type { Doc } from '@blocksuite/store'; +import type { Block, Doc } from '@blocksuite/store'; import { useFramework } from '@toeverything/infra'; import { isEqual } from 'lodash-es'; import { useCallback, useEffect, useRef } from 'react'; @@ -14,52 +14,49 @@ import { map, pairwise } from 'rxjs'; import { snapshotContainer, snapshotTitle } from '../style.css'; import { type DocName, getDocByName } from './docs'; +import { getFrameBlock } from './utils'; interface Props { title: string; docName: DocName; - flavour: BlockSuite.EdgelessModelKeys; keyName: keyof EditorSettingSchema; height?: number; -} - -export function getSurfaceBlock(doc: Doc) { - const blocks = doc.getBlocksByFlavour('affine:surface'); - return blocks.length !== 0 ? (blocks[0].model as SurfaceBlockModel) : null; -} - -function getFrameBlock(doc: Doc) { - const blocks = doc.getBlocksByFlavour('affine:frame'); - return blocks.length !== 0 ? (blocks[0].model as FrameBlockModel) : null; + getElements: (doc: Doc) => Array; + firstUpdate?: (doc: Doc, editorHost: EditorHost) => void; + children?: React.ReactElement; } const boundMap = new Map(); export const EdgelessSnapshot = (props: Props) => { - const { title, docName, flavour, keyName, height = 180 } = props; + const { + title, + docName, + keyName, + height = 180, + getElements, + firstUpdate, + children, + } = props; const wrapperRef = useRef(null); const docRef = useRef(null); const editorHostRef = useRef(null); const framework = useFramework(); const { editorSetting } = framework.get(EditorSettingService); - const updateElement = useCallback( - (props: Record) => { - const editorHost = editorHostRef.current; - const doc = docRef.current; - if (!editorHost || !doc) return; - const edgelessService = editorHost.std.getService( - 'affine:page' - ) as EdgelessRootService; - const blocks = doc.getBlocksByFlavour(flavour); - const surface = getSurfaceBlock(doc); - const elements = surface?.getElementsByType(flavour) || []; - [...blocks, ...elements].forEach(ele => { - edgelessService.updateElement(ele.id, props); - }); - }, - [flavour] - ); + const updateElements = useCallback(() => { + const editorHost = editorHostRef.current; + const doc = docRef.current; + if (!editorHost || !doc) return; + const edgelessService = editorHost.std.getService( + 'affine:page' + ) as EdgelessRootService; + const elements = getElements(doc); + const props = editorSetting.get(keyName) as any; + elements.forEach(element => { + edgelessService.updateElement(element.id, props); + }); + }, [editorSetting, getElements, keyName]); const renderEditor = useCallback(async () => { if (!wrapperRef.current) return; @@ -72,8 +69,12 @@ export const EdgelessSnapshot = (props: Props) => { }).render(); docRef.current = doc; editorHostRef.current = editorHost; - const settings = editorSetting.get(keyName); - updateElement(settings as any); + + if (firstUpdate) { + firstUpdate(doc, editorHost); + } else { + updateElements(); + } // refresh viewport const edgelessService = editorHost.std.getService( @@ -82,20 +83,18 @@ export const EdgelessSnapshot = (props: Props) => { edgelessService.specSlots.viewConnected.once(({ component }) => { const edgelessBlock = component as any; edgelessBlock.editorViewportSelector = 'ref-viewport'; - edgelessBlock.service.viewport.sizeUpdated.once(() => { - const frame = getFrameBlock(doc); - if (frame) { - boundMap.set(docName, Bound.deserialize(frame.xywh)); - doc.deleteBlock(frame); - } - const bound = boundMap.get(docName); - bound && edgelessService.viewport.setViewportByBound(bound); - }); + const frame = getFrameBlock(doc); + if (frame) { + boundMap.set(docName, Bound.deserialize(frame.xywh)); + doc.deleteBlock(frame); + } + const bound = boundMap.get(docName); + bound && edgelessService.viewport.setViewportByBound(bound); }); // append to dom node wrapperRef.current.append(editorHost); - }, [docName, editorSetting, keyName, updateElement]); + }, [docName, firstUpdate, updateElements]); useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -117,11 +116,11 @@ export const EdgelessSnapshot = (props: Props) => { ) .subscribe(([prev, current]) => { if (!isEqual(prev, current)) { - updateElement(current); + updateElements(); } }); return () => sub.unsubscribe(); - }, [editorSetting.provider, flavour, keyName, updateElement]); + }, [editorSetting.provider, keyName, updateElements]); return (
@@ -136,6 +135,7 @@ export const EdgelessSnapshot = (props: Props) => { height, }} >
+ {children} ); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx index b8eaa22b5d..9a8a1a16db 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/edgeless/text.tsx @@ -17,6 +17,7 @@ import { LineColorMap, TextAlign, } from '@blocksuite/blocks'; +import type { Doc } from '@blocksuite/store'; import { useFramework, useLiveData } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; @@ -139,13 +140,18 @@ export const TextSettings = () => { const { color } = settings['affine:edgeless-text']; return getColorFromMap(color, LineColorMap); }, [getColorFromMap, settings]); + + const getElements = useCallback((doc: Doc) => { + return doc.getBlocksByFlavour('affine:edgeless-text') || []; + }, []); + return ( <>