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.ActiveThemes]: [],
[PrefKey.ActiveComponents]: [],
[PrefKey.AlwaysShowSuperToolbar]: true,
} satisfies {
[key in PrefKey]: PrefValue[key]
}

View File

@@ -46,6 +46,7 @@ export enum PrefKey {
ComponentPreferences = 'componentPreferences',
ActiveThemes = 'activeThemes',
ActiveComponents = 'activeComponents',
AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar',
}
export type PrefValue = {
@@ -87,4 +88,5 @@ export type PrefValue = {
[PrefKey.ComponentPreferences]: AllComponentPreferences
[PrefKey.ActiveThemes]: 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 { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent, useState } from 'react'
@@ -6,6 +6,8 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import usePreference from '@/Hooks/usePreference'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
type Props = {
application: WebApplication
@@ -18,16 +20,15 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
() => (application.getValue(AndroidConfirmBeforeExitKey) as boolean) ?? true,
)
const [spellcheck, setSpellcheck] = useState(() =>
application.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck]),
)
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const [addNoteToParentFolders, setAddNoteToParentFolders] = useState(() =>
application.getPreference(PrefKey.NoteAddToParentFolders, PrefDefaults[PrefKey.NoteAddToParentFolders]),
)
const spellcheck = usePreference(PrefKey.EditorSpellcheck)
const addNoteToParentFolders = usePreference(PrefKey.NoteAddToParentFolders)
const alwaysShowSuperToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar)
const toggleSpellcheck = () => {
setSpellcheck(!spellcheck)
application.toggleGlobalSpellcheck().catch(console.error)
}
@@ -72,11 +73,29 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
<Switch
onChange={() => {
application.setPreference(PrefKey.NoteAddToParentFolders, !addNoteToParentFolders).catch(console.error)
setAddNoteToParentFolders(!addNoteToParentFolders)
}}
checked={addNoteToParentFolders}
/>
</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>
</PreferencesGroup>
)

View File

@@ -45,7 +45,7 @@ import { IndentBlock, OutdentBlock } from '../Blocks/IndentOutdent'
import { ParagraphBlock } from '../Blocks/Paragraph'
import { QuoteBlock } from '../Blocks/Quote'
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 { useApplication } from '@/Components/ApplicationProvider'
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
@@ -58,9 +58,13 @@ import Popover from '@/Components/Popover/Popover'
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents'
import Menu from '@/Components/Menu/Menu'
import MenuItem, { MenuItemProps } from '@/Components/Menu/MenuItem'
import { remToPx } from '@/Utils'
import { debounce, remToPx } from '@/Utils'
import FloatingLinkEditor from './FloatingLinkEditor'
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')
@@ -215,73 +219,174 @@ const ToolbarPlugin = () => {
const [canUndo, setCanUndo] = 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 selection = $getSelection()
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode()
let element =
anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent()
return parent !== null && $isRootOrShadowRoot(parent)
})
if (!$isRangeSelection(selection)) {
return
}
if (element === null) {
element = anchorNode.getTopLevelElementOrThrow()
}
const anchorNode = selection.anchor.getNode()
let element =
anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent()
return parent !== null && $isRootOrShadowRoot(parent)
})
const elementKey = element.getKey()
const elementDOM = activeEditor.getElementByKey(elementKey)
if (element === null) {
element = anchorNode.getTopLevelElementOrThrow()
}
// Update text format
setIsBold(selection.hasFormat('bold'))
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'))
const elementKey = element.getKey()
const elementDOM = activeEditor.getElementByKey(elementKey)
// Update links
const node = getSelectedNode(selection)
const parent = node.getParent()
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true)
// Update text format
setIsBold(selection.hasFormat('bold'))
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
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 {
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 {
const type = $isHeadingNode(element) ? element.getTag() : element.getType()
if (type in blockTypeToBlockName) {
setBlockType(type as keyof typeof blockTypeToBlockName)
}
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(() => {
return mergeRegister(
@@ -379,14 +484,12 @@ const ToolbarPlugin = () => {
)
}, [activeEditor, isLink])
const containerRef = useRef<HTMLDivElement>(null)
const dismissButtonRef = useRef<HTMLButtonElement>(null)
const [isFocusInEditor, setIsFocusInEditor] = useState(false)
const [isFocusInToolbar, setIsFocusInToolbar] = useState(false)
const isFocusInEditorOrToolbar = isFocusInEditor || isFocusInToolbar
const [isToolbarVisible, setIsToolbarVisible] = useState(true)
const canShowToolbar = isMobile ? isFocusInEditorOrToolbar : isToolbarVisible
const canShowToolbarOnMobile = isFocusInEditor || isFocusInToolbar
const canShowAllItems = isMobile || isToolbarFixedToTop
useEffect(() => {
const container = containerRef.current
@@ -438,24 +541,32 @@ const ToolbarPlugin = () => {
if (isMobile) {
return
}
event.preventDefault()
if (!isToolbarVisible) {
setIsToolbarVisible(true)
toolbarStore.move(toolbarStore.first())
if (!alwaysShowToolbar) {
return
}
const isFocusInContainer = containerRef.current?.contains(document.activeElement)
if (isFocusInContainer) {
setIsToolbarVisible(false)
editor.focus()
} else {
event.preventDefault()
if (!isToolbarFixedToTop) {
setIsToolbarFixedToTop(true)
clearContainerFloatingStyles()
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 (
<>
@@ -463,8 +574,15 @@ const ToolbarPlugin = () => {
<div
className={classNames(
'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)]',
!canShowToolbar || !isEditable ? 'hidden' : '',
!isEditable ? 'hidden opacity-0' : '',
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"
ref={containerRef}
@@ -487,30 +605,34 @@ const ToolbarPlugin = () => {
ref={toolbarRef}
store={toolbarStore}
>
<ToolbarButton
name="Table of Contents"
iconName="toc"
active={isTOCOpen}
onSelect={() => setIsTOCOpen(!isTOCOpen)}
ref={tocAnchorRef}
/>
<ToolbarButton
name="Search"
iconName="search"
onSelect={() => application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)}
/>
<ToolbarButton
name="Undo"
iconName="undo"
disabled={!canUndo}
onSelect={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
/>
<ToolbarButton
name="Redo"
iconName="redo"
disabled={!canRedo}
onSelect={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
/>
{canShowAllItems && (
<>
<ToolbarButton
name="Table of Contents"
iconName="toc"
active={isTOCOpen}
onSelect={() => setIsTOCOpen(!isTOCOpen)}
ref={tocAnchorRef}
/>
<ToolbarButton
name="Search"
iconName="search"
onSelect={() => application.keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)}
/>
<ToolbarButton
name="Undo"
iconName="undo"
disabled={!canUndo}
onSelect={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
/>
<ToolbarButton
name="Redo"
iconName="redo"
disabled={!canRedo}
onSelect={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
/>
</>
)}
<ToolbarButton
name="Bold"
iconName="bold"
@@ -586,17 +708,19 @@ const ToolbarPlugin = () => {
iconName={OutdentBlock.iconName}
onSelect={() => OutdentBlock.onSelect(editor)}
/>
<ToolbarButton
name="Insert"
onSelect={() => {
setIsInsertMenuOpen(!isInsertMenuOpen)
}}
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" />
</ToolbarButton>
{canShowAllItems && (
<ToolbarButton
name="Insert"
onSelect={() => {
setIsInsertMenuOpen(!isInsertMenuOpen)
}}
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" />
</ToolbarButton>
)}
</Toolbar>
{isMobile && (
<button