mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Mobile: Add Markdown toolbar (#6753)
This commit is contained in:
parent
c6b91cdc5d
commit
b174fcf17b
@ -908,6 +908,24 @@ packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js.map
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
|
||||
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -897,6 +897,24 @@ packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js.map
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
|
||||
|
@ -20,7 +20,7 @@ interface LinkDialogProps {
|
||||
|
||||
const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
// The content of the link selected in the editor (if any)
|
||||
const editorLinkData = props.selectionState.linkData;
|
||||
const editorLinkData = props.selectionState.linkData ?? {};
|
||||
const [linkLabel, setLinkLabel] = useState('');
|
||||
const [linkURL, setLinkURL] = useState('');
|
||||
|
||||
|
@ -0,0 +1,365 @@
|
||||
// A toolbar for the markdown editor.
|
||||
|
||||
const React = require('react');
|
||||
import { Platform, StyleSheet, View } 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 { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import { useEffect } from 'react';
|
||||
import { Keyboard, ViewStyle } from 'react-native';
|
||||
import { EditorControl, EditorSettings, ListType, SearchState } from '../types';
|
||||
import SelectionFormatting from '../SelectionFormatting';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
import Toolbar from './Toolbar';
|
||||
import { buttonSize } from './ToolbarButton';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
|
||||
type OnAttachCallback = ()=> void;
|
||||
|
||||
interface MarkdownToolbarProps {
|
||||
editorControl: EditorControl;
|
||||
selectionState: SelectionFormatting;
|
||||
searchState: SearchState;
|
||||
editorSettings: EditorSettings;
|
||||
onAttach: OnAttachCallback;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const MarkdownToolbar = (props: MarkdownToolbarProps) => {
|
||||
const themeData = props.editorSettings.themeData;
|
||||
const styles = useStyles(props.style, themeData);
|
||||
const selState = props.selectionState;
|
||||
const editorControl = props.editorControl;
|
||||
|
||||
const headerButtons: ButtonSpec[] = [];
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
const active = selState.headerLevel === level;
|
||||
let label;
|
||||
if (!active) {
|
||||
label = _('Create header level %d', level);
|
||||
} else {
|
||||
label = _('Remove level %d header', level);
|
||||
}
|
||||
|
||||
headerButtons.push({
|
||||
icon: `H${level}`,
|
||||
description: label,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
const listButtons: ButtonSpec[] = [];
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="list-ul" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inUnorderedList ? _('Remove unordered list') : _('Create unordered list'),
|
||||
active: selState.inUnorderedList,
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleList(ListType.UnorderedList);
|
||||
}, [editorControl]),
|
||||
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="list-ol" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inOrderedList ? _('Remove ordered list') : _('Create ordered list'),
|
||||
active: selState.inOrderedList,
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleList(ListType.OrderedList);
|
||||
}, [editorControl]),
|
||||
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="tasks" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inChecklist ? _('Remove task list') : _('Create task list'),
|
||||
active: selState.inChecklist,
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleList(ListType.CheckList);
|
||||
}, [editorControl]),
|
||||
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<AntIcon name="indent-left" style={styles.text}/>
|
||||
),
|
||||
description: _('Decrease indent level'),
|
||||
onPress: editorControl.decreaseIndent,
|
||||
|
||||
priority: -1,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<AntIcon name="indent-right" style={styles.text}/>
|
||||
),
|
||||
description: _('Increase indent level'),
|
||||
onPress: editorControl.increaseIndent,
|
||||
|
||||
priority: -1,
|
||||
});
|
||||
|
||||
|
||||
// Inline formatting
|
||||
const inlineFormattingBtns: ButtonSpec[] = [];
|
||||
inlineFormattingBtns.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="bold" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.bolded ? _('Unbold') : _('Bold text'),
|
||||
active: selState.bolded,
|
||||
onPress: editorControl.toggleBolded,
|
||||
|
||||
priority: 3,
|
||||
});
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="italic" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.italicized ? _('Unitalicize') : _('Italicize'),
|
||||
active: selState.italicized,
|
||||
onPress: editorControl.toggleItalicized,
|
||||
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: '{;}',
|
||||
description:
|
||||
selState.inCode ? _('Remove code formatting') : _('Format as code'),
|
||||
active: selState.inCode,
|
||||
onPress: editorControl.toggleCode,
|
||||
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
if (props.editorSettings.katexEnabled) {
|
||||
inlineFormattingBtns.push({
|
||||
icon: '∑',
|
||||
description:
|
||||
selState.inMath ? _('Remove TeX region') : _('Create TeX region'),
|
||||
active: selState.inMath,
|
||||
onPress: editorControl.toggleMath,
|
||||
|
||||
priority: 1,
|
||||
});
|
||||
}
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="link" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inLink ? _('Edit link') : _('Create link'),
|
||||
active: selState.inLink,
|
||||
onPress: editorControl.showLinkDialog,
|
||||
|
||||
priority: -3,
|
||||
});
|
||||
|
||||
|
||||
// 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]),
|
||||
});
|
||||
|
||||
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]),
|
||||
});
|
||||
|
||||
actionButtons.push({
|
||||
icon: (
|
||||
<MaterialIcon name="search" style={styles.text}/>
|
||||
),
|
||||
description: (
|
||||
props.searchState.dialogVisible ? _('Close find and replace') : _('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,
|
||||
});
|
||||
|
||||
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,
|
||||
themeId: props.editorSettings.themeId,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar
|
||||
styleSheet={styleData}
|
||||
buttons={[
|
||||
{
|
||||
title: _('Formatting'),
|
||||
items: inlineFormattingBtns,
|
||||
},
|
||||
{
|
||||
title: _('Headers'),
|
||||
items: headerButtons,
|
||||
},
|
||||
{
|
||||
title: _('Lists'),
|
||||
items: listButtons,
|
||||
},
|
||||
{
|
||||
title: _('Actions'),
|
||||
items: actionButtons,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={{
|
||||
// The keyboard on iOS can overlap the markdown toolbar.
|
||||
// Add additional padding to prevent this.
|
||||
height: (
|
||||
Platform.OS === 'ios' && keyboardVisible ? 16 : 0
|
||||
),
|
||||
}}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = (styleProps: any, theme: Theme) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
button: {
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
buttonDisabledContent: {
|
||||
},
|
||||
buttonActive: {
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
color: theme.color3,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.color3,
|
||||
borderRadius: 6,
|
||||
},
|
||||
buttonActiveContent: {
|
||||
color: theme.color3,
|
||||
},
|
||||
text: {
|
||||
fontSize: 22,
|
||||
color: theme.color,
|
||||
},
|
||||
toolbarRow: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'center',
|
||||
|
||||
// Add a small amount of additional padding for button borders
|
||||
height: buttonSize + 6,
|
||||
...styleProps,
|
||||
},
|
||||
toolbarContainer: {
|
||||
maxHeight: '65%',
|
||||
flexShrink: 1,
|
||||
},
|
||||
toolbarContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
}, [styleProps, theme]);
|
||||
};
|
||||
|
||||
export default MarkdownToolbar;
|
@ -0,0 +1,34 @@
|
||||
const React = require('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 {
|
||||
overflowVisible: boolean;
|
||||
onToggleOverflowVisible: OnToggleOverflowCallback;
|
||||
styleSheet: StyleSheetData;
|
||||
}
|
||||
|
||||
// Button that shows/hides the overflow menu.
|
||||
const ToggleOverflowButton = (props: ToggleOverflowButtonProps) => {
|
||||
const spec: ButtonSpec = {
|
||||
icon: (
|
||||
<MaterialIcon name="more-horiz" style={props.styleSheet.styles.text}/>
|
||||
),
|
||||
description:
|
||||
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
|
||||
active: props.overflowVisible,
|
||||
onPress: props.onToggleOverflowVisible,
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
styleSheet={props.styleSheet}
|
||||
spec={spec}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ToggleOverflowButton;
|
@ -0,0 +1,119 @@
|
||||
const React = require('react');
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
import ToolbarOverflowRows from './ToolbarOverflowRows';
|
||||
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||
|
||||
interface ToolbarProps {
|
||||
buttons: ButtonGroup[];
|
||||
styleSheet: StyleSheetData;
|
||||
}
|
||||
|
||||
// Displays a list of buttons with an overflow menu.
|
||||
const Toolbar = (props: ToolbarProps) => {
|
||||
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
||||
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
||||
|
||||
const allButtonSpecs = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
|
||||
// Sort from highest priority to lowest
|
||||
allButtonSpecs.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
|
||||
const allButtonComponents: ReactElement[] = [];
|
||||
let key = 0;
|
||||
for (const spec of allButtonSpecs) {
|
||||
key++;
|
||||
allButtonComponents.push(
|
||||
<ToolbarButton
|
||||
key={key.toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={spec}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const containerWidth = event.nativeEvent.layout.width;
|
||||
const maxButtonsTotal = Math.floor(containerWidth / buttonSize);
|
||||
setMaxButtonsEachSide(Math.floor(
|
||||
Math.min((maxButtonsTotal - 1) / 2, allButtonSpecs.length / 2)
|
||||
));
|
||||
}, [allButtonSpecs.length]);
|
||||
|
||||
const onToggleOverflowVisible = useCallback(() => {
|
||||
AccessibilityInfo.announceForAccessibility(
|
||||
!overflowButtonsVisible
|
||||
? _('Opened toolbar overflow menu')
|
||||
: _('Closed toolbar overflow menu')
|
||||
);
|
||||
setOverflowPopupVisible(!overflowButtonsVisible);
|
||||
}, [overflowButtonsVisible]);
|
||||
|
||||
const toggleOverflowButton = (
|
||||
<ToggleOverflowButton
|
||||
key={(++key).toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
overflowVisible={overflowButtonsVisible}
|
||||
onToggleOverflowVisible={onToggleOverflowVisible}
|
||||
/>
|
||||
);
|
||||
|
||||
const mainButtons: ReactElement[] = [];
|
||||
if (maxButtonsEachSide < allButtonComponents.length) {
|
||||
// We want the menu to look something like this:
|
||||
// B I (…) 🔍 ⌨
|
||||
// where (…) shows/hides overflow.
|
||||
// Add from the left and right of [allButtonComponents] to ensure that
|
||||
// the (…) button is in the center:
|
||||
mainButtons.push(...allButtonComponents.slice(0, maxButtonsEachSide));
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
|
||||
} else {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
}
|
||||
|
||||
const styles = props.styleSheet.styles;
|
||||
const mainButtonRow = (
|
||||
<View style={styles.toolbarRow}>
|
||||
{!overflowButtonsVisible ? mainButtons : null }
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...styles.toolbarContainer,
|
||||
|
||||
// The number of buttons displayed is based on the width of the
|
||||
// container. As such, we can't base the container's width on the
|
||||
// size of its content.
|
||||
width: '100%',
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
<ScrollView>
|
||||
<ToolbarOverflowRows
|
||||
buttonGroups={props.buttons}
|
||||
styleSheet={props.styleSheet}
|
||||
visible={overflowButtonsVisible}
|
||||
onToggleOverflow={onToggleOverflowVisible}
|
||||
/>
|
||||
{ !overflowButtonsVisible ? mainButtonRow : null }
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default Toolbar;
|
@ -0,0 +1,64 @@
|
||||
import React = require('react');
|
||||
import { useCallback } from 'react';
|
||||
import { Text, TextStyle } from 'react-native';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
import CustomButton from '../../CustomButton';
|
||||
|
||||
export const buttonSize = 54;
|
||||
|
||||
interface ToolbarButtonProps {
|
||||
styleSheet: StyleSheetData;
|
||||
style?: TextStyle;
|
||||
spec: ButtonSpec;
|
||||
onActionComplete?: ()=> void;
|
||||
}
|
||||
|
||||
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 = (
|
||||
<Text style={{ ...styles.text, ...activatedTextStyle, ...disabledTextStyle }}>
|
||||
{spec.icon}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
content = spec.icon;
|
||||
}
|
||||
|
||||
const sourceOnPress = spec.onPress;
|
||||
const onPress = useCallback(() => {
|
||||
if (!disabled) {
|
||||
sourceOnPress();
|
||||
onActionComplete?.();
|
||||
}
|
||||
}, [disabled, sourceOnPress, onActionComplete]);
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
style={{
|
||||
...styles.button, ...activatedStyle, ...disabledStyle, ...style,
|
||||
...(!visible ? { opacity: 0 } : null),
|
||||
}}
|
||||
themeId={styleSheet.themeId}
|
||||
onPress={onPress}
|
||||
description={ spec.description }
|
||||
accessibilityRole="button"
|
||||
disabled={ disabled }
|
||||
>
|
||||
{ content }
|
||||
</CustomButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
@ -0,0 +1,122 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
|
||||
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[];
|
||||
styleSheet: StyleSheetData;
|
||||
visible: boolean;
|
||||
|
||||
// Should be created using useCallback
|
||||
onToggleOverflow: OnToggleOverflowCallback;
|
||||
}
|
||||
|
||||
// Contains buttons that overflow the available space.
|
||||
// Displays all buttons in [props.buttonGroups] if [props.visible].
|
||||
// Otherwise, displays nothing.
|
||||
const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
||||
const overflowRows: ReactElement[] = [];
|
||||
|
||||
let key = 0;
|
||||
for (let i = 0; i < props.buttonGroups.length; i++) {
|
||||
key++;
|
||||
const row: ReactElement[] = [];
|
||||
|
||||
const group = props.buttonGroups[i];
|
||||
for (let j = 0; j < group.items.length; j++) {
|
||||
key++;
|
||||
|
||||
const buttonSpec = group.items[j];
|
||||
row.push(
|
||||
<ToolbarButton
|
||||
key={key.toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={buttonSpec}
|
||||
|
||||
// After invoking this button's action, hide the overflow menu
|
||||
onActionComplete={props.onToggleOverflow}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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) {
|
||||
row.push(
|
||||
<ToggleOverflowButton
|
||||
key={(++key).toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
overflowVisible={true}
|
||||
onToggleOverflowVisible={props.onToggleOverflow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
overflowRows.push(
|
||||
<View
|
||||
key={key.toString()}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
contentContainerStyle={props.styleSheet.styles.toolbarContent}
|
||||
>
|
||||
{row}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const [hasSpaceForCloseBtn, setHasSpaceForCloseBtn] = useState(true);
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
if (props.buttonGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add 1 to account for the close button
|
||||
const totalButtonCount = props.buttonGroups[0].items.length + 1;
|
||||
|
||||
const newWidth = event.nativeEvent.layout.width;
|
||||
setHasSpaceForCloseBtn(newWidth > totalButtonCount * buttonSize);
|
||||
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
|
||||
|
||||
const closeButtonSpec: ButtonSpec = {
|
||||
icon: '⨉',
|
||||
description: _('Close'),
|
||||
onPress: props.onToggleOverflow,
|
||||
};
|
||||
const closeButton = (
|
||||
<ToolbarButton
|
||||
styleSheet={props.styleSheet}
|
||||
spec={closeButtonSpec}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: props.buttonGroups.length * buttonSize,
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
{hasSpaceForCloseBtn ? closeButton : null}
|
||||
{overflowRows}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default ToolbarOverflowRows;
|
@ -0,0 +1,35 @@
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export type OnPressListener = ()=> void;
|
||||
|
||||
export interface ButtonSpec {
|
||||
// Either text that will be shown in place of an icon or a component.
|
||||
icon: string | ReactElement;
|
||||
|
||||
// Tooltip/accessibility label
|
||||
description: string;
|
||||
onPress: OnPressListener;
|
||||
|
||||
// Priority for showing the button in the main toolbar.
|
||||
// Higher priority => more likely to be shown on the left of the toolbar
|
||||
// Lower (negative) priority => more likely to be shown on the right side of the
|
||||
// toolbar.
|
||||
priority?: number;
|
||||
|
||||
// True if the button is connected to an enabled action.
|
||||
// E.g. the cursor is in a header and the button is a header button.
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonGroup {
|
||||
title: string;
|
||||
items: ButtonSpec[];
|
||||
}
|
||||
|
||||
export interface StyleSheetData {
|
||||
themeId: number;
|
||||
styles: any;
|
||||
}
|
@ -5,36 +5,36 @@ import EditLinkDialog from './EditLinkDialog';
|
||||
import { defaultSearchState, SearchPanel } from './SearchPanel';
|
||||
|
||||
const React = require('react');
|
||||
const { forwardRef, useImperativeHandle } = require('react');
|
||||
const { useEffect, useMemo, useState, useCallback, useRef } = require('react');
|
||||
import { forwardRef, RefObject, useImperativeHandle } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { View } = require('react-native');
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
import SelectionFormatting from './SelectionFormatting';
|
||||
import {
|
||||
EditorSettings,
|
||||
EditorControl,
|
||||
|
||||
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent,
|
||||
ListType,
|
||||
SearchState,
|
||||
EditorSettings, EditorControl,
|
||||
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent, ListType, SearchState,
|
||||
} from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
|
||||
type OnAttachCallback = ()=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
initialSelection?: Selection;
|
||||
style: any;
|
||||
style: ViewStyle;
|
||||
contentStyle?: ViewStyle;
|
||||
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
onAttach: OnAttachCallback;
|
||||
}
|
||||
|
||||
function fontFamilyFromSettings() {
|
||||
@ -106,91 +106,15 @@ function editorTheme(themeId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
const [source, setSource] = useState(undefined);
|
||||
const webviewRef = useRef(null);
|
||||
|
||||
const setInitialSelectionJS = props.initialSelection ? `
|
||||
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
|
||||
` : '';
|
||||
|
||||
const editorSettings: EditorSettings = {
|
||||
themeId: props.themeId,
|
||||
themeData: editorTheme(props.themeId),
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
spellcheckEnabled: Setting.value('editor.mobile.spellcheckEnabled'),
|
||||
};
|
||||
|
||||
const injectedJavaScript = `
|
||||
function postMessage(name, data) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
data,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
function logMessage(...msg) {
|
||||
postMessage('onLog', { value: msg });
|
||||
}
|
||||
|
||||
// Globalize logMessage, postMessage
|
||||
window.logMessage = logMessage;
|
||||
window.postMessage = postMessage;
|
||||
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
|
||||
if (!window.cm) {
|
||||
// This variable is not used within this script
|
||||
// but is called using "injectJavaScript" from
|
||||
// the wrapper component.
|
||||
window.cm = null;
|
||||
|
||||
try {
|
||||
${shim.injectedJs('codeMirrorBundle')};
|
||||
|
||||
const parentElement = document.getElementsByClassName('CodeMirror')[0];
|
||||
const initialText = ${JSON.stringify(props.initialText)};
|
||||
const settings = ${JSON.stringify(editorSettings)};
|
||||
|
||||
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, settings);
|
||||
${setInitialSelectionJS}
|
||||
|
||||
window.onresize = () => {
|
||||
cm.scrollSelectionIntoView();
|
||||
};
|
||||
} catch (e) {
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
true;
|
||||
`;
|
||||
|
||||
const css = useCss(props.themeId);
|
||||
const html = useHtml(css);
|
||||
const [selectionState, setSelectionState] = useState(new SelectionFormatting());
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
|
||||
|
||||
// / Runs [js] in the context of the CodeMirror frame.
|
||||
const injectJS = (js: string) => {
|
||||
webviewRef.current.injectJavaScript(`
|
||||
try {
|
||||
${js}
|
||||
}
|
||||
catch(e) {
|
||||
logMessage('Error in injected JS:' + e, e);
|
||||
throw e;
|
||||
};
|
||||
|
||||
true;`);
|
||||
};
|
||||
|
||||
|
||||
const editorControl: EditorControl = {
|
||||
type OnInjectJSCallback = (js: string)=> void;
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
|
||||
setSearchState: OnSearchStateChangeCallback, searchStateRef: RefObject<SearchState>
|
||||
): EditorControl => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
undo() {
|
||||
injectJS('cm.undo();');
|
||||
},
|
||||
@ -266,19 +190,117 @@ function NoteEditor(props: Props, ref: any) {
|
||||
setSearchState(state);
|
||||
},
|
||||
showSearch() {
|
||||
const newSearchState: SearchState = Object.assign({}, searchState);
|
||||
newSearchState.dialogVisible = true;
|
||||
|
||||
setSearchState(newSearchState);
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: true,
|
||||
});
|
||||
},
|
||||
hideSearch() {
|
||||
const newSearchState: SearchState = Object.assign({}, searchState);
|
||||
newSearchState.dialogVisible = false;
|
||||
|
||||
setSearchState(newSearchState);
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
|
||||
};
|
||||
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
const [source, setSource] = useState(undefined);
|
||||
const webviewRef = useRef(null);
|
||||
|
||||
const setInitialSelectionJS = props.initialSelection ? `
|
||||
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
|
||||
` : '';
|
||||
|
||||
const editorSettings: EditorSettings = {
|
||||
themeId: props.themeId,
|
||||
themeData: editorTheme(props.themeId),
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
spellcheckEnabled: Setting.value('editor.mobile.spellcheckEnabled'),
|
||||
};
|
||||
|
||||
const injectedJavaScript = `
|
||||
function postMessage(name, data) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
data,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
function logMessage(...msg) {
|
||||
postMessage('onLog', { value: msg });
|
||||
}
|
||||
|
||||
// Globalize logMessage, postMessage
|
||||
window.logMessage = logMessage;
|
||||
window.postMessage = postMessage;
|
||||
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
|
||||
if (!window.cm) {
|
||||
// This variable is not used within this script
|
||||
// but is called using "injectJavaScript" from
|
||||
// the wrapper component.
|
||||
window.cm = null;
|
||||
|
||||
try {
|
||||
${shim.injectedJs('codeMirrorBundle')};
|
||||
|
||||
const parentElement = document.getElementsByClassName('CodeMirror')[0];
|
||||
const initialText = ${JSON.stringify(props.initialText)};
|
||||
const settings = ${JSON.stringify(editorSettings)};
|
||||
|
||||
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, settings);
|
||||
${setInitialSelectionJS}
|
||||
|
||||
window.onresize = () => {
|
||||
cm.scrollSelectionIntoView();
|
||||
};
|
||||
} catch (e) {
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
true;
|
||||
`;
|
||||
|
||||
const css = useCss(props.themeId);
|
||||
const html = useHtml(css);
|
||||
const [selectionState, setSelectionState] = useState(new SelectionFormatting());
|
||||
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
|
||||
// Having a [searchStateRef] allows [editorControl] to not be re-created
|
||||
// whenever [searchState] changes.
|
||||
const searchStateRef = useRef(defaultSearchState);
|
||||
|
||||
// Keep the reference and the [searchState] in sync
|
||||
useEffect(() => {
|
||||
searchStateRef.current = searchState;
|
||||
}, [searchState]);
|
||||
|
||||
// Runs [js] in the context of the CodeMirror frame.
|
||||
const injectJS = (js: string) => {
|
||||
webviewRef.current.injectJavaScript(`
|
||||
try {
|
||||
${js}
|
||||
}
|
||||
catch(e) {
|
||||
logMessage('Error in injected JS:' + e, e);
|
||||
throw e;
|
||||
};
|
||||
|
||||
true;`);
|
||||
};
|
||||
|
||||
const editorControl = useEditorControl(
|
||||
injectJS, setLinkDialogVisible, setSearchState, searchStateRef
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return editorControl;
|
||||
@ -358,13 +380,12 @@ function NoteEditor(props: Props, ref: any) {
|
||||
} else {
|
||||
console.info('Unsupported CodeMirror message:', msg);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onChange]);
|
||||
}, [props.onSelectionChange, props.onUndoRedoDepthChange, props.onChange, editorControl]);
|
||||
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
const onError = useCallback(() => {
|
||||
console.error('NoteEditor: webview error');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
@ -385,7 +406,8 @@ function NoteEditor(props: Props, ref: any) {
|
||||
<View style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
minHeight: '40%',
|
||||
minHeight: '30%',
|
||||
...props.contentStyle,
|
||||
}}>
|
||||
<WebView
|
||||
style={{
|
||||
@ -411,6 +433,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
searchControl={editorControl.searchControl}
|
||||
searchState={searchState}
|
||||
/>
|
||||
|
||||
<MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
// for it:
|
||||
flexShrink: 1,
|
||||
}}
|
||||
editorSettings={editorSettings}
|
||||
editorControl={editorControl}
|
||||
selectionState={selectionState}
|
||||
searchState={searchState}
|
||||
onAttach={props.onAttach}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -656,7 +656,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
);
|
||||
}
|
||||
|
||||
public static defaultProps: Partial<ScreenHeaderProps> ={
|
||||
public static defaultProps: Partial<ScreenHeaderProps> = {
|
||||
menuOptions: [],
|
||||
};
|
||||
}
|
||||
|
@ -867,6 +867,27 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return output;
|
||||
}
|
||||
|
||||
async showAttachMenu() {
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
|
||||
//
|
||||
// On Android, it will depend on the phone, but usually it will allow browing all files and photos.
|
||||
buttons.push({ text: _('Attach file'), id: 'attachFile' });
|
||||
|
||||
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
|
||||
// because that's only way to browse photos from the camera roll.
|
||||
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
|
||||
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
|
||||
|
||||
const buttonId = await dialogs.pop(this, _('Choose an option'), buttons);
|
||||
|
||||
if (buttonId === 'takePhoto') this.takePhoto_onPress();
|
||||
if (buttonId === 'attachFile') void this.attachFile_onPress();
|
||||
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
|
||||
}
|
||||
|
||||
menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
@ -891,26 +912,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (canAttachPicture) {
|
||||
output.push({
|
||||
title: _('Attach...'),
|
||||
onPress: async () => {
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
|
||||
//
|
||||
// On Android, it will depend on the phone, but usually it will allow browing all files and photos.
|
||||
buttons.push({ text: _('Attach file'), id: 'attachFile' });
|
||||
|
||||
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
|
||||
// because that's only way to browse photos from the camera roll.
|
||||
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
|
||||
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
|
||||
|
||||
const buttonId = await dialogs.pop(this, _('Choose an option'), buttons);
|
||||
|
||||
if (buttonId === 'takePhoto') this.takePhoto_onPress();
|
||||
if (buttonId === 'attachFile') void this.attachFile_onPress();
|
||||
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
|
||||
},
|
||||
onPress: () => this.showAttachMenu(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -1129,6 +1131,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const editorStyle = this.styles().bodyTextInput;
|
||||
|
||||
bodyComponent = <NoteEditor
|
||||
ref={this.editorRef}
|
||||
themeId={this.props.themeId}
|
||||
@ -1137,7 +1141,17 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
onChange={this.onBodyChange}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
style={this.styles().bodyTextInput}
|
||||
onAttach={() => this.showAttachMenu()}
|
||||
style={{
|
||||
...editorStyle,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
contentStyle={{
|
||||
// Apply padding to the editor's content, but not the toolbar.
|
||||
paddingLeft: editorStyle.paddingLeft,
|
||||
paddingRight: editorStyle.paddingRight,
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user