feat(core): optimize the shift multi-selection function of doc list (#6675)

close TOV-701

https://github.com/toeverything/AFFiNE/assets/102217452/8813e603-1cc2-469f-a7c1-b18e49a14871
This commit is contained in:
JimmFly
2024-04-24 07:56:13 +00:00
parent 6525c99631
commit 350fec5397
7 changed files with 153 additions and 12 deletions

View File

@@ -137,6 +137,7 @@ export const dateCell = style({
flexShrink: 0,
flexWrap: 'nowrap',
padding: '0 8px',
userSelect: 'none',
});
export const actionsCellWrapper = style({
display: 'flex',

View File

@@ -4,10 +4,15 @@ import { TagService } from '@affine/core/modules/tag';
import { useDraggable } from '@dnd-kit/core';
import { useLiveData, useService } from '@toeverything/infra';
import type { PropsWithChildren } from 'react';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
import { selectionStateAtom, useAtom } from '../scoped-atoms';
import {
anchorIndexAtom,
rangeIdsAtom,
selectionStateAtom,
useAtom,
} from '../scoped-atoms';
import type { DraggableTitleCellData, PageListItemProps } from '../types';
import { useAllDocDisplayProperties } from '../use-all-doc-display-properties';
import { ColWrapper, formatDate, stopPropagation } from '../utils';
@@ -167,6 +172,7 @@ export const PageListItem = (props: PageListItemProps) => {
pageId={props.pageId}
draggable={props.draggable}
isDragging={isDragging}
pageIds={props.pageIds || []}
>
<ColWrapper flex={9}>
<ColWrapper
@@ -227,6 +233,7 @@ export const PageListItem = (props: PageListItemProps) => {
type PageListWrapperProps = PropsWithChildren<
Pick<PageListItemProps, 'to' | 'pageId' | 'onClick' | 'draggable'> & {
isDragging: boolean;
pageIds: string[];
}
>;
@@ -234,26 +241,84 @@ function PageListItemWrapper({
to,
isDragging,
pageId,
pageIds,
onClick,
children,
draggable,
}: PageListWrapperProps) {
const [selectionState, setSelectionActive] = useAtom(selectionStateAtom);
const [anchorIndex, setAnchorIndex] = useAtom(anchorIndexAtom);
const [rangeIds, setRangeIds] = useAtom(rangeIdsAtom);
const handleShiftClick = useCallback(
(currentIndex: number) => {
if (anchorIndex === undefined) {
setAnchorIndex(currentIndex);
onClick?.();
return;
}
const lowerIndex = Math.min(anchorIndex, currentIndex);
const upperIndex = Math.max(anchorIndex, currentIndex);
const newRangeIds = pageIds.slice(lowerIndex, upperIndex + 1);
const currentSelected = selectionState.selectedIds || [];
// Set operations
const setRange = new Set(rangeIds);
const newSelected = new Set(
currentSelected.filter(id => !setRange.has(id)).concat(newRangeIds)
);
selectionState.onSelectedIdsChange?.([...newSelected]);
setRangeIds(newRangeIds);
},
[
anchorIndex,
onClick,
pageIds,
selectionState,
setAnchorIndex,
rangeIds,
setRangeIds,
]
);
const handleClick = useCallback(
(e: React.MouseEvent) => {
if (!selectionState.selectable) {
return false;
}
stopPropagation(e);
const currentIndex = pageIds.indexOf(pageId);
if (e.shiftKey) {
setSelectionActive(true);
onClick?.();
if (!selectionState.selectionActive) {
setSelectionActive(true);
setAnchorIndex(currentIndex);
onClick?.();
return true;
}
handleShiftClick(currentIndex);
return true;
} else {
setAnchorIndex(undefined);
setRangeIds([]);
onClick?.();
return false;
}
onClick?.();
return false;
},
[onClick, selectionState.selectable, setSelectionActive]
[
handleShiftClick,
onClick,
pageId,
pageIds,
selectionState.selectable,
selectionState.selectionActive,
setAnchorIndex,
setRangeIds,
setSelectionActive,
]
);
const commonProps = useMemo(
@@ -269,6 +334,28 @@ function PageListItemWrapper({
[pageId, draggable, onClick, to, isDragging, handleClick]
);
useEffect(() => {
if (selectionState.selectionActive) {
// listen for shift key up
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setAnchorIndex(undefined);
setRangeIds([]);
}
};
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keyup', handleKeyUp);
};
}
return;
}, [
selectionState.selectionActive,
setAnchorIndex,
setRangeIds,
setSelectionActive,
]);
if (to) {
return (
<WorkbenchLink {...commonProps} to={to}>

View File

@@ -120,6 +120,7 @@ export const tagLabel = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
userSelect: 'none',
});
export const tagRemove = style({

View File

@@ -23,6 +23,7 @@ import { PagePreview } from './page-content-preview';
import * as styles from './page-group.css';
import {
groupCollapseStateAtom,
groupsAtom,
listPropsAtom,
selectionStateAtom,
useAtom,
@@ -224,13 +225,21 @@ const listsPropsAtom = selectAtom(
export const PageListItemRenderer = (item: ListItem) => {
const props = useAtomValue(listsPropsAtom);
const { selectionActive } = useAtomValue(selectionStateAtom);
const groups = useAtomValue(groupsAtom);
const pageItems = groups.flatMap(group => group.items).map(item => item.id);
const page = item as DocMeta;
return (
<PageListItem
{...pageMetaToListItemProp(page, {
...props,
selectable: !!selectionActive,
})}
{...pageMetaToListItemProp(
page,
{
...props,
selectable: !!selectionActive,
},
pageItems
)}
/>
);
};
@@ -298,7 +307,8 @@ const UnifiedPageIcon = ({
function pageMetaToListItemProp(
item: DocMeta,
props: RequiredProps<DocMeta>
props: RequiredProps<DocMeta>,
pageIds?: string[]
): PageListItemProps {
const toggleSelection = props.onSelectedIdsChange
? () => {
@@ -318,6 +328,7 @@ function pageMetaToListItemProp(
: undefined;
const itemProps: PageListItemProps = {
pageId: item.id,
pageIds,
title: <PageTitle id={item.id} />,
preview: (
<PagePreview docCollection={props.docCollection} pageId={item.id} />

View File

@@ -22,6 +22,10 @@ export const listPropsAtom = atom<
// whether or not the table is in selection mode (showing selection checkbox & selection floating bar)
const selectionActiveAtom = atom(false);
export const anchorIndexAtom = atom<number | undefined>(undefined);
export const rangeIdsAtom = atom<string[]>([]);
export const selectionStateAtom = atom(
get => {
const baseAtom = selectAtom(

View File

@@ -23,6 +23,7 @@ export type TagMeta = {
// using type instead of interface to make it Record compatible
export type PageListItemProps = {
pageId: string;
pageIds?: string[];
icon: JSX.Element;
title: ReactNode; // using ReactNode to allow for rich content rendering
preview?: ReactNode; // using ReactNode to allow for rich content rendering