Skip to content

Commit 90aa424

Browse files
authored
experimental: support html elements in content block (#5216)
Ref #3632 Now content block is relying on instances tags instead of component names. Tested all cases. Should work smoothly. Also dialog title now have placeholders inferred from tag as well <img width="1025" alt="image" src="https://github.com/user-attachments/assets/4c7817fb-cf68-4d61-a672-213b1e8d1cc7" />
1 parent 646c5b3 commit 90aa424

File tree

10 files changed

+60
-63
lines changed

10 files changed

+60
-63
lines changed

apps/builder/app/canvas/features/text-editor/text-editor.tsx

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
insertListItemAt,
9898
insertTemplateAt,
9999
} from "~/builder/features/workspace/canvas-tools/outline/block-utils";
100+
import { richTextPlaceholders } from "~/shared/content-model";
100101

101102
const BindInstanceToNodePlugin = ({
102103
refs,
@@ -1024,6 +1025,18 @@ const RichTextContentPlugin = (props: RichTextContentPluginProps) => {
10241025
return <RichTextContentPluginInternal {...props} templates={templates} />;
10251026
};
10261027

1028+
const getTag = (instanceId: Instance["id"]) => {
1029+
const instances = $instances.get();
1030+
const metas = $registeredComponentMetas.get();
1031+
const instance = instances.get(instanceId);
1032+
if (instance === undefined) {
1033+
return;
1034+
}
1035+
const meta = metas.get(instance.component);
1036+
const tags = Object.keys(meta?.presetStyle ?? {});
1037+
return instance.tag ?? tags[0];
1038+
};
1039+
10271040
const RichTextContentPluginInternal = ({
10281041
rootInstanceSelector,
10291042
onOpen,
@@ -1136,20 +1149,14 @@ const RichTextContentPluginInternal = ({
11361149

11371150
if (event.key === "Backspace" || event.key === "Delete") {
11381151
if ($getRoot().getTextContentSize() === 0) {
1139-
const currentInstance = $instances
1140-
.get()
1141-
.get(rootInstanceSelector[0]);
1142-
1143-
if (currentInstance?.component === "ListItem") {
1152+
const tag = getTag(rootInstanceSelector[0]);
1153+
if (tag === "li") {
11441154
onNext(editor.getEditorState(), { reason: "left" });
1145-
11461155
const parentInstanceSelector = rootInstanceSelector.slice(1);
11471156
const parentInstance = $instances
11481157
.get()
11491158
.get(parentInstanceSelector[0]);
1150-
11511159
const isLastChild = parentInstance?.children.length === 1;
1152-
11531160
updateWebstudioData((data) => {
11541161
deleteInstanceMutable(
11551162
data,
@@ -1159,7 +1166,6 @@ const RichTextContentPluginInternal = ({
11591166
)
11601167
);
11611168
});
1162-
11631169
event.preventDefault();
11641170
return true;
11651171
}
@@ -1185,28 +1191,22 @@ const RichTextContentPluginInternal = ({
11851191

11861192
if (menuState === "closed") {
11871193
if (event.key === "Enter" && !event.shiftKey) {
1188-
// Custom logic if we are editing ListItem
1189-
const currentInstance = $instances
1190-
.get()
1191-
.get(rootInstanceSelector[0]);
1192-
1193-
if (
1194-
currentInstance?.component === "ListItem" &&
1195-
$getRoot().getTextContentSize() > 0
1196-
) {
1197-
// Instead of creating block component we need to add a new ListItem
1194+
// Custom logic if we are editing list item
1195+
const tag = getTag(rootInstanceSelector[0]);
1196+
if (tag === "li" && $getRoot().getTextContentSize() > 0) {
1197+
// Instead of creating block component we need to add a new list item
11981198
insertListItemAt(rootInstanceSelector);
11991199
event.preventDefault();
12001200
return true;
12011201
}
12021202

12031203
// Check if it pressed on the last line, last symbol
12041204

1205-
const allowedComponents = ["Paragraph", "Text", "Heading"];
1205+
const allowedTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6"];
12061206

1207-
for (const component of allowedComponents) {
1207+
for (const tag of allowedTags) {
12081208
const templateSelector = templates.find(
1209-
([instance]) => instance.component === component
1209+
([instance]) => getTag(instance.id) === tag
12101210
)?.[1];
12111211

12121212
if (templateSelector === undefined) {
@@ -1249,10 +1249,7 @@ const RichTextContentPluginInternal = ({
12491249

12501250
insertTemplateAt(templateSelector, rootInstanceSelector, false);
12511251

1252-
if (
1253-
currentInstance?.component === "ListItem" &&
1254-
$getRoot().getTextContentSize() === 0
1255-
) {
1252+
if (tag === "li" && $getRoot().getTextContentSize() === 0) {
12561253
const parentInstanceSelector = rootInstanceSelector.slice(1);
12571254
const parentInstance = $instances
12581255
.get()
@@ -1679,11 +1676,13 @@ export const TextEditor = ({
16791676
continue;
16801677
}
16811678
const meta = metas.get(instance.component);
1679+
const tags = Object.keys(meta?.presetStyle ?? {});
1680+
const tag = instance.tag ?? tags[0];
16821681

16831682
// opinionated: Non-collapsed elements without children can act as spacers (they have size for some reason).
16841683
if (
16851684
// Components with pseudo-elements (e.g., ::marker) that prevent content from collapsing
1686-
meta?.placeholder === undefined &&
1685+
richTextPlaceholders.has(tag) === false &&
16871686
instance?.children.length === 0
16881687
) {
16891688
const elt = getElementByInstanceSelector(nextSelector);

apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
editablePlaceholderAttribute,
7474
editingPlaceholderVariable,
7575
} from "~/canvas/shared/styles";
76+
import { richTextPlaceholders } from "~/shared/content-model";
7677

7778
const ContentEditable = ({
7879
placeholder,
@@ -403,23 +404,19 @@ const getEditableComponentPlaceholder = (
403404
mode: "editing" | "editable"
404405
) => {
405406
const meta = metas.get(instance.component);
406-
if (meta?.placeholder === undefined) {
407+
const tags = Object.keys(meta?.presetStyle ?? {});
408+
const tag = instance.tag ?? tags[0];
409+
const placeholder = richTextPlaceholders.get(tag);
410+
if (placeholder === undefined) {
407411
return;
408412
}
409-
410413
const isContentBlockChild =
411414
undefined !== findBlockSelector(instanceSelector, instances);
412-
413-
const isParagraph = instance.component === "Paragraph";
414-
415-
if (isParagraph && isContentBlockChild) {
416-
return mode === "editing"
417-
? "Write something or press '/' for commands..."
418-
: // The paragraph contains only an "editing" placeholder within the content block.
419-
undefined;
415+
// The paragraph contains only an "editing" placeholder within the content block.
416+
if (tag === "p" && isContentBlockChild && mode === "editing") {
417+
return "Write something or press '/' for commands...";
420418
}
421-
422-
return meta.placeholder;
419+
return placeholder;
423420
};
424421

425422
export const WebstudioComponentCanvas = forwardRef<

apps/builder/app/shared/content-model.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,23 @@ export const richTextContentTags = new Set<undefined | string>([
409409
"span",
410410
]);
411411

412-
const richTextContainerTags = new Set<undefined | string>(["a", "span"]);
412+
/**
413+
* textual placeholder is used when no content specified while in builder
414+
* also signals to not insert components inside unless dropped explicitly
415+
*/
416+
export const richTextPlaceholders: Map<undefined | string, string> = new Map([
417+
["h1", "Heading 1"],
418+
["h2", "Heading 2"],
419+
["h3", "Heading 3"],
420+
["h4", "Heading 4"],
421+
["h5", "Heading 5"],
422+
["h6", "Heading 6"],
423+
["p", "Paragraph"],
424+
["blockquote", "Blockquote"],
425+
["li", "List item"],
426+
["a", "Link"],
427+
["span", "Span"],
428+
]);
413429

414430
const findContentTags = ({
415431
instances,
@@ -492,7 +508,7 @@ export const isRichTextTree = ({
492508
setIsSubsetOf(contentTags, richTextContentTags) &&
493509
// rich text cannot contain only span and only link
494510
// those links and spans are containers in such cases
495-
!setIsSubsetOf(contentTags, richTextContainerTags)
511+
!setIsSubsetOf(contentTags, new Set(richTextPlaceholders.keys()))
496512
);
497513
};
498514

@@ -633,7 +649,7 @@ export const findClosestNonTextualContainer = ({
633649
}
634650
if (
635651
instance.children.length === 0 &&
636-
!meta?.placeholder &&
652+
!richTextPlaceholders.has(tag) &&
637653
!richTextContentTags.has(tag)
638654
) {
639655
return instanceSelector.slice(index);

packages/sdk-components-react/src/blockquote.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const presetStyle = {
5959
} satisfies PresetStyle<typeof defaultTag>;
6060

6161
export const meta: WsComponentMeta = {
62-
placeholder: "Blockquote",
6362
icon: BlockquoteIcon,
6463
states: defaultStates,
6564
presetStyle,

packages/sdk-components-react/src/heading.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { h1, h2, h3, h4, h5, h6 } from "@webstudio-is/sdk/normalize.css";
44
import { props } from "./__generated__/heading.props";
55

66
export const meta: WsComponentMeta = {
7-
placeholder: "Heading",
87
icon: HeadingIcon,
98
states: defaultStates,
109
presetStyle: {

packages/sdk-components-react/src/link.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const presetStyle = {
1919
} satisfies PresetStyle<typeof defaultTag>;
2020

2121
export const meta: WsComponentMeta = {
22-
placeholder: "Link",
2322
icon: LinkIcon,
2423
presetStyle,
2524
states: [

packages/sdk-components-react/src/list-item.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { li } from "@webstudio-is/sdk/normalize.css";
44
import { props } from "./__generated__/list-item.props";
55

66
export const meta: WsComponentMeta = {
7-
placeholder: "List item",
87
icon: ListItemIcon,
98
states: defaultStates,
109
presetStyle: { li },
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import { TextAlignLeftIcon } from "@webstudio-is/icons/svg";
2-
import {
3-
defaultStates,
4-
type PresetStyle,
5-
type WsComponentMeta,
6-
} from "@webstudio-is/sdk";
2+
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
73
import { p } from "@webstudio-is/sdk/normalize.css";
8-
import type { defaultTag } from "./paragraph";
94
import { props } from "./__generated__/paragraph.props";
105

11-
const presetStyle = {
12-
p,
13-
} satisfies PresetStyle<typeof defaultTag>;
14-
156
export const meta: WsComponentMeta = {
16-
placeholder: "Paragraph",
177
icon: TextAlignLeftIcon,
188
states: defaultStates,
19-
presetStyle,
9+
presetStyle: { p },
2010
initialProps: ["id", "class"],
2111
props,
2212
};

packages/sdk-components-react/src/time.ws.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export const meta: WsComponentMeta = {
88
description:
99
"Converts machine-readable date and time to a human-readable format.",
1010
icon: CalendarIcon,
11+
contentModel: {
12+
category: "instance",
13+
children: [],
14+
},
1115
states: defaultStates,
1216
presetStyle: {
1317
time,

packages/sdk/src/schema/component-meta.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ export type ContentModel = z.infer<typeof ContentModel>;
8282

8383
export const WsComponentMeta = z.object({
8484
category: z.enum(componentCategories).optional(),
85-
/**
86-
* a property used as textual placeholder when no content specified while in builder
87-
* also signals to not insert components inside unless dropped explicitly
88-
*/
89-
placeholder: z.string().optional(),
9085
contentModel: ContentModel.optional(),
9186
// when this field is specified component receives
9287
// prop with index of same components withiin specified ancestor

0 commit comments

Comments
 (0)