import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, 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 '@/Components/Icon' import { Switch } from '@/Components/Switch' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler } from './EventHandlers' import { FocusModeSwitch } from './FocusModeSwitch' import { ThemesMenuButton } from './ThemesMenuButton' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import { ThemeItem } from './ThemeItem' import { sortThemes } from '@/Utils/SortThemes' 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 onClickOutside: () => void } 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 QuickSettingsMenu: FunctionComponent = observer(({ application, appState, onClickOutside }) => { const { closeQuickSettingsMenu, shouldAnimateCloseMenu, focusModeEnabled, setFocusModeEnabled } = appState.quickSettingsMenu const [themes, setThemes] = useState([]) const [toggleableComponents, setToggleableComponents] = useState([]) 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) const mainRef = useRef(null) useCloseOnClickOutside(mainRef, () => { onClickOutside() }) useEffect(() => { toggleFocusMode(focusModeEnabled) }, [focusModeEnabled]) const reloadThemes = useCallback(() => { const themes = application.items.getDisplayableItems(ContentType.Theme).map((item) => { return { name: item.displayName, identifier: item.identifier, component: item, } }) as ThemeItem[] GetFeatures() .filter((feature) => feature.content_type === ContentType.Theme && !feature.layerable) .forEach((theme) => { if (themes.findIndex((item) => item.identifier === theme.identifier) === -1) { themes.push({ name: theme.name as string, identifier: theme.identifier, }) } }) setThemes(themes.sort(sortThemes)) setDefaultThemeOn(!themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable())) }, [application]) const reloadToggleableComponents = useCallback(() => { const toggleableComponents = application.items .getDisplayableItems(ContentType.Component) .filter( (component) => [ComponentArea.EditorStack].includes(component.area) && component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent, ) setToggleableComponents(toggleableComponents) }, [application]) useEffect(() => { if (!themes.length) { reloadThemes() } }, [reloadThemes, themes.length]) 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, setThemesMenuOpen) const toggleThemesMenu = useCallback(() => { if (!themesMenuOpen && themesButtonRef.current) { const themesButtonRect = themesButtonRef.current.getBoundingClientRect() setThemesMenuPosition({ left: themesButtonRect.right, bottom: document.documentElement.clientHeight - themesButtonRect.bottom, }) setThemesMenuOpen(true) } else { setThemesMenuOpen(false) } }, [themesMenuOpen]) const openPreferences = useCallback(() => { closeQuickSettingsMenu() appState.preferences.openPreferences() }, [appState, closeQuickSettingsMenu]) const toggleComponent = useCallback( (component: SNComponent) => { if (component.isTheme()) { application.mutator.toggleTheme(component).catch(console.error) } else { application.mutator.toggleComponent(component).catch(console.error) } }, [application], ) const handleBtnKeyDown: React.KeyboardEventHandler = useCallback( (event) => { switch (event.key) { case 'Escape': setThemesMenuOpen(false) themesButtonRef.current?.focus() break case 'ArrowRight': if (!themesMenuOpen) { toggleThemesMenu() } } }, [themesMenuOpen, toggleThemesMenu], ) const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler = useCallback( (event) => { quickSettingsKeyDownHandler(closeQuickSettingsMenu, event, quickSettingsMenuRef, themesMenuOpen) }, [closeQuickSettingsMenu, themesMenuOpen], ) const handlePanelKeyDown: React.KeyboardEventHandler = useCallback((event) => { themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef) }, []) const toggleDefaultTheme = useCallback(() => { const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable()) if (activeTheme) { application.mutator.toggleTheme(activeTheme).catch(console.error) } }, [application, themes]) return (
Quick Settings
Themes
Themes
{themes.map((theme) => ( ))}
{toggleableComponents.map((component) => ( ))}
) })