diff --git a/apps/builder/app/shared/content-model.test.tsx b/apps/builder/app/shared/content-model.test.tsx
index a0d4d09453a1..38d4b3380605 100644
--- a/apps/builder/app/shared/content-model.test.tsx
+++ b/apps/builder/app/shared/content-model.test.tsx
@@ -768,6 +768,24 @@ describe("rich text tree", () => {
).toEqual(["paragraphId", "bodyId"]);
});
+ test("treat Link component as container when look for closest rich text", () => {
+ expect(
+ findClosestRichText({
+ ...renderData(
+
+
+ <$.Link ws:id="linkId">
+ <$.Bold ws:id="boldId">link$.Bold>
+ $.Link>
+
+
+ ),
+ metas: defaultMetas,
+ instanceSelector: ["linkId", "spanId", "bodyId"],
+ })
+ ).toEqual(["linkId", "spanId", "bodyId"]);
+ });
+
test("treat body as rich text when has text inside", () => {
expect(
findClosestRichText({
@@ -1049,4 +1067,22 @@ describe("closest non textual container", () => {
})
).toEqual(["divId", "bodyId"]);
});
+
+ test("treat Link component as rich text container", () => {
+ expect(
+ findClosestNonTextualContainer({
+ ...renderData(
+
+
+ <$.Link ws:id="linkId">
+ <$.Bold ws:id="boldId">link$.Bold>
+ $.Link>
+
+
+ ),
+ metas: defaultMetas,
+ instanceSelector: ["boldId", "linkId", "spanId", "bodyId"],
+ })
+ ).toEqual(["spanId", "bodyId"]);
+ });
});
diff --git a/apps/builder/app/shared/content-model.ts b/apps/builder/app/shared/content-model.ts
index 2f0b40195ef6..fcd5705943c7 100644
--- a/apps/builder/app/shared/content-model.ts
+++ b/apps/builder/app/shared/content-model.ts
@@ -1,5 +1,6 @@
import { elementsByTag } from "@webstudio-is/html-data";
import {
+ elementComponent,
parseComponentName,
type ContentModel,
type Instance,
@@ -416,6 +417,15 @@ export const richTextContentTags = new Set([
"span",
]);
+export const richTextContentComponents = new Set([
+ elementComponent,
+ "Subscript",
+ "Bold",
+ "Italic",
+ "RichTextLink",
+ "Span",
+]);
+
/**
* textual placeholder is used when no content specified while in builder
* also signals to not insert components inside unless dropped explicitly
@@ -470,6 +480,34 @@ const findContentTags = ({
return tags;
};
+const findContentComponents = ({
+ instances,
+ instance,
+ _components: components = new Set(),
+}: {
+ instances: Instances;
+ instance: Instance;
+ _components?: Set;
+}) => {
+ for (const child of instance.children) {
+ if (child.type === "id") {
+ const childInstance = instances.get(child.value);
+ // consider collection item as well
+ if (childInstance === undefined) {
+ components.add(undefined);
+ continue;
+ }
+ components.add(childInstance.component);
+ findContentComponents({
+ instances,
+ instance: childInstance,
+ _components: components,
+ });
+ }
+ }
+ return components;
+};
+
export const isRichTextTree = ({
instanceId,
instances,
@@ -510,10 +548,15 @@ export const isRichTextTree = ({
metas,
instance,
});
+ const contentComponents = findContentComponents({
+ instances,
+ instance,
+ });
return (
isRichText &&
// rich text must contain only supported elements in editor
setIsSubsetOf(contentTags, richTextContentTags) &&
+ setIsSubsetOf(contentComponents, richTextContentComponents) &&
// rich text cannot contain only span and only link
// those links and spans are containers in such cases
!setIsSubsetOf(contentTags, new Set(richTextPlaceholders.keys()))
@@ -675,7 +718,14 @@ export const findClosestNonTextualContainer = ({
metas,
instance,
});
- if (setIsSubsetOf(contentTags, richTextContentTags)) {
+ const contentComponents = findContentComponents({
+ instances,
+ instance,
+ });
+ if (
+ setIsSubsetOf(contentTags, richTextContentTags) &&
+ setIsSubsetOf(contentComponents, richTextContentComponents)
+ ) {
hasText = true;
}
if (!hasText) {