feat: Added preference to toggle Super note toolbar visibility. When toggled off, the toolbar will only be visible when text is selected as a floating toolbar. [skip e2e]

This commit is contained in:
Aman Harwara
2023-10-26 01:17:33 +05:30
parent 5f61244ec8
commit a616750aea
4 changed files with 266 additions and 120 deletions

View File

@@ -45,6 +45,7 @@ export const PrefDefaults = {
[PrefKey.ComponentPreferences]: {}, [PrefKey.ComponentPreferences]: {},
[PrefKey.ActiveThemes]: [], [PrefKey.ActiveThemes]: [],
[PrefKey.ActiveComponents]: [], [PrefKey.ActiveComponents]: [],
[PrefKey.AlwaysShowSuperToolbar]: true,
} satisfies { } satisfies {
[key in PrefKey]: PrefValue[key] [key in PrefKey]: PrefValue[key]
} }

View File

@@ -46,6 +46,7 @@ export enum PrefKey {
ComponentPreferences = 'componentPreferences', ComponentPreferences = 'componentPreferences',
ActiveThemes = 'activeThemes', ActiveThemes = 'activeThemes',
ActiveComponents = 'activeComponents', ActiveComponents = 'activeComponents',
AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar',
} }
export type PrefValue = { export type PrefValue = {
@@ -87,4 +88,5 @@ export type PrefValue = {
[PrefKey.ComponentPreferences]: AllComponentPreferences [PrefKey.ComponentPreferences]: AllComponentPreferences
[PrefKey.ActiveThemes]: string[] [PrefKey.ActiveThemes]: string[]
[PrefKey.ActiveComponents]: string[] [PrefKey.ActiveComponents]: string[]
[PrefKey.AlwaysShowSuperToolbar]: boolean
} }

View File

@@ -1,4 +1,4 @@
import { PrefKey, Platform, PrefDefaults } from '@standardnotes/snjs' import { PrefKey, Platform } from '@standardnotes/snjs'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent, useState } from 'react' import { FunctionComponent, useState } from 'react'
@@ -6,6 +6,8 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch' import Switch from '@/Components/Switch/Switch'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import usePreference from '@/Hooks/usePreference'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -18,16 +20,15 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
() => (application.getValue(AndroidConfirmBeforeExitKey) as boolean) ?? true, () => (application.getValue(AndroidConfirmBeforeExitKey) as boolean) ?? true,
) )
const [spellcheck, setSpellcheck] = useState(() => const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
application.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck]),
)
const [addNoteToParentFolders, setAddNoteToParentFolders] = useState(() => const spellcheck = usePreference(PrefKey.EditorSpellcheck)
application.getPreference(PrefKey.NoteAddToParentFolders, PrefDefaults[PrefKey.NoteAddToParentFolders]),
) const addNoteToParentFolders = usePreference(PrefKey.NoteAddToParentFolders)
const alwaysShowSuperToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar)
const toggleSpellcheck = () => { const toggleSpellcheck = () => {
setSpellcheck(!spellcheck)
application.toggleGlobalSpellcheck().catch(console.error) application.toggleGlobalSpellcheck().catch(console.error)
} }
@@ -72,11 +73,29 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
<Switch <Switch
onChange={() => { onChange={() => {
application.setPreference(PrefKey.NoteAddToParentFolders, !addNoteToParentFolders).catch(console.error) application.setPreference(PrefKey.NoteAddToParentFolders, !addNoteToParentFolders).catch(console.error)
setAddNoteToParentFolders(!addNoteToParentFolders)
}} }}
checked={addNoteToParentFolders} checked={addNoteToParentFolders}
/> />
</div> </div>
<HorizontalSeparator classes="my-4" />
{!isMobile && (
<div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col">
<Subtitle>Use always-visible toolbar in Super notes</Subtitle>
<Text>
When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily
toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating
toolbar when text is selected.
</Text>
</div>
<Switch
onChange={() => {
application.setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar).catch(console.error)
}}
checked={alwaysShowSuperToolbar}
/>
</div>
)}
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
) )

View File

