mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
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:
@@ -137,6 +137,7 @@ export const dateCell = style({
|
||||
flexShrink: 0,
|
||||
flexWrap: 'nowrap',
|
||||
padding: '0 8px',
|
||||
userSelect: 'none',
|
||||
});
|
||||
export const actionsCellWrapper = style({
|
||||
display: 'flex',
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -120,6 +120,7 @@ export const tagLabel = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
export const tagRemove = style({
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user