mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 09:38:29 +02:00
Some sidebar fixes
This commit is contained in:
@@ -1,18 +1,23 @@
|
|||||||
import { type } from '@tauri-apps/plugin-os';
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
import { settingsAtom } from '@yaakapp-internal/models';
|
import { settingsAtom } from "@yaakapp-internal/models";
|
||||||
import classNames from 'classnames';
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from "jotai";
|
||||||
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
|
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from "react";
|
||||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
import { useIsFullscreen } from "../hooks/useIsFullscreen";
|
||||||
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
|
import {
|
||||||
import { WindowControls } from './WindowControls';
|
HEADER_SIZE_LG,
|
||||||
|
HEADER_SIZE_MD,
|
||||||
|
WINDOW_CONTROLS_WIDTH,
|
||||||
|
} from "../lib/constants";
|
||||||
|
import { WindowControls } from "./WindowControls";
|
||||||
|
|
||||||
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
|
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
size: 'md' | 'lg';
|
size: "md" | "lg";
|
||||||
ignoreControlsSpacing?: boolean;
|
ignoreControlsSpacing?: boolean;
|
||||||
onlyXWindowControl?: boolean;
|
onlyXWindowControl?: boolean;
|
||||||
|
hideControls?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderSize({
|
export function HeaderSize({
|
||||||
@@ -22,6 +27,7 @@ export function HeaderSize({
|
|||||||
ignoreControlsSpacing,
|
ignoreControlsSpacing,
|
||||||
onlyXWindowControl,
|
onlyXWindowControl,
|
||||||
children,
|
children,
|
||||||
|
hideControls,
|
||||||
}: HeaderSizeProps) {
|
}: HeaderSizeProps) {
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const isFullscreen = useIsFullscreen();
|
const isFullscreen = useIsFullscreen();
|
||||||
@@ -29,10 +35,10 @@ export function HeaderSize({
|
|||||||
const s = { ...style };
|
const s = { ...style };
|
||||||
|
|
||||||
// Set the height (use min-height because scaling font size may make it larger
|
// Set the height (use min-height because scaling font size may make it larger
|
||||||
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
|
if (size === "md") s.minHeight = HEADER_SIZE_MD;
|
||||||
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
|
if (size === "lg") s.minHeight = HEADER_SIZE_LG;
|
||||||
|
|
||||||
if (type() === 'macos') {
|
if (type() === "macos") {
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
// Add large padding for window controls
|
// Add large padding for window controls
|
||||||
s.paddingLeft = 72 / settings.interfaceScale;
|
s.paddingLeft = 72 / settings.interfaceScale;
|
||||||
@@ -57,21 +63,21 @@ export function HeaderSize({
|
|||||||
style={finalStyle}
|
style={finalStyle}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'pt-[1px]', // Make up for bottom border
|
"pt-[1px]", // Make up for bottom border
|
||||||
'select-none relative',
|
"select-none relative",
|
||||||
'w-full border-b border-border-subtle min-w-0',
|
"w-full border-b border-border-subtle min-w-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
|
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid',
|
"pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid",
|
||||||
'px-1', // Give it some space on either end for focus outlines
|
"px-1", // Give it some space on either end for focus outlines
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<WindowControls onlyX={onlyXWindowControl} />
|
{!hideControls && <WindowControls onlyX={onlyXWindowControl} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,11 +141,11 @@ export function Workspace() {
|
|||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'x-theme-sidebar',
|
'x-theme-sidebar',
|
||||||
'absolute top-0 left-0 bottom-0 bg-surface border-r border-border-subtle w-[14rem]',
|
'absolute top-0 left-0 bottom-0 bg-surface border-r border-border-subtle w-[20rem]',
|
||||||
'grid grid-rows-[auto_1fr]',
|
'grid grid-rows-[auto_1fr]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HeaderSize size="lg" className="border-transparent">
|
<HeaderSize hideControls size="lg" className="border-transparent flex items-center">
|
||||||
<SidebarActions />
|
<SidebarActions />
|
||||||
</HeaderSize>
|
</HeaderSize>
|
||||||
<ErrorBoundary name="Sidebar (Floating)">
|
<ErrorBoundary name="Sidebar (Floating)">
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
import type {
|
||||||
|
DragEndEvent,
|
||||||
|
DragMoveEvent,
|
||||||
|
DragStartEvent,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
MeasuringStrategy,
|
MeasuringStrategy,
|
||||||
@@ -7,11 +11,17 @@ import {
|
|||||||
useDroppable,
|
useDroppable,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from "@dnd-kit/core";
|
||||||
import { type } from '@tauri-apps/plugin-os';
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
import classNames from 'classnames';
|
import classNames from "classnames";
|
||||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
import type {
|
||||||
import React, {
|
ComponentType,
|
||||||
|
MouseEvent,
|
||||||
|
ReactElement,
|
||||||
|
Ref,
|
||||||
|
RefAttributes,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -20,14 +30,14 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from "react";
|
||||||
import { useKey, useKeyPressEvent } from 'react-use';
|
import { useKey, useKeyPressEvent } from "react-use";
|
||||||
import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey';
|
import type { HotkeyAction, HotKeyOptions } from "../../../hooks/useHotKey";
|
||||||
import { useHotKey } from '../../../hooks/useHotKey';
|
import { useHotKey } from "../../../hooks/useHotKey";
|
||||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
import { computeSideForDragMove } from "../../../lib/dnd";
|
||||||
import { jotaiStore } from '../../../lib/jotai';
|
import { jotaiStore } from "../../../lib/jotai";
|
||||||
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
import type { ContextMenuProps, DropdownItem } from "../Dropdown";
|
||||||
import { ContextMenu } from '../Dropdown';
|
import { ContextMenu } from "../Dropdown";
|
||||||
import {
|
import {
|
||||||
collapsedFamily,
|
collapsedFamily,
|
||||||
draggingIdsFamily,
|
draggingIdsFamily,
|
||||||
@@ -35,14 +45,23 @@ import {
|
|||||||
hoveredParentFamily,
|
hoveredParentFamily,
|
||||||
isCollapsedFamily,
|
isCollapsedFamily,
|
||||||
selectedIdsFamily,
|
selectedIdsFamily,
|
||||||
} from './atoms';
|
} from "./atoms";
|
||||||
import type { SelectableTreeNode, TreeNode } from './common';
|
import type { SelectableTreeNode, TreeNode } from "./common";
|
||||||
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from './common';
|
import {
|
||||||
import { TreeDragOverlay } from './TreeDragOverlay';
|
closestVisibleNode,
|
||||||
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from './TreeItem';
|
equalSubtree,
|
||||||
import type { TreeItemListProps } from './TreeItemList';
|
getSelectedItems,
|
||||||
import { TreeItemList } from './TreeItemList';
|
hasAncestor,
|
||||||
import { useSelectableItems } from './useSelectableItems';
|
} from "./common";
|
||||||
|
import { TreeDragOverlay } from "./TreeDragOverlay";
|
||||||
|
import type {
|
||||||
|
TreeItemClickEvent,
|
||||||
|
TreeItemHandle,
|
||||||
|
TreeItemProps,
|
||||||
|
} from "./TreeItem";
|
||||||
|
import type { TreeItemListProps } from "./TreeItemList";
|
||||||
|
import { TreeItemList } from "./TreeItemList";
|
||||||
|
import { useSelectableItems } from "./useSelectableItems";
|
||||||
|
|
||||||
/** So we re-calculate after expanding a folder during drag */
|
/** So we re-calculate after expanding a folder during drag */
|
||||||
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
|
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
|
||||||
@@ -51,15 +70,21 @@ export interface TreeProps<T extends { id: string }> {
|
|||||||
root: TreeNode<T>;
|
root: TreeNode<T>;
|
||||||
treeId: string;
|
treeId: string;
|
||||||
getItemKey: (item: T) => string;
|
getItemKey: (item: T) => string;
|
||||||
getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
getContextMenu?: (
|
||||||
|
items: T[],
|
||||||
|
) => ContextMenuProps["items"] | Promise<ContextMenuProps["items"]>;
|
||||||
ItemInner: ComponentType<{ treeId: string; item: T }>;
|
ItemInner: ComponentType<{ treeId: string; item: T }>;
|
||||||
ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>;
|
ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>;
|
||||||
ItemRightSlot?: ComponentType<{ treeId: string; item: T }>;
|
ItemRightSlot?: ComponentType<{ treeId: string; item: T }>;
|
||||||
className?: string;
|
className?: string;
|
||||||
onActivate?: (item: T) => void;
|
onActivate?: (item: T) => void;
|
||||||
onDragEnd?: (opt: { items: T[]; parent: T; children: T[]; insertAt: number }) => void;
|
onDragEnd?: (
|
||||||
|
opt: { items: T[]; parent: T; children: T[]; insertAt: number },
|
||||||
|
) => void;
|
||||||
hotkeys?: {
|
hotkeys?: {
|
||||||
actions: Partial<Record<HotkeyAction, { cb: (items: T[]) => void } & HotKeyOptions>>;
|
actions: Partial<
|
||||||
|
Record<HotkeyAction, { cb: (items: T[]) => void } & HotKeyOptions>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
getEditOptions?: (item: T) => {
|
getEditOptions?: (item: T) => {
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
@@ -96,19 +121,24 @@ function TreeInner<T extends { id: string }>(
|
|||||||
) {
|
) {
|
||||||
const treeRef = useRef<HTMLDivElement>(null);
|
const treeRef = useRef<HTMLDivElement>(null);
|
||||||
const selectableItems = useSelectableItems(root);
|
const selectableItems = useSelectableItems(root);
|
||||||
const [showContextMenu, setShowContextMenu] = useState<{
|
const [showContextMenu, setShowContextMenu] = useState<
|
||||||
items: DropdownItem[];
|
{
|
||||||
x: number;
|
items: DropdownItem[];
|
||||||
y: number;
|
x: number;
|
||||||
} | null>(null);
|
y: number;
|
||||||
|
} | null
|
||||||
|
>(null);
|
||||||
const treeItemRefs = useRef<Record<string, TreeItemHandle>>({});
|
const treeItemRefs = useRef<Record<string, TreeItemHandle>>({});
|
||||||
const handleAddTreeItemRef = useCallback((item: T, r: TreeItemHandle | null) => {
|
const handleAddTreeItemRef = useCallback(
|
||||||
if (r == null) {
|
(item: T, r: TreeItemHandle | null) => {
|
||||||
delete treeItemRefs.current[item.id];
|
if (r == null) {
|
||||||
} else {
|
delete treeItemRefs.current[item.id];
|
||||||
treeItemRefs.current[item.id] = r;
|
} else {
|
||||||
}
|
treeItemRefs.current[item.id] = r;
|
||||||
}, []);
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// Select the first item on first render
|
// Select the first item on first render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -146,7 +176,9 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
const ensureTabbableItem = useCallback(() => {
|
const ensureTabbableItem = useCallback(() => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId);
|
const lastSelectedItem = selectableItems.find((i) =>
|
||||||
|
i.node.item.id === lastSelectedId
|
||||||
|
);
|
||||||
if (lastSelectedItem == null) {
|
if (lastSelectedItem == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -184,7 +216,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
() => ({
|
() => ({
|
||||||
treeId,
|
treeId,
|
||||||
focus: tryFocus,
|
focus: tryFocus,
|
||||||
hasFocus: () => treeRef.current?.contains(document.activeElement) ?? false,
|
hasFocus: () =>
|
||||||
|
treeRef.current?.contains(document.activeElement) ?? false,
|
||||||
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
||||||
selectItem: (id) => {
|
selectItem: (id) => {
|
||||||
setSelected([id], false);
|
setSelected([id], false);
|
||||||
@@ -195,7 +228,9 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const items = getSelectedItems(treeId, selectableItems);
|
const items = getSelectedItems(treeId, selectableItems);
|
||||||
const menuItems = await getContextMenu(items);
|
const menuItems = await getContextMenu(items);
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const rect = lastSelectedId ? treeItemRefs.current[lastSelectedId]?.rect() : null;
|
const rect = lastSelectedId
|
||||||
|
? treeItemRefs.current[lastSelectedId]?.rect()
|
||||||
|
: null;
|
||||||
if (rect == null) return;
|
if (rect == null) return;
|
||||||
setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y });
|
setShowContextMenu({ items: menuItems, x: rect.x, y: rect.y });
|
||||||
},
|
},
|
||||||
@@ -217,42 +252,66 @@ function TreeInner<T extends { id: string }>(
|
|||||||
// If right-clicked an item that was NOT in the multiple-selection, just use that one
|
// If right-clicked an item that was NOT in the multiple-selection, just use that one
|
||||||
// Also update the selection with it
|
// Also update the selection with it
|
||||||
setSelected([item.id], false);
|
setSelected([item.id], false);
|
||||||
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
|
jotaiStore.set(
|
||||||
|
focusIdsFamily(treeId),
|
||||||
|
(prev) => ({ ...prev, lastId: item.id }),
|
||||||
|
);
|
||||||
return getContextMenu([item]);
|
return getContextMenu([item]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
||||||
|
|
||||||
const handleSelect = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
const handleSelect = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
||||||
(item, { shiftKey, metaKey, ctrlKey }) => {
|
(item, { shiftKey, metaKey, ctrlKey }) => {
|
||||||
const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId;
|
const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId;
|
||||||
const selectedIdsAtom = selectedIdsFamily(treeId);
|
const selectedIdsAtom = selectedIdsFamily(treeId);
|
||||||
const selectedIds = jotaiStore.get(selectedIdsAtom);
|
const selectedIds = jotaiStore.get(selectedIdsAtom);
|
||||||
|
|
||||||
// Mark the item as the last one selected
|
// Mark the item as the last one selected
|
||||||
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
|
jotaiStore.set(
|
||||||
|
focusIdsFamily(treeId),
|
||||||
|
(prev) => ({ ...prev, lastId: item.id }),
|
||||||
|
);
|
||||||
|
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
const anchorIndex = selectableItems.findIndex((i) => i.node.item.id === anchorSelectedId);
|
const anchorIndex = selectableItems.findIndex((i) =>
|
||||||
const currIndex = selectableItems.findIndex((v) => v.node.item.id === item.id);
|
i.node.item.id === anchorSelectedId
|
||||||
|
);
|
||||||
|
const currIndex = selectableItems.findIndex((v) =>
|
||||||
|
v.node.item.id === item.id
|
||||||
|
);
|
||||||
// Nothing was selected yet, so just select this item
|
// Nothing was selected yet, so just select this item
|
||||||
if (selectedIds.length === 0 || anchorIndex === -1 || currIndex === -1) {
|
if (
|
||||||
|
selectedIds.length === 0 || anchorIndex === -1 || currIndex === -1
|
||||||
|
) {
|
||||||
setSelected([item.id], true);
|
setSelected([item.id], true);
|
||||||
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, anchorId: item.id }));
|
jotaiStore.set(
|
||||||
|
focusIdsFamily(treeId),
|
||||||
|
(prev) => ({ ...prev, anchorId: item.id }),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
const validSelectableItems = getValidSelectableItems(
|
||||||
|
treeId,
|
||||||
|
selectableItems,
|
||||||
|
);
|
||||||
if (currIndex > anchorIndex) {
|
if (currIndex > anchorIndex) {
|
||||||
// Selecting down
|
// Selecting down
|
||||||
const itemsToSelect = validSelectableItems.slice(anchorIndex, currIndex + 1);
|
const itemsToSelect = validSelectableItems.slice(
|
||||||
|
anchorIndex,
|
||||||
|
currIndex + 1,
|
||||||
|
);
|
||||||
setSelected(
|
setSelected(
|
||||||
itemsToSelect.map((v) => v.node.item.id),
|
itemsToSelect.map((v) => v.node.item.id),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
} else if (currIndex < anchorIndex) {
|
} else if (currIndex < anchorIndex) {
|
||||||
// Selecting up
|
// Selecting up
|
||||||
const itemsToSelect = validSelectableItems.slice(currIndex, anchorIndex + 1);
|
const itemsToSelect = validSelectableItems.slice(
|
||||||
|
currIndex,
|
||||||
|
anchorIndex + 1,
|
||||||
|
);
|
||||||
setSelected(
|
setSelected(
|
||||||
itemsToSelect.map((v) => v.node.item.id),
|
itemsToSelect.map((v) => v.node.item.id),
|
||||||
true,
|
true,
|
||||||
@@ -260,7 +319,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
} else {
|
} else {
|
||||||
setSelected([item.id], true);
|
setSelected([item.id], true);
|
||||||
}
|
}
|
||||||
} else if (type() === 'macos' ? metaKey : ctrlKey) {
|
} else if (type() === "macos" ? metaKey : ctrlKey) {
|
||||||
const withoutCurr = selectedIds.filter((id) => id !== item.id);
|
const withoutCurr = selectedIds.filter((id) => id !== item.id);
|
||||||
if (withoutCurr.length === selectedIds.length) {
|
if (withoutCurr.length === selectedIds.length) {
|
||||||
// It wasn't in there, so add it
|
// It wasn't in there, so add it
|
||||||
@@ -272,13 +331,16 @@ function TreeInner<T extends { id: string }>(
|
|||||||
} else {
|
} else {
|
||||||
// Select single
|
// Select single
|
||||||
setSelected([item.id], true);
|
setSelected([item.id], true);
|
||||||
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, anchorId: item.id }));
|
jotaiStore.set(
|
||||||
|
focusIdsFamily(treeId),
|
||||||
|
(prev) => ({ ...prev, anchorId: item.id }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectableItems, setSelected, treeId],
|
[selectableItems, setSelected, treeId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
const handleClick = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
||||||
(item, e) => {
|
(item, e) => {
|
||||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||||
handleSelect(item, e);
|
handleSelect(item, e);
|
||||||
@@ -293,8 +355,13 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const selectPrevItem = useCallback(
|
const selectPrevItem = useCallback(
|
||||||
(e: TreeItemClickEvent) => {
|
(e: TreeItemClickEvent) => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
const validSelectableItems = getValidSelectableItems(
|
||||||
const index = validSelectableItems.findIndex((i) => i.node.item.id === lastSelectedId);
|
treeId,
|
||||||
|
selectableItems,
|
||||||
|
);
|
||||||
|
const index = validSelectableItems.findIndex((i) =>
|
||||||
|
i.node.item.id === lastSelectedId
|
||||||
|
);
|
||||||
const item = validSelectableItems[index - 1];
|
const item = validSelectableItems[index - 1];
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
handleSelect(item.node.item, e);
|
handleSelect(item.node.item, e);
|
||||||
@@ -306,8 +373,13 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const selectNextItem = useCallback(
|
const selectNextItem = useCallback(
|
||||||
(e: TreeItemClickEvent) => {
|
(e: TreeItemClickEvent) => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const validSelectableItems = getValidSelectableItems(treeId, selectableItems);
|
const validSelectableItems = getValidSelectableItems(
|
||||||
const index = validSelectableItems.findIndex((i) => i.node.item.id === lastSelectedId);
|
treeId,
|
||||||
|
selectableItems,
|
||||||
|
);
|
||||||
|
const index = validSelectableItems.findIndex((i) =>
|
||||||
|
i.node.item.id === lastSelectedId
|
||||||
|
);
|
||||||
const item = validSelectableItems[index + 1];
|
const item = validSelectableItems[index + 1];
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
handleSelect(item.node.item, e);
|
handleSelect(item.node.item, e);
|
||||||
@@ -320,7 +392,8 @@ function TreeInner<T extends { id: string }>(
|
|||||||
(e: TreeItemClickEvent) => {
|
(e: TreeItemClickEvent) => {
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem =
|
const lastSelectedItem =
|
||||||
selectableItems.find((i) => i.node.item.id === lastSelectedId)?.node ?? null;
|
selectableItems.find((i) => i.node.item.id === lastSelectedId)?.node ??
|
||||||
|
null;
|
||||||
if (lastSelectedItem?.parent != null) {
|
if (lastSelectedItem?.parent != null) {
|
||||||
handleSelect(lastSelectedItem.parent.item, e);
|
handleSelect(lastSelectedItem.parent.item, e);
|
||||||
}
|
}
|
||||||
@@ -329,7 +402,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k',
|
(e) => e.key === "ArrowUp" || e.key.toLowerCase() === "k",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -340,7 +413,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j',
|
(e) => e.key === "ArrowDown" || e.key.toLowerCase() === "j",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -352,21 +425,26 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
|
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === 'ArrowRight' || e.key === 'l',
|
(e) => e.key === "ArrowRight" || e.key === "l",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId);
|
const lastSelectedItem = selectableItems.find((i) =>
|
||||||
|
i.node.item.id === lastSelectedId
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastSelectedId &&
|
lastSelectedId &&
|
||||||
lastSelectedItem?.node.children != null &&
|
lastSelectedItem?.node.children != null &&
|
||||||
collapsed[lastSelectedItem.node.item.id] === true
|
collapsed[lastSelectedItem.node.item.id] === true
|
||||||
) {
|
) {
|
||||||
jotaiStore.set(isCollapsedFamily({ treeId, itemId: lastSelectedId }), false);
|
jotaiStore.set(
|
||||||
|
isCollapsedFamily({ treeId, itemId: lastSelectedId }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectNextItem(e);
|
selectNextItem(e);
|
||||||
}
|
}
|
||||||
@@ -378,21 +456,26 @@ function TreeInner<T extends { id: string }>(
|
|||||||
// If the selected item is in a folder, select its parent.
|
// If the selected item is in a folder, select its parent.
|
||||||
// If the selected item is an expanded folder, collapse it.
|
// If the selected item is an expanded folder, collapse it.
|
||||||
useKey(
|
useKey(
|
||||||
(e) => e.key === 'ArrowLeft' || e.key === 'h',
|
(e) => e.key === "ArrowLeft" || e.key === "h",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!isTreeFocused()) return;
|
if (!isTreeFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
const collapsed = jotaiStore.get(collapsedFamily(treeId));
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
const lastSelectedItem = selectableItems.find((i) => i.node.item.id === lastSelectedId);
|
const lastSelectedItem = selectableItems.find((i) =>
|
||||||
|
i.node.item.id === lastSelectedId
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastSelectedId &&
|
lastSelectedId &&
|
||||||
lastSelectedItem?.node.children != null &&
|
lastSelectedItem?.node.children != null &&
|
||||||
collapsed[lastSelectedItem.node.item.id] !== true
|
collapsed[lastSelectedItem.node.item.id] !== true
|
||||||
) {
|
) {
|
||||||
jotaiStore.set(isCollapsedFamily({ treeId, itemId: lastSelectedId }), true);
|
jotaiStore.set(
|
||||||
|
isCollapsedFamily({ treeId, itemId: lastSelectedId }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectParentItem(e);
|
selectParentItem(e);
|
||||||
}
|
}
|
||||||
@@ -401,7 +484,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
[selectableItems, handleSelect],
|
[selectableItems, handleSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeyPressEvent('Escape', async () => {
|
useKeyPressEvent("Escape", async () => {
|
||||||
if (!treeRef.current?.contains(document.activeElement)) return;
|
if (!treeRef.current?.contains(document.activeElement)) return;
|
||||||
clearDragState();
|
clearDragState();
|
||||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||||
@@ -428,24 +511,6 @@ function TreeInner<T extends { id: string }>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overSelectableItem = selectableItems.find((i) => i.node.item.id === over.id) ?? null;
|
|
||||||
if (overSelectableItem == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const draggingItems = jotaiStore.get(draggingIdsFamily(treeId));
|
|
||||||
for (const id of draggingItems) {
|
|
||||||
const item = selectableItems.find((i) => i.node.item.id === id)?.node ?? null;
|
|
||||||
if (item == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSameParent = item.parent?.item.id === overSelectableItem.node.parent?.item.id;
|
|
||||||
if (item.localDrag && !isSameParent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root is anything past the end of the list, so set it to the end
|
// Root is anything past the end of the list, so set it to the end
|
||||||
const hoveringRoot = over.id === root.item.id;
|
const hoveringRoot = over.id === root.item.id;
|
||||||
if (hoveringRoot) {
|
if (hoveringRoot) {
|
||||||
@@ -458,18 +523,41 @@ function TreeInner<T extends { id: string }>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const overSelectableItem =
|
||||||
|
selectableItems.find((i) => i.node.item.id === over.id) ?? null;
|
||||||
|
if (overSelectableItem == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggingItems = jotaiStore.get(draggingIdsFamily(treeId));
|
||||||
|
for (const id of draggingItems) {
|
||||||
|
const item = selectableItems.find((i) => i.node.item.id === id)?.node ??
|
||||||
|
null;
|
||||||
|
if (item == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameParent =
|
||||||
|
item.parent?.item.id === overSelectableItem.node.parent?.item.id;
|
||||||
|
if (item.localDrag && !isSameParent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const node = overSelectableItem.node;
|
const node = overSelectableItem.node;
|
||||||
const side = computeSideForDragMove(node.item.id, e);
|
const side = computeSideForDragMove(node.item.id, e);
|
||||||
|
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
let hoveredParent = node.parent;
|
let hoveredParent = node.parent;
|
||||||
const dragIndex = selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
|
const dragIndex =
|
||||||
|
selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
|
||||||
const hovered = selectableItems[dragIndex]?.node ?? null;
|
const hovered = selectableItems[dragIndex]?.node ?? null;
|
||||||
const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
const hoveredIndex = dragIndex + (side === "above" ? 0 : 1);
|
||||||
let hoveredChildIndex = overSelectableItem.index + (side === 'above' ? 0 : 1);
|
let hoveredChildIndex = overSelectableItem.index +
|
||||||
|
(side === "above" ? 0 : 1);
|
||||||
|
|
||||||
// Move into the folder if it's open and we're moving below it
|
// Move into the folder if it's open and we're moving below it
|
||||||
if (hovered?.children != null && side === 'below') {
|
if (hovered?.children != null && side === "below") {
|
||||||
hoveredParent = hovered;
|
hoveredParent = hovered;
|
||||||
hoveredChildIndex = 0;
|
hoveredChildIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -487,7 +575,12 @@ function TreeInner<T extends { id: string }>(
|
|||||||
childIndex === existing.childIndex
|
childIndex === existing.childIndex
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
jotaiStore.set(hoveredParentFamily(treeId), { parentId, parentDepth, index, childIndex });
|
jotaiStore.set(hoveredParentFamily(treeId), {
|
||||||
|
parentId,
|
||||||
|
parentDepth,
|
||||||
|
index,
|
||||||
|
childIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[root.depth, root.item.id, selectableItems, treeId],
|
[root.depth, root.item.id, selectableItems, treeId],
|
||||||
@@ -496,7 +589,9 @@ function TreeInner<T extends { id: string }>(
|
|||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
function handleDragStart(e: DragStartEvent) {
|
function handleDragStart(e: DragStartEvent) {
|
||||||
const selectedItems = getSelectedItems(treeId, selectableItems);
|
const selectedItems = getSelectedItems(treeId, selectableItems);
|
||||||
const isDraggingSelectedItem = selectedItems.find((i) => i.id === e.active.id);
|
const isDraggingSelectedItem = selectedItems.find((i) =>
|
||||||
|
i.id === e.active.id
|
||||||
|
);
|
||||||
|
|
||||||
// If we started dragging an already-selected item, we'll use that
|
// If we started dragging an already-selected item, we'll use that
|
||||||
if (isDraggingSelectedItem) {
|
if (isDraggingSelectedItem) {
|
||||||
@@ -506,11 +601,17 @@ function TreeInner<T extends { id: string }>(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If we started dragging a non-selected item, only drag that item
|
// If we started dragging a non-selected item, only drag that item
|
||||||
const activeItem = selectableItems.find((i) => i.node.item.id === e.active.id)?.node.item;
|
const activeItem = selectableItems.find((i) =>
|
||||||
|
i.node.item.id === e.active.id
|
||||||
|
)?.node.item;
|
||||||
if (activeItem != null) {
|
if (activeItem != null) {
|
||||||
jotaiStore.set(draggingIdsFamily(treeId), [activeItem.id]);
|
jotaiStore.set(draggingIdsFamily(treeId), [activeItem.id]);
|
||||||
// Also update selection to just be this one
|
// Also update selection to just be this one
|
||||||
handleSelect(activeItem, { shiftKey: false, metaKey: false, ctrlKey: false });
|
handleSelect(activeItem, {
|
||||||
|
shiftKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -543,25 +644,30 @@ function TreeInner<T extends { id: string }>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoveredParentS =
|
const hoveredParentS = hoveredParentId === root.item.id
|
||||||
hoveredParentId === root.item.id
|
? { node: root, depth: 0, index: 0 }
|
||||||
? { node: root, depth: 0, index: 0 }
|
: (selectableItems.find((i) => i.node.item.id === hoveredParentId) ??
|
||||||
: (selectableItems.find((i) => i.node.item.id === hoveredParentId) ?? null);
|
null);
|
||||||
const hoveredParent = hoveredParentS?.node ?? null;
|
const hoveredParent = hoveredParentS?.node ?? null;
|
||||||
|
|
||||||
if (hoveredParent == null || hoveredIndex == null || !draggingItems?.length) {
|
if (
|
||||||
|
hoveredParent == null || hoveredIndex == null || !draggingItems?.length
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the actual tree nodes for each dragged item (keeps order of draggingItems)
|
// Resolve the actual tree nodes for each dragged item (keeps order of draggingItems)
|
||||||
const draggedNodes: TreeNode<T>[] = draggingItems
|
const draggedNodes: TreeNode<T>[] = draggingItems
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
return selectableItems.find((i) => i.node.item.id === id)?.node ?? null;
|
return selectableItems.find((i) => i.node.item.id === id)?.node ??
|
||||||
|
null;
|
||||||
})
|
})
|
||||||
.filter((n) => n != null)
|
.filter((n) => n != null)
|
||||||
// Filter out invalid drags (dragging into descendant)
|
// Filter out invalid drags (dragging into descendant)
|
||||||
.filter(
|
.filter(
|
||||||
(n) => hoveredParent.item.id !== n.item.id && !hasAncestor(hoveredParent, n.item.id),
|
(n) =>
|
||||||
|
hoveredParent.item.id !== n.item.id &&
|
||||||
|
!hasAncestor(hoveredParent, n.item.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Work on a local copy of target children
|
// Work on a local copy of target children
|
||||||
@@ -590,7 +696,7 @@ function TreeInner<T extends { id: string }>(
|
|||||||
|
|
||||||
const treeItemListProps: Omit<
|
const treeItemListProps: Omit<
|
||||||
TreeItemListProps<T>,
|
TreeItemListProps<T>,
|
||||||
'nodes' | 'treeId' | 'activeIdAtom' | 'hoveredParent' | 'hoveredIndex'
|
"nodes" | "treeId" | "activeIdAtom" | "hoveredParent" | "hoveredIndex"
|
||||||
> = {
|
> = {
|
||||||
getItemKey,
|
getItemKey,
|
||||||
getContextMenu: handleGetContextMenu,
|
getContextMenu: handleGetContextMenu,
|
||||||
@@ -613,11 +719,17 @@ function TreeInner<T extends { id: string }>(
|
|||||||
[getContextMenu],
|
[getContextMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TreeHotKeys treeId={treeId} hotkeys={hotkeys} selectableItems={selectableItems} />
|
<TreeHotKeys
|
||||||
|
treeId={treeId}
|
||||||
|
hotkeys={hotkeys}
|
||||||
|
selectableItems={selectableItems}
|
||||||
|
/>
|
||||||
{showContextMenu && (
|
{showContextMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
items={showContextMenu.items}
|
items={showContextMenu.items}
|
||||||
@@ -632,32 +744,31 @@ function TreeInner<T extends { id: string }>(
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
onDragCancel={clearDragState}
|
onDragCancel={clearDragState}
|
||||||
onDragAbort={clearDragState}
|
onDragAbort={clearDragState}
|
||||||
measuring={measuring}
|
|
||||||
onDragMove={handleDragMove}
|
onDragMove={handleDragMove}
|
||||||
|
measuring={measuring}
|
||||||
autoScroll
|
autoScroll
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={treeRef}
|
ref={treeRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'outline-none h-full',
|
"outline-none h-full",
|
||||||
'overflow-y-auto overflow-x-hidden',
|
"overflow-y-auto overflow-x-hidden",
|
||||||
'grid grid-rows-[auto_1fr]',
|
"grid grid-rows-[auto_1fr]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'[&_.tree-item.selected_.tree-item-inner]:text-text',
|
"[&_.tree-item.selected_.tree-item-inner]:text-text",
|
||||||
'[&:focus-within]:[&_.tree-item.selected]:bg-surface-active',
|
"[&:focus-within]:[&_.tree-item.selected]:bg-surface-active",
|
||||||
'[&:not(:focus-within)]:[&_.tree-item.selected]:bg-surface-highlight',
|
"[&:not(:focus-within)]:[&_.tree-item.selected]:bg-surface-highlight",
|
||||||
|
|
||||||
// Round the items, but only if the ends of the selection.
|
// Round the items, but only if the ends of the selection.
|
||||||
// Also account for the drop marker being in between items
|
// Also account for the drop marker being in between items
|
||||||
'[&_.tree-item]:rounded-md',
|
"[&_.tree-item]:rounded-md",
|
||||||
'[&_.tree-item.selected+.tree-item.selected]:rounded-t-none',
|
"[&_.tree-item.selected+.tree-item.selected]:rounded-t-none",
|
||||||
'[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none',
|
"[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none",
|
||||||
'[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none',
|
"[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none",
|
||||||
'[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none',
|
"[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TreeItemList
|
<TreeItemList
|
||||||
@@ -668,7 +779,10 @@ function TreeInner<T extends { id: string }>(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Assign root ID so we can reuse our same move/end logic */}
|
{/* Assign root ID so we can reuse our same move/end logic */}
|
||||||
<DropRegionAfterList id={root.item.id} onContextMenu={handleContextMenu} />
|
<DropRegionAfterList
|
||||||
|
id={root.item.id}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TreeDragOverlay
|
<TreeDragOverlay
|
||||||
treeId={treeId}
|
treeId={treeId}
|
||||||
@@ -690,7 +804,10 @@ export const Tree = memo(
|
|||||||
Tree_,
|
Tree_,
|
||||||
({ root: prevNode, ...prevProps }, { root: nextNode, ...nextProps }) => {
|
({ root: prevNode, ...prevProps }, { root: nextNode, ...nextProps }) => {
|
||||||
for (const key of Object.keys(prevProps)) {
|
for (const key of Object.keys(prevProps)) {
|
||||||
if (prevProps[key as keyof typeof prevProps] !== nextProps[key as keyof typeof nextProps]) {
|
if (
|
||||||
|
prevProps[key as keyof typeof prevProps] !==
|
||||||
|
nextProps[key as keyof typeof nextProps]
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +823,9 @@ function DropRegionAfterList({
|
|||||||
onContextMenu?: (e: MouseEvent<HTMLDivElement>) => void;
|
onContextMenu?: (e: MouseEvent<HTMLDivElement>) => void;
|
||||||
}) {
|
}) {
|
||||||
const { setNodeRef } = useDroppable({ id });
|
const { setNodeRef } = useDroppable({ id });
|
||||||
return <div ref={setNodeRef} onContextMenu={onContextMenu} />;
|
return (
|
||||||
|
<div ref={setNodeRef} onContextMenu={onContextMenu} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TreeHotKeyProps<T extends { id: string }> {
|
interface TreeHotKeyProps<T extends { id: string }> {
|
||||||
@@ -735,7 +854,7 @@ function TreeHotKey<T extends { id: string }>({
|
|||||||
...options,
|
...options,
|
||||||
enable: () => {
|
enable: () => {
|
||||||
if (enable == null) return true;
|
if (enable == null) return true;
|
||||||
if (typeof enable === 'function') return enable();
|
if (typeof enable === "function") return enable();
|
||||||
else return enable;
|
else return enable;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -749,7 +868,7 @@ function TreeHotKeys<T extends { id: string }>({
|
|||||||
selectableItems,
|
selectableItems,
|
||||||
}: {
|
}: {
|
||||||
treeId: string;
|
treeId: string;
|
||||||
hotkeys: TreeProps<T>['hotkeys'];
|
hotkeys: TreeProps<T>["hotkeys"];
|
||||||
selectableItems: SelectableTreeNode<T>[];
|
selectableItems: SelectableTreeNode<T>[];
|
||||||
}) {
|
}) {
|
||||||
if (hotkeys == null) return null;
|
if (hotkeys == null) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user