feat: generic items list (#1035)

This commit is contained in:
Aman Harwara
2022-05-22 18:51:46 +05:30
committed by GitHub
parent 1643311d08
commit 6401da2570
76 changed files with 1808 additions and 1281 deletions

View File

@@ -0,0 +1,120 @@
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import React from 'react'
import { PopoverFileItemAction } from '../AttachedFilesPopover/PopoverFileItemAction'
import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs'
import { FileMenuOptions } from './FileMenuOptions'
type Props = {
appState: AppState
}
export const FileContextMenu: FunctionComponent<Props> = observer(({ appState }) => {
const { selectedFiles, showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = appState.files
const [contextMenuStyle, setContextMenuStyle] = useState<React.CSSProperties>({
top: 0,
left: 0,
visibility: 'hidden',
})
const [contextMenuMaxHeight, setContextMenuMaxHeight] = useState<number | 'auto'>('auto')
const contextMenuRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open))
useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false))
const selectedFile = Object.values(selectedFiles)[0]
if (!showFileContextMenu || !selectedFile) {
return null
}
const reloadContextMenuLayout = useCallback(() => {
const { clientHeight } = document.documentElement
const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize
const maxContextMenuHeight = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
let openUpBottom = true
if (footerHeightInPx) {
const bottomSpace = clientHeight - footerHeightInPx - fileContextMenuLocation.y
const upSpace = fileContextMenuLocation.y
if (maxContextMenuHeight > bottomSpace) {
if (upSpace > maxContextMenuHeight) {
openUpBottom = false
setContextMenuMaxHeight('auto')
} else {
if (upSpace > bottomSpace) {
setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER)
openUpBottom = false
} else {
setContextMenuMaxHeight(bottomSpace - MENU_MARGIN_FROM_APP_BORDER)
}
}
} else {
setContextMenuMaxHeight('auto')
}
}
if (openUpBottom) {
setContextMenuStyle({
top: fileContextMenuLocation.y,
left: fileContextMenuLocation.x,
visibility: 'visible',
})
} else {
setContextMenuStyle({
bottom: clientHeight - fileContextMenuLocation.y,
left: fileContextMenuLocation.x,
visibility: 'visible',
})
}
}, [fileContextMenuLocation.x, fileContextMenuLocation.y])
useEffect(() => {
if (showFileContextMenu) {
reloadContextMenuLayout()
}
}, [reloadContextMenuLayout, showFileContextMenu])
useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout)
return () => {
window.removeEventListener('resize', reloadContextMenuLayout)
}
}, [reloadContextMenuLayout])
const handleFileAction = useCallback(
async (action: PopoverFileItemAction) => {
const { didHandleAction } = await appState.files.handleFileAction(action, PopoverTabs.AllFiles)
return didHandleAction
},
[appState.files],
)
return (
<div
ref={contextMenuRef}
className="sn-dropdown min-w-60 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto fixed"
style={{
...contextMenuStyle,
maxHeight: contextMenuMaxHeight,
}}
>
<FileMenuOptions
file={selectedFile}
handleFileAction={handleFileAction}
closeOnBlur={closeOnBlur}
closeMenu={() => setShowFileContextMenu(false)}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
/>
</div>
)
})

View File

@@ -0,0 +1,141 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
import { FileItem } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact'
import { PopoverFileItemAction, PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
import { Icon } from '@/Components/Icon/Icon'
import { Switch } from '@/Components/Switch/Switch'
type Props = {
closeMenu: () => void
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
file: FileItem
fileProtectionToggleCallback?: (isProtected: boolean) => void
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
isFileAttachedToNote?: boolean
renameToggleCallback?: (isRenamingFile: boolean) => void
shouldShowRenameOption: boolean
shouldShowAttachOption: boolean
}
export const FileMenuOptions: FunctionComponent<Props> = ({
closeMenu,
closeOnBlur,
file,
fileProtectionToggleCallback,
handleFileAction,
isFileAttachedToNote,
renameToggleCallback,
shouldShowRenameOption,
shouldShowAttachOption,
}) => {
return (
<>
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.PreviewFile,
payload: file,
}).catch(console.error)
closeMenu()
}}
>
<Icon type="file" className="mr-2 color-neutral" />
Preview file
</button>
{isFileAttachedToNote ? (
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DetachFileToNote,
payload: file,
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link-off" className="mr-2 color-neutral" />
Detach from note
</button>
) : shouldShowAttachOption ? (
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: file,
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link" className="mr-2 color-neutral" />
Attach to note
</button>
) : null}
<div className="min-h-1px my-1 bg-border"></div>
<button
className="sn-dropdown-item justify-between focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.ToggleFileProtection,
payload: file,
callback: (isProtected: boolean) => {
fileProtectionToggleCallback?.(isProtected)
},
}).catch(console.error)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 color-neutral" />
Password protection
</span>
<Switch className="px-0 pointer-events-none" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} checked={file.protected} />
</button>
<div className="min-h-1px my-1 bg-border"></div>
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DownloadFile,
payload: file,
}).catch(console.error)
closeMenu()
}}
>
<Icon type="download" className="mr-2 color-neutral" />
Download
</button>
{shouldShowRenameOption && (
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
renameToggleCallback?.(true)
}}
>
<Icon type="pencil" className="mr-2 color-neutral" />
Rename
</button>
)}
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DeleteFile,
payload: file,
}).catch(console.error)
closeMenu()
}}
>
<Icon type="trash" className="mr-2 color-danger" />
<span className="color-danger">Delete permanently</span>
</button>
</>
)
}