mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
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:
1 parent
1b899adc48
commit
b4bf640dcc
25 files changed
+99
-156
No files matched your search
@@ -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;
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user