mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Chore: Mobile: Refactor markdown toolbar (#9708)
This commit is contained in:
parent
bc1165be46
commit
4636d1539c
@ -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/Toolbar.js
|
||||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.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/MarkdownToolbar/types.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||||
packages/app-mobile/components/NoteEditor/SearchPanel.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/NoteEditor/types.js
|
||||||
packages/app-mobile/components/NoteList.js
|
packages/app-mobile/components/NoteList.js
|
||||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -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/Toolbar.js
|
||||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.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/MarkdownToolbar/types.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||||
packages/app-mobile/components/NoteEditor/SearchPanel.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/NoteEditor/types.js
|
||||||
packages/app-mobile/components/NoteList.js
|
packages/app-mobile/components/NoteList.js
|
||||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
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 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 {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
@ -9,6 +13,8 @@ interface Props {
|
|||||||
|
|
||||||
// If `null` is given, the content must be labeled elsewhere.
|
// If `null` is given, the content must be labeled elsewhere.
|
||||||
accessibilityLabel: string|null;
|
accessibilityLabel: string|null;
|
||||||
|
|
||||||
|
allowFontScaling?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon: React.FC<Props> = props => {
|
const Icon: React.FC<Props> = props => {
|
||||||
@ -25,20 +31,42 @@ const Icon: React.FC<Props> = props => {
|
|||||||
// to read the characters from the icon font (they don't make sense
|
// to read the characters from the icon font (they don't make sense
|
||||||
// without the icon font applied).
|
// without the icon font applied).
|
||||||
const accessibilityHidden = props.accessibilityLabel === null;
|
const accessibilityHidden = props.accessibilityLabel === null;
|
||||||
|
const importantForAccessibility = accessibilityHidden ? 'no-hide-descendants' : 'yes';
|
||||||
|
|
||||||
|
const sharedProps = {
|
||||||
|
importantForAccessibility,
|
||||||
|
'aria-hidden': accessibilityHidden,
|
||||||
|
accessibilityLabel: props.accessibilityLabel,
|
||||||
|
style: props.style,
|
||||||
|
allowFontScaling: props.allowFontScaling,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (namePrefix.match(/^fa[bsr]?$/)) {
|
||||||
return (
|
return (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
brand={namePrefix.startsWith('fab')}
|
brand={namePrefix.startsWith('fab')}
|
||||||
solid={namePrefix.startsWith('fas')}
|
solid={namePrefix.startsWith('fas')}
|
||||||
accessibilityLabel={props.accessibilityLabel}
|
|
||||||
aria-hidden={accessibilityHidden}
|
|
||||||
importantForAccessibility={
|
|
||||||
accessibilityHidden ? 'no-hide-descendants' : 'yes'
|
|
||||||
}
|
|
||||||
name={nameSuffix}
|
name={nameSuffix}
|
||||||
style={props.style}
|
{...sharedProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (namePrefix === 'ant') {
|
||||||
|
return <AntIcon name={nameSuffix} {...sharedProps}/>;
|
||||||
|
} else if (namePrefix === 'material') {
|
||||||
|
return <MaterialIcon name={nameSuffix} {...sharedProps}/>;
|
||||||
|
} else if (namePrefix === 'text') {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={props.style}
|
||||||
|
aria-hidden={accessibilityHidden}
|
||||||
|
importantForAccessibility={importantForAccessibility}
|
||||||
|
>
|
||||||
|
{nameSuffix}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <FontAwesomeIcon name='cog' {...sharedProps}/>;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Icon;
|
export default Icon;
|
||||||
|
@ -1,288 +1,45 @@
|
|||||||
// A toolbar for the markdown editor.
|
// A toolbar for the markdown editor.
|
||||||
|
|
||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
import { Platform, StyleSheet } from 'react-native';
|
||||||
import { useMemo, useState, useCallback } from 'react';
|
import { useMemo } 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 { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import time from '@joplin/lib/time';
|
import { MarkdownToolbarProps, StyleSheetData } from './types';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { Keyboard, ViewStyle } from 'react-native';
|
|
||||||
import { EditorControl, EditorSettings } from '../types';
|
|
||||||
import { ButtonSpec, StyleSheetData } from './types';
|
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
import { buttonSize } from './ToolbarButton';
|
import { buttonSize } from './ToolbarButton';
|
||||||
import { Theme } from '@joplin/lib/themes/type';
|
import { Theme } from '@joplin/lib/themes/type';
|
||||||
import ToggleSpaceButton from './ToggleSpaceButton';
|
import ToggleSpaceButton from './ToggleSpaceButton';
|
||||||
import { SearchState } from '@joplin/editor/types';
|
import useHeaderButtons from './buttons/useHeaderButtons';
|
||||||
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
|
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 {
|
const MarkdownToolbar: React.FC<MarkdownToolbarProps> = (props: MarkdownToolbarProps) => {
|
||||||
editorControl: EditorControl;
|
|
||||||
selectionState: SelectionFormatting;
|
|
||||||
searchState: SearchState;
|
|
||||||
editorSettings: EditorSettings;
|
|
||||||
onAttach: OnAttachCallback;
|
|
||||||
style?: ViewStyle;
|
|
||||||
readOnly: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MarkdownToolbar = (props: MarkdownToolbarProps) => {
|
|
||||||
const themeData = props.editorSettings.themeData;
|
const themeData = props.editorSettings.themeData;
|
||||||
const styles = useStyles(props.style, themeData);
|
const styles = useStyles(props.style, themeData);
|
||||||
const selState = props.selectionState;
|
|
||||||
const editorControl = props.editorControl;
|
|
||||||
const readOnly = props.readOnly;
|
|
||||||
|
|
||||||
const headerButtons: ButtonSpec[] = [];
|
const { keyboardVisible, hasSoftwareKeyboard } = useKeyboardVisible();
|
||||||
for (let level = 1; level <= 5; level++) {
|
const buttonProps = {
|
||||||
const active = selState.headerLevel === level;
|
...props,
|
||||||
|
iconStyle: styles.text,
|
||||||
|
keyboardVisible,
|
||||||
|
hasSoftwareKeyboard,
|
||||||
|
};
|
||||||
|
const headerButtons = useHeaderButtons(buttonProps);
|
||||||
|
const inlineFormattingBtns = useInlineFormattingButtons(buttonProps);
|
||||||
|
const actionButtons = useActionButtons(buttonProps);
|
||||||
|
const listButtons = useListButtons(buttonProps);
|
||||||
|
|
||||||
headerButtons.push({
|
const styleData: StyleSheetData = useMemo(() => ({
|
||||||
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: (
|
|
||||||
<FontAwesomeIcon name="list-ul" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Unordered list'),
|
|
||||||
active: selState.inUnorderedList,
|
|
||||||
onPress: editorControl.toggleUnorderedList,
|
|
||||||
|
|
||||||
priority: -2,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
listButtons.push({
|
|
||||||
icon: (
|
|
||||||
<FontAwesomeIcon name="list-ol" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Ordered list'),
|
|
||||||
active: selState.inOrderedList,
|
|
||||||
onPress: editorControl.toggleOrderedList,
|
|
||||||
|
|
||||||
priority: -2,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
listButtons.push({
|
|
||||||
icon: (
|
|
||||||
<FontAwesomeIcon name="tasks" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Task list'),
|
|
||||||
active: selState.inChecklist,
|
|
||||||
onPress: editorControl.toggleTaskList,
|
|
||||||
|
|
||||||
priority: -2,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
listButtons.push({
|
|
||||||
icon: (
|
|
||||||
<AntIcon name="indent-left" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Decrease indent level'),
|
|
||||||
onPress: editorControl.decreaseIndent,
|
|
||||||
|
|
||||||
priority: -1,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
listButtons.push({
|
|
||||||
icon: (
|
|
||||||
<AntIcon name="indent-right" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Increase indent level'),
|
|
||||||
onPress: editorControl.increaseIndent,
|
|
||||||
|
|
||||||
priority: -1,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Inline formatting
|
|
||||||
const inlineFormattingBtns: ButtonSpec[] = [];
|
|
||||||
inlineFormattingBtns.push({
|
|
||||||
icon: (
|
|
||||||
<FontAwesomeIcon name="bold" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Bold'),
|
|
||||||
active: selState.bolded,
|
|
||||||
onPress: editorControl.toggleBolded,
|
|
||||||
|
|
||||||
priority: 3,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
inlineFormattingBtns.push({
|
|
||||||
icon: (
|
|
||||||
<FontAwesomeIcon name="italic" style={styles.text}/>
|
|
||||||
),
|
|
||||||
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: (
|
|
||||||
<FontAwesomeIcon name="link" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Link'),
|
|
||||||
active: selState.inLink,
|
|
||||||
onPress: editorControl.showLinkDialog,
|
|
||||||
|
|
||||||
priority: -3,
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
const actionButtons: ButtonSpec[] = [];
|
|
||||||
actionButtons.push({
|
|
||||||
icon: (
|
|
||||||
<FontAwesomeIcon name="calendar-plus" style={styles.text}/>
|
|
||||||
),
|
|
||||||
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: (
|
|
||||||
<MaterialIcon name="attachment" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Attach'),
|
|
||||||
onPress: useCallback(() => {
|
|
||||||
onDismissKeyboard();
|
|
||||||
props.onAttach();
|
|
||||||
}, [props.onAttach, onDismissKeyboard]),
|
|
||||||
disabled: readOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
actionButtons.push({
|
|
||||||
icon: (
|
|
||||||
<MaterialIcon name="search" style={styles.text}/>
|
|
||||||
),
|
|
||||||
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: (
|
|
||||||
<MaterialIcon name="keyboard-hide" style={styles.text}/>
|
|
||||||
),
|
|
||||||
description: _('Hide keyboard'),
|
|
||||||
disabled: !keyboardVisible,
|
|
||||||
visible: hasSoftwareKeyboard && Platform.OS === 'ios',
|
|
||||||
onPress: onDismissKeyboard,
|
|
||||||
|
|
||||||
priority: -3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const styleData: StyleSheetData = {
|
|
||||||
styles: styles,
|
styles: styles,
|
||||||
themeId: props.editorSettings.themeId,
|
themeId: props.editorSettings.themeId,
|
||||||
};
|
}), [styles, props.editorSettings.themeId]);
|
||||||
|
|
||||||
return (
|
const toolbarButtons = useMemo(() => {
|
||||||
<ToggleSpaceButton
|
const buttons = [
|
||||||
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
|
|
||||||
themeId={props.editorSettings.themeId}
|
|
||||||
style={styles.container}
|
|
||||||
>
|
|
||||||
<Toolbar
|
|
||||||
styleSheet={styleData}
|
|
||||||
buttons={[
|
|
||||||
{
|
{
|
||||||
title: _('Formatting'),
|
title: _('Formatting'),
|
||||||
items: inlineFormattingBtns,
|
items: inlineFormattingBtns,
|
||||||
@ -299,7 +56,20 @@ const MarkdownToolbar = (props: MarkdownToolbarProps) => {
|
|||||||
title: _('Actions'),
|
title: _('Actions'),
|
||||||
items: actionButtons,
|
items: actionButtons,
|
||||||
},
|
},
|
||||||
]}
|
];
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}, [headerButtons, inlineFormattingBtns, listButtons, actionButtons]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleSpaceButton
|
||||||
|
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
|
||||||
|
themeId={props.editorSettings.themeId}
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
styleSheet={styleData}
|
||||||
|
buttons={toolbarButtons}
|
||||||
/>
|
/>
|
||||||
</ToggleSpaceButton>
|
</ToggleSpaceButton>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
import { ButtonSpec, StyleSheetData } from './types';
|
import { ButtonSpec, StyleSheetData } from './types';
|
||||||
const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default;
|
|
||||||
|
|
||||||
type OnToggleOverflowCallback = ()=> void;
|
type OnToggleOverflowCallback = ()=> void;
|
||||||
interface ToggleOverflowButtonProps {
|
interface ToggleOverflowButtonProps {
|
||||||
@ -13,11 +12,9 @@ interface ToggleOverflowButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Button that shows/hides the overflow menu.
|
// Button that shows/hides the overflow menu.
|
||||||
const ToggleOverflowButton = (props: ToggleOverflowButtonProps) => {
|
const ToggleOverflowButton: React.FC<ToggleOverflowButtonProps> = (props: ToggleOverflowButtonProps) => {
|
||||||
const spec: ButtonSpec = {
|
const spec: ButtonSpec = {
|
||||||
icon: (
|
icon: 'material more-horiz',
|
||||||
<MaterialIcon name="more-horiz" style={props.styleSheet.styles.text}/>
|
|
||||||
),
|
|
||||||
description:
|
description:
|
||||||
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
|
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
|
||||||
active: props.overflowVisible,
|
active: props.overflowVisible,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||||
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||||
@ -14,7 +14,7 @@ interface ToolbarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Displays a list of buttons with an overflow menu.
|
// Displays a list of buttons with an overflow menu.
|
||||||
const Toolbar = (props: ToolbarProps) => {
|
const Toolbar: React.FC<ToolbarProps> = (props: ToolbarProps) => {
|
||||||
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
||||||
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React = require('react');
|
import * as React from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Text, TextStyle } from 'react-native';
|
import { TextStyle, StyleSheet } from 'react-native';
|
||||||
import { ButtonSpec, StyleSheetData } from './types';
|
import { ButtonSpec, StyleSheetData } from './types';
|
||||||
import CustomButton from '../../CustomButton';
|
import CustomButton from '../../CustomButton';
|
||||||
|
import Icon from '../../Icon';
|
||||||
|
|
||||||
export const buttonSize = 54;
|
export const buttonSize = 54;
|
||||||
|
|
||||||
@ -13,28 +14,39 @@ interface ToolbarButtonProps {
|
|||||||
onActionComplete?: ()=> void;
|
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 ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarButtonProps) => {
|
||||||
const visible = spec.visible ?? true;
|
const visible = spec.visible ?? true;
|
||||||
const disabled = (spec.disabled ?? false) && visible;
|
const disabled = (spec.disabled ?? false) && visible;
|
||||||
const styles = styleSheet.styles;
|
const styles = useStyles(styleSheet.styles, style, spec, visible, disabled);
|
||||||
|
|
||||||
// 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 = (
|
|
||||||
<Text style={{ ...styles.text, ...activatedTextStyle, ...disabledTextStyle }}>
|
|
||||||
{spec.icon}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = spec.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceOnPress = spec.onPress;
|
const sourceOnPress = spec.onPress;
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
@ -46,17 +58,14 @@ const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarBut
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomButton
|
<CustomButton
|
||||||
style={{
|
style={styles.buttonStyle}
|
||||||
...styles.button, ...activatedStyle, ...disabledStyle, ...style,
|
|
||||||
...(!visible ? { opacity: 0 } : null),
|
|
||||||
}}
|
|
||||||
themeId={styleSheet.themeId}
|
themeId={styleSheet.themeId}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
description={ spec.description }
|
description={ spec.description }
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
>
|
>
|
||||||
{ content }
|
<Icon name={spec.icon} style={styles.iconStyle} accessibilityLabel={null}/>
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { ReactElement, useCallback, useState } from 'react';
|
import { ReactElement, useCallback, useState } from 'react';
|
||||||
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
|
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
|
||||||
@ -5,8 +7,6 @@ import ToggleOverflowButton from './ToggleOverflowButton';
|
|||||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||||
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
|
|
||||||
type OnToggleOverflowCallback = ()=> void;
|
type OnToggleOverflowCallback = ()=> void;
|
||||||
interface OverflowPopupProps {
|
interface OverflowPopupProps {
|
||||||
buttonGroups: ButtonGroup[];
|
buttonGroups: ButtonGroup[];
|
||||||
@ -17,10 +17,13 @@ interface OverflowPopupProps {
|
|||||||
onToggleOverflow: OnToggleOverflowCallback;
|
onToggleOverflow: OnToggleOverflowCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specification for a button that acts as padding.
|
||||||
|
const paddingButtonSpec = { visible: false, icon: '', onPress: ()=>{}, description: '' };
|
||||||
|
|
||||||
// Contains buttons that overflow the available space.
|
// Contains buttons that overflow the available space.
|
||||||
// Displays all buttons in [props.buttonGroups] if [props.visible].
|
// Displays all buttons in [props.buttonGroups] if [props.visible].
|
||||||
// Otherwise, displays nothing.
|
// Otherwise, displays nothing.
|
||||||
const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
const ToolbarOverflowRows: React.FC<OverflowPopupProps> = (props: OverflowPopupProps) => {
|
||||||
const overflowRows: ReactElement[] = [];
|
const overflowRows: ReactElement[] = [];
|
||||||
|
|
||||||
let key = 0;
|
let key = 0;
|
||||||
@ -47,7 +50,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
|||||||
// Show the "hide overflow" button if in the center of the last row
|
// Show the "hide overflow" button if in the center of the last row
|
||||||
const isLastRow = i === props.buttonGroups.length - 1;
|
const isLastRow = i === props.buttonGroups.length - 1;
|
||||||
const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2);
|
const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2);
|
||||||
if (isLastRow && isCenterOfRow) {
|
if (isLastRow && (isCenterOfRow || group.items.length === 1)) {
|
||||||
row.push(
|
row.push(
|
||||||
<ToggleOverflowButton
|
<ToggleOverflowButton
|
||||||
key={(++key).toString()}
|
key={(++key).toString()}
|
||||||
@ -59,6 +62,17 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pad to an odd number of items to ensure that buttons are centered properly
|
||||||
|
if (row.length % 2 === 0) {
|
||||||
|
row.push(
|
||||||
|
<ToolbarButton
|
||||||
|
key={`padding-${i}`}
|
||||||
|
styleSheet={props.styleSheet}
|
||||||
|
spec={paddingButtonSpec}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
overflowRows.push(
|
overflowRows.push(
|
||||||
<View
|
<View
|
||||||
key={key.toString()}
|
key={key.toString()}
|
||||||
@ -87,7 +101,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
|||||||
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
|
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
|
||||||
|
|
||||||
const closeButtonSpec: ButtonSpec = {
|
const closeButtonSpec: ButtonSpec = {
|
||||||
icon: '⨉',
|
icon: 'text ⨉',
|
||||||
description: _('Close'),
|
description: _('Close'),
|
||||||
onPress: props.onToggleOverflow,
|
onPress: props.onToggleOverflow,
|
||||||
};
|
};
|
||||||
@ -112,6 +126,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
|||||||
height: props.buttonGroups.length * buttonSize,
|
height: props.buttonGroups.length * buttonSize,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
display: !props.visible ? 'none' : 'flex',
|
||||||
}}
|
}}
|
||||||
onLayout={onContainerLayout}
|
onLayout={onContainerLayout}
|
||||||
>
|
>
|
||||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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 type OnPressListener = ()=> void;
|
||||||
|
|
||||||
export interface ButtonSpec {
|
export interface ButtonSpec {
|
||||||
// Either text that will be shown in place of an icon or a component.
|
// Name of an icon, as accepted by components/Icon.tsx
|
||||||
icon: string | ReactElement;
|
icon: string;
|
||||||
|
|
||||||
// Tooltip/accessibility label
|
// Tooltip/accessibility label
|
||||||
description: string;
|
description: string;
|
||||||
@ -23,7 +26,6 @@ export interface ButtonSpec {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonGroup {
|
export interface ButtonGroup {
|
||||||
title: string;
|
title: string;
|
||||||
items: ButtonSpec[];
|
items: ButtonSpec[];
|
||||||
@ -33,3 +35,18 @@ export interface StyleSheetData {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
styles: any;
|
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;
|
||||||
|
}
|
||||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user