346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants'
|
|
import { ElementIds } from '@/Constants/ElementIDs'
|
|
import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen'
|
|
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
|
import { ApplicationEvent, classNames, PrefKey } from '@standardnotes/snjs'
|
|
import { observer } from 'mobx-react-lite'
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import { usePrevious } from '../ContentListView/Calendar/usePrevious'
|
|
import ContentListView from '../ContentListView/ContentListView'
|
|
import NoteGroupView from '../NoteGroupView/NoteGroupView'
|
|
import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer'
|
|
import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata'
|
|
import { useResponsiveAppPane } from './ResponsivePaneProvider'
|
|
import Navigation from '../Tags/Navigation'
|
|
import { useApplication } from '../ApplicationProvider'
|
|
import {
|
|
animatePaneEntranceTransitionFromOffscreenToTheRight,
|
|
animatePaneExitTransitionOffscreenToTheRight,
|
|
} from '@/Components/Panes/PaneAnimator'
|
|
import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout'
|
|
import { log, LoggingDomain } from '@/Logging'
|
|
|
|
const NAVIGATION_PANEL_MIN_WIDTH = 48
|
|
const ITEMS_PANEL_MIN_WIDTH = 200
|
|
const PLACEHOLDER_NAVIGATION_PANEL_WIDTH = 220
|
|
const PLACEHOLDER_NOTES_PANEL_WIDTH = 400
|
|
|
|
const PanesSystemComponent = () => {
|
|
const application = useApplication()
|
|
const isTabletOrMobileScreenWrapped = useIsTabletOrMobileScreen()
|
|
const { isTabletOrMobile, isTablet, isMobile } = isTabletOrMobileScreenWrapped
|
|
const previousIsTabletOrMobileWrapped = usePrevious(isTabletOrMobileScreenWrapped)
|
|
|
|
const paneController = useResponsiveAppPane()
|
|
const previousPaneController = usePrevious(paneController)
|
|
const [renderPanes, setRenderPanes] = useState<AppPaneId[]>([])
|
|
const [panesPendingEntrance, setPanesPendingEntrance] = useState<AppPaneId[]>([])
|
|
const [panesPendingExit, setPanesPendingExit] = useState<AppPaneId[]>([])
|
|
|
|
const viewControllerManager = application.getViewControllerManager()
|
|
|
|
const [navigationPanelWidth, setNavigationPanelWidth] = useState<number>(
|
|
application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH),
|
|
)
|
|
const [navigationRef, setNavigationRef] = useState<HTMLDivElement | null>(null)
|
|
|
|
const [itemsPanelWidth, setItemsPanelWidth] = useState<number>(
|
|
application.getPreference(PrefKey.NotesPanelWidth, PLACEHOLDER_NOTES_PANEL_WIDTH),
|
|
)
|
|
const [listRef, setListRef] = useState<HTMLDivElement | null>(null)
|
|
|
|
const showPanelResizers = !isTabletOrMobile
|
|
|
|
const [_editorRef, setEditorRef] = useState<HTMLDivElement | null>(null)
|
|
|
|
const animationsSupported = isMobile
|
|
|
|
useEffect(() => {
|
|
if (!animationsSupported) {
|
|
return
|
|
}
|
|
|
|
const panes = paneController.panes
|
|
const previousPanes = previousPaneController?.panes
|
|
if (!previousPanes) {
|
|
setPanesPendingEntrance([])
|
|
return
|
|
}
|
|
|
|
const isPush = isPanesChangePush(previousPanes, panes)
|
|
if (isPush) {
|
|
setPanesPendingEntrance([panes[panes.length - 1]])
|
|
}
|
|
}, [paneController.panes, previousPaneController?.panes, animationsSupported])
|
|
|
|
useEffect(() => {
|
|
if (!animationsSupported) {
|
|
return
|
|
}
|
|
|
|
const panes = paneController.panes
|
|
const previousPanes = previousPaneController?.panes
|
|
if (!previousPanes) {
|
|
setPanesPendingExit([])
|
|
return
|
|
}
|
|
|
|
const isExit = isPanesChangeLeafDismiss(previousPanes, panes)
|
|
if (isExit) {
|
|
setPanesPendingExit([previousPanes[previousPanes.length - 1]])
|
|
}
|
|
}, [paneController.panes, previousPaneController?.panes, animationsSupported])
|
|
|
|
useEffect(() => {
|
|
setRenderPanes(paneController.panes)
|
|
}, [paneController.panes])
|
|
|
|
useEffect(() => {
|
|
if (!panesPendingEntrance || panesPendingEntrance?.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (panesPendingEntrance.length > 1) {
|
|
console.warn('More than one pane pending entrance. This is not supported.')
|
|
return
|
|
}
|
|
|
|
void animatePaneEntranceTransitionFromOffscreenToTheRight(AppPaneIdToDivId[panesPendingEntrance[0]]).then(() => {
|
|
setPanesPendingEntrance([])
|
|
})
|
|
}, [panesPendingEntrance])
|
|
|
|
useEffect(() => {
|
|
if (!panesPendingExit || panesPendingExit?.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (panesPendingExit.length > 1) {
|
|
console.warn('More than one pane pending exit. This is not supported.')
|
|
return
|
|
}
|
|
|
|
void animatePaneExitTransitionOffscreenToTheRight(AppPaneIdToDivId[panesPendingExit[0]]).then(() => {
|
|
setPanesPendingExit([])
|
|
})
|
|
}, [panesPendingExit])
|
|
|
|
useEffect(() => {
|
|
const removeObserver = application.addEventObserver(async () => {
|
|
const width = application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH)
|
|
setNavigationPanelWidth(width)
|
|
}, ApplicationEvent.PreferencesChanged)
|
|
|
|
return () => {
|
|
removeObserver()
|
|
}
|
|
}, [application])
|
|
|
|
const navigationPanelResizeWidthChangeCallback = useCallback((width: number) => {
|
|
setNavigationPanelWidth(width)
|
|
}, [])
|
|
|
|
const itemsPanelResizeWidthChangeCallback = useCallback((width: number) => {
|
|
setItemsPanelWidth(width)
|
|
}, [])
|
|
|
|
const handleInitialItemsListPanelWidthLoad = useCallback((width: number) => {
|
|
setItemsPanelWidth(width)
|
|
}, [])
|
|
|
|
const navigationPanelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
|
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
|
application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, width, isCollapsed)
|
|
},
|
|
[application],
|
|
)
|
|
|
|
const itemsPanelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
|
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
|
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, width, isCollapsed)
|
|
},
|
|
[application],
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (isTablet && !previousIsTabletOrMobileWrapped?.isTablet) {
|
|
if (paneController.selectedPane !== AppPaneId.Navigation) {
|
|
paneController.removePane(AppPaneId.Navigation)
|
|
}
|
|
} else if (
|
|
!isTablet &&
|
|
previousIsTabletOrMobileWrapped?.isTablet &&
|
|
!paneController.panes.includes(AppPaneId.Navigation)
|
|
) {
|
|
paneController.insertPaneAtIndex(AppPaneId.Navigation, 0)
|
|
}
|
|
}, [isTablet, paneController, previousIsTabletOrMobileWrapped])
|
|
|
|
const computeStylesForContainer = (): React.CSSProperties => {
|
|
const panes = paneController.panes
|
|
const numPanes = panes.length
|
|
|
|
if (isMobile) {
|
|
return {}
|
|
}
|
|
|
|
switch (numPanes) {
|
|
case 1: {
|
|
return {
|
|
gridTemplateColumns: 'auto',
|
|
}
|
|
}
|
|
case 2: {
|
|
if (paneController.focusModeEnabled) {
|
|
return {
|
|
gridTemplateColumns: '0 1fr',
|
|
}
|
|
}
|
|
if (isTablet) {
|
|
return {
|
|
gridTemplateColumns: '1fr 2fr',
|
|
}
|
|
} else {
|
|
if (panes[0] === AppPaneId.Navigation) {
|
|
return {
|
|
gridTemplateColumns: `${navigationPanelWidth}px auto`,
|
|
}
|
|
} else {
|
|
return {
|
|
gridTemplateColumns: `${itemsPanelWidth}px auto`,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case 3: {
|
|
if (paneController.focusModeEnabled) {
|
|
return {
|
|
gridTemplateColumns: '0 0 1fr',
|
|
}
|
|
}
|
|
return {
|
|
gridTemplateColumns: `${navigationPanelWidth}px ${itemsPanelWidth}px 2fr`,
|
|
}
|
|
}
|
|
default:
|
|
return {}
|
|
}
|
|
}
|
|
|
|
const computeClassesForPane = (_paneId: AppPaneId, isPendingEntrance: boolean, index: number): string => {
|
|
const common = `app-pane app-pane-${index + 1} h-full content`
|
|
|
|
if (isMobile) {
|
|
return `absolute top-0 left-0 w-full flex flex-col ${common} ${
|
|
isPendingEntrance ? 'translate-x-[100%]' : 'translate-x-0 '
|
|
}`
|
|
} else {
|
|
return `flex flex-col relative overflow-hidden ${common}`
|
|
}
|
|
}
|
|
|
|
const computeClassesForContainer = (): string => {
|
|
if (isMobile) {
|
|
return 'w-full'
|
|
}
|
|
|
|
return 'grid'
|
|
}
|
|
|
|
const renderPanesWithPendingExit = [...renderPanes, ...panesPendingExit]
|
|
|
|
log(LoggingDomain.Panes, 'Rendering panes', renderPanesWithPendingExit)
|
|
|
|
return (
|
|
<div id="app" className={`app ${computeClassesForContainer()}`} style={{ ...computeStylesForContainer() }}>
|
|
{renderPanesWithPendingExit.map((pane, index) => {
|
|
const isPendingEntrance = panesPendingEntrance?.includes(pane)
|
|
|
|
const className = computeClassesForPane(pane, isPendingEntrance ?? false, index)
|
|
|
|
if (pane === AppPaneId.Navigation) {
|
|
return (
|
|
<Navigation
|
|
id={ElementIds.NavigationColumn}
|
|
ref={setNavigationRef}
|
|
className={classNames(className, isTabletOrMobile ? 'w-full' : '')}
|
|
key="navigation-pane"
|
|
application={application}
|
|
>
|
|
{showPanelResizers && navigationRef && (
|
|
<PanelResizer
|
|
collapsable={true}
|
|
defaultWidth={navigationPanelWidth}
|
|
hoverable={true}
|
|
left={0}
|
|
minWidth={NAVIGATION_PANEL_MIN_WIDTH}
|
|
modifyElementWidth={false}
|
|
panel={navigationRef}
|
|
resizeFinishCallback={navigationPanelResizeFinishCallback}
|
|
side={PanelSide.Right}
|
|
type={PanelResizeType.WidthOnly}
|
|
width={navigationPanelWidth}
|
|
widthEventCallback={navigationPanelResizeWidthChangeCallback}
|
|
/>
|
|
)}
|
|
</Navigation>
|
|
)
|
|
} else if (pane === AppPaneId.Items) {
|
|
return (
|
|
<ContentListView
|
|
id={ElementIds.ItemsColumn}
|
|
className={className}
|
|
ref={setListRef}
|
|
key={'content-list-view'}
|
|
application={application}
|
|
onPanelWidthLoad={handleInitialItemsListPanelWidthLoad}
|
|
accountMenuController={viewControllerManager.accountMenuController}
|
|
filesController={viewControllerManager.filesController}
|
|
itemListController={viewControllerManager.itemListController}
|
|
navigationController={viewControllerManager.navigationController}
|
|
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
|
notesController={viewControllerManager.notesController}
|
|
selectionController={viewControllerManager.selectionController}
|
|
searchOptionsController={viewControllerManager.searchOptionsController}
|
|
linkingController={viewControllerManager.linkingController}
|
|
featuresController={viewControllerManager.featuresController}
|
|
historyModalController={viewControllerManager.historyModalController}
|
|
paneController={viewControllerManager.paneController}
|
|
>
|
|
{showPanelResizers && listRef && (
|
|
<PanelResizer
|
|
collapsable={true}
|
|
defaultWidth={itemsPanelWidth}
|
|
hoverable={true}
|
|
left={0}
|
|
minWidth={ITEMS_PANEL_MIN_WIDTH}
|
|
modifyElementWidth={false}
|
|
panel={listRef}
|
|
resizeFinishCallback={itemsPanelResizeFinishCallback}
|
|
side={PanelSide.Right}
|
|
type={PanelResizeType.WidthOnly}
|
|
width={itemsPanelWidth}
|
|
widthEventCallback={itemsPanelResizeWidthChangeCallback}
|
|
/>
|
|
)}
|
|
</ContentListView>
|
|
)
|
|
} else if (pane === AppPaneId.Editor) {
|
|
return (
|
|
<ErrorBoundary key="editor-pane">
|
|
<NoteGroupView
|
|
id={ElementIds.EditorColumn}
|
|
innerRef={(ref) => setEditorRef(ref)}
|
|
className={className}
|
|
application={application}
|
|
/>
|
|
</ErrorBoundary>
|
|
)
|
|
}
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default observer(PanesSystemComponent)
|