mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor(core): use element atom (#4026)
This commit is contained in:
@@ -50,7 +50,6 @@
|
||||
"jotai": "^2.4.0",
|
||||
"jotai-devtools": "^0.6.2",
|
||||
"lit": "^2.8.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lottie-web": "^5.12.2",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"next-auth": "^4.22.1",
|
||||
@@ -77,7 +76,6 @@
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.3.80",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/webpack-env": "^1.18.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
|
||||
5
apps/core/src/atoms/element.ts
Normal file
5
apps/core/src/atoms/element.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { atom } from 'jotai/vanilla';
|
||||
|
||||
export const appHeaderAtom = atom<HTMLDivElement | null>(null);
|
||||
|
||||
export const mainContainerAtom = atom<HTMLDivElement | null>(null);
|
||||
@@ -5,112 +5,40 @@ import {
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { useIsTinyScreen } from '@toeverything/hooks/use-is-tiny-screen';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import debounce from 'lodash.debounce';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type Atom, useAtomValue } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { forwardRef, useRef } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
import { TopTip } from './top-tip';
|
||||
import { WindowsAppControls } from './windows-app-controls';
|
||||
|
||||
interface HeaderPros {
|
||||
left?: ReactNode;
|
||||
right?: ReactNode;
|
||||
center?: ReactNode;
|
||||
left?: ReactElement;
|
||||
right?: ReactElement;
|
||||
center?: ReactElement;
|
||||
mainContainerAtom: Atom<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
const useIsTinyScreen = ({
|
||||
mainContainer,
|
||||
leftStatic,
|
||||
leftSlot,
|
||||
centerDom,
|
||||
rightStatic,
|
||||
rightSlot,
|
||||
}: {
|
||||
mainContainer: HTMLElement;
|
||||
leftStatic: MutableRefObject<HTMLElement | null>;
|
||||
leftSlot: MutableRefObject<HTMLElement | null>[];
|
||||
centerDom: MutableRefObject<HTMLElement | null>;
|
||||
rightStatic: MutableRefObject<HTMLElement | null>;
|
||||
rightSlot: MutableRefObject<HTMLElement | null>[];
|
||||
}) => {
|
||||
const [isTinyScreen, setIsTinyScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = debounce(() => {
|
||||
if (!centerDom.current) {
|
||||
return;
|
||||
}
|
||||
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
|
||||
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
const rightStaticWidth = rightStatic.current?.clientWidth || 0;
|
||||
|
||||
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
if (!leftSlotWidth && !rightSlotWidth) {
|
||||
if (isTinyScreen) {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = mainContainer.getBoundingClientRect();
|
||||
const centerRect = centerDom.current.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
leftStaticWidth + leftSlotWidth + containerRect.left >=
|
||||
centerRect.left ||
|
||||
containerRect.right - centerRect.right <=
|
||||
rightSlotWidth + rightStaticWidth
|
||||
) {
|
||||
setIsTinyScreen(true);
|
||||
} else {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
handleResize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(mainContainer);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [
|
||||
centerDom,
|
||||
isTinyScreen,
|
||||
leftSlot,
|
||||
leftStatic,
|
||||
mainContainer,
|
||||
rightSlot,
|
||||
rightStatic,
|
||||
]);
|
||||
|
||||
return isTinyScreen;
|
||||
};
|
||||
|
||||
// The Header component is used to solve the following problems
|
||||
// 1. Manage layout issues independently of page or business logic
|
||||
// 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed
|
||||
export const Header = ({ left, center, right }: HeaderPros) => {
|
||||
export const Header = forwardRef<HTMLDivElement, HeaderPros>(function Header(
|
||||
{ left, center, right, mainContainerAtom },
|
||||
ref
|
||||
) {
|
||||
const sidebarSwitchRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftSlotRef = useRef<HTMLDivElement | null>(null);
|
||||
const centerSlotRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightSlotRef = useRef<HTMLDivElement | null>(null);
|
||||
const windowControlsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const mainContainer = useAtomValue(mainContainerAtom);
|
||||
|
||||
const isTinyScreen = useIsTinyScreen({
|
||||
mainContainer: document.querySelector('.main-container') || document.body,
|
||||
mainContainer,
|
||||
leftStatic: sidebarSwitchRef,
|
||||
leftSlot: [leftSlotRef],
|
||||
centerDom: centerSlotRef,
|
||||
@@ -130,6 +58,7 @@ export const Header = ({ left, center, right }: HeaderPros) => {
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
data-testid="header"
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className={clsx(style.headerSideContainer, {
|
||||
@@ -175,4 +104,6 @@ export const Header = ({ left, center, right }: HeaderPros) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
SaveCollectionButton,
|
||||
useCollectionManager,
|
||||
} from '@affine/component/page-list';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import {
|
||||
@@ -11,8 +12,10 @@ import {
|
||||
type WorkspaceHeaderProps,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
|
||||
import { useGetPageInfoById } from '../hooks/use-get-page-info';
|
||||
import { useWorkspace } from '../hooks/use-workspace';
|
||||
import { SharePageModal } from './affine/share-page-modal';
|
||||
@@ -76,6 +79,7 @@ export function WorkspaceHeader({
|
||||
currentEntry,
|
||||
}: WorkspaceHeaderProps<WorkspaceFlavour>) {
|
||||
const setting = useCollectionManager(currentWorkspaceId);
|
||||
const setAppHeader = useSetAtom(appHeaderAtom);
|
||||
|
||||
const currentWorkspace = useWorkspace(currentWorkspaceId);
|
||||
const getPageInfoById = useGetPageInfoById(
|
||||
@@ -90,6 +94,8 @@ export function WorkspaceHeader({
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
mainContainerAtom={mainContainerAtom}
|
||||
ref={setAppHeader}
|
||||
left={
|
||||
<CollectionList
|
||||
setting={setting}
|
||||
@@ -112,7 +118,13 @@ export function WorkspaceHeader({
|
||||
(currentEntry.subPath === WorkspaceSubPath.SHARED ||
|
||||
currentEntry.subPath === WorkspaceSubPath.TRASH)
|
||||
) {
|
||||
return <Header center={<WorkspaceModeFilterTab />} />;
|
||||
return (
|
||||
<Header
|
||||
mainContainerAtom={mainContainerAtom}
|
||||
ref={setAppHeader}
|
||||
center={<WorkspaceModeFilterTab />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// route in edit page
|
||||
@@ -128,6 +140,8 @@ export function WorkspaceHeader({
|
||||
) : null;
|
||||
return (
|
||||
<Header
|
||||
mainContainerAtom={mainContainerAtom}
|
||||
ref={setAppHeader}
|
||||
center={
|
||||
<BlockSuiteHeaderTitle
|
||||
workspace={currentWorkspace}
|
||||
@@ -144,5 +158,5 @@ export function WorkspaceHeader({
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
throw new Unreachable();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
openSettingModalAtom,
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { mainContainerAtom } from '../atoms/element';
|
||||
import { useAppSetting } from '../atoms/settings';
|
||||
import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
@@ -206,6 +207,8 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
const location = useLocation();
|
||||
const { pageId } = useParams();
|
||||
|
||||
const setMainContainer = useSetAtom(mainContainerAtom);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||
@@ -234,8 +237,11 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
paths={pathGenerator}
|
||||
/>
|
||||
</Suspense>
|
||||
<Suspense fallback={<MainContainer />}>
|
||||
<MainContainer padding={appSetting.clientBorder}>
|
||||
<Suspense fallback={<MainContainer ref={setMainContainer} />}>
|
||||
<MainContainer
|
||||
ref={setMainContainer}
|
||||
padding={appSetting.clientBorder}
|
||||
>
|
||||
{children}
|
||||
<ToolContainer>
|
||||
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import type { ToastOptions } from '@affine/component';
|
||||
import { toast as basicToast } from '@affine/component';
|
||||
import { assertEquals, assertExists } from '@blocksuite/global/utils';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
|
||||
import { mainContainerAtom } from '../atoms/element';
|
||||
|
||||
export const toast = (message: string, options?: ToastOptions) => {
|
||||
const mainContainer = getCurrentStore().get(mainContainerAtom);
|
||||
const modal = document.querySelector(
|
||||
'[role=presentation]'
|
||||
) as HTMLElement | null;
|
||||
const mainContainer = document.querySelector(
|
||||
'.main-container'
|
||||
) as HTMLElement | null;
|
||||
) as HTMLDivElement | null;
|
||||
assertExists(mainContainer, 'main container should exist');
|
||||
if (modal) {
|
||||
assertEquals(modal.constructor, HTMLDivElement, 'modal should be div');
|
||||
}
|
||||
return basicToast(message, {
|
||||
portal: modal || mainContainer || document.body,
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { AppSidebarFallback } from '../app-sidebar';
|
||||
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
|
||||
@@ -36,23 +37,27 @@ export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
padding?: boolean;
|
||||
}
|
||||
|
||||
export const MainContainer = ({
|
||||
className,
|
||||
padding,
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<MainContainerProps>): ReactElement => {
|
||||
export const MainContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<MainContainerProps>
|
||||
>(function MainContainer(
|
||||
{ className, padding, children, ...props },
|
||||
ref
|
||||
): ReactElement {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(mainContainerStyle, 'main-container', className)}
|
||||
className={clsx(mainContainerStyle, className)}
|
||||
data-is-macos={environment.isDesktop && environment.isMacOs}
|
||||
data-show-padding={!!padding}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
MainContainer.displayName = 'MainContainer';
|
||||
|
||||
export const ToolContainer = (props: PropsWithChildren): ReactElement => {
|
||||
return <div className={toolStyle}>{props.children}</div>;
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"foxact": "^0.2.20"
|
||||
"foxact": "^0.2.20",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine/env": "workspace:*",
|
||||
@@ -16,7 +17,8 @@
|
||||
"@blocksuite/editor": "0.0.0-20230828163942-e5356e86-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230828163942-e5356e86-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230828163942-e5356e86-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230828163942-e5356e86-nightly"
|
||||
"@blocksuite/store": "0.0.0-20230828163942-e5356e86-nightly",
|
||||
"@types/lodash.debounce": "^4.0.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@affine/y-provider": "workspace:*",
|
||||
|
||||
86
packages/hooks/src/use-is-tiny-screen.ts
Normal file
86
packages/hooks/src/use-is-tiny-screen.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'foxact/use-debounced-state';
|
||||
|
||||
import debounce from 'lodash.debounce';
|
||||
import { type RefObject, useEffect, useState } from 'react';
|
||||
|
||||
export function useIsTinyScreen({
|
||||
mainContainer,
|
||||
leftStatic,
|
||||
leftSlot,
|
||||
centerDom,
|
||||
rightStatic,
|
||||
rightSlot,
|
||||
}: {
|
||||
mainContainer: HTMLElement | null;
|
||||
leftStatic: RefObject<HTMLElement>;
|
||||
leftSlot: RefObject<HTMLElement>[];
|
||||
centerDom: RefObject<HTMLElement>;
|
||||
rightStatic: RefObject<HTMLElement>;
|
||||
rightSlot: RefObject<HTMLElement>[];
|
||||
}) {
|
||||
const [isTinyScreen, setIsTinyScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mainContainer) {
|
||||
return;
|
||||
}
|
||||
const handleResize = debounce(() => {
|
||||
if (!centerDom.current) {
|
||||
return;
|
||||
}
|
||||
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
|
||||
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
const rightStaticWidth = rightStatic.current?.clientWidth || 0;
|
||||
|
||||
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
if (!leftSlotWidth && !rightSlotWidth) {
|
||||
if (isTinyScreen) {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = mainContainer.getBoundingClientRect();
|
||||
const centerRect = centerDom.current.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
leftStaticWidth + leftSlotWidth + containerRect.left >=
|
||||
centerRect.left ||
|
||||
containerRect.right - centerRect.right <=
|
||||
rightSlotWidth + rightStaticWidth
|
||||
) {
|
||||
setIsTinyScreen(true);
|
||||
} else {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
handleResize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(mainContainer);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [
|
||||
centerDom,
|
||||
isTinyScreen,
|
||||
leftSlot,
|
||||
leftStatic,
|
||||
mainContainer,
|
||||
rightSlot,
|
||||
rightStatic,
|
||||
]);
|
||||
|
||||
return isTinyScreen;
|
||||
}
|
||||
@@ -262,7 +262,6 @@ __metadata:
|
||||
"@swc/core": ^1.3.80
|
||||
"@toeverything/components": ^0.0.24
|
||||
"@types/lodash-es": ^4.17.8
|
||||
"@types/lodash.debounce": ^4.0.7
|
||||
"@types/webpack-env": ^1.18.1
|
||||
async-call-rpc: ^6.3.1
|
||||
cmdk: ^0.2.0
|
||||
@@ -278,7 +277,6 @@ __metadata:
|
||||
jotai-devtools: ^0.6.2
|
||||
lit: ^2.8.0
|
||||
lodash-es: ^4.17.21
|
||||
lodash.debounce: ^4.0.8
|
||||
lottie-web: ^5.12.2
|
||||
mime-types: ^2.1.35
|
||||
mini-css-extract-plugin: ^2.7.6
|
||||
@@ -12662,7 +12660,9 @@ __metadata:
|
||||
"@blocksuite/global": 0.0.0-20230828163942-e5356e86-nightly
|
||||
"@blocksuite/lit": 0.0.0-20230828163942-e5356e86-nightly
|
||||
"@blocksuite/store": 0.0.0-20230828163942-e5356e86-nightly
|
||||
"@types/lodash.debounce": ^4.0.7
|
||||
foxact: ^0.2.20
|
||||
lodash.debounce: ^4.0.8
|
||||
peerDependencies:
|
||||
"@affine/y-provider": "workspace:*"
|
||||
"@blocksuite/block-std": "*"
|
||||
|
||||
Reference in New Issue
Block a user