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 + + + + ), + 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 + + + + ), + 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) {