Skip to content

Commit afc77fa

Browse files
authored
feat: Block editing with arrow keys (#4334)
## Description - Double click now sets cursor to the click position - Up/Down arrows on a first/last line trying to preserve cursor x position (visible in case of block above block) - Right/Left arrows switch blocks if cursor is at block begin/end. ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent d619454 commit afc77fa

File tree

23 files changed

+1229
-128
lines changed

23 files changed

+1229
-128
lines changed

.storybook/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default {
5555
// storybook use "util" package internally which is bundled with stories
5656
// and gives and error that process is undefined
5757
"process.env.NODE_DEBUG": "undefined",
58+
"process.env.IS_STROYBOOK": "true",
5859
},
5960
resolve: {
6061
...config.resolve,

apps/builder/app/builder/features/workspace/canvas-tools/outline/selected-instance-outline.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const SelectedInstanceOutline = () => {
2323
const isEditingCurrentInstance =
2424
textEditingInstanceSelector !== undefined &&
2525
areInstanceSelectorsEqual(
26-
textEditingInstanceSelector,
26+
textEditingInstanceSelector.selector,
2727
selectedInstanceSelector
2828
);
2929

apps/builder/app/canvas/features/text-editor/text-editor.stories.tsx

Lines changed: 242 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { useEffect } from "react";
1+
import { useEffect, useState } from "react";
22
import { useStore } from "@nanostores/react";
33
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
44
import type { StoryFn, Meta } from "@storybook/react";
55
import { action } from "@storybook/addon-actions";
6-
import { Box } from "@webstudio-is/design-system";
6+
import { Box, Button, Flex } from "@webstudio-is/design-system";
77
import { theme } from "@webstudio-is/design-system";
88
import type { Instance, Instances } from "@webstudio-is/sdk";
9-
import { $textToolbar } from "~/shared/nano-states";
9+
import {
10+
$instances,
11+
$pages,
12+
$registeredComponentMetas,
13+
$selectedPageId,
14+
$textEditingInstanceSelector,
15+
$textToolbar,
16+
} from "~/shared/nano-states";
1017
import { TextEditor } from "./text-editor";
1118
import { emitCommand, subscribeCommands } from "~/canvas/shared/commands";
19+
import { $, renderJsx } from "@webstudio-is/sdk/testing";
1220

1321
export default {
1422
component: TextEditor,
@@ -33,17 +41,21 @@ const createInstancePair = (
3341

3442
const instances: Instances = new Map([
3543
createInstancePair("1", "Text", [
36-
{ type: "text", value: "Paragraph you can edit " },
44+
{ type: "text", value: "Paragraph you can edit Blabla " },
3745
{ type: "id", value: "2" },
3846
{ type: "id", value: "3" },
3947
{ type: "id", value: "5" },
4048
]),
41-
createInstancePair("2", "Bold", [{ type: "text", value: "very bold text " }]),
49+
createInstancePair("2", "Bold", [
50+
{ type: "text", value: "Very Very very bold text " },
51+
]),
4252
createInstancePair("3", "Bold", [{ type: "id", value: "4" }]),
4353
createInstancePair("4", "Italic", [
44-
{ type: "text", value: "with small italic" },
54+
{ type: "text", value: "And Bold Small with small italic" },
55+
]),
56+
createInstancePair("5", "Bold", [
57+
{ type: "text", value: " la la la subtext" },
4558
]),
46-
createInstancePair("5", "Bold", [{ type: "text", value: " subtext" }]),
4759
]);
4860

4961
export const Basic: StoryFn<typeof TextEditor> = ({ onChange }) => {
@@ -128,6 +140,229 @@ export const Basic: StoryFn<typeof TextEditor> = ({ onChange }) => {
128140
);
129141
};
130142

143+
export const CursorPositioning: StoryFn<typeof TextEditor> = ({ onChange }) => {
144+
const textEditingInstanceSelector = useStore($textEditingInstanceSelector);
145+
146+
return (
147+
<>
148+
<Box
149+
css={{
150+
width: 300,
151+
"& > div": {
152+
padding: 40,
153+
backgroundColor: textEditingInstanceSelector
154+
? "unset"
155+
: "rgba(0,0,0,0.1)",
156+
},
157+
border: "1px solid #999",
158+
color: "black",
159+
" *": {
160+
outline: "none",
161+
},
162+
}}
163+
onClick={(event) => {
164+
if (textEditingInstanceSelector !== undefined) {
165+
return;
166+
}
167+
$textEditingInstanceSelector.set({
168+
selector: ["1"],
169+
reason: "click",
170+
mouseX: event.clientX,
171+
mouseY: event.clientY,
172+
});
173+
}}
174+
>
175+
{textEditingInstanceSelector && (
176+
<TextEditor
177+
rootInstanceSelector={["1"]}
178+
instances={instances}
179+
contentEditable={<ContentEditable />}
180+
onChange={onChange}
181+
onSelectInstance={(instanceId) =>
182+
console.info("select instance", instanceId)
183+
}
184+
/>
185+
)}
186+
187+
{!textEditingInstanceSelector && (
188+
<div>
189+
<span>Paragraph you can edit Blabla </span>
190+
<strong>Very Very very bold text </strong>
191+
<strong>
192+
<i>And Bold Small with small italic</i>
193+
</strong>
194+
<strong> la la la subtext</strong>
195+
</div>
196+
)}
197+
</Box>
198+
<br />
199+
<div>
200+
<i>Click on text above, see cursor position and start editing text</i>
201+
</div>
202+
{textEditingInstanceSelector && (
203+
<Button
204+
onClick={() => {
205+
$textEditingInstanceSelector.set(undefined);
206+
}}
207+
>
208+
Reset
209+
</Button>
210+
)}
211+
</>
212+
);
213+
};
214+
215+
export const CursorPositioningUpDown: StoryFn<typeof TextEditor> = () => {
216+
const [{ instances }, setState] = useState(() => {
217+
$pages.set({
218+
folders: [],
219+
homePage: {
220+
id: "homePageId",
221+
rootInstanceId: "bodyId",
222+
meta: {},
223+
path: "",
224+
title: "",
225+
name: "",
226+
systemDataSourceId: "",
227+
},
228+
pages: [
229+
{
230+
id: "pageId",
231+
rootInstanceId: "bodyId",
232+
path: "",
233+
title: "",
234+
name: "",
235+
systemDataSourceId: "",
236+
meta: {},
237+
},
238+
],
239+
});
240+
241+
$selectedPageId.set("pageId");
242+
243+
$registeredComponentMetas.set(
244+
new Map([
245+
[
246+
"Box",
247+
{
248+
type: "container",
249+
icon: "icon",
250+
},
251+
],
252+
[
253+
"Bold",
254+
{
255+
type: "rich-text-child",
256+
icon: "icon",
257+
},
258+
],
259+
])
260+
);
261+
262+
return renderJsx(
263+
<$.Body ws:id="bodyId">
264+
<$.Box ws:id="boxAId">
265+
Hello world <$.Bold ws:id="boldA">Hello world</$.Bold> Hello world
266+
world Hello worldsdsdj skdk ls dk jslkdjklsjdkl sdk jskdj ksjd lksdj
267+
dsj
268+
</$.Box>
269+
<$.Box ws:id="boxBId">
270+
Let it be Let it be <$.Bold ws:id="boldB">Let it be Let</$.Bold> Let
271+
it be Let it be Let it be Let it be Let it be Let it be
272+
</$.Box>
273+
</$.Body>
274+
);
275+
});
276+
277+
useEffect(() => {
278+
$instances.set(instances);
279+
}, [instances]);
280+
281+
const textEditingInstanceSelector = useStore($textEditingInstanceSelector);
282+
283+
return (
284+
<>
285+
<Flex
286+
gap={2}
287+
direction={"column"}
288+
css={{
289+
width: 500,
290+
"& > div > div": {
291+
padding: 5,
292+
border: "1px solid #999",
293+
},
294+
"& *[aria-readonly]": {
295+
backgroundColor: "rgba(0,0,0,0.02)",
296+
},
297+
"& strong": {
298+
fontSize: "1.5em",
299+
},
300+
301+
color: "black",
302+
" *": {
303+
outline: "none",
304+
},
305+
}}
306+
>
307+
<div style={{ display: "contents" }} data-ws-selector="boxAId,bodyId">
308+
<TextEditor
309+
key={textEditingInstanceSelector?.selector[0] ?? ""}
310+
editable={
311+
textEditingInstanceSelector === undefined ||
312+
textEditingInstanceSelector?.selector[0] === "boxAId"
313+
}
314+
rootInstanceSelector={["boxAId", "bodyId"]}
315+
instances={instances}
316+
contentEditable={<ContentEditable />}
317+
onChange={(data) => {
318+
setState((prev) => {
319+
for (const instance of data) {
320+
prev.instances.set(instance.id, instance);
321+
}
322+
return prev;
323+
});
324+
}}
325+
onSelectInstance={(instanceId) =>
326+
console.info("select instance", instanceId)
327+
}
328+
/>
329+
</div>
330+
331+
<div
332+
style={{ display: "contents" }}
333+
data-ws-selector="boxBId,bodyId"
334+
data-ws-collapsed="true"
335+
>
336+
<TextEditor
337+
key={textEditingInstanceSelector?.selector[0] ?? ""}
338+
editable={textEditingInstanceSelector?.selector[0] === "boxBId"}
339+
rootInstanceSelector={["boxBId", "bodyId"]}
340+
instances={instances}
341+
contentEditable={<ContentEditable />}
342+
onChange={(data) => {
343+
setState((prev) => {
344+
for (const instance of data) {
345+
prev.instances.set(instance.id, instance);
346+
}
347+
return prev;
348+
});
349+
}}
350+
onSelectInstance={(instanceId) =>
351+
console.info("select instance", instanceId)
352+
}
353+
/>
354+
</div>
355+
</Flex>
356+
<br />
357+
<i>Use arrows to move between editors, clicks are not working</i>
358+
</>
359+
);
360+
};
361+
131362
Basic.args = {
132363
onChange: action("onChange"),
133364
};
365+
366+
CursorPositioning.args = {
367+
onChange: action("onChange"),
368+
};

0 commit comments

Comments
 (0)