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
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2022-11-28 15:04:53 +03:00
1 parent 1b899adc48
commit b4bf640dcc
25 files changed
+99 -156

No files matched your search

+1 -1
View File
@@ -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.
@@ -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
@@ -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]
@@ -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);
}
}
@@ -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<Instance["component"]>({
elementToData(element) {
@@ -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[];
}
@@ -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";
});
};
@@ -53,13 +53,10 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof Body> = {
type: "container",
label: "Body",
Icon: BodyIcon,
Component: Body,
canAcceptChildren: true,
isContentEditable: false,
label: "Body",
isInlineOnly: false,
isListed: false,
defaultStyle,
};
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Bold } from "./bold";
const meta: WsComponentMeta<typeof Bold> = {
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;
+2 -5
View File
@@ -10,14 +10,11 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof Box> = {
type: "container",
label: "Box",
Icon: SquareIcon,
Component: Box,
defaultStyle,
canAcceptChildren: true,
isContentEditable: false,
isInlineOnly: false,
isListed: true,
label: "Box",
};
export default meta;
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Button } from "./button";
const meta: WsComponentMeta<typeof Button> = {
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"],
};
@@ -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<ComponentType> = {
Component: ComponentType;
Icon: React.FunctionComponent<IconProps>;
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<IconProps>;
defaultStyle?: Style;
children?: Array<string>;
};
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<WsComponentMeta<unknown>>;
@@ -15,14 +15,11 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof Form> = {
type: "container",
label: "Form",
Icon: FormIcon,
Component: Form,
defaultStyle,
canAcceptChildren: true,
isContentEditable: false,
isInlineOnly: false,
isListed: true,
label: "Form",
};
export default meta;
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Heading } from "./heading";
const meta: WsComponentMeta<typeof Heading> = {
type: "rich-text",
label: "Heading",
Icon: HeadingIcon,
Component: Heading,
canAcceptChildren: false,
isContentEditable: true,
isInlineOnly: false,
isListed: true,
label: "Heading",
children: ["Heading you can edit"],
};
@@ -18,14 +18,11 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof Image> = {
type: "embed",
label: "Image",
Icon: ImageIcon,
Component: Image,
canAcceptChildren: false,
defaultStyle,
isContentEditable: false,
isInlineOnly: false,
isListed: true,
label: "Image",
};
export default meta;
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Input } from "./input";
const meta: WsComponentMeta<typeof Input> = {
type: "control",
label: "Input",
Icon: InputIcon,
Component: Input,
canAcceptChildren: false,
isContentEditable: false,
isInlineOnly: false,
isListed: true,
label: "Input",
};
export default meta;
@@ -10,14 +10,11 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof Italic> = {
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;
@@ -15,14 +15,11 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof Link> = {
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"],
};
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Paragraph } from "./paragraph";
const meta: WsComponentMeta<typeof Paragraph> = {
type: "rich-text",
label: "Paragraph",
Icon: TextAlignLeftIcon,
Component: Paragraph,
canAcceptChildren: false,
isContentEditable: true,
isInlineOnly: false,
isListed: true,
label: "Paragraph",
children: ["Pragraph you can edit"],
};
@@ -3,13 +3,10 @@ import { RichTextLink } from "./rich-text-link";
import type { WsComponentMeta } from "./component-type";
const meta: WsComponentMeta<typeof RichTextLink> = {
type: "rich-text-child",
label: "Link",
Icon: Link2Icon,
Component: RichTextLink,
canAcceptChildren: false,
isContentEditable: false,
isInlineOnly: true,
isListed: false,
label: "Link",
};
export default meta;
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Span } from "./span";
const meta: WsComponentMeta<typeof Span> = {
type: "rich-text",
label: "Styled Text",
Icon: BrushIcon,
Component: Span,
canAcceptChildren: false,
isContentEditable: false,
label: "Styled Text",
isInlineOnly: true,
isListed: false,
};
export default meta;
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Subscript } from "./subscript";
const meta: WsComponentMeta<typeof Subscript> = {
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;
@@ -3,13 +3,10 @@ import type { WsComponentMeta } from "./component-type";
import { Superscript } from "./superscript";
const meta: WsComponentMeta<typeof Superscript> = {
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;
@@ -11,14 +11,11 @@ const defaultStyle = {
} as const;
const meta: WsComponentMeta<typeof TextBlock> = {
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"],
};
+5 -1
View File
@@ -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"