Files
webstudio/apps/builder/app/canvas/instance-selection.ts
T
Ivan Starkov 36ba02d30f feat: Add builderMode, add mode "edit" (#4376)
## Description
Part 1 Of #3994 

This PR introduces a feature flag, `contentEditableMode`, that hides the
`EditorButton` in the top bar. This button is now intended for **testing
purposes only** and will **never be used in production**.

<img width="154" alt="image"
src="https://github.com/user-attachments/assets/f4a37334-71b9-43d8-b4a8-afd518fc6833">

## Most Notable Things In The Content Edit Mode

1. **Notion-like Editing**: Elements are editable with a single click.
2. **Restricted Element Editing**: Binded elements, such as expressions,
are non-editable.
3. **Selective Property Editing**: Only properties for `img` and `link`
elements are editable.

Additional changes:
- **Copy/Paste and Drag/Drop**: Disabled to maintain a controlled
testing environment.

Refer to the issue `Part 1` for further details on upcoming changes and
planned enhancements.


## Steps for reproduction

Play play 

## 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
2024-11-14 11:06:29 +04:00

154 lines
3.7 KiB
TypeScript

import { getInstanceSelectorFromElement } from "~/shared/dom-utils";
import { findClosestEditableInstanceSelector } from "~/shared/instance-utils";
import {
$hoveredInstanceOutline,
$hoveredInstanceSelector,
$instances,
$isContentMode,
$registeredComponentMetas,
} from "~/shared/nano-states";
import { $textEditingInstanceSelector } from "~/shared/nano-states";
import { emitCommand } from "./shared/commands";
import { shallowEqual } from "shallow-equal";
import { $awareness, selectInstance } from "~/shared/awareness";
const isElementBeingEdited = (element: Element) => {
if (element.closest("[contenteditable=true]")) {
return true;
}
return false;
};
const handleSelect = (event: MouseEvent) => {
const element = event.target;
if (!(element instanceof Element)) {
return;
}
if (isElementBeingEdited(element)) {
return;
}
const instanceSelector = getInstanceSelectorFromElement(element);
if (instanceSelector === undefined) {
return;
}
// Prevent unnecessary updates (2 clicks are registered before a double click)
if ($textEditingInstanceSelector.get() !== undefined) {
$textEditingInstanceSelector.set(undefined);
}
// Prevent unnecessary updates (2 clicks are registered before a double click)
if (!shallowEqual(instanceSelector, $awareness.get()?.instanceSelector)) {
selectInstance(instanceSelector);
}
};
const handleEdit = (event: MouseEvent) => {
const element = event.target;
if (!(element instanceof Element)) {
return;
}
if (isElementBeingEdited(element)) {
return;
}
const instanceSelector = getInstanceSelectorFromElement(element);
if (instanceSelector === undefined) {
return;
}
const instances = $instances.get();
let editableInstanceSelector = findClosestEditableInstanceSelector(
instanceSelector,
instances,
$registeredComponentMetas.get()
);
// Do not allow edit bindable text instances with expression children in Content Mode
if (editableInstanceSelector !== undefined && $isContentMode.get()) {
const instance = instances.get(editableInstanceSelector[0]);
if (instance === undefined) {
return false;
}
const hasExpressionChildren = instance.children.some(
(child) => child.type === "expression"
);
if (hasExpressionChildren) {
editableInstanceSelector = undefined;
}
}
if (editableInstanceSelector === undefined) {
// Handle non-editable instances in Content Mode:
// Reset editing state when clicking from an editable text to a non-editable instance
if ($textEditingInstanceSelector.get() !== undefined) {
$textEditingInstanceSelector.set(undefined);
}
// Select the instance when no editable parent is found
if (!shallowEqual(instanceSelector, $awareness.get()?.instanceSelector)) {
selectInstance(instanceSelector);
}
return;
}
// Avoid redundant selection if the instance is already selected
if (
!shallowEqual($awareness.get()?.instanceSelector, editableInstanceSelector)
) {
selectInstance(editableInstanceSelector);
}
$hoveredInstanceOutline.set(undefined);
$hoveredInstanceSelector.set(undefined);
$textEditingInstanceSelector.set({
selector: editableInstanceSelector,
reason: "click",
mouseX: event.clientX,
mouseY: event.clientY,
});
};
export const subscribeInstanceSelection = ({
signal,
}: {
signal: AbortSignal;
}) => {
addEventListener(
"click",
(event) => {
emitCommand("clickCanvas");
if ($isContentMode.get()) {
handleEdit(event);
return;
}
handleSelect(event);
},
{ passive: true, signal }
);
addEventListener(
"dblclick",
(event) => {
handleEdit(event);
},
{ passive: true, signal }
);
};