From b4bf640dcc7b1ff23a39abb571388aeaab2acab1 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Mon, 28 Nov 2022 15:04:53 +0300 Subject: [PATCH] Replace flags with component type (#532) Boolean flags may have a lot of combinations which can lead to unexpected bugs. Better approach is to categorize nodes with specific behaviours. Here I added 5 types - container - can accept other components with dnd - control - usually form controls like inputs, without children - embed - images, videos or other embeddable components, without children - rich-text - can be edited on double click - rich-text-child - formatted text fragment, not listed in components list --- apps/designer/app/canvas/shared/instance.ts | 2 +- .../app/canvas/shared/use-drag-drop.ts | 4 +- .../app/canvas/shared/use-shortcuts.ts | 13 ++-- .../shared/use-track-selected-element.ts | 11 ++- .../panels/components/components.tsx | 12 ++- .../app/designer/shared/tree/index.tsx | 15 ++-- .../find-closest-non-inline-parent.ts | 7 +- packages/react-sdk/src/components/body.ws.tsx | 7 +- packages/react-sdk/src/components/bold.ws.tsx | 7 +- packages/react-sdk/src/components/box.ws.ts | 7 +- .../react-sdk/src/components/button.ws.tsx | 7 +- .../src/components/component-type.ts | 73 +++++++------------ packages/react-sdk/src/components/form.ws.tsx | 7 +- .../react-sdk/src/components/heading.ws.tsx | 7 +- .../react-sdk/src/components/image.ws.tsx | 7 +- .../react-sdk/src/components/input.ws.tsx | 7 +- .../react-sdk/src/components/italic.ws.tsx | 7 +- packages/react-sdk/src/components/link.ws.tsx | 7 +- .../react-sdk/src/components/paragraph.ws.tsx | 7 +- .../src/components/rich-text-link.ws.tsx | 7 +- packages/react-sdk/src/components/span.ws.tsx | 7 +- .../react-sdk/src/components/subscript.ws.tsx | 7 +- .../src/components/superscript.ws.tsx | 7 +- .../src/components/text-block.ws.tsx | 7 +- tsconfig.json | 6 +- 25 files changed, 99 insertions(+), 156 deletions(-) diff --git a/apps/designer/app/canvas/shared/instance.ts b/apps/designer/app/canvas/shared/instance.ts index 4028e169ba..bc3250bc63 100644 --- a/apps/designer/app/canvas/shared/instance.ts +++ b/apps/designer/app/canvas/shared/instance.ts @@ -78,7 +78,7 @@ export const findInsertLocation = ( path.reverse(); const parentIndex = path.findIndex( - ({ item }) => getComponentMeta(item.component).canAcceptChildren + ({ item }) => getComponentMeta(item.component).type === "container" ); // Just in case selected Instance is not in the tree for some reason. diff --git a/apps/designer/app/canvas/shared/use-drag-drop.ts b/apps/designer/app/canvas/shared/use-drag-drop.ts index 9ea65efec1..2e79d037a5 100644 --- a/apps/designer/app/canvas/shared/use-drag-drop.ts +++ b/apps/designer/app/canvas/shared/use-drag-drop.ts @@ -116,7 +116,7 @@ export const useDragAndDrop = () => { } const data = path.find( - (instance) => getComponentMeta(instance.component).canAcceptChildren + (instance) => getComponentMeta(instance.component).type === "container" ); if (data === undefined) { @@ -179,7 +179,7 @@ export const useDragAndDrop = () => { } // When trying to drag an inline instance, drag its parent instead - if (getComponentMeta(instance.component).isInlineOnly) { + if (getComponentMeta(instance.component).type === "rich-text-child") { const nonInlineParent = utils.tree.findClosestNonInlineParent( rootInstance, instance.id diff --git a/apps/designer/app/canvas/shared/use-shortcuts.ts b/apps/designer/app/canvas/shared/use-shortcuts.ts index a2a5001f1d..5255e8f549 100644 --- a/apps/designer/app/canvas/shared/use-shortcuts.ts +++ b/apps/designer/app/canvas/shared/use-shortcuts.ts @@ -117,13 +117,12 @@ export const useShortcuts = () => { "enter", (event) => { if (selectedInstance === undefined) return; - const { isContentEditable } = getComponentMeta( - selectedInstance.component - ); - if (isContentEditable === false) return; - // Prevents inserting a newline when entering text-editing mode - event.preventDefault(); - setEditingInstanceId(selectedInstance.id); + const { type } = getComponentMeta(selectedInstance.component); + if (type === "rich-text") { + // Prevents inserting a newline when entering text-editing mode + event.preventDefault(); + setEditingInstanceId(selectedInstance.id); + } }, options, [selectedInstance, setEditingInstanceId] diff --git a/apps/designer/app/canvas/shared/use-track-selected-element.ts b/apps/designer/app/canvas/shared/use-track-selected-element.ts index 51cd52764e..e72070bb10 100644 --- a/apps/designer/app/canvas/shared/use-track-selected-element.ts +++ b/apps/designer/app/canvas/shared/use-track-selected-element.ts @@ -99,22 +99,25 @@ export const useTrackSelectedElement = () => { if (component === undefined || !components.includes(component)) { return; } - const { isInlineOnly, isContentEditable } = getComponentMeta(component); + const { type } = getComponentMeta(component); // When user double clicks on an inline instance, we need to select the parent instance and put it indo text editing mode. // Inline instances are not editable directly, only through parent instance. - if (isInlineOnly) { + if (type === "rich-text-child") { const parent = rootInstance && utils.tree.findClosestNonInlineParent(rootInstance, id); - if (parent && getComponentMeta(parent.component).isContentEditable) { + if ( + parent && + getComponentMeta(parent.component).type === "rich-text" + ) { selectInstance(parent.id); setEditingInstanceId(parent.id); } return; } - if (isContentEditable) { + if (type === "rich-text") { setEditingInstanceId(id); } } diff --git a/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx b/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx index de92671cf2..be986cb1f4 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx @@ -96,9 +96,15 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => { return { x: (x - canvasRect.x) / scale, y: (y - canvasRect.y) / scale }; }; - const listedComponentNames = getComponentNames().filter( - (name) => getComponentMeta(name).isListed - ); + const listedComponentNames = getComponentNames().filter((name) => { + const { type } = getComponentMeta(name); + return ( + type === "container" || + type === "control" || + type === "embed" || + type === "rich-text" + ); + }); const useDragHandlers = useDrag({ elementToData(element) { diff --git a/apps/designer/app/designer/shared/tree/index.tsx b/apps/designer/app/designer/shared/tree/index.tsx index 5628c991d4..5c068af186 100644 --- a/apps/designer/app/designer/shared/tree/index.tsx +++ b/apps/designer/app/designer/shared/tree/index.tsx @@ -15,20 +15,19 @@ const instanceRelatedProps = { getItemPath: utils.tree.getInstancePath, getItemPathWithPositions: utils.tree.getInstancePathWithPositions, canLeaveParent(item: Instance) { - return getComponentMeta(item.component).isInlineOnly !== true; + const { type } = getComponentMeta(item.component); + return type !== "rich-text-child"; }, canAcceptChild(item: Instance) { - return getComponentMeta(item.component).canAcceptChildren; + const { type } = getComponentMeta(item.component); + return type === "container"; }, getItemChildren(item: Instance) { - const component = getComponentMeta(item.component); + const { type } = getComponentMeta(item.component); // We want to avoid calling .filter() unnecessarily, because this is a hot path for performance. - // We rely on the fact that only content editable or inline components may have `string` children. - if ( - component.isContentEditable === false && - component.isInlineOnly === false - ) { + // We rely on the fact that only rich-text or rich-text-child components may have `string` children. + if (type === "container" || type === "control" || type === "embed") { return item.children as Instance[]; } diff --git a/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts b/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts index 0126842d62..b04a5acc00 100644 --- a/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts +++ b/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts @@ -7,7 +7,8 @@ export const findClosestNonInlineParent = ( ): Instance | undefined => { const path = getInstancePath(rootInstance, instanceId); path.reverse(); - return path.find( - (item) => getComponentMeta(item.component).isInlineOnly === false - ); + return path.find((item) => { + const { type } = getComponentMeta(item.component); + return type !== "rich-text-child"; + }); }; diff --git a/packages/react-sdk/src/components/body.ws.tsx b/packages/react-sdk/src/components/body.ws.tsx index 78df09c85b..91665ad0ea 100644 --- a/packages/react-sdk/src/components/body.ws.tsx +++ b/packages/react-sdk/src/components/body.ws.tsx @@ -53,13 +53,10 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "container", + label: "Body", Icon: BodyIcon, Component: Body, - canAcceptChildren: true, - isContentEditable: false, - label: "Body", - isInlineOnly: false, - isListed: false, defaultStyle, }; diff --git a/packages/react-sdk/src/components/bold.ws.tsx b/packages/react-sdk/src/components/bold.ws.tsx index 3d3c70f984..3e633f073e 100644 --- a/packages/react-sdk/src/components/bold.ws.tsx +++ b/packages/react-sdk/src/components/bold.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Bold } from "./bold"; const meta: WsComponentMeta = { + type: "rich-text-child", + label: "Bold Text", Icon: FontBoldIcon, Component: Bold, - canAcceptChildren: false, - isContentEditable: false, - label: "Bold Text", - isInlineOnly: true, - isListed: false, }; export default meta; diff --git a/packages/react-sdk/src/components/box.ws.ts b/packages/react-sdk/src/components/box.ws.ts index 2a03ed1e2c..08d4d9f081 100644 --- a/packages/react-sdk/src/components/box.ws.ts +++ b/packages/react-sdk/src/components/box.ws.ts @@ -10,14 +10,11 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "container", + label: "Box", Icon: SquareIcon, Component: Box, defaultStyle, - canAcceptChildren: true, - isContentEditable: false, - isInlineOnly: false, - isListed: true, - label: "Box", }; export default meta; diff --git a/packages/react-sdk/src/components/button.ws.tsx b/packages/react-sdk/src/components/button.ws.tsx index 4e0ad94c0d..c0611659ed 100644 --- a/packages/react-sdk/src/components/button.ws.tsx +++ b/packages/react-sdk/src/components/button.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Button } from "./button"; const meta: WsComponentMeta = { + type: "rich-text", + label: "Button", Icon: ButtonIcon, Component: Button, - canAcceptChildren: false, - isContentEditable: true, - isInlineOnly: false, - isListed: true, - label: "Button", children: ["Button text you can edit"], }; diff --git a/packages/react-sdk/src/components/component-type.ts b/packages/react-sdk/src/components/component-type.ts index 48bf860dd0..4e1933298e 100644 --- a/packages/react-sdk/src/components/component-type.ts +++ b/packages/react-sdk/src/components/component-type.ts @@ -1,58 +1,37 @@ import { z } from "zod"; -import React from "react"; +import type { FunctionComponent } from "react"; import { IconProps } from "@webstudio-is/icons"; import type { Style } from "@webstudio-is/css-data"; export type WsComponentMeta = { - Component: ComponentType; - Icon: React.FunctionComponent; - defaultStyle?: Style; - canAcceptChildren: boolean; - // Should children of the component be editable? - // Should only be possible for components like paragraph, heading etc. - isContentEditable: boolean; - // Components that render inside text editor only. - isInlineOnly: boolean; - // Should be listed in the components list. - isListed: boolean; + /** + * container - can accept other components with dnd + * control - usually form controls like inputs, without children + * embed - images, videos or other embeddable components, without children + * rich-text - editable text component + * rich-text-child - formatted text fragment, not listed in components list + */ + type: "container" | "control" | "embed" | "rich-text" | "rich-text-child"; label: string; + Component: ComponentType; + Icon: FunctionComponent; + defaultStyle?: Style; children?: Array; }; export const WsComponentMeta = z.lazy(() => - z - .object({ - Component: z.any(), - Icon: z.any(), - defaultStyle: z.optional(z.any()), - canAcceptChildren: z.boolean(), - isContentEditable: z.boolean(), - isInlineOnly: z.boolean(), - isListed: z.boolean(), - label: z.string(), - children: z.optional(z.array(z.string())), - }) - - // We need these restrictions because of the limitation of the current drag&drop implementation. - // Its position detection logic will be confused if drop target has `string` children. - .refine( - (val) => - val.isContentEditable === false || val.canAcceptChildren === false, - { - message: - "Content editable componetns are not allowed to accept children via drag&drop, because they may have `string` children", - path: ["canAcceptChildren"], - } - ) - .refine( - (val) => - val.canAcceptChildren === false || - val.children === undefined || - val.children.every((child) => typeof child !== "string"), - { - message: - "Components that can accept children via drag&drop are not allowed to have `string` children", - path: ["children"], - } - ) + z.object({ + type: z.enum([ + "container", + "control", + "embed", + "rich-text", + "rich-text-child", + ]), + label: z.string(), + Component: z.any(), + Icon: z.any(), + defaultStyle: z.optional(z.any()), + children: z.optional(z.array(z.string())), + }) ) as z.ZodType>; diff --git a/packages/react-sdk/src/components/form.ws.tsx b/packages/react-sdk/src/components/form.ws.tsx index 8dad7ef2a2..5a696a3efd 100644 --- a/packages/react-sdk/src/components/form.ws.tsx +++ b/packages/react-sdk/src/components/form.ws.tsx @@ -15,14 +15,11 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "container", + label: "Form", Icon: FormIcon, Component: Form, defaultStyle, - canAcceptChildren: true, - isContentEditable: false, - isInlineOnly: false, - isListed: true, - label: "Form", }; export default meta; diff --git a/packages/react-sdk/src/components/heading.ws.tsx b/packages/react-sdk/src/components/heading.ws.tsx index 1eb5286357..08f1c68b79 100644 --- a/packages/react-sdk/src/components/heading.ws.tsx +++ b/packages/react-sdk/src/components/heading.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Heading } from "./heading"; const meta: WsComponentMeta = { + type: "rich-text", + label: "Heading", Icon: HeadingIcon, Component: Heading, - canAcceptChildren: false, - isContentEditable: true, - isInlineOnly: false, - isListed: true, - label: "Heading", children: ["Heading you can edit"], }; diff --git a/packages/react-sdk/src/components/image.ws.tsx b/packages/react-sdk/src/components/image.ws.tsx index 27e8fae0bf..4dbff04b89 100644 --- a/packages/react-sdk/src/components/image.ws.tsx +++ b/packages/react-sdk/src/components/image.ws.tsx @@ -18,14 +18,11 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "embed", + label: "Image", Icon: ImageIcon, Component: Image, - canAcceptChildren: false, defaultStyle, - isContentEditable: false, - isInlineOnly: false, - isListed: true, - label: "Image", }; export default meta; diff --git a/packages/react-sdk/src/components/input.ws.tsx b/packages/react-sdk/src/components/input.ws.tsx index cad322ca8f..0220174ac5 100644 --- a/packages/react-sdk/src/components/input.ws.tsx +++ b/packages/react-sdk/src/components/input.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Input } from "./input"; const meta: WsComponentMeta = { + type: "control", + label: "Input", Icon: InputIcon, Component: Input, - canAcceptChildren: false, - isContentEditable: false, - isInlineOnly: false, - isListed: true, - label: "Input", }; export default meta; diff --git a/packages/react-sdk/src/components/italic.ws.tsx b/packages/react-sdk/src/components/italic.ws.tsx index d7aa041251..5870e33a96 100644 --- a/packages/react-sdk/src/components/italic.ws.tsx +++ b/packages/react-sdk/src/components/italic.ws.tsx @@ -10,14 +10,11 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "rich-text-child", + label: "Italic Text", Icon: FontItalicIcon, Component: Italic, defaultStyle, - canAcceptChildren: false, - isContentEditable: false, - isInlineOnly: true, - label: "Italic Text", - isListed: false, }; export default meta; diff --git a/packages/react-sdk/src/components/link.ws.tsx b/packages/react-sdk/src/components/link.ws.tsx index caed8e571c..59e5b7ae68 100644 --- a/packages/react-sdk/src/components/link.ws.tsx +++ b/packages/react-sdk/src/components/link.ws.tsx @@ -15,14 +15,11 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "rich-text", + label: "Link", Icon: Link2Icon, Component: Link, defaultStyle, - canAcceptChildren: false, - isContentEditable: true, - isInlineOnly: false, - isListed: true, - label: "Link", children: ["Link text you can edit"], }; diff --git a/packages/react-sdk/src/components/paragraph.ws.tsx b/packages/react-sdk/src/components/paragraph.ws.tsx index f950724a1b..22a2fd4e56 100644 --- a/packages/react-sdk/src/components/paragraph.ws.tsx +++ b/packages/react-sdk/src/components/paragraph.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Paragraph } from "./paragraph"; const meta: WsComponentMeta = { + type: "rich-text", + label: "Paragraph", Icon: TextAlignLeftIcon, Component: Paragraph, - canAcceptChildren: false, - isContentEditable: true, - isInlineOnly: false, - isListed: true, - label: "Paragraph", children: ["Pragraph you can edit"], }; diff --git a/packages/react-sdk/src/components/rich-text-link.ws.tsx b/packages/react-sdk/src/components/rich-text-link.ws.tsx index 5074f6e1da..24ebc5551f 100644 --- a/packages/react-sdk/src/components/rich-text-link.ws.tsx +++ b/packages/react-sdk/src/components/rich-text-link.ws.tsx @@ -3,13 +3,10 @@ import { RichTextLink } from "./rich-text-link"; import type { WsComponentMeta } from "./component-type"; const meta: WsComponentMeta = { + type: "rich-text-child", + label: "Link", Icon: Link2Icon, Component: RichTextLink, - canAcceptChildren: false, - isContentEditable: false, - isInlineOnly: true, - isListed: false, - label: "Link", }; export default meta; diff --git a/packages/react-sdk/src/components/span.ws.tsx b/packages/react-sdk/src/components/span.ws.tsx index 91a962ac0f..ceabfbd016 100644 --- a/packages/react-sdk/src/components/span.ws.tsx +++ b/packages/react-sdk/src/components/span.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Span } from "./span"; const meta: WsComponentMeta = { + type: "rich-text", + label: "Styled Text", Icon: BrushIcon, Component: Span, - canAcceptChildren: false, - isContentEditable: false, - label: "Styled Text", - isInlineOnly: true, - isListed: false, }; export default meta; diff --git a/packages/react-sdk/src/components/subscript.ws.tsx b/packages/react-sdk/src/components/subscript.ws.tsx index 21d3d1037e..9b9db9a409 100644 --- a/packages/react-sdk/src/components/subscript.ws.tsx +++ b/packages/react-sdk/src/components/subscript.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Subscript } from "./subscript"; const meta: WsComponentMeta = { + type: "rich-text-child", + label: "Subscript Text", Icon: SubscriptIcon, Component: Subscript, - canAcceptChildren: false, - isContentEditable: false, - label: "Subscript Text", - isInlineOnly: true, - isListed: false, }; export default meta; diff --git a/packages/react-sdk/src/components/superscript.ws.tsx b/packages/react-sdk/src/components/superscript.ws.tsx index bb0e6976a6..288d32886e 100644 --- a/packages/react-sdk/src/components/superscript.ws.tsx +++ b/packages/react-sdk/src/components/superscript.ws.tsx @@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type"; import { Superscript } from "./superscript"; const meta: WsComponentMeta = { + type: "rich-text-child", + label: "Superscript Text", Icon: SuperscriptIcon, Component: Superscript, - canAcceptChildren: false, - isContentEditable: false, - label: "Superscript Text", - isInlineOnly: true, - isListed: false, }; export default meta; diff --git a/packages/react-sdk/src/components/text-block.ws.tsx b/packages/react-sdk/src/components/text-block.ws.tsx index 1b620930c7..f28a0a9f13 100644 --- a/packages/react-sdk/src/components/text-block.ws.tsx +++ b/packages/react-sdk/src/components/text-block.ws.tsx @@ -11,14 +11,11 @@ const defaultStyle = { } as const; const meta: WsComponentMeta = { + type: "rich-text", + label: "Text Block", Icon: TextIcon, Component: TextBlock, defaultStyle, - canAcceptChildren: false, - isContentEditable: true, - isInlineOnly: false, - isListed: true, - label: "Text Block", children: ["Block of text you can edit"], }; diff --git a/tsconfig.json b/tsconfig.json index 927e224b54..c062a41be7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "references": [ { "path": "apps/designer" }, { "path": "packages/asset-uploader" }, + { "path": "packages/build" }, { "path": "packages/css-data" }, { "path": "packages/css-engine" }, { "path": "packages/css-vars" }, @@ -11,7 +12,10 @@ { "path": "packages/fonts" }, { "path": "packages/generate-arg-types" }, { "path": "packages/http-client" }, - { "path": "packages/icons" }, + + { + "path": "packages/icons" + }, { "path": "packages/image"