mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 22:14:29 +09:00
Migrate text editing to selector (#1287)
The last piece to address instances by selectors - the mode is represented as text editing instance selector - reset editing mode along with selection on user input instead of ssubscribing to selected store - moved enter/esc shortcuts to shared between builder and canvas - got rid of legacy subscriptions to stores
This commit is contained in:
1 parent
92342a22a4
commit
3592df7bba
18 files changed
+104
-131
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
selectedInstanceSelectorStore,
|
||||
} from "~/shared/nano-states";
|
||||
import { getAncestorInstanceSelector } from "~/shared/tree-utils";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
|
||||
type BreadcrumbProps = {
|
||||
children: JSX.Element | string;
|
||||
@@ -67,6 +68,7 @@ export const Breadcrumbs = () => {
|
||||
instance.id
|
||||
)
|
||||
);
|
||||
textEditingInstanceSelectorStore.set(undefined);
|
||||
}}
|
||||
>
|
||||
{instance.component}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { HoveredInstanceOutline, SelectedInstanceOutline } from "./outline";
|
||||
import { useSubscribeTextToolbar, TextToolbar } from "./text-toolbar";
|
||||
import { useSubscribeInstanceRect } from "./hooks/use-subscribe-instance-rect";
|
||||
import { useSubscribeTextEditingInstanceId } from "./hooks/use-subscribe-editing-instance-id";
|
||||
import { useSubscribeSwitchPage } from "~/shared/pages";
|
||||
import { Label } from "./outline/label";
|
||||
import { Outline } from "./outline/outline";
|
||||
@@ -38,7 +37,6 @@ export const CanvasTools = ({ publish }: CanvasToolsProps) => {
|
||||
useSubscribeTextToolbar();
|
||||
useSubscribeScrollState();
|
||||
useSubscribeDragAndDropState();
|
||||
useSubscribeTextEditingInstanceId();
|
||||
useSubscribeSwitchPage();
|
||||
|
||||
const [isPreviewMode] = useIsPreviewMode();
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { useSubscribe } from "~/shared/pubsub";
|
||||
import { useTextEditingInstanceId } from "~/shared/nano-states";
|
||||
|
||||
export const useSubscribeTextEditingInstanceId = () => {
|
||||
const [, setInstanceId] = useTextEditingInstanceId();
|
||||
useSubscribe("textEditingInstanceId", setInstanceId);
|
||||
};
|
||||
+5
-3
@@ -3,8 +3,8 @@ import {
|
||||
hoveredInstanceSelectorStore,
|
||||
hoveredInstanceOutlineStore,
|
||||
selectedInstanceSelectorStore,
|
||||
useTextEditingInstanceId,
|
||||
} from "~/shared/nano-states";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
import { areInstanceSelectorsEqual } from "~/shared/tree-utils";
|
||||
import { Outline } from "./outline";
|
||||
import { Label } from "./label";
|
||||
@@ -13,9 +13,11 @@ export const HoveredInstanceOutline = () => {
|
||||
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
|
||||
const hoveredInstanceSelector = useStore(hoveredInstanceSelectorStore);
|
||||
const instanceOutline = useStore(hoveredInstanceOutlineStore);
|
||||
const [textEditingInstanceId] = useTextEditingInstanceId();
|
||||
const textEditingInstanceSelector = useStore(
|
||||
textEditingInstanceSelectorStore
|
||||
);
|
||||
|
||||
const isEditingText = textEditingInstanceId !== undefined;
|
||||
const isEditingText = textEditingInstanceSelector !== undefined;
|
||||
const isHoveringSelectedInstance = areInstanceSelectorsEqual(
|
||||
selectedInstanceSelector,
|
||||
hoveredInstanceSelector
|
||||
|
||||
+12
-4
@@ -1,20 +1,28 @@
|
||||
import { useStore } from "@nanostores/react";
|
||||
import {
|
||||
selectedInstanceSelectorStore,
|
||||
selectedInstanceStore,
|
||||
useSelectedInstanceOutline,
|
||||
useTextEditingInstanceId,
|
||||
} from "~/shared/nano-states";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
import { areInstanceSelectorsEqual } from "~/shared/tree-utils";
|
||||
import { Outline } from "./outline";
|
||||
import { Label } from "./label";
|
||||
|
||||
export const SelectedInstanceOutline = () => {
|
||||
const [{ rect, visible }] = useSelectedInstanceOutline();
|
||||
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
|
||||
const selectedInstance = useStore(selectedInstanceStore);
|
||||
const [textEditingInstanceId] = useTextEditingInstanceId();
|
||||
const textEditingInstanceSelector = useStore(
|
||||
textEditingInstanceSelectorStore
|
||||
);
|
||||
|
||||
const isEditingCurrentInstance =
|
||||
textEditingInstanceId !== undefined &&
|
||||
textEditingInstanceId === selectedInstance?.id;
|
||||
textEditingInstanceSelector !== undefined &&
|
||||
areInstanceSelectorsEqual(
|
||||
textEditingInstanceSelector,
|
||||
selectedInstanceSelector
|
||||
);
|
||||
|
||||
if (
|
||||
selectedInstance === undefined ||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { theme, Box, Flex, Toaster } from "@webstudio-is/design-system";
|
||||
import { useCanvasWidth } from "~/builder/shared/nano-states";
|
||||
import type { Publish } from "~/shared/pubsub";
|
||||
import { selectedInstanceSelectorStore } from "~/shared/nano-states";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
import {
|
||||
workspaceRectStore,
|
||||
zoomStore,
|
||||
@@ -61,6 +62,7 @@ export const Workspace = ({
|
||||
|
||||
const handleWorkspaceClick = () => {
|
||||
selectedInstanceSelectorStore.set(undefined);
|
||||
textEditingInstanceSelectorStore.set(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import type { InstanceSelector } from "~/shared/tree-utils";
|
||||
import { InstanceTree } from "./tree";
|
||||
import { reparentInstance } from "~/shared/instance-utils";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
|
||||
export const NavigatorTree = () => {
|
||||
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
|
||||
@@ -33,6 +34,11 @@ export const NavigatorTree = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSelect = useCallback((instanceSelector: InstanceSelector) => {
|
||||
selectedInstanceSelectorStore.set(instanceSelector);
|
||||
textEditingInstanceSelectorStore.set(undefined);
|
||||
}, []);
|
||||
|
||||
if (rootInstance === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -43,7 +49,7 @@ export const NavigatorTree = () => {
|
||||
selectedItemSelector={selectedInstanceSelector}
|
||||
dragItemSelector={dragItemSelector}
|
||||
dropTarget={state.dropTarget}
|
||||
onSelect={selectedInstanceSelectorStore.set}
|
||||
onSelect={handleSelect}
|
||||
onHover={hoveredInstanceSelectorStore.set}
|
||||
onDragItemChange={(dragInstanceSelector) => {
|
||||
setState({
|
||||
|
||||
@@ -18,9 +18,7 @@ import { publish } from "~/shared/pubsub";
|
||||
import { registerContainers, useCanvasStore } from "~/shared/sync";
|
||||
import { useSharedShortcuts } from "~/shared/shortcuts";
|
||||
import { useShortcuts } from "./shared/use-shortcuts";
|
||||
import { usePublishTextEditingInstanceId } from "./shared/instance";
|
||||
import { useManageDesignModeStyles, GlobalStyles } from "./shared/styles";
|
||||
import { useTrackSelectedElement } from "./shared/use-track-selected-element";
|
||||
import { WebstudioComponentDev } from "./features/webstudio-component";
|
||||
import {
|
||||
propsIndexStore,
|
||||
@@ -72,10 +70,8 @@ const useElementsTree = (getComponent: GetComponent) => {
|
||||
|
||||
const DesignMode = () => {
|
||||
useManageDesignModeStyles();
|
||||
useTrackSelectedElement();
|
||||
usePublishScrollState();
|
||||
useSubscribeScrollState();
|
||||
usePublishTextEditingInstanceId();
|
||||
useDragAndDrop();
|
||||
// We need to initialize this in both canvas and builder,
|
||||
// because the events will fire in either one, depending on where the focus is
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
selectedInstanceSelectorStore,
|
||||
useInstanceProps,
|
||||
useInstanceStyles,
|
||||
useTextEditingInstanceId,
|
||||
} from "~/shared/nano-states";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
import { useCssRules } from "~/canvas/shared/styles";
|
||||
import {
|
||||
areInstanceSelectorsEqual,
|
||||
@@ -105,9 +105,10 @@ export const WebstudioComponentDev = ({
|
||||
useCssRules({ instanceId: instance.id, instanceStyles });
|
||||
const instances = useStore(instancesStore);
|
||||
|
||||
const [editingInstanceId, setTextEditingInstanceId] =
|
||||
useTextEditingInstanceId();
|
||||
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
|
||||
const textEditingInstanceSelector = useStore(
|
||||
textEditingInstanceSelectorStore
|
||||
);
|
||||
|
||||
const instanceProps = useInstanceProps(instance.id);
|
||||
const userProps = useMemo(() => {
|
||||
@@ -185,8 +186,10 @@ export const WebstudioComponentDev = ({
|
||||
</>
|
||||
);
|
||||
|
||||
// @todo compare instance selectors
|
||||
if (editingInstanceId !== instance.id) {
|
||||
if (
|
||||
areInstanceSelectorsEqual(textEditingInstanceSelector, instanceSelector) ===
|
||||
false
|
||||
) {
|
||||
return instanceElement;
|
||||
}
|
||||
|
||||
@@ -216,13 +219,13 @@ export const WebstudioComponentDev = ({
|
||||
});
|
||||
}}
|
||||
onSelectInstance={(instanceId) => {
|
||||
setTextEditingInstanceId(undefined);
|
||||
const instances = instancesStore.get();
|
||||
const newSelectedSelector = getInstanceSelector(
|
||||
instances,
|
||||
instanceSelector,
|
||||
instanceId
|
||||
);
|
||||
textEditingInstanceSelectorStore.set(undefined);
|
||||
selectedInstanceSelectorStore.set(newSelectedSelector);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getInstanceSelectorFromElement } from "~/shared/dom-utils";
|
||||
import {
|
||||
instancesStore,
|
||||
selectedInstanceSelectorStore,
|
||||
textEditingInstanceIdStore,
|
||||
} from "~/shared/nano-states";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
import { publish } from "~/shared/pubsub";
|
||||
import {
|
||||
type InstanceSelector,
|
||||
@@ -69,7 +69,7 @@ export const subscribeInstanceSelection = () => {
|
||||
if (event.detail === 1) {
|
||||
selectedInstanceSelectorStore.set(instanceSelector);
|
||||
// reset text editor when another instance is selected
|
||||
textEditingInstanceIdStore.set(undefined);
|
||||
textEditingInstanceSelectorStore.set(undefined);
|
||||
}
|
||||
|
||||
// the second click in a double click.
|
||||
@@ -80,7 +80,7 @@ export const subscribeInstanceSelection = () => {
|
||||
// enable text editor when double click on its instance or one of its descendants
|
||||
if (richTextInstanceSelector) {
|
||||
selectedInstanceSelectorStore.set(richTextInstanceSelector);
|
||||
textEditingInstanceIdStore.set(richTextInstanceSelector[0]);
|
||||
textEditingInstanceSelectorStore.set(richTextInstanceSelector);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import type { Instance } from "@webstudio-is/project-build";
|
||||
import { useTextEditingInstanceId } from "~/shared/nano-states";
|
||||
import { publish } from "~/shared/pubsub";
|
||||
|
||||
declare module "~/shared/pubsub" {
|
||||
export interface PubsubMap {
|
||||
textEditingInstanceId?: Instance["id"];
|
||||
}
|
||||
}
|
||||
|
||||
export const usePublishTextEditingInstanceId = () => {
|
||||
const [editingInstanceId] = useTextEditingInstanceId();
|
||||
useEffect(() => {
|
||||
publish({
|
||||
type: "textEditingInstanceId",
|
||||
payload: editingInstanceId,
|
||||
});
|
||||
}, [editingInstanceId]);
|
||||
};
|
||||
@@ -10,11 +10,8 @@ import {
|
||||
computeIndicatorPlacement,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { getComponentMeta } from "@webstudio-is/react-sdk";
|
||||
import {
|
||||
instancesStore,
|
||||
selectedPageStore,
|
||||
useTextEditingInstanceId,
|
||||
} from "~/shared/nano-states";
|
||||
import { instancesStore, selectedPageStore } from "~/shared/nano-states";
|
||||
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
|
||||
import { publish, useSubscribe } from "~/shared/pubsub";
|
||||
import {
|
||||
insertNewComponentInstance,
|
||||
@@ -120,8 +117,6 @@ const getDefaultDropTarget = () => {
|
||||
};
|
||||
|
||||
export const useDragAndDrop = () => {
|
||||
const [textEditingInstanceId] = useTextEditingInstanceId();
|
||||
|
||||
const state = useRef({ ...initialState });
|
||||
|
||||
const autoScrollHandlers = useAutoScroll({ fullscreen: true });
|
||||
@@ -212,7 +207,12 @@ export const useDragAndDrop = () => {
|
||||
return false;
|
||||
}
|
||||
// cannot drag while editing text
|
||||
if (instanceSelector[0] === textEditingInstanceId) {
|
||||
if (
|
||||
areInstanceSelectorsEqual(
|
||||
instanceSelector,
|
||||
textEditingInstanceSelectorStore.get()
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// When trying to drag an instance inside editor, drag the editor instead
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { getComponentMeta } from "@webstudio-is/react-sdk";
|
||||
import { shortcuts, options } from "~/shared/shortcuts";
|
||||
import { publish, useSubscribe } from "~/shared/pubsub";
|
||||
import {
|
||||
selectedInstanceSelectorStore,
|
||||
selectedInstanceStore,
|
||||
useTextEditingInstanceId,
|
||||
isPreviewModeStore,
|
||||
} from "~/shared/nano-states";
|
||||
import { isPreviewModeStore } from "~/shared/nano-states";
|
||||
|
||||
declare module "~/shared/pubsub" {
|
||||
export interface PubsubMap {
|
||||
@@ -29,50 +23,12 @@ const publishCancelCurrentDrag = () => {
|
||||
};
|
||||
|
||||
export const useShortcuts = () => {
|
||||
const [editingInstanceId, setEditingInstanceId] = useTextEditingInstanceId();
|
||||
|
||||
const shortcutHandlerMap = {
|
||||
preview: togglePreviewMode,
|
||||
breakpointsMenu: publishOpenBreakpointsMenu,
|
||||
esc: publishCancelCurrentDrag,
|
||||
} as const;
|
||||
|
||||
useHotkeys(
|
||||
"esc",
|
||||
() => {
|
||||
const selectedInstanceSelector = selectedInstanceSelectorStore.get();
|
||||
if (selectedInstanceSelector === undefined) {
|
||||
return;
|
||||
}
|
||||
// Since we are in text editing mode, we want to first exit that mode without unselecting the instance.
|
||||
if (editingInstanceId) {
|
||||
setEditingInstanceId(undefined);
|
||||
return;
|
||||
}
|
||||
selectedInstanceSelectorStore.set(undefined);
|
||||
},
|
||||
{ ...options, enableOnContentEditable: true },
|
||||
[editingInstanceId]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"enter",
|
||||
(event) => {
|
||||
const selectedInstance = selectedInstanceStore.get();
|
||||
if (selectedInstance === undefined) {
|
||||
return;
|
||||
}
|
||||
const meta = getComponentMeta(selectedInstance.component);
|
||||
if (meta?.type === "rich-text") {
|
||||
// Prevents inserting a newline when entering text-editing mode
|
||||
event.preventDefault();
|
||||
setEditingInstanceId(selectedInstance.id);
|
||||
}
|
||||
},
|
||||
options,
|
||||
[setEditingInstanceId]
|
||||
);
|
||||
|
||||
useHotkeys(shortcuts.preview, shortcutHandlerMap.preview, options, []);
|
||||
|
||||
useHotkeys(
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import {
|
||||
selectedInstanceSelectorStore,
|
||||
textEditingInstanceIdStore,
|
||||
} from "~/shared/nano-states";
|
||||
|
||||
export const useTrackSelectedElement = () => {
|
||||
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
|
||||
const selectedInstanceId = selectedInstanceSelector?.[0];
|
||||
|
||||
// @todo disable based on user input instead of tracking selected
|
||||
useEffect(() => {
|
||||
const editingInstanceId = textEditingInstanceIdStore.get();
|
||||
if (
|
||||
editingInstanceId !== undefined &&
|
||||
// @todo compare instance selectors
|
||||
selectedInstanceId !== editingInstanceId
|
||||
) {
|
||||
textEditingInstanceIdStore.set(undefined);
|
||||
}
|
||||
}, [selectedInstanceId]);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { atom } from "nanostores";
|
||||
import { getComponentMeta } from "@webstudio-is/react-sdk";
|
||||
import type { InstanceSelector } from "../tree-utils";
|
||||
import {
|
||||
selectedInstanceSelectorStore,
|
||||
selectedInstanceStore,
|
||||
} from "./nano-states";
|
||||
|
||||
export const textEditingInstanceSelectorStore = atom<
|
||||
undefined | InstanceSelector
|
||||
>();
|
||||
|
||||
export const enterEditingMode = (event: KeyboardEvent) => {
|
||||
const selectedInstanceSelector = selectedInstanceSelectorStore.get();
|
||||
const selectedInstance = selectedInstanceStore.get();
|
||||
if (
|
||||
selectedInstance === undefined ||
|
||||
selectedInstanceSelector === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const meta = getComponentMeta(selectedInstance.component);
|
||||
if (meta?.type === "rich-text") {
|
||||
// Prevents inserting a newline when entering text-editing mode
|
||||
event.preventDefault();
|
||||
textEditingInstanceSelectorStore.set(selectedInstanceSelector);
|
||||
}
|
||||
};
|
||||
|
||||
export const escapeSelection = () => {
|
||||
const selectedInstanceSelector = selectedInstanceSelectorStore.get();
|
||||
const textEditingInstanceSelector = textEditingInstanceSelectorStore.get();
|
||||
if (selectedInstanceSelector === undefined) {
|
||||
return;
|
||||
}
|
||||
// exit text editing mode first without unselecting instance
|
||||
if (textEditingInstanceSelector) {
|
||||
textEditingInstanceSelectorStore.set(undefined);
|
||||
return;
|
||||
}
|
||||
selectedInstanceSelectorStore.set(undefined);
|
||||
};
|
||||
|
||||
export const synchronizedInstancesStores = [
|
||||
["textEditingInstanceSelector", textEditingInstanceSelectorStore],
|
||||
] as const;
|
||||
@@ -423,11 +423,6 @@ export const useSelectedInstanceOutline = () =>
|
||||
const isScrollingContainer = atom<boolean>(false);
|
||||
export const useIsScrolling = () => useValue(isScrollingContainer);
|
||||
|
||||
// We are editing the text of that instance in text editor.
|
||||
export const textEditingInstanceIdStore = atom<Instance["id"] | undefined>();
|
||||
export const useTextEditingInstanceId = () =>
|
||||
useValue(textEditingInstanceIdStore);
|
||||
|
||||
export type DragAndDropState = {
|
||||
isDragging: boolean;
|
||||
dropTarget?: ItemDropTarget;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
zoomOut,
|
||||
selectBreakpointByOrderNumber,
|
||||
} from "../nano-states/breakpoints";
|
||||
import { enterEditingMode, escapeSelection } from "../nano-states/instances";
|
||||
import { deleteSelectedInstance } from "../instance-utils";
|
||||
|
||||
export const shortcuts = {
|
||||
@@ -89,4 +90,8 @@ export const useSharedShortcuts = () => {
|
||||
{ enableOnFormTags: true, enableOnContentEditable: true },
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys("enter", enterEditingMode, {}, []);
|
||||
|
||||
useHotkeys("esc", escapeSelection, { enableOnContentEditable: true }, []);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isPreviewModeStore,
|
||||
} from "~/shared/nano-states";
|
||||
import { synchronizedBreakpointsStores } from "~/shared/nano-states/breakpoints";
|
||||
import { synchronizedInstancesStores } from "~/shared/nano-states/instances";
|
||||
|
||||
enableMapSet();
|
||||
|
||||
@@ -71,6 +72,9 @@ export const registerContainers = () => {
|
||||
for (const [name, store] of synchronizedBreakpointsStores) {
|
||||
clientStores.set(name, store);
|
||||
}
|
||||
for (const [name, store] of synchronizedInstancesStores) {
|
||||
clientStores.set(name, store);
|
||||
}
|
||||
};
|
||||
|
||||
const syncStoresChanges = (name: SyncEventSource, publish: Publish) => {
|
||||
|
||||
Reference in new issue
Block a user