mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-10 06:24:35 +09:00
## 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
154 lines
3.7 KiB
TypeScript
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 }
|
|
);
|
|
};
|