Skip to content

Commit

Permalink
Improve text editor styling and enable auto focus (#504)
Browse files Browse the repository at this point in the history
Ref #107

- fixed bold italic styling
- populate default style for instances created from text editor
- added unmount support to css engine
- auto focus text editor when start editing
  • Loading branch information
TrySound committed Nov 23, 2022
1 parent 9b0d7f2 commit 7f44d78
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 23 deletions.
24 changes: 17 additions & 7 deletions apps/designer/app/canvas/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from "./shared/breakpoints";
import {
rootInstanceContainer,
useBreakpoints,
useRootInstance,
useSubscribeScrollState,
} from "~/shared/nano-states";
Expand All @@ -48,15 +49,24 @@ registerContainers();

const useElementsTree = () => {
const [rootInstance] = useRootInstance();
const [breakpoints] = useBreakpoints();

const onChangeChildren: OnChangeChildren = useCallback((change) => {
store.createTransaction([rootInstanceContainer], (rootInstance) => {
if (rootInstance === undefined) return;
const onChangeChildren: OnChangeChildren = useCallback(
(change) => {
store.createTransaction([rootInstanceContainer], (rootInstance) => {
if (rootInstance === undefined) return;

const { instanceId, updates } = change;
utils.tree.setInstanceChildrenMutable(instanceId, updates, rootInstance);
});
}, []);
const { instanceId, updates } = change;
utils.tree.setInstanceChildrenMutable(
instanceId,
updates,
rootInstance,
breakpoints[0].id
);
});
},
[breakpoints]
);

return useMemo(() => {
if (rootInstance === undefined) return;
Expand Down
16 changes: 8 additions & 8 deletions apps/designer/app/canvas/features/text-editor/interop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { $createLinkNode, $isLinkNode } from "@lexical/link";
import type { ChildrenUpdates, Instance } from "@webstudio-is/react-sdk";
import { $isSpanNode, $setNodeSpan } from "./toolbar-connector";

// Map<nodeKey, Instance>
export type Refs = Map<string, Instance>;
// Map<nodeKey, instanceId>
export type Refs = Map<string, string>;

const lexicalFormats = [
["bold", "Bold"],
Expand Down Expand Up @@ -44,7 +44,7 @@ const $writeUpdates = (
}
}
if ($isLinkNode(child)) {
const id = refs.get(child.getKey())?.id;
const id = refs.get(child.getKey());
const childrenUpdates: ChildrenUpdates = [];
$writeUpdates(child, childrenUpdates, refs);
updates.push({ id, component: "Link", children: childrenUpdates });
Expand All @@ -56,7 +56,7 @@ const $writeUpdates = (
const text = child.getTextContent();
let parentUpdates = updates;
if ($isSpanNode(child)) {
const id = refs.get(`${child.getKey()}:span`)?.id;
const id = refs.get(`${child.getKey()}:span`);
const update: ChildrenUpdates[number] = {
id,
component: "Span",
Expand All @@ -68,7 +68,7 @@ const $writeUpdates = (
// convert all lexical formats
for (const [format, component] of lexicalFormats) {
if (child.hasFormat(format)) {
const id = refs.get(`${child.getKey()}:${format}`)?.id;
const id = refs.get(`${child.getKey()}:${format}`);
const update: ChildrenUpdates[number] = {
id,
component,
Expand Down Expand Up @@ -117,7 +117,7 @@ const $writeLexical = (
// convert instances
if (child.component === "Link" && $isElementNode(parent)) {
const linkNode = $createLinkNode("");
refs.set(linkNode.getKey(), child);
refs.set(linkNode.getKey(), child.id);
parent.append(linkNode);
$writeLexical(linkNode, child.children, refs);
}
Expand All @@ -130,7 +130,7 @@ const $writeLexical = (
parent.append(textNode);
}
$setNodeSpan(textNode);
refs.set(`${textNode.getKey()}:span`, child);
refs.set(`${textNode.getKey()}:span`, child.id);
$writeLexical(textNode, child.children, refs);
}
// convert all lexical formats
Expand All @@ -144,7 +144,7 @@ const $writeLexical = (
parent.append(textNode);
}
textNode.toggleFormat(format);
refs.set(`${textNode.getKey()}:${format}`, child);
refs.set(`${textNode.getKey()}:${format}`, child.id);
$writeLexical(textNode, child.children, refs);
}
}
Expand Down
37 changes: 35 additions & 2 deletions apps/designer/app/canvas/features/text-editor/text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { nanoid } from "nanoid";
import { createCssEngine } from "@webstudio-is/css-engine";
import type { ChildrenUpdates, Instance } from "@webstudio-is/react-sdk";
import { idAttribute } from "@webstudio-is/react-sdk";
import { ToolbarConnectorPlugin } from "./toolbar-connector";
Expand All @@ -15,18 +17,28 @@ import { type Refs, $convertToLexical, $convertToUpdates } from "./interop";
const BindInstanceToNodePlugin = ({ refs }: { refs: Refs }) => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
for (const [nodeKey, instance] of refs) {
for (const [nodeKey, instanceId] of refs) {
// extract key from stored key:style format
const [key] = nodeKey.split(":");
const element = editor.getElementByKey(key);
if (element) {
element.setAttribute(idAttribute, instance.id);
element.setAttribute(idAttribute, instanceId);
}
}
}, [editor, refs]);
return null;
};

const AutofocusPlugin = () => {
const [editor] = useLexicalComposerContext();

useEffect(() => {
editor.focus();
}, [editor]);

return null;
};

const onError = (error: Error) => {
throw error;
};
Expand All @@ -42,12 +54,32 @@ export const TextEditor = ({
contentEditable,
onChange,
}: TextEditorProps) => {
const [italicClassName] = useState(() => nanoid());

// initially instance styles are applied to all nodes
// so not necessary to use layout effect
useEffect(() => {
const engine = createCssEngine();
engine.addPlaintextRule(`
.${italicClassName} { font-style: italic; }
`);
engine.render();
return () => {
engine.unmount();
};
}, [italicClassName]);

// store references separately because lexical nodes
// cannot store custom data
// Map<nodeKey, Instance>
const [refs] = useState<Refs>(() => new Map());
const initialConfig = {
namespace: "WsTextEditor",
theme: {
text: {
italic: italicClassName,
},
},
editorState: () => {
// text editor is unmounted when change properties in side panel
// so assume new nodes don't need to preserve instance id
Expand All @@ -60,6 +92,7 @@ export const TextEditor = ({

return (
<LexicalComposer initialConfig={initialConfig}>
<AutofocusPlugin />
<ToolbarConnectorPlugin />
<BindInstanceToNodePlugin refs={refs} />
<RichTextPlugin
Expand Down
3 changes: 3 additions & 0 deletions packages/css-engine/src/core/css-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export class CssEngine {
// This isn't going to do anything if the `cssText` hasn't changed.
this.#sheet.replaceSync(this.cssText);
}
unmount() {
this.#element.unmount();
}
get cssText() {
if (this.#isDirty === false) {
return this.#cssText;
Expand Down
19 changes: 13 additions & 6 deletions packages/project/src/shared/tree-utils/set-instance-children.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { type ChildrenUpdates } from "@webstudio-is/react-sdk";
import { type Instance } from "@webstudio-is/react-sdk";
import type { Breakpoint } from "@webstudio-is/css-data";
import type { ChildrenUpdates, Instance } from "@webstudio-is/react-sdk";
import { createInstance, createInstanceId } from "./create-instance";
import { findInstanceById } from "./find-instance";
import { populateInstance } from "./populate";

type InstanceChild = Instance | string;

const hydrateTree = (parent: Instance, updates: ChildrenUpdates) => {
const hydrateTree = (
parent: Instance,
updates: ChildrenUpdates,
breakpoint: Breakpoint["id"]
) => {
const children: InstanceChild[] = [];
for (const update of updates) {
// Set a string as a child
Expand All @@ -22,9 +27,10 @@ const hydrateTree = (parent: Instance, updates: ChildrenUpdates) => {
component: update.component,
children: [],
});
populateInstance(child, breakpoint);
}
children.push(child);
hydrateTree(child, update.children);
hydrateTree(child, update.children, breakpoint);
}
parent.children = children;
};
Expand All @@ -38,10 +44,11 @@ export const setInstanceChildrenMutable = (
// Not a consistent format now, maybe better:
// [{set: 'string'}, {set: instance}, {update: {id,text}}]
updates: ChildrenUpdates,
rootInstance: Instance
rootInstance: Instance,
breakpoint: Breakpoint["id"] = ""
) => {
const instance = findInstanceById(rootInstance, id);
if (instance === undefined) return false;
hydrateTree(instance, updates);
hydrateTree(instance, updates, breakpoint);
return true;
};
8 changes: 8 additions & 0 deletions packages/react-sdk/src/components/italic.ws.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import { FontItalicIcon } from "@webstudio-is/icons";
import type { WsComponentMeta } from "./component-type";
import { Italic } from "./italic";

const defaultStyle = {
fontStyle: {
type: "keyword",
value: "italic",
},
} as const;

const meta: WsComponentMeta<typeof Italic> = {
Icon: FontItalicIcon,
Component: Italic,
defaultStyle,
canAcceptChildren: false,
isContentEditable: false,
isInlineOnly: true,
Expand Down

0 comments on commit 7f44d78

Please sign in to comment.