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
53 changes: 26 additions & 27 deletions apps/builder/app/canvas/features/text-editor/text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
insertListItemAt,
insertTemplateAt,
} from "~/builder/features/workspace/canvas-tools/outline/block-utils";
import { richTextPlaceholders } from "~/shared/content-model";

const BindInstanceToNodePlugin = ({
refs,
Expand Down Expand Up @@ -1024,6 +1025,18 @@ const RichTextContentPlugin = (props: RichTextContentPluginProps) => {
return <RichTextContentPluginInternal {...props} templates={templates} />;
};

const getTag = (instanceId: Instance["id"]) => {
const instances = $instances.get();
const metas = $registeredComponentMetas.get();
const instance = instances.get(instanceId);
if (instance === undefined) {
return;
}
const meta = metas.get(instance.component);
const tags = Object.keys(meta?.presetStyle ?? {});
return instance.tag ?? tags[0];
};

const RichTextContentPluginInternal = ({
rootInstanceSelector,
onOpen,
Expand Down Expand Up @@ -1136,20 +1149,14 @@ const RichTextContentPluginInternal = ({

if (event.key === "Backspace" || event.key === "Delete") {
if ($getRoot().getTextContentSize() === 0) {
const currentInstance = $instances
.get()
.get(rootInstanceSelector[0]);

if (currentInstance?.component === "ListItem") {
const tag = getTag(rootInstanceSelector[0]);
if (tag === "li") {
onNext(editor.getEditorState(), { reason: "left" });

const parentInstanceSelector = rootInstanceSelector.slice(1);
const parentInstance = $instances
.get()
.get(parentInstanceSelector[0]);

const isLastChild = parentInstance?.children.length === 1;

updateWebstudioData((data) => {
deleteInstanceMutable(
data,
Expand All @@ -1159,7 +1166,6 @@ const RichTextContentPluginInternal = ({
)
);
});

event.preventDefault();
return true;
}
Expand All @@ -1185,28 +1191,22 @@ const RichTextContentPluginInternal = ({

if (menuState === "closed") {
if (event.key === "Enter" && !event.shiftKey) {
// Custom logic if we are editing ListItem
const currentInstance = $instances
.get()
.get(rootInstanceSelector[0]);

if (
currentInstance?.component === "ListItem" &&
$getRoot().getTextContentSize() > 0
) {
// Instead of creating block component we need to add a new ListItem
// Custom logic if we are editing list item
const tag = getTag(rootInstanceSelector[0]);
if (tag === "li" && $getRoot().getTextContentSize() > 0) {
// Instead of creating block component we need to add a new list item
insertListItemAt(rootInstanceSelector);
event.preventDefault();
return true;
}

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

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

for (const component of allowedComponents) {
for (const tag of allowedTags) {
const templateSelector = templates.find(
([instance]) => instance.component === component
([instance]) => getTag(instance.id) === tag
)?.[1];

if (templateSelector === undefined) {
Expand Down Expand Up @@ -1249,10 +1249,7 @@ const RichTextContentPluginInternal = ({

insertTemplateAt(templateSelector, rootInstanceSelector, false);

if (
currentInstance?.component === "ListItem" &&
$getRoot().getTextContentSize() === 0
) {
if (tag === "li" && $getRoot().getTextContentSize() === 0) {
const parentInstanceSelector = rootInstanceSelector.slice(1);
const parentInstance = $instances
.get()
Expand Down Expand Up @@ -1679,11 +1676,13 @@ export const TextEditor = ({
continue;
}
const meta = metas.get(instance.component);
const tags = Object.keys(meta?.presetStyle ?? {});
const tag = instance.tag ?? tags[0];

// opinionated: Non-collapsed elements without children can act as spacers (they have size for some reason).
if (
// Components with pseudo-elements (e.g., ::marker) that prevent content from collapsing
meta?.placeholder === undefined &&
richTextPlaceholders.has(tag) === false &&
instance?.children.length === 0
) {
const elt = getElementByInstanceSelector(nextSelector);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
editablePlaceholderAttribute,
editingPlaceholderVariable,
} from "~/canvas/shared/styles";
import { richTextPlaceholders } from "~/shared/content-model";

const ContentEditable = ({
placeholder,
Expand Down Expand Up @@ -403,23 +404,19 @@ const getEditableComponentPlaceholder = (
mode: "editing" | "editable"
) => {
const meta = metas.get(instance.component);
if (meta?.placeholder === undefined) {
const tags = Object.keys(meta?.presetStyle ?? {});
const tag = instance.tag ?? tags[0];
const placeholder = richTextPlaceholders.get(tag);
if (placeholder === undefined) {
return;
}

const isContentBlockChild =
undefined !== findBlockSelector(instanceSelector, instances);

const isParagraph = instance.component === "Paragraph";

if (isParagraph && isContentBlockChild) {
return mode === "editing"
? "Write something or press '/' for commands..."
: // The paragraph contains only an "editing" placeholder within the content block.
undefined;
// The paragraph contains only an "editing" placeholder within the content block.
if (tag === "p" && isContentBlockChild && mode === "editing") {
return "Write something or press '/' for commands...";
}

return meta.placeholder;
return placeholder;
};

export const WebstudioComponentCanvas = forwardRef<
Expand Down
22 changes: 19 additions & 3 deletions apps/builder/app/shared/content-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,23 @@ export const richTextContentTags = new Set<undefined | string>([
"span",
]);

const richTextContainerTags = new Set<undefined | string>(["a", "span"]);
/**
* textual placeholder is used when no content specified while in builder
* also signals to not insert components inside unless dropped explicitly
*/
export const richTextPlaceholders: Map<undefined | string, string> = new Map([
["h1", "Heading 1"],
["h2", "Heading 2"],
["h3", "Heading 3"],
["h4", "Heading 4"],
["h5", "Heading 5"],
["h6", "Heading 6"],
["p", "Paragraph"],
["blockquote", "Blockquote"],
["li", "List item"],
["a", "Link"],
["span", "Span"],
]);

const findContentTags = ({
instances,
Expand Down Expand Up @@ -492,7 +508,7 @@ export const isRichTextTree = ({
setIsSubsetOf(contentTags, richTextContentTags) &&
// rich text cannot contain only span and only link
// those links and spans are containers in such cases
!setIsSubsetOf(contentTags, richTextContainerTags)
!setIsSubsetOf(contentTags, new Set(richTextPlaceholders.keys()))
);
};

Expand Down Expand Up @@ -633,7 +649,7 @@ export const findClosestNonTextualContainer = ({
}
if (
instance.children.length === 0 &&
!meta?.placeholder &&
!richTextPlaceholders.has(tag) &&
!richTextContentTags.has(tag)
) {
return instanceSelector.slice(index);
Expand Down
1 change: 0 additions & 1 deletion packages/sdk-components-react/src/blockquote.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ const presetStyle = {
} satisfies PresetStyle<typeof defaultTag>;

export const meta: WsComponentMeta = {
placeholder: "Blockquote",
icon: BlockquoteIcon,
states: defaultStates,
presetStyle,
Expand Down
1 change: 0 additions & 1 deletion packages/sdk-components-react/src/heading.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { h1, h2, h3, h4, h5, h6 } from "@webstudio-is/sdk/normalize.css";
import { props } from "./__generated__/heading.props";

export const meta: WsComponentMeta = {
placeholder: "Heading",
icon: HeadingIcon,
states: defaultStates,
presetStyle: {
Expand Down
1 change: 0 additions & 1 deletion packages/sdk-components-react/src/link.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const presetStyle = {
} satisfies PresetStyle<typeof defaultTag>;

export const meta: WsComponentMeta = {
placeholder: "Link",
icon: LinkIcon,
presetStyle,
states: [
Expand Down
1 change: 0 additions & 1 deletion packages/sdk-components-react/src/list-item.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { li } from "@webstudio-is/sdk/normalize.css";
import { props } from "./__generated__/list-item.props";

export const meta: WsComponentMeta = {
placeholder: "List item",
icon: ListItemIcon,
states: defaultStates,
presetStyle: { li },
Expand Down
14 changes: 2 additions & 12 deletions packages/sdk-components-react/src/paragraph.ws.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import { TextAlignLeftIcon } from "@webstudio-is/icons/svg";
import {
defaultStates,
type PresetStyle,
type WsComponentMeta,
} from "@webstudio-is/sdk";
import { defaultStates, type WsComponentMeta } from "@webstudio-is/sdk";
import { p } from "@webstudio-is/sdk/normalize.css";
import type { defaultTag } from "./paragraph";
import { props } from "./__generated__/paragraph.props";

const presetStyle = {
p,
} satisfies PresetStyle<typeof defaultTag>;

export const meta: WsComponentMeta = {
placeholder: "Paragraph",
icon: TextAlignLeftIcon,
states: defaultStates,
presetStyle,
presetStyle: { p },
initialProps: ["id", "class"],
props,
};
4 changes: 4 additions & 0 deletions packages/sdk-components-react/src/time.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export const meta: WsComponentMeta = {
description:
"Converts machine-readable date and time to a human-readable format.",
icon: CalendarIcon,
contentModel: {
category: "instance",
children: [],
},
states: defaultStates,
presetStyle: {
time,
Expand Down
5 changes: 0 additions & 5 deletions packages/sdk/src/schema/component-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,6 @@ export type ContentModel = z.infer<typeof ContentModel>;

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