136 lines
4.7 KiB
TypeScript
136 lines
4.7 KiB
TypeScript
import React, { useEffect, useCallback } from 'react';
|
|
import { useDocumentViewLogic, DocumentViewLogic } from './hooks/useDocumentViewLogic';
|
|
import { useDocumentsNavigation } from './hooks/useDocumentsNavigation';
|
|
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
|
import { usePreviewContext } from '../preview/PreviewContext';
|
|
import DocumentsListRow from './components/DocumentsListRow';
|
|
import DocumentsGridCard from './components/DocumentsGridCard';
|
|
import DocumentsListContainer from './components/DocumentsListContainer';
|
|
import DocumentsGridContainer from './components/DocumentsGridContainer';
|
|
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
|
|
|
interface AbstractDocumentsViewProps<CProps extends { clearSelection: () => void; children: React.ReactNode }> extends DocumentsViewProps {
|
|
ContainerComponent: React.ComponentType<CProps>;
|
|
ItemComponent: React.ComponentType<{ entry: any; viewLogic: DocumentViewLogic } & DocumentsViewProps>;
|
|
containerProps?: Omit<CProps, 'children' | 'clearSelection'>;
|
|
[key: string]: any;
|
|
}
|
|
|
|
const AbstractDocumentsView = <CProps extends { clearSelection: () => void; children: React.ReactNode }>({
|
|
ContainerComponent,
|
|
ItemComponent,
|
|
containerProps,
|
|
...props
|
|
}: AbstractDocumentsViewProps<CProps>) => {
|
|
const { entries, onDocumentRename, onFolderRename, onFolderSelect, scrollRef, viewId } = props;
|
|
const { openPreview } = usePreviewContext();
|
|
const viewLogic = useDocumentViewLogic({
|
|
onDocumentRename,
|
|
onFolderRename,
|
|
});
|
|
const { handleKeyDown, handleFocus } = useDocumentsNavigation({
|
|
entries,
|
|
onFolderSelect,
|
|
onPreview: openPreview,
|
|
viewMode: props.viewMode,
|
|
scrollRef: props.scrollRef,
|
|
});
|
|
const { clearSelection } = viewLogic;
|
|
|
|
useEffect(() => {
|
|
if (scrollRef?.current) {
|
|
scrollRef.current.scrollTop = 0;
|
|
}
|
|
}, [scrollRef, viewId]);
|
|
|
|
const { focusedEntryKey } = useWorkspaceSelectionContext();
|
|
|
|
const ensureFocusedEntryVisible = useCallback(() => {
|
|
if (!focusedEntryKey) return;
|
|
const container = scrollRef?.current;
|
|
if (!container) return;
|
|
let selector = null;
|
|
if (focusedEntryKey.startsWith('document:')) {
|
|
selector = `#document-${focusedEntryKey.slice('document:'.length)}`;
|
|
} else if (focusedEntryKey.startsWith('folder:')) {
|
|
selector = `#folder-${focusedEntryKey.slice('folder:'.length)}`;
|
|
}
|
|
if (!selector) {
|
|
return;
|
|
}
|
|
const entry = container.querySelector(selector) as HTMLElement;
|
|
if (!entry || !container.contains(entry)) {
|
|
return;
|
|
}
|
|
|
|
entry.scrollIntoView({ block: 'nearest' });
|
|
}, [focusedEntryKey, scrollRef]);
|
|
|
|
useEffect(() => {
|
|
ensureFocusedEntryVisible();
|
|
}, [ensureFocusedEntryVisible]);
|
|
|
|
const { detailPanelOpen } = usePanelManager();
|
|
|
|
useEffect(() => {
|
|
const container = scrollRef?.current;
|
|
if (!container) return;
|
|
|
|
const handleTransitionEnd = () => {
|
|
ensureFocusedEntryVisible();
|
|
};
|
|
|
|
container.addEventListener('transitionend', handleTransitionEnd);
|
|
|
|
// Immediate check in case there is no transition or it finished already
|
|
ensureFocusedEntryVisible();
|
|
|
|
return () => {
|
|
container.removeEventListener('transitionend', handleTransitionEnd);
|
|
};
|
|
}, [scrollRef, ensureFocusedEntryVisible, detailPanelOpen]);
|
|
|
|
return (
|
|
<ContainerComponent
|
|
clearSelection={clearSelection}
|
|
onKeyDown={handleKeyDown}
|
|
onFocus={handleFocus}
|
|
tabIndex={0}
|
|
{...(containerProps as any)}
|
|
>
|
|
{entries.map((entry) => (
|
|
<ItemComponent
|
|
key={entry.key}
|
|
entry={entry}
|
|
viewLogic={viewLogic}
|
|
{...props}
|
|
onPreview={openPreview}
|
|
/>
|
|
))}
|
|
</ContainerComponent>
|
|
);
|
|
};
|
|
|
|
export const DocumentsList: React.FC<DocumentsViewProps> = (props) => {
|
|
return (
|
|
<AbstractDocumentsView
|
|
ContainerComponent={DocumentsListContainer}
|
|
ItemComponent={DocumentsListRow}
|
|
viewMode="list"
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const DocumentsGrid: React.FC<DocumentsViewProps & { gridIconSize?: number }> = (props) => {
|
|
return (
|
|
<AbstractDocumentsView
|
|
ContainerComponent={DocumentsGridContainer}
|
|
ItemComponent={DocumentsGridCard}
|
|
containerProps={{ gridIconSize: props.gridIconSize }}
|
|
viewMode="grid"
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|