Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/builder/app/shared/content-model.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ws.element ws:tag="body" ws:id="bodyId">
<ws.element ws:tag="span" ws:id="spanId">
<$.Link ws:id="linkId">
<$.Bold ws:id="boldId">link</$.Bold>
</$.Link>
</ws.element>
</ws.element>
),
metas: defaultMetas,
instanceSelector: ["linkId", "spanId", "bodyId"],
})
).toEqual(["linkId", "spanId", "bodyId"]);
});

test("treat body as rich text when has text inside", () => {
expect(
findClosestRichText({
Expand Down Expand Up @@ -1049,4 +1067,22 @@ describe("closest non textual container", () => {
})
).toEqual(["divId", "bodyId"]);
});

test("treat Link component as rich text container", () => {
expect(
findClosestNonTextualContainer({
...renderData(
<ws.element ws:tag="body" ws:id="bodyId">
<ws.element ws:tag="span" ws:id="spanId">
<$.Link ws:id="linkId">
<$.Bold ws:id="boldId">link</$.Bold>
</$.Link>
</ws.element>
</ws.element>
),
metas: defaultMetas,
instanceSelector: ["boldId", "linkId", "spanId", "bodyId"],
})
).toEqual(["spanId", "bodyId"]);
});
});
52 changes: 51 additions & 1 deletion apps/builder/app/shared/content-model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { elementsByTag } from "@webstudio-is/html-data";
import {
elementComponent,
parseComponentName,
type ContentModel,
type Instance,
Expand Down Expand Up @@ -416,6 +417,15 @@ export const richTextContentTags = new Set<undefined | string>([
"span",
]);

export const richTextContentComponents = new Set<undefined | string>([
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
Expand Down Expand Up @@ -470,6 +480,34 @@ const findContentTags = ({
return tags;
};

const findContentComponents = ({
instances,
instance,
_components: components = new Set(),
}: {
instances: Instances;
instance: Instance;
_components?: Set<undefined | string>;
}) => {
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,
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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) {
Expand Down