diff --git a/.eslintignore b/.eslintignore index 8f936e72e..8d804e416 100644 --- a/.eslintignore +++ b/.eslintignore @@ -485,10 +485,15 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js packages/app-mobile/components/NoteEditor/NoteEditor.test.js packages/app-mobile/components/NoteEditor/NoteEditor.js packages/app-mobile/components/NoteEditor/SearchPanel.js +packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js packages/app-mobile/components/NoteEditor/types.js packages/app-mobile/components/NoteList.js packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js diff --git a/.gitignore b/.gitignore index 6763ede60..34e1afc60 100644 --- a/.gitignore +++ b/.gitignore @@ -465,10 +465,15 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js +packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js packages/app-mobile/components/NoteEditor/NoteEditor.test.js packages/app-mobile/components/NoteEditor/NoteEditor.js packages/app-mobile/components/NoteEditor/SearchPanel.js +packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js packages/app-mobile/components/NoteEditor/types.js packages/app-mobile/components/NoteList.js packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js diff --git a/packages/app-mobile/components/Icon.tsx b/packages/app-mobile/components/Icon.tsx index 741f0ab6d..a074623e4 100644 --- a/packages/app-mobile/components/Icon.tsx +++ b/packages/app-mobile/components/Icon.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; -import { TextStyle } from 'react-native'; +import { TextStyle, Text } from 'react-native'; + const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default; +const AntIcon = require('react-native-vector-icons/AntDesign').default; +const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default; + interface Props { name: string; @@ -9,6 +13,8 @@ interface Props { // If `null` is given, the content must be labeled elsewhere. accessibilityLabel: string|null; + + allowFontScaling?: boolean; } const Icon: React.FC = props => { @@ -25,20 +31,42 @@ const Icon: React.FC = props => { // to read the characters from the icon font (they don't make sense // without the icon font applied). const accessibilityHidden = props.accessibilityLabel === null; + const importantForAccessibility = accessibilityHidden ? 'no-hide-descendants' : 'yes'; - return ( - - ); + const sharedProps = { + importantForAccessibility, + 'aria-hidden': accessibilityHidden, + accessibilityLabel: props.accessibilityLabel, + style: props.style, + allowFontScaling: props.allowFontScaling, + }; + + if (namePrefix.match(/^fa[bsr]?$/)) { + return ( + + ); + } else if (namePrefix === 'ant') { + return ; + } else if (namePrefix === 'material') { + return ; + } else if (namePrefix === 'text') { + return ( + + {nameSuffix} + + ); + } else { + return ; + } }; export default Icon; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx index ed8e38f8b..a40803c4d 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx @@ -1,278 +1,65 @@ // A toolbar for the markdown editor. -const React = require('react'); +import * as React from 'react'; import { Platform, StyleSheet } from 'react-native'; -import { useMemo, useState, useCallback } from 'react'; - -// See https://oblador.github.io/react-native-vector-icons/ for a list of -// available icons. -const AntIcon = require('react-native-vector-icons/AntDesign').default; -const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default; -const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default; +import { useMemo } from 'react'; import { _ } from '@joplin/lib/locale'; -import time from '@joplin/lib/time'; -import { useEffect } from 'react'; -import { Keyboard, ViewStyle } from 'react-native'; -import { EditorControl, EditorSettings } from '../types'; -import { ButtonSpec, StyleSheetData } from './types'; +import { MarkdownToolbarProps, StyleSheetData } from './types'; import Toolbar from './Toolbar'; import { buttonSize } from './ToolbarButton'; import { Theme } from '@joplin/lib/themes/type'; import ToggleSpaceButton from './ToggleSpaceButton'; -import { SearchState } from '@joplin/editor/types'; -import SelectionFormatting from '@joplin/editor/SelectionFormatting'; +import useHeaderButtons from './buttons/useHeaderButtons'; +import useInlineFormattingButtons from './buttons/useInlineFormattingButtons'; +import useActionButtons from './buttons/useActionButtons'; +import useListButtons from './buttons/useListButtons'; +import useKeyboardVisible from '../hooks/useKeyboardVisible'; -type OnAttachCallback = ()=> void; -interface MarkdownToolbarProps { - editorControl: EditorControl; - selectionState: SelectionFormatting; - searchState: SearchState; - editorSettings: EditorSettings; - onAttach: OnAttachCallback; - style?: ViewStyle; - readOnly: boolean; -} - -const MarkdownToolbar = (props: MarkdownToolbarProps) => { +const MarkdownToolbar: React.FC = (props: MarkdownToolbarProps) => { const themeData = props.editorSettings.themeData; const styles = useStyles(props.style, themeData); - const selState = props.selectionState; - const editorControl = props.editorControl; - const readOnly = props.readOnly; - const headerButtons: ButtonSpec[] = []; - for (let level = 1; level <= 5; level++) { - const active = selState.headerLevel === level; + const { keyboardVisible, hasSoftwareKeyboard } = useKeyboardVisible(); + const buttonProps = { + ...props, + iconStyle: styles.text, + keyboardVisible, + hasSoftwareKeyboard, + }; + const headerButtons = useHeaderButtons(buttonProps); + const inlineFormattingBtns = useInlineFormattingButtons(buttonProps); + const actionButtons = useActionButtons(buttonProps); + const listButtons = useListButtons(buttonProps); - headerButtons.push({ - icon: `H${level}`, - description: _('Header %d', level), - active, - - // We only call addHeaderButton 5 times and in the same order, so - // the linter error is safe to ignore. - // eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks - onPress: useCallback(() => { - editorControl.toggleHeaderLevel(level); - }, [editorControl, level]), - - // Make it likely for the first three header buttons to show, less likely for - // the others. - priority: level < 3 ? 2 : 0, - disabled: readOnly, - }); - } - - const listButtons: ButtonSpec[] = []; - listButtons.push({ - icon: ( - - ), - description: _('Unordered list'), - active: selState.inUnorderedList, - onPress: editorControl.toggleUnorderedList, - - priority: -2, - disabled: readOnly, - }); - - listButtons.push({ - icon: ( - - ), - description: _('Ordered list'), - active: selState.inOrderedList, - onPress: editorControl.toggleOrderedList, - - priority: -2, - disabled: readOnly, - }); - - listButtons.push({ - icon: ( - - ), - description: _('Task list'), - active: selState.inChecklist, - onPress: editorControl.toggleTaskList, - - priority: -2, - disabled: readOnly, - }); - - - listButtons.push({ - icon: ( - - ), - description: _('Decrease indent level'), - onPress: editorControl.decreaseIndent, - - priority: -1, - disabled: readOnly, - }); - - listButtons.push({ - icon: ( - - ), - description: _('Increase indent level'), - onPress: editorControl.increaseIndent, - - priority: -1, - disabled: readOnly, - }); - - - // Inline formatting - const inlineFormattingBtns: ButtonSpec[] = []; - inlineFormattingBtns.push({ - icon: ( - - ), - description: _('Bold'), - active: selState.bolded, - onPress: editorControl.toggleBolded, - - priority: 3, - disabled: readOnly, - }); - - inlineFormattingBtns.push({ - icon: ( - - ), - description: _('Italic'), - active: selState.italicized, - onPress: editorControl.toggleItalicized, - - priority: 2, - disabled: readOnly, - }); - - inlineFormattingBtns.push({ - icon: '{;}', - description: _('Code'), - active: selState.inCode, - onPress: editorControl.toggleCode, - - priority: 2, - disabled: readOnly, - }); - - if (props.editorSettings.katexEnabled) { - inlineFormattingBtns.push({ - icon: '∑', - description: _('KaTeX'), - active: selState.inMath, - onPress: editorControl.toggleMath, - - priority: 1, - disabled: readOnly, - }); - } - - inlineFormattingBtns.push({ - icon: ( - - ), - description: _('Link'), - active: selState.inLink, - onPress: editorControl.showLinkDialog, - - priority: -3, - disabled: readOnly, - }); - - - // Actions - const actionButtons: ButtonSpec[] = []; - actionButtons.push({ - icon: ( - - ), - description: _('Insert time'), - onPress: useCallback(() => { - editorControl.insertText(time.formatDateToLocal(new Date())); - }, [editorControl]), - disabled: readOnly, - }); - - const onDismissKeyboard = useCallback(() => { - // Keyboard.dismiss() doesn't dismiss the keyboard if it's editing the WebView. - Keyboard.dismiss(); - - // As such, dismiss the keyboard by sending a message to the View. - editorControl.hideKeyboard(); - }, [editorControl]); - - actionButtons.push({ - icon: ( - - ), - description: _('Attach'), - onPress: useCallback(() => { - onDismissKeyboard(); - props.onAttach(); - }, [props.onAttach, onDismissKeyboard]), - disabled: readOnly, - }); - - actionButtons.push({ - icon: ( - - ), - description: ( - props.searchState.dialogVisible ? _('Close') : _('Find and replace') - ), - active: props.searchState.dialogVisible, - onPress: useCallback(() => { - if (props.searchState.dialogVisible) { - editorControl.searchControl.hideSearch(); - } else { - editorControl.searchControl.showSearch(); - } - }, [editorControl, props.searchState.dialogVisible]), - - priority: -3, - disabled: readOnly, - }); - - const [keyboardVisible, setKeyboardVisible] = useState(false); - const [hasSoftwareKeyboard, setHasSoftwareKeyboard] = useState(false); - useEffect(() => { - const showListener = Keyboard.addListener('keyboardDidShow', () => { - setKeyboardVisible(true); - setHasSoftwareKeyboard(true); - }); - const hideListener = Keyboard.addListener('keyboardDidHide', () => { - setKeyboardVisible(false); - }); - - return (() => { - showListener.remove(); - hideListener.remove(); - }); - }); - - actionButtons.push({ - icon: ( - - ), - description: _('Hide keyboard'), - disabled: !keyboardVisible, - visible: hasSoftwareKeyboard && Platform.OS === 'ios', - onPress: onDismissKeyboard, - - priority: -3, - }); - - const styleData: StyleSheetData = { + const styleData: StyleSheetData = useMemo(() => ({ styles: styles, themeId: props.editorSettings.themeId, - }; + }), [styles, props.editorSettings.themeId]); + + const toolbarButtons = useMemo(() => { + const buttons = [ + { + title: _('Formatting'), + items: inlineFormattingBtns, + }, + { + title: _('Headers'), + items: headerButtons, + }, + { + title: _('Lists'), + items: listButtons, + }, + { + title: _('Actions'), + items: actionButtons, + }, + ]; + + return buttons; + }, [headerButtons, inlineFormattingBtns, listButtons, actionButtons]); return ( { > ); diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx index 9c8f01be6..b528dbb34 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.tsx @@ -1,9 +1,8 @@ -const React = require('react'); +import * as React from 'react'; import { _ } from '@joplin/lib/locale'; import ToolbarButton from './ToolbarButton'; import { ButtonSpec, StyleSheetData } from './types'; -const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default; type OnToggleOverflowCallback = ()=> void; interface ToggleOverflowButtonProps { @@ -13,11 +12,9 @@ interface ToggleOverflowButtonProps { } // Button that shows/hides the overflow menu. -const ToggleOverflowButton = (props: ToggleOverflowButtonProps) => { +const ToggleOverflowButton: React.FC = (props: ToggleOverflowButtonProps) => { const spec: ButtonSpec = { - icon: ( - - ), + icon: 'material more-horiz', description: props.overflowVisible ? _('Hide more actions') : _('Show more actions'), active: props.overflowVisible, diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.tsx index 47b7f26c3..ac6ed5215 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.tsx @@ -1,4 +1,4 @@ -const React = require('react'); +import * as React from 'react'; import { ReactElement, useCallback, useMemo, useState } from 'react'; import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native'; @@ -14,7 +14,7 @@ interface ToolbarProps { } // Displays a list of buttons with an overflow menu. -const Toolbar = (props: ToolbarProps) => { +const Toolbar: React.FC = (props: ToolbarProps) => { const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false); const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0); diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx index e2762ee5a..55718114d 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.tsx @@ -1,8 +1,9 @@ -import React = require('react'); -import { useCallback } from 'react'; -import { Text, TextStyle } from 'react-native'; +import * as React from 'react'; +import { useCallback, useMemo } from 'react'; +import { TextStyle, StyleSheet } from 'react-native'; import { ButtonSpec, StyleSheetData } from './types'; import CustomButton from '../../CustomButton'; +import Icon from '../../Icon'; export const buttonSize = 54; @@ -13,28 +14,39 @@ interface ToolbarButtonProps { onActionComplete?: ()=> void; } +const useStyles = (baseStyleSheet: any, baseButtonStyle: any, buttonSpec: ButtonSpec, visible: boolean, disabled: boolean) => { + return useMemo(() => { + const activatedStyle = buttonSpec.active ? baseStyleSheet.buttonActive : {}; + const disabledStyle = disabled ? baseStyleSheet.buttonDisabled : {}; + + const activatedTextStyle = buttonSpec.active ? baseStyleSheet.buttonActiveContent : {}; + const disabledTextStyle = disabled ? baseStyleSheet.buttonDisabledContent : {}; + + return StyleSheet.create({ + iconStyle: { + ...activatedTextStyle, + ...disabledTextStyle, + ...baseStyleSheet.text, + }, + buttonStyle: { + ...baseStyleSheet.button, + ...activatedStyle, + ...disabledStyle, + ...baseButtonStyle, + ...(!visible ? { opacity: 0 } : null), + }, + }); + }, [ + baseStyleSheet.button, baseStyleSheet.text, baseButtonStyle, baseStyleSheet.buttonActive, + baseStyleSheet.buttonDisabled, baseStyleSheet.buttonActiveContent, baseStyleSheet.buttonDisabledContent, + buttonSpec.active, visible, disabled, + ]); +}; + const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarButtonProps) => { const visible = spec.visible ?? true; const disabled = (spec.disabled ?? false) && visible; - const styles = styleSheet.styles; - - // Additional styles if activated - const activatedStyle = spec.active ? styles.buttonActive : {}; - const activatedTextStyle = spec.active ? styles.buttonActiveContent : {}; - const disabledStyle = disabled ? styles.buttonDisabled : {}; - const disabledTextStyle = disabled ? styles.buttonDisabledContent : {}; - - let content; - - if (typeof spec.icon === 'string') { - content = ( - - {spec.icon} - - ); - } else { - content = spec.icon; - } + const styles = useStyles(styleSheet.styles, style, spec, visible, disabled); const sourceOnPress = spec.onPress; const onPress = useCallback(() => { @@ -46,17 +58,14 @@ const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarBut return ( - { content } + ); }; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.tsx b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.tsx index 0145bc574..aa7fd9750 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.tsx +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import { _ } from '@joplin/lib/locale'; import { ReactElement, useCallback, useState } from 'react'; import { LayoutChangeEvent, ScrollView, View } from 'react-native'; @@ -5,8 +7,6 @@ import ToggleOverflowButton from './ToggleOverflowButton'; import ToolbarButton, { buttonSize } from './ToolbarButton'; import { ButtonGroup, ButtonSpec, StyleSheetData } from './types'; -const React = require('react'); - type OnToggleOverflowCallback = ()=> void; interface OverflowPopupProps { buttonGroups: ButtonGroup[]; @@ -17,10 +17,13 @@ interface OverflowPopupProps { onToggleOverflow: OnToggleOverflowCallback; } +// Specification for a button that acts as padding. +const paddingButtonSpec = { visible: false, icon: '', onPress: ()=>{}, description: '' }; + // Contains buttons that overflow the available space. // Displays all buttons in [props.buttonGroups] if [props.visible]. // Otherwise, displays nothing. -const ToolbarOverflowRows = (props: OverflowPopupProps) => { +const ToolbarOverflowRows: React.FC = (props: OverflowPopupProps) => { const overflowRows: ReactElement[] = []; let key = 0; @@ -47,7 +50,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => { // Show the "hide overflow" button if in the center of the last row const isLastRow = i === props.buttonGroups.length - 1; const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2); - if (isLastRow && isCenterOfRow) { + if (isLastRow && (isCenterOfRow || group.items.length === 1)) { row.push( { } } + // Pad to an odd number of items to ensure that buttons are centered properly + if (row.length % 2 === 0) { + row.push( + , + ); + } + overflowRows.push( { }, [setHasSpaceForCloseBtn, props.buttonGroups]); const closeButtonSpec: ButtonSpec = { - icon: '⨉', + icon: 'text ⨉', description: _('Close'), onPress: props.onToggleOverflow, }; @@ -112,6 +126,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => { height: props.buttonGroups.length * buttonSize, flexDirection: 'column', flexGrow: 1, + display: !props.visible ? 'none' : 'flex', }} onLayout={onContainerLayout} > diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts new file mode 100644 index 000000000..47d7afaa5 --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.ts @@ -0,0 +1,83 @@ +import { useCallback, useMemo } from 'react'; +import { ButtonSpec } from '../types'; +import { _ } from '@joplin/lib/locale'; +import { ButtonRowProps } from '../types'; +import time from '@joplin/lib/time'; +import { Keyboard, Platform } from 'react-native'; + +export interface ActionButtonRowProps extends ButtonRowProps { + keyboardVisible: boolean; + hasSoftwareKeyboard: boolean; +} + +const useActionButtons = (props: ActionButtonRowProps) => { + const onDismissKeyboard = useCallback(() => { + // Keyboard.dismiss() doesn't dismiss the keyboard if it's editing the WebView. + Keyboard.dismiss(); + + // As such, dismiss the keyboard by sending a message to the View. + props.editorControl.hideKeyboard(); + }, [props.editorControl]); + + const onSearch = useCallback(() => { + if (props.searchState.dialogVisible) { + props.editorControl.searchControl.hideSearch(); + } else { + props.editorControl.searchControl.showSearch(); + } + }, [props.editorControl, props.searchState.dialogVisible]); + + const onAttach = useCallback(() => { + onDismissKeyboard(); + props.onAttach(); + }, [props.onAttach, onDismissKeyboard]); + + return useMemo(() => { + const actionButtons: ButtonSpec[] = []; + actionButtons.push({ + icon: 'fa calendar-plus', + description: _('Insert time'), + onPress: () => { + props.editorControl.insertText(time.formatDateToLocal(new Date())); + }, + disabled: props.readOnly, + }); + + actionButtons.push({ + icon: 'material attachment', + description: _('Attach'), + onPress: onAttach, + disabled: props.readOnly, + }); + + actionButtons.push({ + icon: 'material search', + description: ( + props.searchState.dialogVisible ? _('Close') : _('Find and replace') + ), + active: props.searchState.dialogVisible, + onPress: onSearch, + + priority: -3, + disabled: props.readOnly, + }); + + actionButtons.push({ + icon: 'material keyboard-hide', + description: _('Hide keyboard'), + disabled: !props.keyboardVisible, + visible: props.hasSoftwareKeyboard && Platform.OS === 'ios', + onPress: onDismissKeyboard, + + priority: -3, + }); + + return actionButtons; + }, [ + props.editorControl, props.keyboardVisible, props.hasSoftwareKeyboard, + props.readOnly, props.searchState.dialogVisible, + onAttach, onDismissKeyboard, onSearch, + ]); +}; + +export default useActionButtons; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts new file mode 100644 index 000000000..87ba65007 --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { ButtonSpec } from '../types'; +import { _ } from '@joplin/lib/locale'; +import { ButtonRowProps } from '../types'; + +const useHeaderButtons = ({ selectionState, editorControl, readOnly }: ButtonRowProps) => { + return useMemo(() => { + const headerButtons: ButtonSpec[] = []; + for (let level = 1; level <= 5; level++) { + const active = selectionState.headerLevel === level; + + headerButtons.push({ + icon: `text H${level}`, + description: _('Header %d', level), + active, + + // We only call addHeaderButton 5 times and in the same order, so + // the linter error is safe to ignore. + // eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks + onPress: () => { + editorControl.toggleHeaderLevel(level); + }, + + // Make it likely for the first three header buttons to show, less likely for + // the others. + priority: level < 3 ? 2 : 0, + disabled: readOnly, + }); + } + return headerButtons; + }, [selectionState, editorControl, readOnly]); +}; + +export default useHeaderButtons; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.ts new file mode 100644 index 000000000..129955aac --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.ts @@ -0,0 +1,67 @@ +import { useMemo } from 'react'; +import { ButtonSpec } from '../types'; +import { _ } from '@joplin/lib/locale'; +import { ButtonRowProps } from '../types'; + + +const useInlineFormattingButtons = ({ selectionState, editorControl, readOnly, editorSettings }: ButtonRowProps) => { + const { bolded, italicized, inCode, inMath, inLink } = selectionState; + + return useMemo(() => { + const inlineFormattingBtns: ButtonSpec[] = []; + inlineFormattingBtns.push({ + icon: 'fa bold', + description: _('Bold'), + active: bolded, + onPress: editorControl.toggleBolded, + + priority: 3, + disabled: readOnly, + }); + + inlineFormattingBtns.push({ + icon: 'fa italic', + description: _('Italic'), + active: italicized, + onPress: editorControl.toggleItalicized, + + priority: 2, + disabled: readOnly, + }); + + inlineFormattingBtns.push({ + icon: 'text {;}', + description: _('Code'), + active: inCode, + onPress: editorControl.toggleCode, + + priority: 2, + disabled: readOnly, + }); + + if (editorSettings.katexEnabled) { + inlineFormattingBtns.push({ + icon: 'text ∑', + description: _('KaTeX'), + active: inMath, + onPress: editorControl.toggleMath, + + priority: 1, + disabled: readOnly, + }); + } + + inlineFormattingBtns.push({ + icon: 'fa link', + description: _('Link'), + active: inLink, + onPress: editorControl.showLinkDialog, + + priority: -3, + disabled: readOnly, + }); + return inlineFormattingBtns; + }, [readOnly, editorControl, editorSettings.katexEnabled, inLink, inMath, inCode, italicized, bolded]); +}; + +export default useInlineFormattingButtons; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.ts new file mode 100644 index 000000000..bd83a964e --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.ts @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; +import { ButtonSpec } from '../types'; +import { _ } from '@joplin/lib/locale'; +import { ButtonRowProps } from '../types'; + +const useListButtons = ({ selectionState, editorControl, readOnly }: ButtonRowProps) => { + return useMemo(() => { + const listButtons: ButtonSpec[] = []; + + listButtons.push({ + icon: 'fa list-ul', + description: _('Unordered list'), + active: selectionState.inUnorderedList, + onPress: editorControl.toggleUnorderedList, + + priority: -2, + disabled: readOnly, + }); + + listButtons.push({ + icon: 'fa list-ol', + description: _('Ordered list'), + active: selectionState.inOrderedList, + onPress: editorControl.toggleOrderedList, + + priority: -2, + disabled: readOnly, + }); + + listButtons.push({ + icon: 'fa tasks', + description: _('Task list'), + active: selectionState.inChecklist, + onPress: editorControl.toggleTaskList, + + priority: -2, + disabled: readOnly, + }); + + + listButtons.push({ + icon: 'ant indent-left', + description: _('Decrease indent level'), + onPress: editorControl.decreaseIndent, + + priority: -1, + disabled: readOnly, + }); + + listButtons.push({ + icon: 'ant indent-right', + description: _('Increase indent level'), + onPress: editorControl.increaseIndent, + + priority: -1, + disabled: readOnly, + }); + + return listButtons; + }, [readOnly, editorControl, selectionState]); +}; + +export default useListButtons; diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.ts index fb94cced1..8754494d0 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.ts +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.ts @@ -1,11 +1,14 @@ -import { ReactElement } from 'react'; +import { TextStyle, ViewStyle } from 'react-native'; +import { EditorControl, EditorSettings } from '../types'; +import SelectionFormatting from '@joplin/editor/SelectionFormatting'; +import { SearchState } from '@joplin/editor/types'; export type OnPressListener = ()=> void; export interface ButtonSpec { - // Either text that will be shown in place of an icon or a component. - icon: string | ReactElement; + // Name of an icon, as accepted by components/Icon.tsx + icon: string; // Tooltip/accessibility label description: string; @@ -23,7 +26,6 @@ export interface ButtonSpec { disabled?: boolean; visible?: boolean; } - export interface ButtonGroup { title: string; items: ButtonSpec[]; @@ -33,3 +35,18 @@ export interface StyleSheetData { themeId: number; styles: any; } + +type OnAttachCallback = ()=> void; +export interface MarkdownToolbarProps { + editorControl: EditorControl; + selectionState: SelectionFormatting; + searchState: SearchState; + editorSettings: EditorSettings; + onAttach: OnAttachCallback; + style?: ViewStyle; + readOnly: boolean; +} + +export interface ButtonRowProps extends MarkdownToolbarProps { + iconStyle: TextStyle; +} diff --git a/packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.ts b/packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.ts new file mode 100644 index 000000000..a6aa8499c --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.ts @@ -0,0 +1,27 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Keyboard } from 'react-native'; + +const useKeyboardVisible = () => { + const [keyboardVisible, setKeyboardVisible] = useState(false); + const [hasSoftwareKeyboard, setHasSoftwareKeyboard] = useState(false); + useEffect(() => { + const showListener = Keyboard.addListener('keyboardDidShow', () => { + setKeyboardVisible(true); + setHasSoftwareKeyboard(true); + }); + const hideListener = Keyboard.addListener('keyboardDidHide', () => { + setKeyboardVisible(false); + }); + + return (() => { + showListener.remove(); + hideListener.remove(); + }); + }); + + return useMemo(() => { + return { keyboardVisible, hasSoftwareKeyboard }; + }, [keyboardVisible, hasSoftwareKeyboard]); +}; + +export default useKeyboardVisible;