Skip to content

Commit

Permalink
experimental: Rich Text list item support (#4633)
Browse files Browse the repository at this point in the history
## Description

ref #4595

## Bugs 
- [x] - Empty list items are not considered as editable

## 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
  • Loading branch information
istarkov authored Dec 21, 2024
1 parent 4f1c4b0 commit 1670ea5
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,75 @@ const getInsertionIndex = (
return insertBefore ? index : index + 1;
};

export const insertListItemAt = (listItemSelector: InstanceSelector) => {
const instances = $instances.get();

const parentSelector = listItemSelector.slice(1);

const parentInstance = instances.get(parentSelector[0]);

if (parentInstance === undefined) {
return;
}

const position =
1 +
parentInstance.children.findIndex(
(child) => child.type === "id" && child.value === listItemSelector[0]
);

if (position === 0) {
return;
}

const target: DroppableTarget = {
parentSelector,
position,
};

const fragment = extractWebstudioFragment(
getWebstudioData(),
listItemSelector[0]
);

fragment.instances = structuredClone(fragment.instances);
fragment.instances.splice(1);
fragment.instances[0].children = [];

updateWebstudioData((data) => {
const { newInstanceIds } = insertWebstudioFragmentCopy({
data,
fragment,
availableDataSources: findAvailableDataSources(
data.dataSources,
data.instances,
target.parentSelector
),
});
const newRootInstanceId = newInstanceIds.get(fragment.instances[0].id);
if (newRootInstanceId === undefined) {
return;
}
const children: Instance["children"] = [
{ type: "id", value: newRootInstanceId },
];

insertInstanceChildrenMutable(data, children, target);

const selectedInstanceSelector = [
newRootInstanceId,
...target.parentSelector,
];

$textEditingInstanceSelector.set({
selector: selectedInstanceSelector,
reason: "new",
});

selectInstance(selectedInstanceSelector);
});
};

export const insertTemplateAt = (
templateSelector: InstanceSelector,
anchor: InstanceSelector,
Expand Down
66 changes: 58 additions & 8 deletions apps/builder/app/canvas/features/text-editor/text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ import {
selectInstance,
} from "~/shared/awareness";
import { shallowEqual } from "shallow-equal";
import { insertTemplateAt } from "~/builder/features/workspace/canvas-tools/outline/block-utils";
import {
insertListItemAt,
insertTemplateAt,
} from "~/builder/features/workspace/canvas-tools/outline/block-utils";

const BindInstanceToNodePlugin = ({
refs,
Expand Down Expand Up @@ -572,12 +575,21 @@ const InitCursorPlugin = () => {
if ($isTextNode(node)) {
selection.anchor.set(node.getKey(), domOffset, "text");
selection.focus.set(node.getKey(), domOffset, "text");
const normalizedSelection =
$normalizeSelection__EXPERIMENTAL(selection);

$setSelection(normalizedSelection);
return;
}
const normalizedSelection =
$normalizeSelection__EXPERIMENTAL(selection);
}

$setSelection(normalizedSelection);
return;
if (domNode instanceof Element) {
const rect = domNode.getBoundingClientRect();
if (mouseX > rect.right) {
const selection = $getRoot().selectEnd();
$setSelection(selection);
return;
}
}
}
}
Expand Down Expand Up @@ -1075,16 +1087,31 @@ const RichTextContentPluginInternal = ({

if (event.key === "Backspace" || event.key === "Delete") {
const rootNodeContent = $getRoot().getTextContent().trim();
// Delete current

if (rootNodeContent.length === 0) {
const currentInstance = $instances
.get()
.get(rootInstanceSelector[0]);

if (currentInstance?.component === "ListItem") {
onNext(editor.getEditorState(), { reason: "left" });

updateWebstudioData((data) => {
deleteInstanceMutable(data, rootInstanceSelector);
});

event.preventDefault();
return true;
}

const blockChildSelector =
findBlockChildSelector(rootInstanceSelector);

if (blockChildSelector) {
onNext(editor.getEditorState(), { reason: "left" });

updateWebstudioData((data) => {
deleteInstanceMutable(data, rootInstanceSelector);
deleteInstanceMutable(data, blockChildSelector);
});

event.preventDefault();
Expand All @@ -1095,6 +1122,18 @@ 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") {
// Instead of creating block component we need to add a new ListItem
insertListItemAt(rootInstanceSelector);
event.preventDefault();
return true;
}

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

const allowedComponents = ["Paragraph", "Text", "Heading"];
Expand Down Expand Up @@ -1531,9 +1570,20 @@ export const TextEditor = ({

const instance = instances.get(nextSelector[0]);

if (instance === undefined) {
continue;
}

// Components with pseudo-elements (e.g., ::marker) that prevent content from collapsing
const componentsWithPseudoElementChildren = ["ListItem"];

// opinionated: Non-collapsed elements without children can act as spacers (they have size for some reason).
if (instance?.children.length === 0) {
if (
!componentsWithPseudoElementChildren.includes(instance.component) &&
instance?.children.length === 0
) {
const elt = getElementByInstanceSelector(nextSelector);

if (elt === undefined) {
continue;
}
Expand Down
7 changes: 4 additions & 3 deletions apps/builder/app/canvas/instance-hovering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@ export const subscribeInstanceHovering = ({
updateOnMouseMove = false;
};

const unsubscribeTextEditingInstance = $textEditingInstanceSelector.listen(
window.addEventListener(
"click",
() => {
// Fixes the bug if initial editable instance is empty and has collapsed paddings
setTimeout(updateEditableOutline, 0);
}
},
eventOptions
);

window.addEventListener("mousemove", updateEditableOutline, eventOptions);
Expand Down Expand Up @@ -216,6 +218,5 @@ export const subscribeInstanceHovering = ({
clearTimeout(mouseOutTimeoutId);
unsubscribeHoveredInstanceId();
usubscribeSelectedInstanceSelector();
unsubscribeTextEditingInstance();
});
};
Loading

0 comments on commit 1670ea5

Please sign in to comment.