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:
Bogdan Chadkin authored and GitHub committed 2023-03-25 11:35:29 +07:00
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();
@@ -1,7 +0,0 @@
import { useSubscribe } from "~/shared/pubsub";
import { useTextEditingInstanceId } from "~/shared/nano-states";
export const useSubscribeTextEditingInstanceId = () => {
const [, setInstanceId] = useTextEditingInstanceId();
useSubscribe("textEditingInstanceId", setInstanceId);
};
@@ -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
@@ -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({
-4
View File
@@ -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) => {