import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { Disclosure, DisclosureButton, DisclosurePanel, } from '@reach/disclosure'; import { ComponentArea, ContentType, SNComponent, SNTheme, } from '@standardnotes/snjs'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { JSXInternal } from 'preact/src/jsx'; import { Icon } from '../Icon'; import { Switch } from '../Switch'; import { toDirective, useCloseOnBlur } from '../utils'; import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler, } from './eventHandlers'; import { FocusModeSwitch } from './FocusModeSwitch'; import { ThemesMenuButton } from './ThemesMenuButton'; const focusModeAnimationDuration = 1255; const MENU_CLASSNAME = 'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto'; type MenuProps = { appState: AppState; application: WebApplication; }; const toggleFocusMode = (enabled: boolean) => { if (enabled) { document.body.classList.add('focus-mode'); } else { if (document.body.classList.contains('focus-mode')) { document.body.classList.add('disable-focus-mode'); document.body.classList.remove('focus-mode'); setTimeout(() => { document.body.classList.remove('disable-focus-mode'); }, focusModeAnimationDuration); } } }; export const sortThemes = (a: SNTheme, b: SNTheme) => { const aIsLayerable = a.isLayerable(); const bIsLayerable = b.isLayerable(); if (aIsLayerable && !bIsLayerable) { return 1; } else if (!aIsLayerable && bIsLayerable) { return -1; } else { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; } }; const QuickSettingsMenu: FunctionComponent = observer( ({ application, appState }) => { const { closeQuickSettingsMenu, shouldAnimateCloseMenu, focusModeEnabled, setFocusModeEnabled, } = appState.quickSettingsMenu; const [themes, setThemes] = useState([]); const [toggleableComponents, setToggleableComponents] = useState< SNComponent[] >([]); const [themesMenuOpen, setThemesMenuOpen] = useState(false); const [themesMenuPosition, setThemesMenuPosition] = useState({}); const [defaultThemeOn, setDefaultThemeOn] = useState(false); const themesMenuRef = useRef(null); const themesButtonRef = useRef(null); const prefsButtonRef = useRef(null); const quickSettingsMenuRef = useRef(null); const defaultThemeButtonRef = useRef(null); useEffect(() => { toggleFocusMode(focusModeEnabled); }, [focusModeEnabled]); const reloadThemes = useCallback(() => { const themes = application.getDisplayableItems( ContentType.Theme ) as SNTheme[]; setThemes(themes.sort(sortThemes)); setDefaultThemeOn( !themes.find((theme) => theme.active && !theme.isLayerable()) ); }, [application]); const reloadToggleableComponents = useCallback(() => { const toggleableComponents = ( application.getDisplayableItems(ContentType.Component) as SNComponent[] ).filter((component) => [ComponentArea.EditorStack, ComponentArea.TagsList].includes( component.area ) ); setToggleableComponents(toggleableComponents); }, [application]); useEffect(() => { const cleanupItemStream = application.streamItems( ContentType.Theme, () => { reloadThemes(); } ); return () => { cleanupItemStream(); }; }, [application, reloadThemes]); useEffect(() => { const cleanupItemStream = application.streamItems( ContentType.Component, () => { reloadToggleableComponents(); } ); return () => { cleanupItemStream(); }; }, [application, reloadToggleableComponents]); useEffect(() => { if (themesMenuOpen) { defaultThemeButtonRef.current?.focus(); } }, [themesMenuOpen]); useEffect(() => { prefsButtonRef.current?.focus(); }, []); const [closeOnBlur] = useCloseOnBlur( themesMenuRef as any, setThemesMenuOpen ); const toggleThemesMenu = () => { if (!themesMenuOpen && themesButtonRef.current) { const themesButtonRect = themesButtonRef.current.getBoundingClientRect(); setThemesMenuPosition({ left: themesButtonRect.right, bottom: document.documentElement.clientHeight - themesButtonRect.bottom, }); setThemesMenuOpen(true); } else { setThemesMenuOpen(false); } }; const openPreferences = () => { closeQuickSettingsMenu(); appState.preferences.openPreferences(); }; const toggleComponent = (component: SNComponent) => { if (component.isTheme()) { application.toggleTheme(component); } else { application.toggleComponent(component); } }; const handleBtnKeyDown: React.KeyboardEventHandler = ( event ) => { switch (event.key) { case 'Escape': setThemesMenuOpen(false); themesButtonRef.current?.focus(); break; case 'ArrowRight': if (!themesMenuOpen) { toggleThemesMenu(); } } }; const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler< HTMLDivElement > = (event) => { quickSettingsKeyDownHandler( closeQuickSettingsMenu, event, quickSettingsMenuRef, themesMenuOpen ); }; const handlePanelKeyDown: React.KeyboardEventHandler = ( event ) => { themesMenuKeyDownHandler( event, themesMenuRef, setThemesMenuOpen, themesButtonRef ); }; const toggleDefaultTheme = () => { const activeTheme = themes.find( (theme) => theme.active && !theme.isLayerable() ); if (activeTheme) application.toggleTheme(activeTheme); }; return (
Quick Settings
{themes && themes.length ? (
Themes
Themes
{themes.map((theme) => ( ))}
) : null} {toggleableComponents.map((component) => ( ))}
); } ); export const QuickSettingsMenuDirective = toDirective(QuickSettingsMenu);