@@ -45,7 +45,7 @@ import { IndentBlock, OutdentBlock } from '../Blocks/IndentOutdent'
import { ParagraphBlock } from '../Blocks/Paragraph' import { ParagraphBlock } from '../Blocks/Paragraph'
import { QuoteBlock } from '../Blocks/Quote' import { QuoteBlock } from '../Blocks/Quote'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { classNames } from '@standardnotes/snjs' import { PrefKey, classNames } from '@standardnotes/snjs'
import { SUPER_TOGGLE_SEARCH, SUPER_TOGGLE_TOOLBAR } from '@standardnotes/ui-services' import { SUPER_TOGGLE_SEARCH, SUPER_TOGGLE_TOOLBAR } from '@standardnotes/ui-services'
import { useApplication } from '@/Components/ApplicationProvider' import { useApplication } from '@/Components/ApplicationProvider'
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin' import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
@@ -58,9 +58,13 @@ import Popover from '@/Components/Popover/Popover'
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents' import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents'
import Menu from '@/Components/Menu/Menu' import Menu from '@/Components/Menu/Menu'
import MenuItem, { MenuItemProps } from '@/Components/Menu/MenuItem' import MenuItem, { MenuItemProps } from '@/Components/Menu/MenuItem'
import { remToPx } from '@/Utils' import { debounce, remToPx } from '@/Utils'
import FloatingLinkEditor from './FloatingLinkEditor' import FloatingLinkEditor from './FloatingLinkEditor'
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
import { useStateRef } from '@/Hooks/useStateRef'
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
import usePreference from '@/Hooks/usePreference'
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND') const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
@@ -215,73 +219,174 @@ const ToolbarPlugin = () => {
const [canUndo, setCanUndo] = useState(false) const [canUndo, setCanUndo] = useState(false)
const [canRedo, setCanRedo] = useState(false) const [canRedo, setCanRedo] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const alwaysShowToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar)
const [isToolbarFixedToTop, setIsToolbarFixedToTop] = useState(alwaysShowToolbar)
const isToolbarFixedRef = useStateRef(isToolbarFixedToTop)
const updateToolbarFloatingPosition = useCallback(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return
}
if (isMobile) {
return
}
if (isToolbarFixedRef.current) {
return
}
const containerElement = containerRef.current
if (!containerElement) {
return
}
if (selection.getTextContent() === '') {
containerElement.style.removeProperty('opacity')
return
}
const nativeSelection = window.getSelection()
const rootElement = activeEditor.getRootElement()
if (nativeSelection !== null && rootElement !== null && rootElement.contains(nativeSelection.anchorNode)) {
const rangeRect = getDOMRangeRect(nativeSelection, rootElement)
const containerRect = containerElement.getBoundingClientRect()
const rootRect = rootElement.getBoundingClientRect()
const calculatedStyles = getPositionedPopoverStyles({
align: 'start',
side: 'top',
anchorRect: rangeRect,
popoverRect: containerRect,
documentRect: rootRect,
offset: 8,
maxHeightFunction: () => 'none',
})
if (calculatedStyles) {
Object.entries(calculatedStyles).forEach(([key, value]) => {
if (key === 'transform') {
return
}
containerElement.style.setProperty(key, value)
})
containerElement.style.setProperty('opacity', '1')
}
}
}, [activeEditor, isMobile, isToolbarFixedRef])
const $updateToolbar = useCallback(() => { const $updateToolbar = useCallback(() => {
const selection = $getSelection() const selection = $getSelection()
if ($isRangeSelection(selection)) { if (!$isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode() return
let element = }
anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent()
return parent !== null && $isRootOrShadowRoot(parent)
})
if (element === null) { const anchorNode = selection.anchor.getNode()
element = anchorNode.getTopLevelElementOrThrow() let element =
} anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent()
return parent !== null && $isRootOrShadowRoot(parent)
})
const elementKey = element.getKey() if (element === null) {
const elementDOM = activeEditor.getElementByKey(elementKey) element = anchorNode.getTopLevelElementOrThrow()
}
// Update text format const elementKey = element.getKey()
setIsBold(selection.hasFormat('bold')) const elementDOM = activeEditor.getElementByKey(elementKey)
setIsItalic(selection.hasFormat('italic'))
setIsUnderline(selection.hasFormat('underline'))
setIsStrikethrough(selection.hasFormat('strikethrough'))
setIsSubscript(selection.hasFormat('subscript'))
setIsSuperscript(selection.hasFormat('superscript'))
setIsCode(selection.hasFormat('code'))
setIsHighlight(selection.hasFormat('highlight'))
// Update links // Update text format
const node = getSelectedNode(selection) setIsBold(selection.hasFormat('bold'))
const parent = node.getParent() setIsItalic(selection.hasFormat('italic'))
if ($isLinkNode(parent) || $isLinkNode(node)) { setIsUnderline(selection.hasFormat('underline'))
setIsLink(true) setIsStrikethrough(selection.hasFormat('strikethrough'))
setIsSubscript(selection.hasFormat('subscript'))
setIsSuperscript(selection.hasFormat('superscript'))
setIsCode(selection.hasFormat('code'))
setIsHighlight(selection.hasFormat('highlight'))
// Update links
const node = getSelectedNode(selection)
const parent = node.getParent()
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true)
} else {
setIsLink(false)
}
setLinkUrl($isLinkNode(parent) ? parent.getURL() : $isLinkNode(node) ? node.getURL() : '')
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
setIsAutoLink(true)
} else {
setIsAutoLink(false)
}
if ($isLinkTextNode(node, selection)) {
setIsLinkText(true)
setLinkText(node.getTextContent())
} else {
setIsLinkText(false)
setLinkText('')
}
if (elementDOM !== null) {
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType<ListNode>(anchorNode, ListNode)
const type = parentList ? parentList.getListType() : element.getListType()
setBlockType(type)
} else { } else {
setIsLink(false) const type = $isHeadingNode(element) ? element.getTag() : element.getType()
} if (type in blockTypeToBlockName) {
setLinkUrl($isLinkNode(parent) ? parent.getURL() : $isLinkNode(node) ? node.getURL() : '') setBlockType(type as keyof typeof blockTypeToBlockName)
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
setIsAutoLink(true)
} else {
setIsAutoLink(false)
}
if ($isLinkTextNode(node, selection)) {
setIsLinkText(true)
setLinkText(node.getTextContent())
} else {
setIsLinkText(false)
setLinkText('')
}
if (elementDOM !== null) {
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType<ListNode>(anchorNode, ListNode)
const type = parentList ? parentList.getListType() : element.getListType()
setBlockType(type)
} else {
const type = $isHeadingNode(element) ? element.getTag() : element.getType()
if (type in blockTypeToBlockName) {
setBlockType(type as keyof typeof blockTypeToBlockName)
}
} }
} }
setElementFormat(($isElementNode(node) ? node.getFormatType() : parent?.getFormatType()) || 'left')
} }
}, [activeEditor])
setElementFormat(($isElementNode(node) ? node.getFormatType() : parent?.getFormatType()) || 'left')
updateToolbarFloatingPosition()
}, [activeEditor, updateToolbarFloatingPosition])
const clearContainerFloatingStyles = useCallback(() => {
const containerElement = containerRef.current
if (!containerElement) {
return
}
containerElement.style.removeProperty('--translate-x')
containerElement.style.removeProperty('--translate-y')
containerElement.style.removeProperty('transform')
containerElement.style.removeProperty('transform-origin')
containerElement.style.removeProperty('opacity')
}, [])
useEffect(() => {
const scrollerElem = activeEditor.getRootElement()
const update = () => {
activeEditor.getEditorState().read(() => {
updateToolbarFloatingPosition()
})
}
const debouncedUpdate = debounce(update, 50)
window.addEventListener('resize', debouncedUpdate)
if (scrollerElem) {
scrollerElem.addEventListener('scroll', debouncedUpdate)
}
return () => {
window.removeEventListener('resize', debouncedUpdate)
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', debouncedUpdate)
}
}
}, [activeEditor, updateToolbarFloatingPosition])
useEffect(() => { useEffect(() => {
return mergeRegister( return mergeRegister(
@@ -379,14 +484,12 @@ const ToolbarPlugin = () => {
) )
}, [activeEditor, isLink]) }, [activeEditor, isLink])
const containerRef = useRef<HTMLDivElement>(null)
const dismissButtonRef = useRef<HTMLButtonElement>(null) const dismissButtonRef = useRef<HTMLButtonElement>(null)
const [isFocusInEditor, setIsFocusInEditor] = useState(false) const [isFocusInEditor, setIsFocusInEditor] = useState(false)
const [isFocusInToolbar, setIsFocusInToolbar] = useState(false) const [isFocusInToolbar, setIsFocusInToolbar] = useState(false)
const isFocusInEditorOrToolbar = isFocusInEditor || isFocusInToolbar const canShowToolbarOnMobile = isFocusInEditor || isFocusInToolbar
const [isToolbarVisible, setIsToolbarVisible] = useState(true) const canShowAllItems = isMobile || isToolbarFixedToTop
const canShowToolbar = isMobile ? isFocusInEditorOrToolbar : isToolbarVisible
useEffect(() => { useEffect(() => {
const container = containerRef.current const container = containerRef.current
@@ -438,24 +541,32 @@ const ToolbarPlugin = () => {
if (isMobile) { if (isMobile) {
return return
} }
event.preventDefault() if (!alwaysShowToolbar) {
if (!isToolbarVisible) {
setIsToolbarVisible(true)
toolbarStore.move(toolbarStore.first())
return return
} }
const isFocusInContainer = containerRef.current?.contains(document.activeElement) event.preventDefault()
if (isFocusInContainer) {
setIsToolbarVisible(false) if (!isToolbarFixedToTop) {
editor.focus() setIsToolbarFixedToTop(true)
} else { clearContainerFloatingStyles()
toolbarStore.move(toolbarStore.first()) toolbarStore.move(toolbarStore.first())
return
} else {
setIsToolbarFixedToTop(false)
editor.focus()
} }
}, },
}) })
}, [application.keyboardService, editor, isMobile, isToolbarVisible, toolbarStore]) }, [
alwaysShowToolbar,
application.keyboardService,
clearContainerFloatingStyles,
editor,
isMobile,
isToolbarFixedToTop,
toolbarStore,
])
return ( return (
<> <>
@@ -463,8 +574,15 @@ const ToolbarPlugin = () => {
<div <div
className={classNames( className={classNames(
'bg-contrast', 'bg-contrast',
'md:w-full md:border-b md:border-border md:px-1 md:py-1 md:translucent-ui:border-[--popover-border-color] md:translucent-ui:bg-[--popover-background-color] md:translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]', !isEditable ? 'hidden opacity-0' : '',
!canShowToolbar || !isEditable ? 'hidden' : '', isMobile && !canShowToolbarOnMobile ? 'hidden' : '',
!isMobile &&
'border-b border-border translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
!isMobile
? !isToolbarFixedToTop
? 'fixed left-0 top-0 z-tooltip translate-x-[--translate-x] translate-y-[--translate-y] rounded border py-0.5 opacity-0'
: 'w-full px-1 py-1'
: '',
)} )}
id="super-mobile-toolbar" id="super-mobile-toolbar"
ref={containerRef} ref={containerRef}
@@ -487,30 +605,34 @@ const ToolbarPlugin = () => {
ref={toolbarRef} ref={toolbarRef}
store={toolbarStore} store={toolbarStore}
> >
<ToolbarButton {canShowAllItems && (
name="Table of Contents" <>
iconName="toc" <ToolbarButton
active={isTOCOpen} name="Table of Contents"
onSelect={() => setIsTOCOpen(!isTOCOpen)} iconName="toc"
ref={tocAnchorRef} active={isTOCOpen}
/> onSelect={() => setIsTOCOpen(!isTOCOpen)}
<ToolbarButton ref={tocAnchorRef}
name="Search" />
iconName="search" <ToolbarButton
onSelect={() => application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)} name="Search"
/> iconName="search"
<ToolbarButton onSelect={() => application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)}
name="Undo" />
iconName="undo" <ToolbarButton
disabled={!canUndo} name="Undo"
onSelect={() => editor.dispatchCommand(UNDO_COMMAND, undefined)} iconName="undo"
/> disabled={!canUndo}
<ToolbarButton onSelect={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
name="Redo" />
iconName="redo" <ToolbarButton
disabled={!canRedo} name="Redo"
onSelect={() => editor.dispatchCommand(REDO_COMMAND, undefined)} iconName="redo"
/> disabled={!canRedo}
onSelect={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
/>
</>
)}
<ToolbarButton <ToolbarButton
name="Bold" name="Bold"
iconName="bold" iconName="bold"
@@ -586,17 +708,19 @@ const ToolbarPlugin = () => {
iconName={OutdentBlock.iconName} iconName={OutdentBlock.iconName}
onSelect={() => OutdentBlock.onSelect(editor)} onSelect={() => OutdentBlock.onSelect(editor)}
/> />
<ToolbarButton {canShowAllItems && (
name="Insert" <ToolbarButton
onSelect={() => { name="Insert"
setIsInsertMenuOpen(!isInsertMenuOpen) onSelect={() => {
}} setIsInsertMenuOpen(!isInsertMenuOpen)
ref={insertAnchorRef} }}
className={isInsertMenuOpen ? 'md:bg-default' : ''} ref={insertAnchorRef}
> className={isInsertMenuOpen ? 'md:bg-default' : ''}
<Icon type="add" size="custom" className="h-4 w-4 md:h-3.5 md:w-3.5" /> >
<Icon type="chevron-down" size="custom" className="ml-2 h-4 w-4 md:h-3.5 md:w-3.5" /> <Icon type="add" size="custom" className="h-4 w-4 md:h-3.5 md:w-3.5" />
</ToolbarButton> <Icon type="chevron-down" size="custom" className="ml-2 h-4 w-4 md:h-3.5 md:w-3.5" />
</ToolbarButton>
)}
</Toolbar> </Toolbar>
{isMobile && ( {isMobile && (
<button <button