From c8b8f37fdce0f894b3bae2884b052f9a9dcbda82 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Wed, 23 Jul 2025 17:39:26 +0200 Subject: [PATCH 1/2] fix: consider Text and Link components as rich text This prevents editing Text and Link components parent and instead enforces editing these components as rich text containers. --- apps/builder/app/canvas/instance-selection.ts | 5 ++ .../builder/app/shared/content-model.test.tsx | 36 +++++++++++++ apps/builder/app/shared/content-model.ts | 52 ++++++++++++++++++- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/apps/builder/app/canvas/instance-selection.ts b/apps/builder/app/canvas/instance-selection.ts index c8145f9247a1..737fc1a55b01 100644 --- a/apps/builder/app/canvas/instance-selection.ts +++ b/apps/builder/app/canvas/instance-selection.ts @@ -68,12 +68,17 @@ const handleEdit = (event: MouseEvent) => { const instances = $instances.get(); + console.log(instanceSelector, instances.get(instanceSelector[0])); let editableInstanceSelector = findClosestRichText({ instanceSelector, instances, props: $props.get(), metas: $registeredComponentMetas.get(), }); + console.log( + editableInstanceSelector, + instances.get(editableInstanceSelector[0]) + ); // Do not allow edit bindable text instances with expression children in Content Mode if (editableInstanceSelector !== undefined && $isContentMode.get()) { 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) { From a8e508f105f549a0fc5b7c35f43bfe3197e72fec Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Wed, 23 Jul 2025 17:41:11 +0200 Subject: [PATCH 2/2] Remove logs --- apps/builder/app/canvas/instance-selection.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/builder/app/canvas/instance-selection.ts b/apps/builder/app/canvas/instance-selection.ts index 737fc1a55b01..c8145f9247a1 100644 --- a/apps/builder/app/canvas/instance-selection.ts +++ b/apps/builder/app/canvas/instance-selection.ts @@ -68,17 +68,12 @@ const handleEdit = (event: MouseEvent) => { const instances = $instances.get(); - console.log(instanceSelector, instances.get(instanceSelector[0])); let editableInstanceSelector = findClosestRichText({ instanceSelector, instances, props: $props.get(), metas: $registeredComponentMetas.get(), }); - console.log( - editableInstanceSelector, - instances.get(editableInstanceSelector[0]) - ); // Do not allow edit bindable text instances with expression children in Content Mode if (editableInstanceSelector !== undefined && $isContentMode.get()) {