Skip to content

Commit 9806caa

Browse files
authored
feat: Allow select link selector during text edit (#4505)
## Description Before, there was no way to select a link inside the text on the canvas in content mode (it was only possible through the navigation menu). Now, when the cursor is over a link, it is displayed in the settings panel. This does not work with newly created links. Will do in the following PR. https://github.com/user-attachments/assets/7472bfd1-3cf7-4ad5-9d39-1bd2effdab26 ## 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: 0000) - [ ] 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 82d1bd2 commit 9806caa

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ import {
55
$selectedInstanceSelector,
66
} from "~/shared/nano-states";
77
import { $textEditingInstanceSelector } from "~/shared/nano-states";
8-
import { areInstanceSelectorsEqual } from "~/shared/tree-utils";
8+
import { type InstanceSelector } from "~/shared/tree-utils";
99
import { Outline } from "./outline";
1010
import { applyScale } from "./apply-scale";
1111
import { $scale } from "~/builder/shared/nano-states";
1212
import { findClosestSlot } from "~/shared/instance-utils";
1313
import { $ephemeralStyles } from "~/canvas/stores";
1414

15+
const isDescendantOrSelf = (
16+
descendant: InstanceSelector,
17+
self: InstanceSelector
18+
) => {
19+
return descendant.join(",").endsWith(self.join(","));
20+
};
21+
1522
export const SelectedInstanceOutline = () => {
1623
const instances = useStore($instances);
1724
const selectedInstanceSelector = useStore($selectedInstanceSelector);
@@ -20,17 +27,20 @@ export const SelectedInstanceOutline = () => {
2027
const scale = useStore($scale);
2128
const ephemeralStyles = useStore($ephemeralStyles);
2229

30+
if (selectedInstanceSelector === undefined) {
31+
return;
32+
}
33+
2334
const isEditingCurrentInstance =
2435
textEditingInstanceSelector !== undefined &&
25-
areInstanceSelectorsEqual(
26-
textEditingInstanceSelector.selector,
27-
selectedInstanceSelector
36+
isDescendantOrSelf(
37+
selectedInstanceSelector,
38+
textEditingInstanceSelector.selector
2839
);
2940

3041
if (
3142
isEditingCurrentInstance ||
3243
outline === undefined ||
33-
selectedInstanceSelector === undefined ||
3444
ephemeralStyles.length !== 0
3545
) {
3646
return;

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

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
$hoveredInstanceOutline,
6363
$hoveredInstanceSelector,
6464
$registeredComponentMetas,
65+
$selectedInstanceSelector,
6566
$textEditingInstanceSelector,
6667
} from "~/shared/nano-states";
6768
import {
@@ -71,6 +72,7 @@ import {
7172
import deepEqual from "fast-deep-equal";
7273
import { setDataCollapsed } from "~/canvas/collapsed";
7374
import { $selectedPage, selectInstance } from "~/shared/awareness";
75+
import { shallowEqual } from "shallow-equal";
7476

7577
const BindInstanceToNodePlugin = ({
7678
refs,
@@ -93,7 +95,7 @@ const BindInstanceToNodePlugin = ({
9395
// @todo: A normal selector must be used, but it would require significantly more code to detect the tree structure.
9496
element.setAttribute(
9597
selectorIdAttribute,
96-
[idAttribute, ...rootInstanceSelector].join(",")
98+
[instanceId, ...rootInstanceSelector].join(",")
9799
);
98100
}
99101
}
@@ -174,6 +176,67 @@ const OnChangeOnBlurPlugin = ({
174176
return null;
175177
};
176178

179+
const LinkSelectionPlugin = () => {
180+
const [editor] = useLexicalComposerContext();
181+
const [preservedSelection] = useState($selectedInstanceSelector.get());
182+
183+
useEffect(() => {
184+
if (!editor.isEditable()) {
185+
return;
186+
}
187+
188+
const removeUpdateListener = editor.registerUpdateListener(
189+
({ editorState }) => {
190+
editorState.read(() => {
191+
const selection = $getSelection();
192+
// console.log(selection);
193+
if (!$isRangeSelection(selection)) {
194+
return false;
195+
}
196+
const key = selection.anchor.getNode().getKey();
197+
198+
const elt = editor.getElementByKey(key);
199+
const link = elt?.closest(`a[${selectorIdAttribute}]`);
200+
201+
if (link == null) {
202+
if (
203+
shallowEqual(preservedSelection, $selectedInstanceSelector.get())
204+
) {
205+
return false;
206+
}
207+
208+
selectInstance(preservedSelection);
209+
210+
return false;
211+
}
212+
213+
const selectorAttribute = link
214+
.getAttribute(selectorIdAttribute)
215+
?.split(",");
216+
217+
if (selectorAttribute === undefined) {
218+
return false;
219+
}
220+
221+
if (
222+
shallowEqual(selectorAttribute, $selectedInstanceSelector.get())
223+
) {
224+
return false;
225+
}
226+
227+
selectInstance(selectorAttribute);
228+
});
229+
}
230+
);
231+
232+
return () => {
233+
removeUpdateListener();
234+
};
235+
}, [editor, preservedSelection]);
236+
237+
return null;
238+
};
239+
177240
const RemoveParagaphsPlugin = () => {
178241
const [editor] = useLexicalComposerContext();
179242

@@ -434,6 +497,7 @@ const InitCursorPlugin = () => {
434497
}
435498
const normalizedSelection =
436499
$normalizeSelection__EXPERIMENTAL(selection);
500+
437501
$setSelection(normalizedSelection);
438502
return;
439503
}
@@ -1071,12 +1135,14 @@ export const TextEditor = ({
10711135
placeholder={<></>}
10721136
/>
10731137
<LinkPlugin />
1138+
10741139
<LinkSanitizePlugin />
10751140
<HistoryPlugin />
10761141

10771142
<SwitchBlockPlugin onNext={handleNext} />
10781143
<OnChangeOnBlurPlugin onChange={handleChange} />
10791144
<InitCursorPlugin />
1145+
<LinkSelectionPlugin />
10801146
<AnyKeyDownPlugin onKeyDown={handleAnyKeydown} />
10811147
<InitialJSONStatePlugin
10821148
onInitialState={(json) => {

0 commit comments

Comments
 (0)