feat: screen presentation and dismiss animations for mobile (#2073)
This commit is contained in:
@@ -22,7 +22,7 @@ declare global {
|
||||
plansUrl: string
|
||||
purchaseUrl: string
|
||||
startApplication: StartApplication
|
||||
zip: any
|
||||
zip: unknown
|
||||
electronMainEvents: any
|
||||
}
|
||||
}
|
||||
@@ -88,15 +88,15 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
|
||||
/*
|
||||
Title bar events
|
||||
*/
|
||||
document.getElementById('menu-btn')!.addEventListener('click', () => {
|
||||
document.getElementById('menu-btn')?.addEventListener('click', () => {
|
||||
remoteBridge.displayAppMenu()
|
||||
})
|
||||
|
||||
document.getElementById('min-btn')!.addEventListener('click', () => {
|
||||
document.getElementById('min-btn')?.addEventListener('click', () => {
|
||||
remoteBridge.minimizeWindow()
|
||||
})
|
||||
|
||||
document.getElementById('max-btn')!.addEventListener('click', async () => {
|
||||
document.getElementById('max-btn')?.addEventListener('click', async () => {
|
||||
if (remoteBridge.isWindowMaximized()) {
|
||||
remoteBridge.unmaximizeWindow()
|
||||
} else {
|
||||
@@ -104,15 +104,12 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('close-btn')!.addEventListener('click', () => {
|
||||
document.getElementById('close-btn')?.addEventListener('click', () => {
|
||||
remoteBridge.closeWindow()
|
||||
})
|
||||
|
||||
// For Mac inset window
|
||||
const sheet = document.styleSheets[0]
|
||||
if (isMacOS) {
|
||||
sheet.insertRule('#navigation-content { padding-top: 25px !important; }', sheet.cssRules.length)
|
||||
}
|
||||
|
||||
if (isMacOS || useSystemMenuBar) {
|
||||
// !important is important here because #desktop-title-bar has display: flex.
|
||||
@@ -141,7 +138,7 @@ window.electronMainEvents.handlePerformAutomatedBackup(() => {
|
||||
void window.device.downloadBackup()
|
||||
})
|
||||
|
||||
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: any) => {
|
||||
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: { success: boolean }) => {
|
||||
window.webClient.didFinishBackup(data.success)
|
||||
})
|
||||
|
||||
|
||||
@@ -10,18 +10,14 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.mac-desktop .app-column.selected {
|
||||
padding-top: 18px;
|
||||
.mac-desktop .app-pane {
|
||||
padding-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.mac-desktop #app.collapsed-notes.collapsed-navigation #editor-column {
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.mac-desktop #app.collapsed-navigation #items-column {
|
||||
padding-top: 18px;
|
||||
.mac-desktop .app-pane-1 {
|
||||
padding-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"build": "yarn clean && yarn copy:components && webpack --config web.webpack.prod.js && yarn tsc",
|
||||
"clean": "rm -fr dist",
|
||||
"format": "prettier --write src/javascripts",
|
||||
"lint": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint src/javascripts",
|
||||
"lint:fix": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint src/javascripts --fix",
|
||||
"lint": "eslint src/javascripts && yarn tsc",
|
||||
"lint:fix": "eslint src/javascripts --fix",
|
||||
"start": "webpack-dev-server --config web.webpack.dev.js",
|
||||
"start-secure": "yarn start --server-type https",
|
||||
"test": "jest --config jest.config.js --coverage",
|
||||
@@ -81,7 +81,6 @@
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
|
||||
@@ -166,11 +166,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
}
|
||||
}
|
||||
|
||||
publishPanelDidResizeEvent(name: string, collapsed: boolean) {
|
||||
publishPanelDidResizeEvent(name: string, width: number, collapsed: boolean) {
|
||||
const data: PanelResizedData = {
|
||||
panel: name,
|
||||
collapsed: collapsed,
|
||||
collapsed,
|
||||
width,
|
||||
}
|
||||
|
||||
this.notifyWebEvent(WebAppEvent.PanelResized, data)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,31 @@
|
||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { getPlatformString } from '@/Utils'
|
||||
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
|
||||
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||
import { alertDialog, RouteType } from '@standardnotes/ui-services'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import Navigation from '@/Components/Tags/Navigation'
|
||||
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
||||
import Footer from '@/Components/Footer/Footer'
|
||||
import SessionsModal from '@/Components/SessionsModal/SessionsModal'
|
||||
import PreferencesViewWrapper from '@/Components/Preferences/PreferencesViewWrapper'
|
||||
import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal'
|
||||
import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu'
|
||||
import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
|
||||
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
import { ToastContainer } from '@standardnotes/toast'
|
||||
import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal'
|
||||
import ContentListView from '@/Components/ContentListView/ContentListView'
|
||||
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
||||
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
|
||||
import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import FileDragNDropProvider from '../FileDragNDropProvider'
|
||||
import ResponsivePaneProvider from '../Panes/ResponsivePaneProvider'
|
||||
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
||||
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
|
||||
import ApplicationProvider from './ApplicationProvider'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import CommandProvider from './CommandProvider'
|
||||
import ApplicationProvider from '../ApplicationProvider'
|
||||
import CommandProvider from '../CommandProvider'
|
||||
import PanesSystemComponent from '../Panes/PanesSystemComponent'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -45,8 +40,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
|
||||
const viewControllerManager = application.getViewControllerManager()
|
||||
|
||||
const appColumnContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const desktopService = application.getDesktopService()
|
||||
|
||||
@@ -137,30 +130,8 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
}, [application, onAppLaunch, onAppStart])
|
||||
|
||||
useEffect(() => {
|
||||
const removeObserver = application.addWebEventObserver(async (eventName, data) => {
|
||||
if (eventName === WebAppEvent.PanelResized) {
|
||||
if (!appColumnContainerRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const { panel, collapsed } = data as PanelResizedData
|
||||
|
||||
if (panel === PANEL_NAME_NOTES) {
|
||||
if (collapsed) {
|
||||
appColumnContainerRef.current.classList.add('collapsed-notes')
|
||||
} else {
|
||||
appColumnContainerRef.current.classList.remove('collapsed-notes')
|
||||
}
|
||||
}
|
||||
|
||||
if (panel === PANEL_NAME_NAVIGATION) {
|
||||
if (collapsed) {
|
||||
appColumnContainerRef.current.classList.add('collapsed-navigation')
|
||||
} else {
|
||||
appColumnContainerRef.current.classList.remove('collapsed-navigation')
|
||||
}
|
||||
}
|
||||
} else if (eventName === WebAppEvent.WindowDidFocus) {
|
||||
const removeObserver = application.addWebEventObserver(async (eventName) => {
|
||||
if (eventName === WebAppEvent.WindowDidFocus) {
|
||||
if (!(await application.isLocked())) {
|
||||
application.sync.sync().catch(console.error)
|
||||
}
|
||||
@@ -206,30 +177,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
>
|
||||
<div className={platformString + ' main-ui-view sn-component h-full'}>
|
||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
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}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<NoteGroupView application={application} />
|
||||
</ErrorBoundary>
|
||||
<PanesSystemComponent />
|
||||
</FileDragNDropProvider>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
} from '@standardnotes/ui-services'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||
import { FileItem, PrefKey } from '@standardnotes/snjs'
|
||||
import { FileItem, PrefKey, WebAppEvent } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import ContentList from '@/Components/ContentListView/ContentList'
|
||||
import NoAccountWarning from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
@@ -24,19 +23,19 @@ import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import ContentListHeader from './Header/ContentListHeader'
|
||||
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { StreamingFileReader } from '@standardnotes/filepicker'
|
||||
import SearchBar from '../SearchBar/SearchBar'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import DailyContentList from './Daily/DailyContentList'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
import { FeatureName } from '@/Controllers/FeatureName'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import { useForwardedRef } from '@/Hooks/useForwardedRef'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
@@ -49,9 +48,15 @@ type Props = {
|
||||
selectionController: SelectedItemsController
|
||||
searchOptionsController: SearchOptionsController
|
||||
linkingController: LinkingController
|
||||
className?: string
|
||||
id: string
|
||||
children?: React.ReactNode
|
||||
onPanelWidthLoad: (width: number) => void
|
||||
}
|
||||
|
||||
const ContentListView: FunctionComponent<Props> = ({
|
||||
const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
accountMenuController,
|
||||
application,
|
||||
filesController,
|
||||
@@ -62,14 +67,54 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
selectionController,
|
||||
searchOptionsController,
|
||||
linkingController,
|
||||
}) => {
|
||||
const { isNotesListVisibleOnTablets, toggleAppPane } = useResponsiveAppPane()
|
||||
className,
|
||||
id,
|
||||
children,
|
||||
onPanelWidthLoad,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { toggleAppPane, panes } = useResponsiveAppPane()
|
||||
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||
const { selected: selectedTag, selectedAsTag } = navigationController
|
||||
const {
|
||||
completedFullSync,
|
||||
createNewNote,
|
||||
optionsSubtitle,
|
||||
paginate,
|
||||
panelTitle,
|
||||
renderedItems,
|
||||
items,
|
||||
isCurrentNoteTemplate,
|
||||
} = itemListController
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
const innerRef = useForwardedRef(ref)
|
||||
|
||||
const { addDragTarget, removeDragTarget } = useFileDragNDrop()
|
||||
|
||||
useEffect(() => {
|
||||
return application.addWebEventObserver((event, data) => {
|
||||
if (event === WebAppEvent.PanelResized) {
|
||||
const { panel, width } = data as PanelResizedData
|
||||
if (panel === PANEL_NAME_NOTES) {
|
||||
if (selectedAsTag) {
|
||||
void navigationController.setPanelWidthForTag(selectedAsTag, width)
|
||||
} else {
|
||||
void application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [application, navigationController, selectedAsTag])
|
||||
|
||||
useEffect(() => {
|
||||
const panelWidth = selectedTag?.preferences?.panelWidth || application.getPreference(PrefKey.NotesPanelWidth)
|
||||
if (panelWidth) {
|
||||
onPanelWidthLoad(panelWidth)
|
||||
}
|
||||
}, [selectedTag, application, onPanelWidthLoad])
|
||||
|
||||
const fileDropCallback = useCallback(
|
||||
async (files: FileItem[]) => {
|
||||
const currentTag = navigationController.selected
|
||||
@@ -91,7 +136,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const target = itemsViewPanelRef.current
|
||||
const target = innerRef.current
|
||||
const currentTag = navigationController.selected
|
||||
const shouldAddDropTarget = !navigationController.isInAnySystemView() && !navigationController.isInSmartView()
|
||||
|
||||
@@ -107,23 +152,14 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
removeDragTarget(target)
|
||||
}
|
||||
}
|
||||
}, [addDragTarget, fileDropCallback, navigationController, navigationController.selected, removeDragTarget])
|
||||
|
||||
const {
|
||||
completedFullSync,
|
||||
createNewNote,
|
||||
optionsSubtitle,
|
||||
paginate,
|
||||
panelTitle,
|
||||
panelWidth,
|
||||
renderedItems,
|
||||
items,
|
||||
isCurrentNoteTemplate,
|
||||
} = itemListController
|
||||
|
||||
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||
|
||||
const { selected: selectedTag, selectedAsTag } = navigationController
|
||||
}, [
|
||||
addDragTarget,
|
||||
fileDropCallback,
|
||||
navigationController,
|
||||
navigationController.selected,
|
||||
removeDragTarget,
|
||||
innerRef,
|
||||
])
|
||||
|
||||
const icon = selectedTag?.iconString
|
||||
|
||||
@@ -211,19 +247,14 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
},
|
||||
},
|
||||
])
|
||||
}, [addNewItem, application.keyboardService, createNewNote, selectNextItem, selectPreviousItem, selectionController])
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
||||
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
||||
if (selectedAsTag) {
|
||||
void navigationController.setPanelWidthForTag(selectedAsTag, width)
|
||||
} else {
|
||||
void application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
|
||||
}
|
||||
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed)
|
||||
},
|
||||
[application, selectedAsTag, navigationController],
|
||||
)
|
||||
}, [
|
||||
addNewItem,
|
||||
application.keyboardService,
|
||||
createNewNote,
|
||||
selectNextItem,
|
||||
selectPreviousItem,
|
||||
selectionController,
|
||||
])
|
||||
|
||||
const shortcutForCreate = useMemo(
|
||||
() => application.keyboardService.keyboardShortcutForCommand(CREATE_NEW_NOTE_KEYBOARD_COMMAND),
|
||||
@@ -236,10 +267,6 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
|
||||
}, [isFilesSmartView, shortcutForCreate])
|
||||
|
||||
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
||||
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
||||
|
||||
const dailyMode = selectedAsTag?.isDailyEntry
|
||||
|
||||
const handleDailyListSelection = useCallback(
|
||||
@@ -254,30 +281,19 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0 || isCurrentNoteTemplate
|
||||
const hasEditorPane = panes.includes(AppPaneId.Editor)
|
||||
if (!hasEditorPane) {
|
||||
itemsViewPanelRef.current?.style.removeProperty('width')
|
||||
innerRef.current?.style.removeProperty('width')
|
||||
}
|
||||
}, [selectedUuids, itemsViewPanelRef, isCurrentNoteTemplate, renderedItems])
|
||||
|
||||
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0 || isCurrentNoteTemplate
|
||||
}, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes])
|
||||
|
||||
return (
|
||||
<div
|
||||
id="items-column"
|
||||
className={classNames(
|
||||
'sn-component section app-column flex h-full flex-col overflow-hidden pt-safe-top',
|
||||
hasEditorPane ? 'xl:w-[24rem] xsm-only:!w-full sm-only:!w-full' : 'w-full md:min-w-[400px]',
|
||||
hasEditorPane
|
||||
? isTabletScreenSize && !isNotesListVisibleOnTablets
|
||||
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
|
||||
: 'pointer-coarse:md-only:!w-60 pointer-coarse:lg-only:!w-60'
|
||||
: '',
|
||||
)}
|
||||
id={id}
|
||||
className={classNames(className, 'sn-component section h-full overflow-hidden pt-safe-top')}
|
||||
aria-label={'Notes & Files'}
|
||||
ref={itemsViewPanelRef}
|
||||
ref={innerRef}
|
||||
>
|
||||
<ResponsivePaneContent className="overflow-hidden" paneId={AppPaneId.Items}>
|
||||
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
|
||||
<div id="items-title-bar-container">
|
||||
<input
|
||||
@@ -325,14 +341,16 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
onSelect={handleDailyListSelection}
|
||||
/>
|
||||
)}
|
||||
{!dailyMode && renderedItems.length ? (
|
||||
<>
|
||||
{completedFullSync && !renderedItems.length ? (
|
||||
{!dailyMode && completedFullSync && !renderedItems.length ? (
|
||||
<p className="empty-items-list opacity-50">No items.</p>
|
||||
) : null}
|
||||
{!completedFullSync && !renderedItems.length ? (
|
||||
|
||||
{!dailyMode && !completedFullSync && !renderedItems.length ? (
|
||||
<p className="empty-items-list opacity-50">Loading...</p>
|
||||
) : null}
|
||||
|
||||
{!dailyMode && renderedItems.length ? (
|
||||
<>
|
||||
<ContentList
|
||||
items={renderedItems}
|
||||
selectedUuids={selectedUuids}
|
||||
@@ -347,22 +365,10 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
</>
|
||||
) : null}
|
||||
<div className="absolute bottom-0 h-safe-bottom w-full" />
|
||||
</ResponsivePaneContent>
|
||||
{hasEditorPane && itemsViewPanelRef.current && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
hoverable={true}
|
||||
defaultWidth={300}
|
||||
panel={itemsViewPanelRef.current}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.WidthOnly}
|
||||
resizeFinishCallback={panelResizeFinishCallback}
|
||||
width={panelWidth}
|
||||
left={0}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default observer(ContentListView)
|
||||
|
||||
@@ -2,8 +2,8 @@ import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, us
|
||||
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../../Panes/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../../Panes/AppPaneMetadata'
|
||||
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
||||
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||
import { DailyItemCell } from './DailyItemCell'
|
||||
|
||||
@@ -6,13 +6,13 @@ import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
|
||||
@@ -7,8 +7,6 @@ import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
import ListItemNotePreviewText from './ListItemNotePreviewText'
|
||||
import { ListItemTitle } from './ListItemTitle'
|
||||
@@ -31,8 +29,6 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
isPreviousItemTiled,
|
||||
isNextItemTiled,
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const listItemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const noteType = item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
|
||||
@@ -65,11 +61,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
}
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
const { didSelect } = await onSelect(item, true)
|
||||
if (didSelect) {
|
||||
toggleAppPane(AppPaneId.Editor)
|
||||
}
|
||||
}, [item, onSelect, toggleAppPane])
|
||||
await onSelect(item, true)
|
||||
}, [item, onSelect])
|
||||
|
||||
useContextMenuEvent(listItemRef, openContextMenu)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { FileBackupRecord, FileItem } from '@standardnotes/snjs'
|
||||
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { FilesController } from '@/Controllers/FilesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
|
||||
import { StreamingFileReader } from '@standardnotes/filepicker'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEffect, useContext, memo } from 'react'
|
||||
import Portal from '../Portal/Portal'
|
||||
import Portal from './Portal/Portal'
|
||||
|
||||
type FileDragTargetData = {
|
||||
tooltipText: string
|
||||
@@ -3,8 +3,8 @@ import { getBase64FromBlob } from '@/Utils'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
|
||||
import ImagePreview from './ImagePreview'
|
||||
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
|
||||
|
||||
@@ -9,7 +9,7 @@ import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||
import Popover from '../Popover/Popover'
|
||||
import FilePreviewInfoPanel from '../FilePreview/FilePreviewInfoPanel'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
|
||||
const SyncTimeoutNoDebounceMs = 100
|
||||
|
||||
@@ -19,7 +19,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import Menu from '../Menu/Menu'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
linkingController: LinkingController
|
||||
|
||||
@@ -8,7 +8,7 @@ import Icon from '../Icon/Icon'
|
||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -3,14 +3,14 @@ import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import LinkedItemBubble from './LinkedItemBubble'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { ContentType } from '@standardnotes/snjs'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
|
||||
type Props = {
|
||||
linkingController: LinkingController
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag
|
||||
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { classNames } from '@standardnotes/utils'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import ClearInputButton from '../ClearInputButton/ClearInputButton'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import Icon from '../Icon/Icon'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||
import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
|
||||
/** This button is displayed in the items list header */
|
||||
export const NavigationMenuButton = () => {
|
||||
const { selectedPane, toggleAppPane } = useResponsiveAppPane()
|
||||
const { setPaneLayout } = useResponsiveAppPane()
|
||||
const { isTabletOrMobile } = useIsTabletOrMobileScreen()
|
||||
|
||||
return (
|
||||
<RoundIconButton
|
||||
className="mr-3 md:hidden pointer-coarse:md-only:flex pointer-coarse:lg-only:flex"
|
||||
className={classNames(isTabletOrMobile ? 'flex' : 'hidden', 'mr-3')}
|
||||
onClick={() => {
|
||||
if (selectedPane === AppPaneId.Items || selectedPane === AppPaneId.Editor) {
|
||||
toggleAppPane(AppPaneId.Navigation)
|
||||
} else {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}
|
||||
setPaneLayout(PaneLayout.TagSelection)
|
||||
}}
|
||||
label="Open navigation menu"
|
||||
icon="menu-variant"
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { useMediaQuery, MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { classNames, IconType } from '@standardnotes/snjs'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen'
|
||||
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||
|
||||
const MobileItemsListButton = () => {
|
||||
const { toggleAppPane, isNotesListVisibleOnTablets, toggleNotesListOnTablets } = useResponsiveAppPane()
|
||||
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
||||
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
||||
const { panes, replacePanes, setPaneLayout } = useResponsiveAppPane()
|
||||
|
||||
const iconType: IconType = isTabletScreenSize && !isNotesListVisibleOnTablets ? 'chevron-right' : 'chevron-left'
|
||||
const label = isTabletScreenSize
|
||||
? isNotesListVisibleOnTablets
|
||||
? 'Hide items list'
|
||||
: 'Show items list'
|
||||
: 'Go to items list'
|
||||
const { isTablet, isTabletOrMobile, isMobile } = useIsTabletOrMobileScreen()
|
||||
|
||||
const itemsShown = panes.includes(AppPaneId.Items)
|
||||
|
||||
const iconType: IconType = isTablet && !itemsShown ? 'chevron-right' : 'chevron-left'
|
||||
|
||||
const label = isTablet ? (itemsShown ? 'Hide items list' : 'Show items list') : 'Go to items list'
|
||||
|
||||
return (
|
||||
<RoundIconButton
|
||||
className="mr-3 md:hidden pointer-coarse:md-only:flex pointer-coarse:lg-only:flex"
|
||||
className={classNames(isTabletOrMobile ? 'flex' : 'hidden', 'mr-3')}
|
||||
onClick={() => {
|
||||
if (isTabletScreenSize) {
|
||||
toggleNotesListOnTablets()
|
||||
if (isMobile) {
|
||||
void setPaneLayout(PaneLayout.ItemSelection)
|
||||
} else {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
if (itemsShown) {
|
||||
void replacePanes([AppPaneId.Editor])
|
||||
} else {
|
||||
void setPaneLayout(PaneLayout.ItemSelection)
|
||||
}
|
||||
}
|
||||
}}
|
||||
label={label}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import FileView from '../FileView/FileView'
|
||||
import NoteView from '../NoteView/NoteView'
|
||||
import { NoteViewController } from '../NoteView/Controller/NoteViewController'
|
||||
@@ -22,6 +20,9 @@ type State = {
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
className?: string
|
||||
innerRef: (ref: HTMLDivElement) => void
|
||||
id: string
|
||||
}
|
||||
|
||||
class NoteGroupView extends AbstractComponent<Props, State> {
|
||||
@@ -97,11 +98,12 @@ class NoteGroupView extends AbstractComponent<Props, State> {
|
||||
|
||||
const hasControllers = this.state.controllers.length > 0
|
||||
|
||||
const canRenderEditorView = this.state.selectedPane === AppPaneId.Editor || !this.state.isInMobileView
|
||||
|
||||
return (
|
||||
<div id={ElementIds.EditorColumn} className="app-column app-column-third flex h-full flex-col pt-safe-top">
|
||||
<ResponsivePaneContent paneId={AppPaneId.Editor} className="flex-grow">
|
||||
<div
|
||||
id={this.props.id}
|
||||
className={`flex h-full flex-grow flex-col pt-safe-top ${this.props.className}`}
|
||||
ref={this.props.innerRef}
|
||||
>
|
||||
{this.state.showMultipleSelectedNotes && (
|
||||
<MultipleSelectedNotes
|
||||
application={this.application}
|
||||
@@ -118,7 +120,7 @@ class NoteGroupView extends AbstractComponent<Props, State> {
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
/>
|
||||
)}
|
||||
{shouldNotShowMultipleSelectedItems && hasControllers && canRenderEditorView && (
|
||||
{shouldNotShowMultipleSelectedItems && hasControllers && (
|
||||
<>
|
||||
{this.state.controllers.map((controller) => {
|
||||
return controller instanceof NoteViewController ? (
|
||||
@@ -134,7 +136,6 @@ class NoteGroupView extends AbstractComponent<Props, State> {
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</ResponsivePaneContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { classNames } from '@standardnotes/utils'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { IconType, PrefKey } from '@standardnotes/snjs'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
export type NoteStatus = {
|
||||
type: 'saving' | 'saved' | 'error'
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
||||
import { debounce, isDesktopApplication, isMobileScreen, isTabletOrMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
@@ -672,10 +672,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
PrefDefaults[PrefKey.EditorMonospaceEnabled],
|
||||
)
|
||||
|
||||
const marginResizersEnabled = this.application.getPreference(
|
||||
PrefKey.EditorResizersEnabled,
|
||||
PrefDefaults[PrefKey.EditorResizersEnabled],
|
||||
)
|
||||
const marginResizersEnabled =
|
||||
!isTabletOrMobileScreen() &&
|
||||
this.application.getPreference(PrefKey.EditorResizersEnabled, PrefDefaults[PrefKey.EditorResizersEnabled])
|
||||
|
||||
const updateSavingIndicator = this.application.getPreference(
|
||||
PrefKey.UpdateSavingStatusIndicator,
|
||||
@@ -687,7 +686,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
this.setState({
|
||||
monospaceFont,
|
||||
marginResizersEnabled,
|
||||
|
||||
updateSavingIndicator,
|
||||
})
|
||||
|
||||
@@ -929,6 +927,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
left={this.state.leftResizerOffset}
|
||||
width={this.state.leftResizerWidth}
|
||||
resizeFinishCallback={this.onPanelResizeFinish}
|
||||
modifyElementWidth={true}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -980,6 +979,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
left={this.state.rightResizerOffset}
|
||||
width={this.state.rightResizerWidth}
|
||||
resizeFinishCallback={this.onPanelResizeFinish}
|
||||
modifyElementWidth={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FilesController } from '@/Controllers/FilesController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { useEffect } from 'react'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||
|
||||
type Props = {
|
||||
note: SNNote
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ElementFormatType, NodeKey } from 'lexical'
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import FilePreview from '@/Components/FilePreview/FilePreview'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { downloadBlobOnAndroid } from '@/NativeMobileWeb/DownloadBlobOnAndroid'
|
||||
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
@@ -13,7 +13,7 @@ import { useCallback, useEffect } from 'react'
|
||||
import { $convertToMarkdownString } from '@lexical/markdown'
|
||||
import { MarkdownTransformers } from '@standardnotes/blocks-editor'
|
||||
import { $generateHtmlFromNodes } from '@lexical/html'
|
||||
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '@/Components/CommandProvider'
|
||||
|
||||
export const ExportPlugin = () => {
|
||||
const application = useApplication()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import LinkedItemBubble from '@/Components/LinkedItems/LinkedItemBubble'
|
||||
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
|
||||
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { useResponsiveAppPane } from '@/Components/ResponsivePane/ResponsivePaneProvider'
|
||||
import { useResponsiveAppPane } from '@/Components/Panes/ResponsivePaneProvider'
|
||||
import { LexicalNode } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TextNode } from 'lexical'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
|
||||
import { ItemOption } from './ItemOption'
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { ContentType, SNNote } from '@standardnotes/snjs'
|
||||
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { SNNote, ContentType } from '@standardnotes/snjs'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
|
||||
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '@/Components/CommandProvider'
|
||||
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
|
||||
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
|
||||
import { ExportPlugin } from './Plugins/ExportPlugin/ExportPlugin'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import Popover from '../Popover/Popover'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import Menu from '../Menu/Menu'
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import AddTagOption from './AddTagOption'
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||
import { NotesOptionsProps } from './NotesOptionsProps'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
|
||||
import { shareSelectedNotes } from '@/NativeMobileWeb/ShareSelectedNotes'
|
||||
import { downloadSelectedNotesOnAndroid } from '@/NativeMobileWeb/DownloadSelectedNotesOnAndroid'
|
||||
@@ -27,7 +27,7 @@ import { NoteAttributes } from './NoteAttributes'
|
||||
import { SpellcheckOptions } from './SpellcheckOptions'
|
||||
import { NoteSizeWarning } from './NoteSizeWarning'
|
||||
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
|
||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { iconClass } from './ClassNames'
|
||||
import SuperNoteOptions from './SuperNoteOptions'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
@@ -160,6 +160,10 @@ const NotesOptions = ({
|
||||
return <ProtectedUnauthorizedLabel />
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{notes.length === 1 && (
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
SUPER_EXPORT_MARKDOWN,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||
import Menu from '../Menu/Menu'
|
||||
|
||||
@@ -31,7 +31,8 @@ type Props = {
|
||||
side: PanelSide
|
||||
type: PanelResizeType
|
||||
resizeFinishCallback?: ResizeFinishCallback
|
||||
widthEventCallback?: () => void
|
||||
widthEventCallback?: (width: number) => void
|
||||
modifyElementWidth: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -82,9 +83,12 @@ class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
|
||||
override componentDidUpdate(prevProps: Props) {
|
||||
this.lastWidth = this.props.panel.scrollWidth
|
||||
|
||||
if (this.props.width != prevProps.width) {
|
||||
this.setWidth(this.props.width)
|
||||
}
|
||||
|
||||
if (this.props.left !== prevProps.left) {
|
||||
this.setLeft(this.props.left)
|
||||
this.setWidth(this.props.width)
|
||||
@@ -108,6 +112,10 @@ class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
|
||||
getParentRect() {
|
||||
if (!this.props.panel.parentNode) {
|
||||
return new DOMRect()
|
||||
}
|
||||
|
||||
return (this.props.panel.parentNode as HTMLElement).getBoundingClientRect()
|
||||
}
|
||||
|
||||
@@ -131,7 +139,7 @@ class PanelResizer extends Component<Props, State> {
|
||||
})
|
||||
}
|
||||
|
||||
setWidth = (width: number, finish = false): void => {
|
||||
setWidth = (width: number, finish = false): number => {
|
||||
if (width === 0) {
|
||||
width = this.computeMaxWidth()
|
||||
}
|
||||
@@ -150,6 +158,7 @@ class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
|
||||
const isFullWidth = Math.round(width + this.lastLeft) === Math.round(parentRect.width)
|
||||
if (this.props.modifyElementWidth) {
|
||||
if (isFullWidth) {
|
||||
if (this.props.type === PanelResizeType.WidthOnly) {
|
||||
this.props.panel.style.removeProperty('width')
|
||||
@@ -159,13 +168,19 @@ class PanelResizer extends Component<Props, State> {
|
||||
} else {
|
||||
this.props.panel.style.width = width + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
this.lastWidth = width
|
||||
|
||||
if (finish) {
|
||||
this.finishSettingWidth()
|
||||
|
||||
if (this.props.resizeFinishCallback) {
|
||||
this.props.resizeFinishCallback(this.lastWidth, this.lastLeft, this.isAtMaxWidth(), this.isCollapsed())
|
||||
}
|
||||
}
|
||||
|
||||
return width
|
||||
}
|
||||
|
||||
setLeft = (left: number) => {
|
||||
@@ -187,10 +202,6 @@ class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
|
||||
handleWidthEvent(event?: MouseEvent) {
|
||||
if (this.props.widthEventCallback) {
|
||||
this.props.widthEventCallback()
|
||||
}
|
||||
|
||||
let x
|
||||
if (event) {
|
||||
x = event.clientX
|
||||
@@ -201,7 +212,11 @@ class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
const deltaX = x - this.lastDownX
|
||||
const newWidth = this.startWidth + deltaX
|
||||
this.setWidth(newWidth, false)
|
||||
const adjustedWidth = this.setWidth(newWidth, false)
|
||||
|
||||
if (this.props.widthEventCallback) {
|
||||
this.props.widthEventCallback(adjustedWidth)
|
||||
}
|
||||
}
|
||||
|
||||
handleLeftEvent(event: MouseEvent) {
|
||||
@@ -309,6 +324,7 @@ class PanelResizer extends Component<Props, State> {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'panel-resizer',
|
||||
'absolute right-0 top-0 z-panel-resizer',
|
||||
'hidden h-full w-[4px] cursor-col-resize border-y-0 bg-[color:var(--panel-resizer-background-color)] md:block',
|
||||
this.props.alwaysVisible || this.state.collapsed || this.state.pressed ? ' opacity-100' : 'opacity-0',
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
|
||||
export enum AppPaneId {
|
||||
Navigation = 'NavigationColumn',
|
||||
Items = 'ItemsColumn',
|
||||
Editor = 'EditorColumn',
|
||||
}
|
||||
|
||||
export const AppPaneIdToDivId = {
|
||||
[AppPaneId.Navigation]: ElementIds.NavigationColumn,
|
||||
[AppPaneId.Items]: ElementIds.ItemsColumn,
|
||||
[AppPaneId.Editor]: ElementIds.EditorColumn,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
|
||||
const ENTRANCE_DURATION = 200
|
||||
const EXIT_DURATION = 200
|
||||
|
||||
export async function animatePaneEntranceTransitionFromOffscreenToTheRight(elementId: string): Promise<void> {
|
||||
log(LoggingDomain.Panes, 'Animating pane entrance transition from offscreen to the right', elementId)
|
||||
const element = document.getElementById(elementId)
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
const animation = element.animate(
|
||||
[
|
||||
{
|
||||
transform: 'translateX(100%)',
|
||||
},
|
||||
{
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: ENTRANCE_DURATION,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards',
|
||||
},
|
||||
)
|
||||
|
||||
await animation.finished
|
||||
|
||||
animation.finish()
|
||||
}
|
||||
|
||||
export async function animatePaneExitTransitionOffscreenToTheRight(elementId: string): Promise<void> {
|
||||
log(LoggingDomain.Panes, 'Animating pane exit transition offscreen to the right', elementId)
|
||||
const element = document.getElementById(elementId)
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
const animation = element.animate(
|
||||
[
|
||||
{
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
{
|
||||
transform: 'translateX(100%)',
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: EXIT_DURATION,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards',
|
||||
},
|
||||
)
|
||||
|
||||
await animation.finished
|
||||
|
||||
animation.finish()
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
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}
|
||||
>
|
||||
{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)
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import {
|
||||
useEffect,
|
||||
@@ -7,25 +6,31 @@ import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
memo,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
MutableRefObject,
|
||||
} from 'react'
|
||||
import { AppPaneId } from './AppPaneMetadata'
|
||||
import { PaneController } from '../../Controllers/PaneController'
|
||||
import { PaneController } from '../../Controllers/PaneController/PaneController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
type ResponsivePaneData = {
|
||||
selectedPane: AppPaneId
|
||||
toggleAppPane: (paneId: AppPaneId) => void
|
||||
toggleNotesListOnTablets: () => void
|
||||
toggleListPane: () => void
|
||||
toggleNavigationPane: () => void
|
||||
isNotesListVisibleOnTablets: boolean
|
||||
isListPaneCollapsed: boolean
|
||||
isNavigationPaneCollapsed: boolean
|
||||
panes: PaneController['panes']
|
||||
toggleAppPane: (paneId: AppPaneId) => void
|
||||
presentPane: PaneController['presentPane']
|
||||
popToPane: PaneController['popToPane']
|
||||
dismissLastPane: PaneController['dismissLastPane']
|
||||
replacePanes: PaneController['replacePanes']
|
||||
removePane: PaneController['removePane']
|
||||
insertPaneAtIndex: PaneController['insertPaneAtIndex']
|
||||
setPaneLayout: PaneController['setPaneLayout']
|
||||
focusModeEnabled: PaneController['focusModeEnabled']
|
||||
}
|
||||
|
||||
const ResponsivePaneContext = createContext<ResponsivePaneData | undefined>(undefined)
|
||||
@@ -62,27 +67,15 @@ const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||
|
||||
const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) => {
|
||||
const currentSelectedPane = paneController.currentPane
|
||||
const previousSelectedPane = paneController.previousPane
|
||||
const currentSelectedPaneRef = useStateRef<AppPaneId>(currentSelectedPane)
|
||||
|
||||
const toggleAppPane = useCallback(
|
||||
(paneId: AppPaneId) => {
|
||||
paneController.setPreviousPane(currentSelectedPane)
|
||||
paneController.setCurrentPane(paneId)
|
||||
paneController.presentPane(paneId)
|
||||
},
|
||||
[paneController, currentSelectedPane],
|
||||
[paneController],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (previousSelectedPane) {
|
||||
const previousPaneElement = document.getElementById(ElementIds[previousSelectedPane])
|
||||
previousPaneElement?.removeAttribute('data-selected-pane')
|
||||
}
|
||||
|
||||
const currentPaneElement = document.getElementById(ElementIds[currentSelectedPane])
|
||||
currentPaneElement?.setAttribute('data-selected-pane', '')
|
||||
}, [currentSelectedPane, previousSelectedPane])
|
||||
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -104,32 +97,40 @@ const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) =>
|
||||
}
|
||||
}, [addAndroidBackHandler, currentSelectedPaneRef, toggleAppPane])
|
||||
|
||||
const [isNotesListVisibleOnTablets, setNotesListVisibleOnTablets] = useState(true)
|
||||
|
||||
const toggleNotesListOnTablets = useCallback(() => {
|
||||
setNotesListVisibleOnTablets((visible) => !visible)
|
||||
}, [])
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
(): ResponsivePaneData => ({
|
||||
selectedPane: currentSelectedPane,
|
||||
toggleAppPane,
|
||||
isNotesListVisibleOnTablets,
|
||||
toggleNotesListOnTablets,
|
||||
presentPane: paneController.presentPane,
|
||||
isListPaneCollapsed: paneController.isListPaneCollapsed,
|
||||
isNavigationPaneCollapsed: paneController.isNavigationPaneCollapsed,
|
||||
toggleListPane: paneController.toggleListPane,
|
||||
toggleNavigationPane: paneController.toggleNavigationPane,
|
||||
panes: paneController.panes,
|
||||
popToPane: paneController.popToPane,
|
||||
dismissLastPane: paneController.dismissLastPane,
|
||||
replacePanes: paneController.replacePanes,
|
||||
removePane: paneController.removePane,
|
||||
insertPaneAtIndex: paneController.insertPaneAtIndex,
|
||||
setPaneLayout: paneController.setPaneLayout,
|
||||
focusModeEnabled: paneController.focusModeEnabled,
|
||||
}),
|
||||
[
|
||||
currentSelectedPane,
|
||||
isNotesListVisibleOnTablets,
|
||||
toggleAppPane,
|
||||
toggleNotesListOnTablets,
|
||||
paneController.toggleListPane,
|
||||
paneController.toggleNavigationPane,
|
||||
paneController.panes,
|
||||
paneController.isListPaneCollapsed,
|
||||
paneController.isNavigationPaneCollapsed,
|
||||
paneController.toggleListPane,
|
||||
paneController.toggleNavigationPane,
|
||||
paneController.presentPane,
|
||||
paneController.popToPane,
|
||||
paneController.dismissLastPane,
|
||||
paneController.replacePanes,
|
||||
paneController.removePane,
|
||||
paneController.insertPaneAtIndex,
|
||||
paneController.setPaneLayout,
|
||||
paneController.focusModeEnabled,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { keyboardStringForShortcut, PIN_NOTE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ContentType, ItemCounter } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND } from '@standardnotes/ui-services'
|
||||
import { useMemo } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
|
||||
const PanelSettingsSection = () => {
|
||||
@@ -25,7 +25,7 @@ const PanelSettingsSection = () => {
|
||||
<div className="hidden md:block pointer-coarse:md-only:hidden pointer-coarse:lg-only:hidden">
|
||||
<MenuSwitchButtonItem
|
||||
className="items-center"
|
||||
checked={isNavigationPaneCollapsed}
|
||||
checked={!isNavigationPaneCollapsed}
|
||||
onChange={toggleNavigationPane}
|
||||
shortcut={navigationShortcut}
|
||||
>
|
||||
@@ -33,7 +33,7 @@ const PanelSettingsSection = () => {
|
||||
</MenuSwitchButtonItem>
|
||||
<MenuSwitchButtonItem
|
||||
className="items-center"
|
||||
checked={isListPaneCollapsed}
|
||||
checked={!isListPaneCollapsed}
|
||||
onChange={toggleListPane}
|
||||
shortcut={listShortcut}
|
||||
>
|
||||
|
||||
@@ -23,15 +23,14 @@ import Menu from '../Menu/Menu'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||
|
||||
export const focusModeAnimationDuration = 1255
|
||||
|
||||
type MenuProps = {
|
||||
quickSettingsMenuController: QuickSettingsController
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSettingsMenuController }) => {
|
||||
const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController
|
||||
const { focusModeEnabled, setFocusModeEnabled } = application.paneController
|
||||
const { closeQuickSettingsMenu } = quickSettingsMenuController
|
||||
const [themes, setThemes] = useState<ThemeItem[]>([])
|
||||
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum AppPaneId {
|
||||
Navigation = 'NavigationColumn',
|
||||
Items = 'ItemsColumn',
|
||||
Editor = 'EditorColumn',
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useMemo, ReactNode } from 'react'
|
||||
import { AppPaneId } from './AppPaneMetadata'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useResponsiveAppPane } from './ResponsivePaneProvider'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
contentElementId?: string
|
||||
paneId: AppPaneId
|
||||
}
|
||||
|
||||
const ResponsivePaneContent = ({ children, className, contentElementId, paneId }: Props) => {
|
||||
const { selectedPane } = useResponsiveAppPane()
|
||||
|
||||
const isSelectedPane = useMemo(() => selectedPane === paneId, [paneId, selectedPane])
|
||||
|
||||
return (
|
||||
<div
|
||||
id={contentElementId}
|
||||
className={classNames('content flex flex-col', isSelectedPane ? 'h-full' : 'hidden md:flex', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResponsivePaneContent
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors'
|
||||
import { NoteType } from '@standardnotes/snjs'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import SmartViewsSection from '@/Components/Tags/SmartViewsSection'
|
||||
import TagsSection from '@/Components/Tags/TagsSection'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
||||
import { ApplicationEvent, PrefKey, WebAppEvent } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
||||
import { forwardRef, useEffect, useMemo, useState } from 'react'
|
||||
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import UpgradeNow from '../Footer/UpgradeNow'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import { isIOS } from '@/Utils'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
id: string
|
||||
}
|
||||
|
||||
const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className, children, id }, ref) => {
|
||||
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
|
||||
const [panelElement, setPanelElement] = useState<HTMLDivElement>()
|
||||
const [panelWidth, setPanelWidth] = useState<number>(0)
|
||||
const { selectedPane, toggleAppPane } = useResponsiveAppPane()
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [hasPasscode, setHasPasscode] = useState(() => application.hasPasscode())
|
||||
useEffect(() => {
|
||||
@@ -34,25 +34,15 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
const removeObserver = application.addEventObserver(async () => {
|
||||
const width = application.getPreference(PrefKey.TagsPanelWidth)
|
||||
if (width) {
|
||||
setPanelWidth(width)
|
||||
}
|
||||
}, ApplicationEvent.PreferencesChanged)
|
||||
|
||||
return () => {
|
||||
removeObserver()
|
||||
}
|
||||
}, [application])
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
||||
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
||||
return application.addWebEventObserver((event, data) => {
|
||||
if (event === WebAppEvent.PanelResized) {
|
||||
const { panel, width } = data as PanelResizedData
|
||||
if (panel === PANEL_NAME_NAVIGATION) {
|
||||
application.setPreference(PrefKey.TagsPanelWidth, width).catch(console.error)
|
||||
application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, isCollapsed)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [application])
|
||||
|
||||
const NavigationFooter = useMemo(() => {
|
||||
return (
|
||||
@@ -115,26 +105,19 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id="navigation"
|
||||
id={id}
|
||||
className={classNames(
|
||||
'pb-[50px] md:pb-0',
|
||||
'sn-component section app-column h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0',
|
||||
'w-[220px] xl:w-[220px] xsm-only:!w-full sm-only:!w-full',
|
||||
selectedPane === AppPaneId.Navigation
|
||||
? 'pointer-coarse:md-only:!w-48 pointer-coarse:lg-only:!w-48'
|
||||
: 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0',
|
||||
className,
|
||||
'sn-component section pb-[50px] md:pb-0',
|
||||
'h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0',
|
||||
)}
|
||||
ref={(element) => {
|
||||
if (element) {
|
||||
setPanelElement(element)
|
||||
}
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
|
||||
<div
|
||||
id="navigation-content"
|
||||
className={classNames(
|
||||
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay] pointer-coarse:md:overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
<SmartViewsSection
|
||||
@@ -145,22 +128,10 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
<TagsSection viewControllerManager={viewControllerManager} />
|
||||
</div>
|
||||
{NavigationFooter}
|
||||
</ResponsivePaneContent>
|
||||
{panelElement && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
defaultWidth={150}
|
||||
panel={panelElement}
|
||||
hoverable={true}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.WidthOnly}
|
||||
resizeFinishCallback={panelResizeFinishCallback}
|
||||
width={panelWidth}
|
||||
left={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default observer(Navigation)
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
|
||||
@@ -37,8 +35,6 @@ const getIconClass = (view: SmartView, isSelected: boolean): string => {
|
||||
}
|
||||
|
||||
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEditingSmartView }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [title, setTitle] = useState(view.title || '')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -54,8 +50,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEdit
|
||||
await tagsState.setSelectedTag(view, 'views', {
|
||||
userTriggered: true,
|
||||
})
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}, [tagsState, toggleAppPane, view])
|
||||
}, [tagsState, view])
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
tagsState.save(view, title).catch(console.error)
|
||||
|
||||
@@ -19,10 +19,8 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { TagListSectionType } from './TagListSection'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
@@ -44,8 +42,6 @@ const PADDING_PER_LEVEL_PX = 21
|
||||
|
||||
export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
({ tag, type, features, navigationController: navigationController, level, onContextMenu, linkingController }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [title, setTitle] = useState(tag.title || '')
|
||||
const [subtagTitle, setSubtagTitle] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -94,8 +90,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
await navigationController.setSelectedTag(tag, type, {
|
||||
userTriggered: true,
|
||||
})
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}, [navigationController, tag, type, toggleAppPane])
|
||||
}, [navigationController, tag, type])
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
navigationController.save(tag, title).catch(console.error)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
|
||||
import { CREATE_NEW_TAG_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useMemo } from 'react'
|
||||
import { useCommandService } from '../ApplicationView/CommandProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
|
||||
type Props = {
|
||||
tags: NavigationController
|
||||
|
||||
@@ -8,7 +8,7 @@ export const PrefDefaults = {
|
||||
[PrefKey.EditorLeft]: null,
|
||||
[PrefKey.EditorMonospaceEnabled]: false,
|
||||
[PrefKey.EditorSpellcheck]: true,
|
||||
[PrefKey.EditorResizersEnabled]: true,
|
||||
[PrefKey.EditorResizersEnabled]: false,
|
||||
[PrefKey.EditorLineHeight]: EditorLineHeight.Normal,
|
||||
[PrefKey.EditorFontSize]: EditorFontSize.Normal,
|
||||
[PrefKey.SortNotesBy]: CollectionSort.CreatedAt,
|
||||
|
||||
@@ -52,7 +52,6 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
notesToDisplay = 0
|
||||
pageSize = 0
|
||||
panelTitle = 'Notes'
|
||||
panelWidth = 0
|
||||
renderedItems: ListableContentItem[] = []
|
||||
searchSubmitted = false
|
||||
showDisplayOptionsMenu = false
|
||||
@@ -189,7 +188,6 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
notes: observable,
|
||||
notesToDisplay: observable,
|
||||
panelTitle: observable,
|
||||
panelWidth: observable,
|
||||
items: observable,
|
||||
renderedItems: observable,
|
||||
showDisplayOptionsMenu: observable,
|
||||
@@ -439,6 +437,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
const activeController = this.getActiveItemController()
|
||||
|
||||
if (this.shouldLeaveSelectionUnchanged(activeController)) {
|
||||
log(LoggingDomain.Selection, 'Leaving selection unchanged')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -451,7 +450,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
|
||||
if (this.shouldSelectFirstItem(itemsReloadSource)) {
|
||||
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
|
||||
this.selectionController.selectNextItem()
|
||||
this.selectionController.selectNextItem({ userTriggered: false })
|
||||
}
|
||||
} else if (activeItem && this.shouldSelectActiveItem(activeItem)) {
|
||||
log(LoggingDomain.Selection, 'Selecting active item')
|
||||
@@ -460,6 +459,8 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
await this.selectFirstItem()
|
||||
} else if (this.shouldSelectNextItemOrCreateNewNote(activeItem)) {
|
||||
await this.selectNextItemOrCreateNewNote()
|
||||
} else {
|
||||
log(LoggingDomain.Selection, 'No selection change')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,13 +580,6 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
this.displayOptions = newDisplayOptions
|
||||
this.webDisplayOptions = newWebDisplayOptions
|
||||
|
||||
const listColumnWidth =
|
||||
selectedTag?.preferences?.panelWidth || this.application.getPreference(PrefKey.NotesPanelWidth)
|
||||
|
||||
if (listColumnWidth && listColumnWidth !== this.panelWidth) {
|
||||
this.panelWidth = listColumnWidth
|
||||
}
|
||||
|
||||
if (!displayOptionsChanged) {
|
||||
return { didReloadItems: false }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PopoverFileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
||||
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
||||
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
|
||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||
|
||||
@@ -25,6 +25,7 @@ import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
import { Persistable } from '../Abstract/Persistable'
|
||||
import { TagListSectionType } from '@/Components/Tags/TagListSection'
|
||||
import { PaneLayout } from '../PaneController/PaneLayout'
|
||||
|
||||
export class NavigationController
|
||||
extends AbstractViewController
|
||||
@@ -260,6 +261,10 @@ export class NavigationController
|
||||
return this.selectedUuid === SystemViewId.Files
|
||||
}
|
||||
|
||||
isTagFilesView(tag: AnyTag): boolean {
|
||||
return tag.uuid === SystemViewId.Files
|
||||
}
|
||||
|
||||
public isInAnySystemView(): boolean {
|
||||
return (
|
||||
this.selected instanceof SmartView && Object.values(SystemViewId).includes(this.selected.uuid as SystemViewId)
|
||||
@@ -458,10 +463,10 @@ export class NavigationController
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const selectionHasNotChanged = this.selected_?.uuid === tag?.uuid && location === this.selectedLocation
|
||||
|
||||
if (selectionHasNotChanged) {
|
||||
return
|
||||
if (tag && this.isTagFilesView(tag)) {
|
||||
this.application.paneController.setPaneLayout(PaneLayout.FilesView)
|
||||
} else if (userTriggered) {
|
||||
this.application.paneController.setPaneLayout(PaneLayout.ItemSelection)
|
||||
}
|
||||
|
||||
this.previouslySelected_ = this.selected_
|
||||
@@ -497,7 +502,7 @@ export class NavigationController
|
||||
}
|
||||
|
||||
get filesNavigationView(): SmartView {
|
||||
return this.smartViews.find((view) => view.uuid === SystemViewId.Files) as SmartView
|
||||
return this.smartViews.find(this.isTagFilesView) as SmartView
|
||||
}
|
||||
|
||||
private setSelectedTagInstance(tag: AnyTag | undefined): void {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND } from '@standardnotes/ui-services'
|
||||
import { ApplicationEvent, InternalEventBus, PrefKey } from '@standardnotes/snjs'
|
||||
import { AppPaneId } from './../Components/ResponsivePane/AppPaneMetadata'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { makeObservable, observable, action, computed } from 'mobx'
|
||||
import { Disposer } from '@/Types/Disposer'
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||
|
||||
const WidthForCollapsedPanel = 5
|
||||
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
|
||||
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
|
||||
|
||||
export class PaneController extends AbstractViewController {
|
||||
currentPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor
|
||||
previousPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor
|
||||
isInMobileView = isMobileScreen()
|
||||
protected disposers: Disposer[] = []
|
||||
|
||||
currentNavPanelWidth = 0
|
||||
currentItemsPanelWidth = 0
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||
super(application, eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
currentPane: observable,
|
||||
previousPane: observable,
|
||||
isInMobileView: observable,
|
||||
currentNavPanelWidth: observable,
|
||||
currentItemsPanelWidth: observable,
|
||||
|
||||
isListPaneCollapsed: computed,
|
||||
isNavigationPaneCollapsed: computed,
|
||||
|
||||
setCurrentPane: action,
|
||||
setPreviousPane: action,
|
||||
setIsInMobileView: action,
|
||||
toggleListPane: action,
|
||||
toggleNavigationPane: action,
|
||||
setCurrentItemsPanelWidth: action,
|
||||
setCurrentNavPanelWidth: action,
|
||||
})
|
||||
|
||||
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
|
||||
const mediaQuery = window.matchMedia(MediaQueryBreakpoints.md)
|
||||
if (mediaQuery?.addEventListener != undefined) {
|
||||
mediaQuery.addEventListener('change', this.mediumScreenMQHandler)
|
||||
} else {
|
||||
mediaQuery.addListener(this.mediumScreenMQHandler)
|
||||
}
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
|
||||
application.keyboardService.addCommandHandler({
|
||||
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.toggleListPane()
|
||||
},
|
||||
}),
|
||||
application.keyboardService.addCommandHandler({
|
||||
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.toggleNavigationPane()
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
setCurrentNavPanelWidth(width: number) {
|
||||
this.currentNavPanelWidth = width
|
||||
}
|
||||
|
||||
setCurrentItemsPanelWidth(width: number) {
|
||||
this.currentItemsPanelWidth = width
|
||||
}
|
||||
|
||||
deinit() {
|
||||
super.deinit()
|
||||
const mq = window.matchMedia(MediaQueryBreakpoints.md)
|
||||
if (mq?.removeEventListener != undefined) {
|
||||
mq.removeEventListener('change', this.mediumScreenMQHandler)
|
||||
} else {
|
||||
mq.removeListener(this.mediumScreenMQHandler)
|
||||
}
|
||||
}
|
||||
|
||||
mediumScreenMQHandler = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
this.setIsInMobileView(false)
|
||||
} else {
|
||||
this.setIsInMobileView(true)
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentPane(pane: AppPaneId): void {
|
||||
this.currentPane = pane
|
||||
}
|
||||
|
||||
setPreviousPane(pane: AppPaneId): void {
|
||||
this.previousPane = pane
|
||||
}
|
||||
|
||||
setIsInMobileView(isInMobileView: boolean) {
|
||||
this.isInMobileView = isInMobileView
|
||||
}
|
||||
|
||||
toggleListPane = () => {
|
||||
const currentItemsPanelWidth = this.application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)
|
||||
|
||||
const isCollapsed = currentItemsPanelWidth <= WidthForCollapsedPanel
|
||||
if (isCollapsed) {
|
||||
void this.application.setPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)
|
||||
} else {
|
||||
void this.application.setPreference(PrefKey.NotesPanelWidth, WidthForCollapsedPanel)
|
||||
}
|
||||
|
||||
this.application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, !isCollapsed)
|
||||
}
|
||||
|
||||
toggleNavigationPane = () => {
|
||||
const currentNavPanelWidth = this.application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)
|
||||
|
||||
const isCollapsed = currentNavPanelWidth <= WidthForCollapsedPanel
|
||||
if (isCollapsed) {
|
||||
void this.application.setPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)
|
||||
} else {
|
||||
void this.application.setPreference(PrefKey.TagsPanelWidth, WidthForCollapsedPanel)
|
||||
}
|
||||
|
||||
this.application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, !isCollapsed)
|
||||
}
|
||||
|
||||
get isListPaneCollapsed() {
|
||||
return this.currentItemsPanelWidth > WidthForCollapsedPanel
|
||||
}
|
||||
|
||||
get isNavigationPaneCollapsed() {
|
||||
return this.currentNavPanelWidth > WidthForCollapsedPanel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
TOGGLE_FOCUS_MODE_COMMAND,
|
||||
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { ApplicationEvent, InternalEventBus, PrefKey, removeFromArray } from '@standardnotes/snjs'
|
||||
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { makeObservable, observable, action, computed } from 'mobx'
|
||||
import { Disposer } from '@/Types/Disposer'
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { PaneLayout } from './PaneLayout'
|
||||
import { panesForLayout } from './panesForLayout'
|
||||
import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen'
|
||||
|
||||
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
|
||||
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
|
||||
const FOCUS_MODE_CLASS_NAME = 'focus-mode'
|
||||
const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode'
|
||||
const FOCUS_MODE_ANIMATION_DURATION = 1255
|
||||
|
||||
export class PaneController extends AbstractViewController {
|
||||
isInMobileView = isMobileScreen()
|
||||
protected disposers: Disposer[] = []
|
||||
panes: AppPaneId[] = []
|
||||
|
||||
currentNavPanelWidth = 0
|
||||
currentItemsPanelWidth = 0
|
||||
focusModeEnabled = false
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||
super(application, eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
panes: observable,
|
||||
isInMobileView: observable,
|
||||
currentNavPanelWidth: observable,
|
||||
currentItemsPanelWidth: observable,
|
||||
focusModeEnabled: observable,
|
||||
|
||||
currentPane: computed,
|
||||
previousPane: computed,
|
||||
isListPaneCollapsed: computed,
|
||||
isNavigationPaneCollapsed: computed,
|
||||
|
||||
setIsInMobileView: action,
|
||||
toggleListPane: action,
|
||||
toggleNavigationPane: action,
|
||||
setCurrentItemsPanelWidth: action,
|
||||
setCurrentNavPanelWidth: action,
|
||||
presentPane: action,
|
||||
dismissLastPane: action,
|
||||
replacePanes: action,
|
||||
popToPane: action,
|
||||
removePane: action,
|
||||
insertPaneAtIndex: action,
|
||||
setPaneLayout: action,
|
||||
setFocusModeEnabled: action,
|
||||
})
|
||||
|
||||
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
|
||||
const screen = getIsTabletOrMobileScreen(application)
|
||||
|
||||
this.panes = screen.isTabletOrMobile
|
||||
? [AppPaneId.Navigation, AppPaneId.Items]
|
||||
: [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
|
||||
|
||||
const mediaQuery = window.matchMedia(MediaQueryBreakpoints.md)
|
||||
if (mediaQuery?.addEventListener != undefined) {
|
||||
mediaQuery.addEventListener('change', this.mediumScreenMQHandler)
|
||||
} else {
|
||||
mediaQuery.addListener(this.mediumScreenMQHandler)
|
||||
}
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
|
||||
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
|
||||
application.keyboardService.addCommandHandler({
|
||||
command: TOGGLE_FOCUS_MODE_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.setFocusModeEnabled(!this.focusModeEnabled)
|
||||
return true
|
||||
},
|
||||
}),
|
||||
application.keyboardService.addCommandHandler({
|
||||
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.toggleListPane()
|
||||
},
|
||||
}),
|
||||
application.keyboardService.addCommandHandler({
|
||||
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.toggleNavigationPane()
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
setCurrentNavPanelWidth(width: number) {
|
||||
this.currentNavPanelWidth = width
|
||||
}
|
||||
|
||||
setCurrentItemsPanelWidth(width: number) {
|
||||
this.currentItemsPanelWidth = width
|
||||
}
|
||||
|
||||
deinit() {
|
||||
super.deinit()
|
||||
const mq = window.matchMedia(MediaQueryBreakpoints.md)
|
||||
if (mq?.removeEventListener != undefined) {
|
||||
mq.removeEventListener('change', this.mediumScreenMQHandler)
|
||||
} else {
|
||||
mq.removeListener(this.mediumScreenMQHandler)
|
||||
}
|
||||
}
|
||||
|
||||
get currentPane(): AppPaneId {
|
||||
return this.panes[this.panes.length - 1] || this.panes[0]
|
||||
}
|
||||
|
||||
get previousPane(): AppPaneId {
|
||||
return this.panes[this.panes.length - 2] || this.panes[0]
|
||||
}
|
||||
|
||||
mediumScreenMQHandler = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
this.setIsInMobileView(false)
|
||||
} else {
|
||||
this.setIsInMobileView(true)
|
||||
}
|
||||
}
|
||||
|
||||
setIsInMobileView = (isInMobileView: boolean) => {
|
||||
this.isInMobileView = isInMobileView
|
||||
}
|
||||
|
||||
setPaneLayout = (layout: PaneLayout) => {
|
||||
log(LoggingDomain.Panes, 'Set pane layout', layout)
|
||||
|
||||
this.replacePanes(panesForLayout(layout, this.application))
|
||||
}
|
||||
|
||||
replacePanes = (panes: AppPaneId[]) => {
|
||||
log(LoggingDomain.Panes, 'Replacing panes', panes)
|
||||
|
||||
this.panes = panes
|
||||
}
|
||||
|
||||
presentPane = (pane: AppPaneId) => {
|
||||
log(LoggingDomain.Panes, 'Presenting pane', pane)
|
||||
|
||||
if (pane === this.currentPane) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pane === AppPaneId.Items && this.currentPane === AppPaneId.Editor) {
|
||||
this.dismissLastPane()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.currentPane !== pane) {
|
||||
this.panes.push(pane)
|
||||
}
|
||||
}
|
||||
|
||||
insertPaneAtIndex = (pane: AppPaneId, index: number) => {
|
||||
log(LoggingDomain.Panes, 'Inserting pane', pane, 'at index', index)
|
||||
|
||||
this.panes.splice(index, 0, pane)
|
||||
}
|
||||
|
||||
dismissLastPane = (): AppPaneId | undefined => {
|
||||
log(LoggingDomain.Panes, 'Dismissing last pane')
|
||||
|
||||
return this.panes.pop()
|
||||
}
|
||||
|
||||
removePane = (pane: AppPaneId) => {
|
||||
log(LoggingDomain.Panes, 'Removing pane', pane)
|
||||
|
||||
removeFromArray(this.panes, pane)
|
||||
}
|
||||
|
||||
popToPane = (pane: AppPaneId) => {
|
||||
log(LoggingDomain.Panes, 'Popping to pane', pane)
|
||||
|
||||
let index = this.panes.length - 1
|
||||
while (index >= 0) {
|
||||
if (this.panes[index] === pane) {
|
||||
break
|
||||
}
|
||||
|
||||
this.dismissLastPane()
|
||||
index--
|
||||
}
|
||||
}
|
||||
|
||||
toggleListPane = () => {
|
||||
if (this.panes.includes(AppPaneId.Items)) {
|
||||
this.removePane(AppPaneId.Items)
|
||||
} else {
|
||||
if (this.panes.includes(AppPaneId.Navigation)) {
|
||||
this.insertPaneAtIndex(AppPaneId.Items, 1)
|
||||
} else {
|
||||
this.insertPaneAtIndex(AppPaneId.Items, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleNavigationPane = () => {
|
||||
if (this.panes.includes(AppPaneId.Navigation)) {
|
||||
this.removePane(AppPaneId.Navigation)
|
||||
} else {
|
||||
this.insertPaneAtIndex(AppPaneId.Navigation, 0)
|
||||
}
|
||||
}
|
||||
|
||||
get isListPaneCollapsed() {
|
||||
return !this.panes.includes(AppPaneId.Items)
|
||||
}
|
||||
|
||||
get isNavigationPaneCollapsed() {
|
||||
return !this.panes.includes(AppPaneId.Navigation)
|
||||
}
|
||||
|
||||
setFocusModeEnabled = (enabled: boolean): void => {
|
||||
this.focusModeEnabled = enabled
|
||||
|
||||
if (enabled) {
|
||||
document.body.classList.add(FOCUS_MODE_CLASS_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
if (document.body.classList.contains(FOCUS_MODE_CLASS_NAME)) {
|
||||
document.body.classList.add(DISABLING_FOCUS_MODE_CLASS_NAME)
|
||||
document.body.classList.remove(FOCUS_MODE_CLASS_NAME)
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove(DISABLING_FOCUS_MODE_CLASS_NAME)
|
||||
}, FOCUS_MODE_ANIMATION_DURATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum PaneLayout {
|
||||
TagSelection = 'tag-selection',
|
||||
ItemSelection = 'item-selection',
|
||||
FilesView = 'files-view',
|
||||
Editing = 'editing',
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
|
||||
import { PaneLayout } from './PaneLayout'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen'
|
||||
|
||||
export function panesForLayout(layout: PaneLayout, application: WebApplication): AppPaneId[] {
|
||||
const screen = getIsTabletOrMobileScreen(application)
|
||||
if (screen.isTablet) {
|
||||
if (layout === PaneLayout.TagSelection) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items]
|
||||
} else if (
|
||||
layout === PaneLayout.ItemSelection ||
|
||||
layout === PaneLayout.Editing ||
|
||||
layout === PaneLayout.FilesView
|
||||
) {
|
||||
return [AppPaneId.Items, AppPaneId.Editor]
|
||||
}
|
||||
} else if (screen.isMobile) {
|
||||
if (layout === PaneLayout.TagSelection) {
|
||||
return [AppPaneId.Navigation]
|
||||
} else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.FilesView) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items]
|
||||
} else if (layout === PaneLayout.Editing) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
|
||||
}
|
||||
} else {
|
||||
if (layout === PaneLayout.FilesView) {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items]
|
||||
} else {
|
||||
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
|
||||
}
|
||||
}
|
||||
|
||||
throw Error(`Unhandled pane layout ${layout}`)
|
||||
}
|
||||
|
||||
export function isPanesChangeLeafDismiss(from: AppPaneId[], to: AppPaneId[]): boolean {
|
||||
const fromWithoutLast = from.slice(0, from.length - 1)
|
||||
|
||||
return fromWithoutLast.length === to.length && fromWithoutLast.every((pane, index) => pane === to[index])
|
||||
}
|
||||
|
||||
export function isPanesChangePush(from: AppPaneId[], to: AppPaneId[]): boolean {
|
||||
const toWithoutLast = to.slice(0, to.length - 1)
|
||||
|
||||
return toWithoutLast.length === from.length && toWithoutLast.every((pane, index) => pane === from[index])
|
||||
}
|
||||
@@ -2,13 +2,10 @@ import { InternalEventBus } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { TOGGLE_FOCUS_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { toggleFocusMode } from '@/Utils/toggleFocusMode'
|
||||
|
||||
export class QuickSettingsController extends AbstractViewController {
|
||||
open = false
|
||||
shouldAnimateCloseMenu = false
|
||||
focusModeEnabled = false
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||
super(application, eventBus)
|
||||
@@ -16,25 +13,12 @@ export class QuickSettingsController extends AbstractViewController {
|
||||
makeObservable(this, {
|
||||
open: observable,
|
||||
shouldAnimateCloseMenu: observable,
|
||||
focusModeEnabled: observable,
|
||||
|
||||
setOpen: action,
|
||||
setShouldAnimateCloseMenu: action,
|
||||
setFocusModeEnabled: action,
|
||||
toggle: action,
|
||||
closeQuickSettingsMenu: action,
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
application.keyboardService.addCommandHandler({
|
||||
command: TOGGLE_FOCUS_MODE_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
this.setFocusModeEnabled(!this.focusModeEnabled)
|
||||
return true
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
setOpen = (open: boolean): void => {
|
||||
@@ -45,12 +29,6 @@ export class QuickSettingsController extends AbstractViewController {
|
||||
this.shouldAnimateCloseMenu = shouldAnimate
|
||||
}
|
||||
|
||||
setFocusModeEnabled = (enabled: boolean): void => {
|
||||
this.focusModeEnabled = enabled
|
||||
|
||||
toggleFocusMode(enabled)
|
||||
}
|
||||
|
||||
toggle = (): void => {
|
||||
if (this.open) {
|
||||
this.closeQuickSettingsMenu()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { Persistable } from './Abstract/Persistable'
|
||||
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||
import { ItemListController } from './ItemList/ItemListController'
|
||||
import { PaneLayout } from './PaneController/PaneLayout'
|
||||
|
||||
export class SelectedItemsController
|
||||
extends AbstractViewController
|
||||
@@ -139,6 +141,7 @@ export class SelectedItemsController
|
||||
}
|
||||
|
||||
setSelectedUuids = (selectedUuids: Set<UuidString>) => {
|
||||
log(LoggingDomain.Selection, 'Setting selected uuids', selectedUuids)
|
||||
this.selectedUuids = new Set(selectedUuids)
|
||||
this.setSelectedItems()
|
||||
}
|
||||
@@ -150,6 +153,7 @@ export class SelectedItemsController
|
||||
}
|
||||
|
||||
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
|
||||
log(LoggingDomain.Selection, 'Deselecting item', item.uuid)
|
||||
this.removeSelectedItem(item.uuid)
|
||||
|
||||
if (item.uuid === this.lastSelectedItem?.uuid) {
|
||||
@@ -228,7 +232,7 @@ export class SelectedItemsController
|
||||
this.lastSelectedItem = undefined
|
||||
}
|
||||
|
||||
openSingleSelectedItem = async () => {
|
||||
openSingleSelectedItem = async ({ userTriggered } = { userTriggered: true }) => {
|
||||
if (this.selectedItemsCount === 1) {
|
||||
const item = this.firstSelectedItem
|
||||
|
||||
@@ -237,6 +241,10 @@ export class SelectedItemsController
|
||||
} else if (item.content_type === ContentType.File) {
|
||||
await this.itemListController.openFile(item.uuid)
|
||||
}
|
||||
|
||||
if (!this.application.paneController.isInMobileView || userTriggered) {
|
||||
void this.application.paneController.setPaneLayout(PaneLayout.Editing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +262,7 @@ export class SelectedItemsController
|
||||
}
|
||||
}
|
||||
|
||||
log(LoggingDomain.Selection, 'selectItem', item.uuid)
|
||||
log(LoggingDomain.Selection, 'Select item', item.uuid)
|
||||
|
||||
const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta)
|
||||
const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl)
|
||||
@@ -278,7 +286,7 @@ export class SelectedItemsController
|
||||
}
|
||||
}
|
||||
|
||||
await this.openSingleSelectedItem()
|
||||
await this.openSingleSelectedItem({ userTriggered: userTriggered ?? false })
|
||||
|
||||
return {
|
||||
didSelect: this.selectedUuids.has(uuid),
|
||||
@@ -293,7 +301,9 @@ export class SelectedItemsController
|
||||
): Promise<void> => {
|
||||
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
|
||||
|
||||
if (didSelect && scrollIntoView) {
|
||||
const avoidMobileScrollingDueToIncompatibilityWithPaneAnimations = isMobileScreen()
|
||||
|
||||
if (didSelect && scrollIntoView && !avoidMobileScrollingDueToIncompatibilityWithPaneAnimations) {
|
||||
this.scrollToItem(item, animated)
|
||||
}
|
||||
}
|
||||
@@ -319,11 +329,11 @@ export class SelectedItemsController
|
||||
this.setSelectedUuids(new Set(Uuids(itemsForUuids)))
|
||||
|
||||
if (itemsForUuids.length === 1) {
|
||||
void this.openSingleSelectedItem()
|
||||
void this.openSingleSelectedItem({ userTriggered })
|
||||
}
|
||||
}
|
||||
|
||||
selectNextItem = () => {
|
||||
selectNextItem = ({ userTriggered } = { userTriggered: true }) => {
|
||||
const displayableItems = this.itemListController.items
|
||||
|
||||
const currentIndex = displayableItems.findIndex((candidate) => {
|
||||
@@ -341,7 +351,7 @@ export class SelectedItemsController
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectItemWithScrollHandling(nextItem, { userTriggered: true }).catch(console.error)
|
||||
this.selectItemWithScrollHandling(nextItem, { userTriggered }).catch(console.error)
|
||||
|
||||
const nextNoteElement = document.getElementById(nextItem.uuid)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PaneController } from './PaneController'
|
||||
import { PaneController } from './PaneController/PaneController'
|
||||
import {
|
||||
PersistedStateValue,
|
||||
PersistenceKey,
|
||||
|
||||
19
packages/web/src/javascripts/Hooks/useForwardedRef.tsx
Normal file
19
packages/web/src/javascripts/Hooks/useForwardedRef.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ForwardedRef, useEffect, useRef } from 'react'
|
||||
|
||||
export const useForwardedRef = <T,>(ref: ForwardedRef<T>, initialValue = null) => {
|
||||
const targetRef = useRef<T>(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof ref === 'function') {
|
||||
ref(targetRef.current)
|
||||
} else {
|
||||
ref.current = targetRef.current
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return targetRef
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { isMobileScreen, isTabletOrMobileScreen, isTabletScreen } from '@/Utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function getIsTabletOrMobileScreen(application: WebApplication) {
|
||||
const isNativeMobile = application.isNativeMobileWeb()
|
||||
const isTabletOrMobile = isTabletOrMobileScreen() || isNativeMobile
|
||||
const isTablet = isTabletScreen() || (isNativeMobile && !isMobileScreen())
|
||||
const isMobile = isMobileScreen() || (isNativeMobile && !isTablet)
|
||||
|
||||
return {
|
||||
isTabletOrMobile,
|
||||
isTablet,
|
||||
isMobile,
|
||||
}
|
||||
}
|
||||
|
||||
export default function useIsTabletOrMobileScreen() {
|
||||
const [_windowSize, setWindowSize] = useState(0)
|
||||
const application = useApplication()
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize(window.innerWidth)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
handleResize()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return getIsTabletOrMobileScreen(application)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export enum LoggingDomain {
|
||||
Selection,
|
||||
BlockEditor,
|
||||
Purchasing,
|
||||
Panes,
|
||||
}
|
||||
|
||||
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||
@@ -21,6 +22,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||
[LoggingDomain.Selection]: false,
|
||||
[LoggingDomain.BlockEditor]: false,
|
||||
[LoggingDomain.Purchasing]: false,
|
||||
[LoggingDomain.Panes]: false,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type PanelResizedData = {
|
||||
panel: string
|
||||
collapsed: boolean
|
||||
width: number
|
||||
}
|
||||
|
||||
@@ -205,9 +205,12 @@ export const disableIosTextFieldZoom = () => {
|
||||
}
|
||||
|
||||
export const isMobileScreen = () => !window.matchMedia(MediaQueryBreakpoints.md).matches
|
||||
|
||||
export const isTabletScreen = () =>
|
||||
!window.matchMedia(MediaQueryBreakpoints.sm).matches && !window.matchMedia(MediaQueryBreakpoints.lg).matches
|
||||
|
||||
export const isTabletOrMobileScreen = () => isMobileScreen() || isTabletScreen()
|
||||
|
||||
export const getBase64FromBlob = (blob: Blob) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { focusModeAnimationDuration } from '../Components/QuickSettingsMenu/QuickSettingsMenu'
|
||||
|
||||
export const FOCUS_MODE_CLASS_NAME = 'focus-mode'
|
||||
export const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode'
|
||||
|
||||
export const toggleFocusMode = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
document.body.classList.add(FOCUS_MODE_CLASS_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
if (document.body.classList.contains(FOCUS_MODE_CLASS_NAME)) {
|
||||
document.body.classList.add(DISABLING_FOCUS_MODE_CLASS_NAME)
|
||||
document.body.classList.remove(FOCUS_MODE_CLASS_NAME)
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove(DISABLING_FOCUS_MODE_CLASS_NAME)
|
||||
}, focusModeAnimationDuration)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
.animate-slide-in-top {
|
||||
animation: slide-in-top 0.1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-in-top {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
.app-column-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto auto 2fr;
|
||||
}
|
||||
}
|
||||
|
||||
.app-column {
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
&[data-selected-pane] {
|
||||
flex-grow: 1;
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#editor-content {
|
||||
width: 100% !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
&:not([data-selected-pane]) {
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
padding-top: 35px;
|
||||
}
|
||||
|
||||
.app {
|
||||
transition: grid-template-columns 0.25s;
|
||||
}
|
||||
|
||||
.mac-desktop #editor-column:before {
|
||||
content: '';
|
||||
display: block;
|
||||
@@ -28,8 +32,8 @@
|
||||
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
|
||||
.content {
|
||||
box-shadow: 0 0 4px 1px var(--sn-stylekit-shadow-color);
|
||||
.editor {
|
||||
box-shadow: 0 0 5px 0.5px var(--sn-stylekit-shadow-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
user-select: none;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
border-left: 1px solid var(--items-column-border-left-color);
|
||||
border-right: 1px solid var(--items-column-border-right-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,6 @@ body,
|
||||
width: 100%;
|
||||
|
||||
.section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.section-title-bar {
|
||||
|
||||
@@ -8,6 +8,10 @@ $content-horizontal-padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
border-right: 1px solid var(--items-column-border-right-color);
|
||||
}
|
||||
|
||||
&,
|
||||
#navigation-content {
|
||||
background-color: var(--navigation-column-background-color);
|
||||
|
||||
@@ -19,7 +19,6 @@ $blocks-editor-icons-path: '../../../blocks-editor/src/Lexical/Icons';
|
||||
@import 'preferences';
|
||||
@import 'focused';
|
||||
@import 'sn';
|
||||
@import 'columns';
|
||||
@import 'animation';
|
||||
@import '../../../blocks-editor/src/Lexical/Theme/lexical.scss';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const { merge } = require('webpack-merge')
|
||||
const config = require('./web.webpack.config.js')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const mergeWithEnvDefaults = require('./web.webpack-defaults.js')
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user