feat(core): add draghandle to doc page title (#9079)

fix AF-1846
This commit is contained in:
pengx17
2024-12-10 02:13:08 +00:00
parent 115caa7cc6
commit 4335b0dc79
11 changed files with 161 additions and 75 deletions

View File

@@ -2,8 +2,8 @@ import { Loading, Scrollable } from '@affine/component';
import { EditorLoading } from '@affine/component/page-detail-skeleton';
import { Button, IconButton } from '@affine/component/ui/button';
import { Modal, useConfirmModal } from '@affine/component/ui/modal';
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { EditorService } from '@affine/core/modules/editor';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
@@ -433,7 +433,10 @@ const PageHistoryManager = ({
const editor = useService(EditorService).editor;
const [mode, setMode] = useState<DocMode>(editor.mode$.value);
const title = useDocCollectionPageTitle(docCollection, pageId);
const docDisplayMetaService = useService(DocDisplayMetaService);
const i18n = useI18n();
const title = useLiveData(docDisplayMetaService.title$(pageId));
const onConfirmRestore = useCallback(() => {
openConfirmModal({
@@ -467,7 +470,7 @@ const PageHistoryManager = ({
snapshotPage={snapshotPage}
mode={mode}
onModeChange={setMode}
title={title}
title={i18n.t(title)}
/>
<PageHistoryList

View File

@@ -3,7 +3,6 @@ import { style, type StyleRule } from '@vanilla-extract/css';
export const docEditorRoot = style({
display: 'block',
background: cssVar('backgroundPrimaryColor'),
overflowX: 'clip',
});

View File

@@ -1,61 +0,0 @@
import { assertExists } from '@blocksuite/affine/global/utils';
import type { DocCollection } from '@blocksuite/affine/store';
import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { useJournalInfoHelper } from './use-journal';
const weakMap = new WeakMap<DocCollection, Map<string, Atom<string>>>();
function getAtom(w: DocCollection, pageId: string): Atom<string> {
if (!weakMap.has(w)) {
weakMap.set(w, new Map());
}
const map = weakMap.get(w);
assertExists(map);
if (!map.has(pageId)) {
const baseAtom = atom<string>(w.getDoc(pageId)?.meta?.title || 'Untitled');
baseAtom.onMount = set => {
const disposable = w.meta.docMetaUpdated.on(() => {
const page = w.getDoc(pageId);
set(page?.meta?.title || 'Untitled');
});
return () => {
disposable.dispose();
};
};
map.set(pageId, baseAtom);
return baseAtom;
} else {
return map.get(pageId) as Atom<string>;
}
}
/**
* @deprecated use `useDocTitle(docId: string)` instead
*/
export function useDocCollectionPageTitle(
docCollection: DocCollection,
pageId: string
) {
const titleAtom = getAtom(docCollection, pageId);
assertExists(titleAtom);
const title = useAtomValue(titleAtom);
const { localizedJournalDate } = useJournalInfoHelper(pageId);
return localizedJournalDate || title;
}
// This hook is NOT reactive to the page title change
export function useGetDocCollectionPageTitle(docCollection: DocCollection) {
const { getLocalizedJournalDateString } = useJournalInfoHelper();
return useCallback(
(pageId: string) => {
return (
getLocalizedJournalDateString(pageId) ||
docCollection.getDoc(pageId)?.meta?.title
);
},
[docCollection, getLocalizedJournalDateString]
);
}

View File

@@ -1,5 +1,11 @@
import { style } from '@vanilla-extract/css';
export const root = style({
position: 'relative',
height: '100%',
width: '100%',
});
export const header = style({
display: 'flex',
height: '100%',
@@ -24,3 +30,18 @@ export const iconButtonContainer = style({
alignItems: 'center',
gap: 10,
});
export const dragHandle = style({
cursor: 'grab',
position: 'absolute',
top: 0,
bottom: 0,
left: -16,
width: 16,
opacity: 0,
selectors: {
[`${root}:hover &, ${root}[data-dragging="true"] &`]: {
opacity: 1,
},
},
});

View File

@@ -1,7 +1,9 @@
import {
Divider,
DragHandle,
type InlineEditHandle,
observeResize,
useDraggable,
} from '@affine/component';
import { SharePageButton } from '@affine/core/components/affine/share-page-modal';
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
@@ -13,11 +15,13 @@ import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuit
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import { useRegisterCopyLinkCommands } from '@affine/core/components/hooks/affine/use-register-copy-link-commands';
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
import { HeaderDivider } from '@affine/core/components/pure/header';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { EditorService } from '@affine/core/modules/editor';
import { JournalService } from '@affine/core/modules/journal';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n';
import type { Doc } from '@blocksuite/affine/store';
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
@@ -60,7 +64,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
const { hideShare, hideToday } =
useDetailPageHeaderResponsive(containerWidth);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
const docDisplayMetaService = useService(DocDisplayMetaService);
const i18n = useI18n();
const title = i18n.t(useLiveData(docDisplayMetaService.title$(page.id)));
return (
<Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
@@ -106,7 +114,10 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
);
}, []);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
const docDisplayMetaService = useService(DocDisplayMetaService);
const i18n = useI18n();
const title = i18n.t(useLiveData(docDisplayMetaService.title$(page.id)));
const editor = useService(EditorService).editor;
const currentMode = useLiveData(editor.mode$);
@@ -148,8 +159,12 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
);
}
export function DetailPageHeader(props: PageHeaderProps) {
const { page, workspace } = props;
export function DetailPageHeader(
props: PageHeaderProps & {
onDragging?: (dragging: boolean) => void;
}
) {
const { page, workspace, onDragging } = props;
const journalService = useService(JournalService);
const isJournal = !!useLiveData(journalService.journalDate$(page.id));
const isInTrash = page.meta?.trash;
@@ -159,9 +174,42 @@ export function DetailPageHeader(props: PageHeaderProps) {
docId: page.id,
});
return isJournal && !isInTrash ? (
<JournalPageHeader {...props} />
) : (
<NormalPageHeader {...props} />
const { dragRef, dragHandleRef, dragging } =
useDraggable<AffineDNDData>(() => {
return {
data: {
from: {
at: 'doc-detail:header',
docId: page.id,
},
entity: {
type: 'doc',
id: page.id,
},
},
disableDragPreview: true,
};
}, [page.id]);
const inner =
isJournal && !isInTrash ? (
<JournalPageHeader {...props} />
) : (
<NormalPageHeader {...props} />
);
useEffect(() => {
onDragging?.(dragging);
}, [dragging, onDragging]);
return (
<div className={styles.root} ref={dragRef} data-dragging={dragging}>
<DragHandle
ref={dragHandleRef}
dragging={dragging}
className={styles.dragHandle}
/>
{inner}
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const mainContainer = style({
@@ -39,6 +40,11 @@ export const affineDocViewport = style({
zIndex: -1,
},
},
selectors: {
'&[data-dragging="true"]': {
backgroundColor: cssVarV2.layer.background.hoverOverlay,
},
},
});
export const scrollbar = style({

View File

@@ -249,10 +249,16 @@ const DetailPageImpl = memo(function DetailPageImpl() {
setHasScrollTop(hasScrollTop);
}, []);
const [dragging, setDragging] = useState(false);
return (
<FrameworkScope scope={editor.scope}>
<ViewHeader>
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
<DetailPageHeader
page={doc.blockSuiteDoc}
workspace={workspace}
onDragging={setDragging}
/>
</ViewHeader>
<ViewBody>
<div
@@ -267,6 +273,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
<Scrollable.Viewport
onScroll={handleScroll}
ref={scrollViewportRef}
data-dragging={dragging}
className={clsx(
'affine-page-viewport',
styles.affineDocViewport,

View File

@@ -75,6 +75,10 @@ export interface AffineDNDData extends DNDData {
at: 'doc-property:manager';
workspaceId: string;
}
| {
at: 'doc-detail:header';
docId: string;
}
| {
at: 'external'; // for blocksuite or external apps
};