diff --git a/apps/builder/app/canvas/features/text-editor/text-editor.tsx b/apps/builder/app/canvas/features/text-editor/text-editor.tsx
index ac8101ee32d4..7f98b7b2fbad 100644
--- a/apps/builder/app/canvas/features/text-editor/text-editor.tsx
+++ b/apps/builder/app/canvas/features/text-editor/text-editor.tsx
@@ -97,6 +97,7 @@ import {
insertListItemAt,
insertTemplateAt,
} from "~/builder/features/workspace/canvas-tools/outline/block-utils";
+import { richTextPlaceholders } from "~/shared/content-model";
const BindInstanceToNodePlugin = ({
refs,
@@ -1024,6 +1025,18 @@ const RichTextContentPlugin = (props: RichTextContentPluginProps) => {
return ;
};
+const getTag = (instanceId: Instance["id"]) => {
+ const instances = $instances.get();
+ const metas = $registeredComponentMetas.get();
+ const instance = instances.get(instanceId);
+ if (instance === undefined) {
+ return;
+ }
+ const meta = metas.get(instance.component);
+ const tags = Object.keys(meta?.presetStyle ?? {});
+ return instance.tag ?? tags[0];
+};
+
const RichTextContentPluginInternal = ({
rootInstanceSelector,
onOpen,
@@ -1136,20 +1149,14 @@ const RichTextContentPluginInternal = ({
if (event.key === "Backspace" || event.key === "Delete") {
if ($getRoot().getTextContentSize() === 0) {
- const currentInstance = $instances
- .get()
- .get(rootInstanceSelector[0]);
-
- if (currentInstance?.component === "ListItem") {
+ const tag = getTag(rootInstanceSelector[0]);
+ if (tag === "li") {
onNext(editor.getEditorState(), { reason: "left" });
-
const parentInstanceSelector = rootInstanceSelector.slice(1);
const parentInstance = $instances
.get()
.get(parentInstanceSelector[0]);
-
const isLastChild = parentInstance?.children.length === 1;
-
updateWebstudioData((data) => {
deleteInstanceMutable(
data,
@@ -1159,7 +1166,6 @@ const RichTextContentPluginInternal = ({
)
);
});
-
event.preventDefault();
return true;
}
@@ -1185,16 +1191,10 @@ const RichTextContentPluginInternal = ({
if (menuState === "closed") {
if (event.key === "Enter" && !event.shiftKey) {
- // Custom logic if we are editing ListItem
- const currentInstance = $instances
- .get()
- .get(rootInstanceSelector[0]);
-
- if (
- currentInstance?.component === "ListItem" &&
- $getRoot().getTextContentSize() > 0
- ) {
- // Instead of creating block component we need to add a new ListItem
+ // Custom logic if we are editing list item
+ const tag = getTag(rootInstanceSelector[0]);
+ if (tag === "li" && $getRoot().getTextContentSize() > 0) {
+ // Instead of creating block component we need to add a new list item
insertListItemAt(rootInstanceSelector);
event.preventDefault();
return true;
@@ -1202,11 +1202,11 @@ const RichTextContentPluginInternal = ({
// Check if it pressed on the last line, last symbol
- const allowedComponents = ["Paragraph", "Text", "Heading"];
+ const allowedTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6"];
- for (const component of allowedComponents) {
+ for (const tag of allowedTags) {
const templateSelector = templates.find(
- ([instance]) => instance.component === component
+ ([instance]) => getTag(instance.id) === tag
)?.[1];
if (templateSelector === undefined) {
@@ -1249,10 +1249,7 @@ const RichTextContentPluginInternal = ({
insertTemplateAt(templateSelector, rootInstanceSelector, false);
- if (
- currentInstance?.component === "ListItem" &&
- $getRoot().getTextContentSize() === 0
- ) {
+ if (tag === "li" && $getRoot().getTextContentSize() === 0) {
const parentInstanceSelector = rootInstanceSelector.slice(1);
const parentInstance = $instances
.get()
@@ -1679,11 +1676,13 @@ export const TextEditor = ({
continue;
}
const meta = metas.get(instance.component);
+ const tags = Object.keys(meta?.presetStyle ?? {});
+ const tag = instance.tag ?? tags[0];
// opinionated: Non-collapsed elements without children can act as spacers (they have size for some reason).
if (
// Components with pseudo-elements (e.g., ::marker) that prevent content from collapsing
- meta?.placeholder === undefined &&
+ richTextPlaceholders.has(tag) === false &&
instance?.children.length === 0
) {
const elt = getElementByInstanceSelector(nextSelector);
diff --git a/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx b/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx
index 060f711d402a..5405f9c3bb7a 100644
--- a/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx
+++ b/apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx
@@ -73,6 +73,7 @@ import {
editablePlaceholderAttribute,
editingPlaceholderVariable,
} from "~/canvas/shared/styles";
+import { richTextPlaceholders } from "~/shared/content-model";
const ContentEditable = ({
placeholder,
@@ -403,23 +404,19 @@ const getEditableComponentPlaceholder = (
mode: "editing" | "editable"
) => {
const meta = metas.get(instance.component);
- if (meta?.placeholder === undefined) {
+ const tags = Object.keys(meta?.presetStyle ?? {});
+ const tag = instance.tag ?? tags[0];
+ const placeholder = richTextPlaceholders.get(tag);
+ if (placeholder === undefined) {
return;
}
-
const isContentBlockChild =
undefined !== findBlockSelector(instanceSelector, instances);
-
- const isParagraph = instance.component === "Paragraph";
-
- if (isParagraph && isContentBlockChild) {
- return mode === "editing"
- ? "Write something or press '/' for commands..."
- : // The paragraph contains only an "editing" placeholder within the content block.
- undefined;
+ // The paragraph contains only an "editing" placeholder within the content block.
+ if (tag === "p" && isContentBlockChild && mode === "editing") {
+ return "Write something or press '/' for commands...";
}
-
- return meta.placeholder;
+ return placeholder;
};
export const WebstudioComponentCanvas = forwardRef<
diff --git a/apps/builder/app/shared/content-model.ts b/apps/builder/app/shared/content-model.ts
index cf6ed532db0e..ae544dfeb705 100644
--- a/apps/builder/app/shared/content-model.ts
+++ b/apps/builder/app/shared/content-model.ts
@@ -409,7 +409,23 @@ export const richTextContentTags = new Set([
"span",
]);
-const richTextContainerTags = new Set(["a", "span"]);
+/**
+ * textual placeholder is used when no content specified while in builder
+ * also signals to not insert components inside unless dropped explicitly
+ */
+export const richTextPlaceholders: Map = new Map([
+ ["h1", "Heading 1"],
+ ["h2", "Heading 2"],
+ ["h3", "Heading 3"],
+ ["h4", "Heading 4"],
+ ["h5", "Heading 5"],
+ ["h6", "Heading 6"],
+ ["p", "Paragraph"],
+ ["blockquote", "Blockquote"],
+ ["li", "List item"],
+ ["a", "Link"],
+ ["span", "Span"],
+]);
const findContentTags = ({
instances,
@@ -492,7 +508,7 @@ export const isRichTextTree = ({
setIsSubsetOf(contentTags, richTextContentTags) &&
// rich text cannot contain only span and only link
// those links and spans are containers in such cases
- !setIsSubsetOf(contentTags, richTextContainerTags)
+ !setIsSubsetOf(contentTags, new Set(richTextPlaceholders.keys()))
);
};
@@ -633,7 +649,7 @@ export const findClosestNonTextualContainer = ({
}
if (
instance.children.length === 0 &&
- !meta?.placeholder &&
+ !richTextPlaceholders.has(tag) &&
!richTextContentTags.has(tag)
) {
return instanceSelector.slice(index);
diff --git a/packages/sdk-components-react/src/blockquote.ws.ts b/packages/sdk-components-react/src/blockquote.ws.ts
index 17e16783543d..ef5f066b692d 100644
--- a/packages/sdk-components-react/src/blockquote.ws.ts
+++ b/packages/sdk-components-react/src/blockquote.ws.ts
@@ -59,7 +59,6 @@ const presetStyle = {
} satisfies PresetStyle;
export const meta: WsComponentMeta = {
- placeholder: "Blockquote",
icon: BlockquoteIcon,
states: defaultStates,
presetStyle,
diff --git a/packages/sdk-components-react/src/heading.ws.ts b/packages/sdk-components-react/src/heading.ws.ts
index 1e488429a028..100be098e508 100644
--- a/packages/sdk-components-react/src/heading.ws.ts
+++ b/packages/sdk-components-react/src/heading.ws.ts
@@ -4,7 +4,6 @@ import { h1, h2, h3, h4, h5, h6 } from "@webstudio-is/sdk/normalize.css";
import { props } from "./__generated__/heading.props";
export const meta: WsComponentMeta = {
- placeholder: "Heading",
icon: HeadingIcon,
states: defaultStates,
presetStyle: {
diff --git a/packages/sdk-components-react/src/link.ws.ts b/packages/sdk-components-react/src/link.ws.ts
index 77b439b3c09c..39ab444d6ab6 100644
--- a/packages/sdk-components-react/src/link.ws.ts
+++ b/packages/sdk-components-react/src/link.ws.ts
@@ -19,7 +19,6 @@ const presetStyle = {
} satisfies PresetStyle;
export const meta: WsComponentMeta = {
- placeholder: "Link",
icon: LinkIcon,
presetStyle,
states: [
diff --git a/packages/sdk-components-react/src/list-item.ws.ts b/packages/sdk-components-react/src/list-item.ws.ts
index afbe23338e89..817892dfc45e 100644
--- a/packages/sdk-components-react/src/list-item.ws.ts
+++ b/packages/sdk-components-react/src/list-item.ws.ts
@@ -4,7 +4,6 @@ import { li } from "@webstudio-is/sdk/normalize.css";
import { props } from "./__generated__/list-item.props";
export const meta: WsComponentMeta = {
- placeholder: "List item",
icon: ListItemIcon,
states: defaultStates,
presetStyle: { li },
diff --git a/packages/sdk-components-react/src/paragraph.ws.ts b/packages/sdk-components-react/src/paragraph.ws.ts
index 2b698824eda5..777c7250170d 100644
--- a/packages/sdk-components-react/src/paragraph.ws.ts
+++ b/packages/sdk-components-react/src/paragraph.ws.ts
@@ -1,22 +1,12 @@
import { TextAlignLeftIcon } from "@webstudio-is/icons/svg";
-import {
- defaultStates,
- type PresetStyle,
- type WsComponentMeta,
-} from "@webstudio-is/sdk";
+import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
import { p } from "@webstudio-is/sdk/normalize.css";
-import type { defaultTag } from "./paragraph";
import { props } from "./__generated__/paragraph.props";
-const presetStyle = {
- p,
-} satisfies PresetStyle;
-
export const meta: WsComponentMeta = {
- placeholder: "Paragraph",
icon: TextAlignLeftIcon,
states: defaultStates,
- presetStyle,
+ presetStyle: { p },
initialProps: ["id", "class"],
props,
};
diff --git a/packages/sdk-components-react/src/time.ws.ts b/packages/sdk-components-react/src/time.ws.ts
index 803ff7a8f0b4..05bd5370907e 100644
--- a/packages/sdk-components-react/src/time.ws.ts
+++ b/packages/sdk-components-react/src/time.ws.ts
@@ -8,6 +8,10 @@ export const meta: WsComponentMeta = {
description:
"Converts machine-readable date and time to a human-readable format.",
icon: CalendarIcon,
+ contentModel: {
+ category: "instance",
+ children: [],
+ },
states: defaultStates,
presetStyle: {
time,
diff --git a/packages/sdk/src/schema/component-meta.ts b/packages/sdk/src/schema/component-meta.ts
index 52cfa29c9ba5..2b2c53c902eb 100644
--- a/packages/sdk/src/schema/component-meta.ts
+++ b/packages/sdk/src/schema/component-meta.ts
@@ -82,11 +82,6 @@ export type ContentModel = z.infer;
export const WsComponentMeta = z.object({
category: z.enum(componentCategories).optional(),
- /**
- * a property used as textual placeholder when no content specified while in builder
- * also signals to not insert components inside unless dropped explicitly
- */
- placeholder: z.string().optional(),
contentModel: ContentModel.optional(),
// when this field is specified component receives
// prop with index of same components withiin specified ancestor