Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 101 additions & 11 deletions src/components/ExcalidrawMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { useCallback, memo } from 'react'
import { useCallback, useEffect, useRef, memo } from 'react'
import type { KeyboardEvent as ReactKeyboardEvent } from 'react'
import { Icon } from '@mdi/react'
import { mdiMonitorScreenshot, mdiImageMultiple } from '@mdi/js'
import { MainMenu } from '@nextcloud/excalidraw'
import { MainMenu, CaptureUpdateAction } from '@nextcloud/excalidraw'
import { RecordingMenuItem } from './Recording'
import { PresentationMenuItem } from './Presentation'
import { CreatorMenuItem } from './CreatorMenuItem'
import { t } from '@nextcloud/l10n'
import { useShallow } from 'zustand/react/shallow'
import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'
import { useExcalidrawStore } from '../stores/useExcalidrawStore'

interface RecordingState {
isRecording: boolean
Expand Down Expand Up @@ -60,6 +64,9 @@ interface ExcalidrawMenuProps {

export const ExcalidrawMenu = memo(function ExcalidrawMenu({ fileNameWithoutExtension, recordingState, presentationState }: ExcalidrawMenuProps) {
const isMacPlatform = typeof navigator !== 'undefined' && (navigator.userAgentData?.platform === 'macOS' || /Mac|iPhone|iPad/.test(navigator.platform ?? ''))
const { excalidrawAPI } = useExcalidrawStore(useShallow(state => ({
excalidrawAPI: state.excalidrawAPI,
})))

const openExportDialog = useCallback(() => {
// Trigger export by dispatching the keyboard shortcut to the Excalidraw canvas
Expand All @@ -83,17 +90,99 @@ export const ExcalidrawMenu = memo(function ExcalidrawMenu({ fileNameWithoutExte
}, [isMacPlatform])

const takeScreenshot = useCallback(() => {
const canvas = document.querySelector('.excalidraw__canvas') as HTMLCanvasElement
if (canvas) {
const dataUrl = canvas.toDataURL('image/png')
const downloadLink = document.createElement('a')
downloadLink.href = dataUrl
downloadLink.download = `${fileNameWithoutExtension} Screenshot.png`
document.body.appendChild(downloadLink)
downloadLink.click()
const canvas = document.querySelector('.excalidraw__canvas') as HTMLCanvasElement | null
if (!canvas) {
return
}

const excalidrawContainer = document.querySelector('.excalidraw') as HTMLElement | null
const previouslyFocused = document.activeElement as HTMLElement | null

const dataUrl = canvas.toDataURL('image/png')
const downloadLink = document.createElement('a')
downloadLink.href = dataUrl
downloadLink.download = `${fileNameWithoutExtension} Screenshot.png`
document.body.appendChild(downloadLink)
downloadLink.click()
downloadLink.remove()

const restoreFocus = () => {
const focusTarget
= previouslyFocused && previouslyFocused !== document.body
? previouslyFocused
: excalidrawContainer

if (focusTarget && typeof focusTarget.focus === 'function') {
try {
focusTarget.focus({ preventScroll: true })
} catch {
focusTarget.focus()
}
}
}

requestAnimationFrame(restoreFocus)
}, [fileNameWithoutExtension])

const takeScreenshotRef = useRef(takeScreenshot)
useEffect(() => {
takeScreenshotRef.current = takeScreenshot
}, [takeScreenshot])

const isMacPlatformRef = useRef(isMacPlatform)
useEffect(() => {
isMacPlatformRef.current = isMacPlatform
}, [isMacPlatform])

const registeredApiRef = useRef<ExcalidrawImperativeAPI | null>(null)
useEffect(() => {
if (!excalidrawAPI) {
registeredApiRef.current = null
return
}

if (registeredApiRef.current === excalidrawAPI) {
return
}

const screenshotShortcutAction = {
name: 'whiteboard-download-screenshot',
label: () => 'Download screenshot',
trackEvent: false,
viewMode: true,
keyTest: (event: KeyboardEvent | ReactKeyboardEvent) => {
if (event.repeat || !event.altKey) {
return false
}

const shouldUseMetaKey = isMacPlatformRef.current
const hasRequiredModifier = shouldUseMetaKey ? event.metaKey : event.ctrlKey
if (!hasRequiredModifier) {
return false
}

const keyCode = typeof event.code === 'string' ? event.code.toLowerCase() : ''
if (keyCode !== 'keys') {
return false
}

const target = event.target
if (target instanceof Element && target.closest('input, textarea, [contenteditable="true"]')) {
return false
}

return true
},
perform: () => {
takeScreenshotRef.current()
return { captureUpdate: CaptureUpdateAction.NEVER }
},
} as unknown as Parameters<ExcalidrawImperativeAPI['registerAction']>[0]

excalidrawAPI.registerAction(screenshotShortcutAction)
registeredApiRef.current = excalidrawAPI
}, [excalidrawAPI])

return (
<MainMenu>
<MainMenu.DefaultItems.ToggleTheme />
Expand All @@ -107,7 +196,8 @@ export const ExcalidrawMenu = memo(function ExcalidrawMenu({ fileNameWithoutExte
</MainMenu.Item>
<MainMenu.Item
icon={<Icon path={mdiMonitorScreenshot} size={0.75} />}
onSelect={takeScreenshot}>
onSelect={takeScreenshot}
shortcut={isMacPlatform ? '⌘+⌥+S' : 'Ctrl+Alt+S'}>
{t('whiteboard', 'Download screenshot')}
</MainMenu.Item>
<RecordingMenuItem
Expand Down
Loading