Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render tree with normalized data #1267

Merged
merged 1 commit into from Mar 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -2,9 +2,9 @@ import { useCallback } from "react";
import { useStore } from "@nanostores/react";
import { Flex } from "@webstudio-is/design-system";
import {
rootInstanceStore,
selectedInstanceSelectorStore,
hoveredInstanceSelectorStore,
useRootInstance,
} from "~/shared/nano-states";
import { InstanceTree } from "~/builder/shared/tree";
import { reparentInstance } from "~/shared/instance-utils";
Expand All @@ -18,15 +18,15 @@ type NavigatorProps = {

export const Navigator = ({ isClosable, onClose }: NavigatorProps) => {
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
const [rootInstance] = useRootInstance();
const rootInstance = useStore(rootInstanceStore);

const handleDragEnd = useCallback(
(payload: {
itemSelector: InstanceSelector;
dropTarget: { itemId: string; position: number | "end" };
dropTarget: { itemSelector: InstanceSelector; position: number | "end" };
}) => {
reparentInstance(payload.itemSelector[0], {
parentId: payload.dropTarget.itemId,
parentId: payload.dropTarget.itemSelector[0],
position: payload.dropTarget.position,
});
},
Expand Down
63 changes: 28 additions & 35 deletions apps/builder/app/builder/shared/tree/index.tsx
Expand Up @@ -9,19 +9,16 @@ import {
type TreeProps,
type TreeItemRenderProps,
} from "@webstudio-is/design-system";
import type { Instance } from "@webstudio-is/project-build";
import type { Instance, InstancesItem } from "@webstudio-is/project-build";
import {
getComponentMeta,
type WsComponentMeta,
} from "@webstudio-is/react-sdk";
import { instancesIndexStore } from "~/shared/nano-states";
import {
createInstancesIndex,
getInstanceAncestorsAndSelf,
} from "~/shared/tree-utils";
import { instancesStore } from "~/shared/nano-states";
import { createInstancesIndex } from "~/shared/tree-utils";

const instanceRelatedProps = {
renderItem(props: TreeItemRenderProps<Instance>) {
renderItem(props: TreeItemRenderProps<InstancesItem | Instance>) {
const meta = getComponentMeta(props.itemData.component);
if (meta === undefined) {
return <></>;
Expand Down Expand Up @@ -60,70 +57,66 @@ const getInstanceChildren = (instance: undefined | Instance) => {

export const InstanceTree = (
props: Omit<
TreeProps<Instance>,
TreeProps<InstancesItem>,
| keyof typeof instanceRelatedProps
| "findItemById"
| "getItemPath"
| "canLeaveParent"
| "canAcceptChild"
| "getItemChildren"
>
) => {
const instancesIndex = useStore(instancesIndexStore);
const { instancesById } = instancesIndex;

const findItemById = useCallback(
(_rootInstance: Instance, instanceId: Instance["id"]) => {
return instancesById.get(instanceId);
},
[instancesById]
);

const getItemPath = useCallback(
(_rootInstance: Instance, instanceId: Instance["id"]) => {
return getInstanceAncestorsAndSelf(instancesIndex, instanceId);
},
[instancesIndex]
);
const instances = useStore(instancesStore);

const canLeaveParent = useCallback(
(instanceId: Instance["id"]) => {
const instance = instancesIndex.instancesById.get(instanceId);
const instance = instances.get(instanceId);
if (instance === undefined) {
return false;
}
const meta = getComponentMeta(instance.component);
return meta?.type !== "rich-text-child";
},
[instancesIndex]
[instances]
);

const canAcceptChild = useCallback(
(instanceId: Instance["id"]) => {
const instance = instancesIndex.instancesById.get(instanceId);
const instance = instances.get(instanceId);
if (instance === undefined) {
return false;
}
const meta = getComponentMeta(instance.component);
return meta?.type === "body" || meta?.type === "container";
},
[instancesIndex]
[instances]
);

const getItemChildren = useCallback(
(instanceId: Instance["id"]) => {
const instance = instancesIndex.instancesById.get(instanceId);
return getInstanceChildren(instance);
const instance = instances.get(instanceId);
const children: InstancesItem[] = [];
if (instance === undefined) {
return children;
}

for (const child of instance.children) {
if (child.type !== "id") {
continue;
}
const childInstance = instances.get(child.value);
if (childInstance === undefined) {
continue;
}
children.push(childInstance);
}
return children;
},
[instancesIndex]
[instances]
);

return (
<Tree
{...props}
{...instanceRelatedProps}
findItemById={findItemById}
getItemPath={getItemPath}
canLeaveParent={canLeaveParent}
canAcceptChild={canAcceptChild}
getItemChildren={getItemChildren}
Expand Down
10 changes: 10 additions & 0 deletions apps/builder/app/shared/nano-states/nano-states.ts
Expand Up @@ -78,6 +78,16 @@ export const useSetInstances = (
});
};

export const rootInstanceStore = computed(
[instancesStore, selectedPageStore],
(instances, selectedPage) => {
if (selectedPage === undefined) {
return undefined;
}
return instances.get(selectedPage.rootInstanceId);
}
);

// @todo will be removed soon
const denormalizeTree = (
instances: Instances,
Expand Down
6 changes: 1 addition & 5 deletions packages/design-system/src/components/tree/item-utils.ts
Expand Up @@ -20,11 +20,7 @@ export const getElementByItemSelector = (
.map((id) => `[data-drop-target-id="${id}"]`)
.reverse()
.join(" ");
const [itemId] = itemSelector;
return (
root?.querySelector(`${domSelector} [data-item-button-id="${itemId}"]`) ??
undefined
);
return root?.querySelector(domSelector) ?? undefined;
};

export const getItemSelectorFromElement = (element: Element) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/design-system/src/components/tree/test-tree-data.ts
Expand Up @@ -52,7 +52,7 @@ export const reparent = (
}: {
itemSelector: ItemSelector;
dropTarget: {
itemId: string;
itemSelector: ItemSelector;
position: number | "end";
};
}
Expand All @@ -62,7 +62,7 @@ export const reparent = (
const path = getItemPath(draft, itemId);
const item = path[path.length - 1];
const currentParent = path[path.length - 2];
const newParent = findItemById(draft, dropTarget.itemId);
const newParent = findItemById(draft, dropTarget.itemSelector[0]);

if (
item === undefined ||
Expand Down
4 changes: 1 addition & 3 deletions packages/design-system/src/components/tree/tree.stories.tsx
@@ -1,7 +1,7 @@
import type { ComponentMeta } from "@storybook/react";
import { useState } from "react";
import { Tree } from "./tree";
import { findItemById, getItemPath, Item, reparent } from "./test-tree-data";
import { findItemById, Item, reparent } from "./test-tree-data";
import { Flex } from "../flex";
import { TreeItemLabel, TreeItemBody } from "./tree-node";
import type { ItemSelector } from "./item-utils";
Expand Down Expand Up @@ -73,8 +73,6 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
return (
<Flex css={{ width: 300, height: 500, flexDirection: "column" }}>
<Tree
findItemById={findItemById}
getItemPath={getItemPath}
canAcceptChild={(itemId) =>
findItemById(root, itemId)?.canAcceptChildren ?? false
}
Expand Down
81 changes: 34 additions & 47 deletions packages/design-system/src/components/tree/tree.tsx
Expand Up @@ -27,8 +27,6 @@ export type TreeProps<Data extends { id: string }> = {

canLeaveParent: (itemId: ItemId) => boolean;
canAcceptChild: (itemId: ItemId) => boolean;
findItemById: (root: Data, id: string) => Data | undefined;
getItemPath: (root: Data, id: string) => Data[];
getItemChildren: (itemId: ItemId) => Data[];
renderItem: (props: TreeItemRenderProps<Data>) => React.ReactNode;

Expand All @@ -37,7 +35,7 @@ export type TreeProps<Data extends { id: string }> = {
animate?: boolean;
onDragEnd: (event: {
itemSelector: ItemSelector;
dropTarget: { itemId: string; position: number | "end" };
dropTarget: { itemSelector: ItemSelector; position: number | "end" };
}) => void;
};

Expand All @@ -46,8 +44,6 @@ export const Tree = <Data extends { id: string }>({
selectedItemSelector,
canLeaveParent,
canAcceptChild,
findItemById,
getItemPath,
getItemChildren,
renderItem,
onSelect,
Expand Down Expand Up @@ -83,7 +79,7 @@ export const Tree = <Data extends { id: string }>({

const getFallbackDropTarget = () => {
return {
data: root,
data: [root.id],
element: getDropTargetElement(root.id) as HTMLElement,
final: true,
};
Expand All @@ -103,61 +99,62 @@ export const Tree = <Data extends { id: string }>({
},
});

const dropHandlers = useDrop<Data>({
const dropHandlers = useDrop<ItemSelector>({
emulatePointerAlwaysInRootBounds: true,

placementPadding: 0,

elementToData: (element) => {
const id = (element as HTMLElement).dataset.dropTargetId;
const instance = id && findItemById(root, id);
return instance || false;
return getItemSelectorFromElement(element);
},

swapDropTarget: (dropTarget) => {
if (dragItemSelector === undefined || dropTarget === undefined) {
return getFallbackDropTarget();
}

if (dropTarget.data.id === root.id) {
// drop target is the root
if (dropTarget.data.length === 1) {
return dropTarget;
}

const path = getItemPath(root, dropTarget.data.id);
path.reverse();
const newDropItemSelector = dropTarget.data.slice();

if (dropTarget.area === "top" || dropTarget.area === "bottom") {
path.shift();
newDropItemSelector.shift();
}

// Don't allow to drop inside drag item or any of its children
const [dragItemId] = dragItemSelector;
const dragItemIndex = path.findIndex(
(instance) => instance.id === dragItemId
);
const dragItemIndex = newDropItemSelector.indexOf(dragItemId);
if (dragItemIndex !== -1) {
path.splice(0, dragItemIndex + 1);
newDropItemSelector.splice(0, dragItemIndex + 1);
}

const data = path.find((item) => canAcceptChild(item.id));

if (data === undefined) {
// select closest droppable
const ancestorIndex = newDropItemSelector.findIndex((itemId) =>
canAcceptChild(itemId)
);
if (ancestorIndex === -1) {
return getFallbackDropTarget();
}
newDropItemSelector.slice(ancestorIndex);

const element = getDropTargetElement(data.id);
const element = getElementByItemSelector(
rootRef.current ?? undefined,
newDropItemSelector
);

if (element == null) {
if (element === undefined) {
return getFallbackDropTarget();
}

return { data, element, final: true };
return { data: newDropItemSelector, element, final: true };
},

onDropTargetChange: (dropTarget) => {
const itemDropTarget = {
itemSelector: getItemSelectorFromElement(dropTarget.element),
data: dropTarget.data,
itemSelector: dropTarget.data,
rect: dropTarget.rect,
indexWithinChildren: dropTarget.indexWithinChildren,
placement: dropTarget.placement,
Expand All @@ -178,35 +175,24 @@ export const Tree = <Data extends { id: string }>({
childrenOrientation: { type: "vertical", reverse: false },
});

const dragHandlers = useDrag<Data>({
const dragHandlers = useDrag<ItemSelector>({
shiftDistanceThreshold: INDENT,

elementToData: (element) => {
const dragItemElement = element.closest("[data-drag-item-id]");
if (!(dragItemElement instanceof HTMLElement)) {
return false;
}
const id = dragItemElement.dataset.dragItemId;
if (id === undefined || id === root.id) {
const dragItemSelector = getItemSelectorFromElement(element);
// tree root is not draggable
if (dragItemSelector.length === 1) {
return false;
}

const instance = findItemById(root, id);

if (instance === undefined) {
return false;
}

if (canLeaveParent(instance.id) === false) {
const [dragItemId] = dragItemSelector;
if (canLeaveParent(dragItemId) === false) {
return false;
}

return instance;
return dragItemSelector;
},
onStart: ({ data }) => {
const itemSelector = getItemPath(root, data.id)
.reverse()
.map((item) => item.id);
onStart: ({ data: itemSelector }) => {
onSelect?.(itemSelector);
setDragItemSelector(itemSelector);
dropHandlers.handleStart();
Expand All @@ -224,7 +210,7 @@ export const Tree = <Data extends { id: string }>({
onDragEnd({
itemSelector: dragItemSelector,
dropTarget: {
itemId: shiftedDropTarget.itemSelector[0],
itemSelector: shiftedDropTarget.itemSelector,
position: shiftedDropTarget.position,
},
});
Expand Down Expand Up @@ -384,10 +370,11 @@ const useKeyboardNavigation = <Data extends { id: string }>({

const setFocus = useCallback(
(itemSelector: ItemSelector, reason: "restoring" | "changing") => {
const [itemId] = itemSelector;
const itemButton = getElementByItemSelector(
rootRef.current ?? undefined,
itemSelector
);
)?.querySelector(`[data-item-button-id="${itemId}"]`);
if (itemButton instanceof HTMLElement) {
itemButton.focus({ preventScroll: reason === "restoring" });
}
Expand Down