You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-29 22:48:10 +02:00
Mobile: Allow selecting, deleting and moving multiple notes
This commit is contained in:
@@ -87,7 +87,10 @@ class Dropdown extends React.Component {
|
||||
let headerLabel = '...';
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.value === this.props.selectedValue) headerLabel = item.label;
|
||||
if (item.value === this.props.selectedValue) {
|
||||
headerLabel = item.label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const closeList = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { ListView, Text, TouchableHighlight, View, StyleSheet } = require('react-native');
|
||||
const { ListView, Text, TouchableOpacity , View, StyleSheet } = require('react-native');
|
||||
const { Log } = require('lib/log.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Checkbox } = require('lib/components/checkbox.js');
|
||||
@@ -41,13 +41,16 @@ class NoteItemComponent extends Component {
|
||||
paddingRight: theme.marginRight,
|
||||
paddingTop: theme.itemMarginTop,
|
||||
paddingBottom: theme.itemMarginBottom,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
//backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
listItemText: {
|
||||
flex: 1,
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
selectionWrapper: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
styles.listItemWithCheckbox = Object.assign({}, styles.listItem);
|
||||
@@ -59,6 +62,9 @@ class NoteItemComponent extends Component {
|
||||
styles.listItemTextWithCheckbox.marginTop = styles.listItem.paddingTop - 1;
|
||||
styles.listItemTextWithCheckbox.marginBottom = styles.listItem.paddingBottom;
|
||||
|
||||
styles.selectionWrapperSelected = Object.assign({}, styles.selectionWrapper);
|
||||
styles.selectionWrapperSelected.backgroundColor = theme.selectedColor;
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
@@ -76,10 +82,26 @@ class NoteItemComponent extends Component {
|
||||
onPress() {
|
||||
if (!this.props.note) return;
|
||||
|
||||
if (this.props.noteSelectionEnabled) {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECTION_TOGGLE',
|
||||
id: this.props.note.id,
|
||||
});
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: this.props.note.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onLongPress() {
|
||||
if (!this.props.note) return;
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: this.props.note.id,
|
||||
type: 'NOTE_SELECTION_START',
|
||||
id: this.props.note.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,18 +127,23 @@ class NoteItemComponent extends Component {
|
||||
const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem;
|
||||
const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText;
|
||||
const rootStyle = isTodo && checkboxChecked ? {opacity: 0.4} : {};
|
||||
const isSelected = this.props.noteSelectionEnabled && this.props.selectedNoteIds.indexOf(note.id) >= 0;
|
||||
|
||||
const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper;
|
||||
|
||||
return (
|
||||
<TouchableHighlight onPress={() => this.onPress()} underlayColor="#0066FF" style={rootStyle}>
|
||||
<View style={ listItemStyle }>
|
||||
<Checkbox
|
||||
style={checkboxStyle}
|
||||
checked={checkboxChecked}
|
||||
onChange={(checked) => this.todoCheckbox_change(checked)}
|
||||
/>
|
||||
<Text style={listItemTextStyle}>{note.title}</Text>
|
||||
<TouchableOpacity onPress={() => this.onPress()} style={rootStyle} onLongPress={() => this.onLongPress()}>
|
||||
<View style={ selectionWrapperStyle }>
|
||||
<View style={ listItemStyle }>
|
||||
<Checkbox
|
||||
style={checkboxStyle}
|
||||
checked={checkboxChecked}
|
||||
onChange={(checked) => this.todoCheckbox_change(checked)}
|
||||
/>
|
||||
<Text style={listItemTextStyle}>{note.title}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,6 +153,8 @@ const NoteItem = connect(
|
||||
(state) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
};
|
||||
}
|
||||
)(NoteItemComponent)
|
||||
|
||||
@@ -113,6 +113,7 @@ const NoteList = connect(
|
||||
items: state.notes,
|
||||
notesSource: state.notesSource,
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
};
|
||||
}
|
||||
)(NoteListComponent)
|
||||
|
||||
@@ -8,6 +8,7 @@ const { ReportService } = require('lib/services/report.js');
|
||||
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
@@ -16,6 +17,8 @@ const { ItemList } = require('lib/components/ItemList.js');
|
||||
const { Dropdown } = require('lib/components/Dropdown.js');
|
||||
const { time } = require('lib/time-utils');
|
||||
const RNFS = require('react-native-fs');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
// Rather than applying a padding to the whole bar, it is applied to each
|
||||
// individual component (button, picker, etc.) so that the touchable areas
|
||||
@@ -147,7 +150,6 @@ class ScreenHeaderComponent extends Component {
|
||||
|
||||
async backButton_press() {
|
||||
await BackButtonService.back();
|
||||
//this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
searchButton_press() {
|
||||
@@ -157,6 +159,18 @@ class ScreenHeaderComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteButton_press() {
|
||||
// Dialog needs to be displayed as a child of the parent component, otherwise
|
||||
// it won't be visible within the header component.
|
||||
if (!this.props.parentComponent) throw new Error('parentComponent not set');
|
||||
const ok = await dialogs.confirm(this.props.parentComponent, _('Delete these notes?'));
|
||||
if (!ok) return;
|
||||
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
await Note.batchDelete(noteIds);
|
||||
}
|
||||
|
||||
menu_select(value) {
|
||||
if (typeof(value) == 'function') {
|
||||
value();
|
||||
@@ -256,59 +270,93 @@ class ScreenHeaderComponent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
let key = 0;
|
||||
let menuOptionComponents = [];
|
||||
for (let i = 0; i < this.props.menuOptions.length; i++) {
|
||||
let o = this.props.menuOptions[i];
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={o.onPress} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
|
||||
</MenuOption>);
|
||||
function deleteButton(styles, onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={styles.iconButton}>
|
||||
<Icon name='md-trash' style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.showAdvancedOptions) {
|
||||
let key = 0;
|
||||
let menuOptionComponents = [];
|
||||
|
||||
if (!this.props.noteSelectionEnabled) {
|
||||
for (let i = 0; i < this.props.menuOptions.length; i++) {
|
||||
let o = this.props.menuOptions[i];
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={o.onPress} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
|
||||
</MenuOption>);
|
||||
}
|
||||
|
||||
if (this.props.showAdvancedOptions) {
|
||||
if (menuOptionComponents.length) {
|
||||
menuOptionComponents.push(<View key={'menuOption_showAdvancedOptions'} style={this.styles().divider}/>);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.log_press()} key={'menuOption_log'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Log')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.status_press()} key={'menuOption_status'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Status')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.debugReport_press()} key={'menuOption_debugReport'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Export Debug Report')}</Text>
|
||||
</MenuOption>);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuOptionComponents.length) {
|
||||
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.log_press()} key={'menuOption_log'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Log')}</Text>
|
||||
<MenuOption value={() => this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
} else {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.status_press()} key={'menuOption_status'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Status')}</Text>
|
||||
<MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Delete')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.debugReport_press()} key={'menuOption_debugReport'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Export Debug Report')}</Text>
|
||||
</MenuOption>);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuOptionComponents.length) {
|
||||
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.config_press()} key={'menuOption_' + key++} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
const createTitleComponent = () => {
|
||||
const themeId = Setting.value('theme');
|
||||
const theme = themeStyle(themeId);
|
||||
const folderPickerOptions = this.props.folderPickerOptions;
|
||||
|
||||
if (folderPickerOptions && folderPickerOptions.enabled) {
|
||||
|
||||
const titlePickerItems = (mustSelect) => {
|
||||
let output = [];
|
||||
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
|
||||
for (let i = 0; i < this.props.folders.length; i++) {
|
||||
let f = this.props.folders[i];
|
||||
output.push({ label: f.title, value: f.id });
|
||||
}
|
||||
output.sort((a, b) => {
|
||||
if (a.value === null) return -1;
|
||||
if (b.value === null) return +1;
|
||||
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
const p = this.props.titlePicker;
|
||||
if (p) {
|
||||
return (
|
||||
<Dropdown
|
||||
items={p.items}
|
||||
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
|
||||
itemHeight={35}
|
||||
selectedValue={p.selectedValue}
|
||||
selectedValue={('selectedFolderId' in folderPickerOptions) ? folderPickerOptions.selectedFolderId : null}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
@@ -320,7 +368,10 @@ class ScreenHeaderComponent extends Component {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue, itemIndex) => { if (p.onValueChange) p.onValueChange(itemValue, itemIndex); }}
|
||||
onValueChange={(itemValue, itemIndex) => {
|
||||
if (!folderPickerOptions.onValueChange) return;
|
||||
folderPickerOptions.onValueChange(itemValue, itemIndex);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -330,22 +381,32 @@ class ScreenHeaderComponent extends Component {
|
||||
}
|
||||
|
||||
const titleComp = createTitleComponent();
|
||||
const sideMenuComp = this.props.noteSelectionEnabled ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
|
||||
const backButtonComp = backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack);
|
||||
const searchButtonComp = this.props.noteSelectionEnabled ? null : searchButton(this.styles(), () => this.searchButton_press());
|
||||
const deleteButtonComp = this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press()) : null;
|
||||
|
||||
const menuComp = (
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
|
||||
<Text style={this.styles().contextMenuTrigger}> ⋮</Text>
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
{ menuOptionComponents }
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.styles().container} >
|
||||
{ sideMenuButton(this.styles(), () => this.sideMenuButton_press()) }
|
||||
{ backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack) }
|
||||
{ sideMenuComp }
|
||||
{ backButtonComp }
|
||||
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
|
||||
{ titleComp }
|
||||
{ searchButton(this.styles(), () => this.searchButton_press()) }
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
|
||||
<Text style={this.styles().contextMenuTrigger}> ⋮</Text>
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
{ menuOptionComponents }
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
{ searchButtonComp }
|
||||
{ deleteButtonComp }
|
||||
{ menuComp }
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -361,8 +422,11 @@ const ScreenHeader = connect(
|
||||
return {
|
||||
historyCanGoBack: state.historyCanGoBack,
|
||||
locale: state.settings.locale,
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
};
|
||||
}
|
||||
)(ScreenHeaderComponent)
|
||||
|
||||
@@ -448,15 +448,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />
|
||||
}
|
||||
|
||||
const titlePickerItems = () => {
|
||||
let output = [];
|
||||
for (let i = 0; i < this.props.folders.length; i++) {
|
||||
let f = this.props.folders[i];
|
||||
output.push({ label: f.title, value: f.id });
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
const actionButtonComp = renderActionButton();
|
||||
|
||||
let showSaveButton = this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
|
||||
@@ -506,19 +497,10 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader
|
||||
titlePicker={{
|
||||
items: titlePickerItems(),
|
||||
selectedValue: folder ? folder.id : null,
|
||||
folderPickerOptions={{
|
||||
enabled: true,
|
||||
selectedFolderId: folder ? folder.id : null,
|
||||
onValueChange: async (itemValue, itemIndex) => {
|
||||
let note = Object.assign({}, this.state.note);
|
||||
|
||||
// RN bug: https://github.com/facebook/react-native/issues/9220
|
||||
// The Picker fires the onValueChange when the component is initialized
|
||||
// so we need to check that it has actually changed.
|
||||
if (note.parent_id == itemValue) return;
|
||||
|
||||
reg.logger().info('Moving note: ' + note.parent_id + ' => ' + itemValue);
|
||||
|
||||
if (note.id) await Note.moveToFolder(note.id, itemValue);
|
||||
note.parent_id = itemValue;
|
||||
|
||||
@@ -529,7 +511,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
note: note,
|
||||
folder: folder,
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
menuOptions={this.menuOptions()}
|
||||
showSaveButton={showSaveButton}
|
||||
|
||||
@@ -142,10 +142,34 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
|
||||
let title = parent ? parent.title : null;
|
||||
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
|
||||
const thisComp = this;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={title} menuOptions={this.menuOptions()} />
|
||||
<ScreenHeader
|
||||
title={title}
|
||||
menuOptions={this.menuOptions()}
|
||||
parentComponent={thisComp}
|
||||
folderPickerOptions={{
|
||||
enabled: this.props.noteSelectionEnabled,
|
||||
mustSelect: true,
|
||||
onValueChange: async (folderId, itemIndex) => {
|
||||
if (!folderId) return;
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const folder = await Folder.load(folderId);
|
||||
|
||||
const ok = await dialogs.confirm(this, _('Move %d note(s) to notebook "%s"?', noteIds.length, folder.title));
|
||||
if (!ok) return;
|
||||
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
await Note.moveToFolder(noteIds[i], folderId);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<NoteList style={{flex: 1}}/>
|
||||
<ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
@@ -160,6 +184,7 @@ const NotesScreen = connect(
|
||||
folders: state.folders,
|
||||
tags: state.tags,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedTagId: state.selectedTagId,
|
||||
notesParentType: state.notesParentType,
|
||||
notes: state.notes,
|
||||
@@ -167,6 +192,7 @@ const NotesScreen = connect(
|
||||
notesSource: state.notesSource,
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
};
|
||||
}
|
||||
)(NotesScreenComponent)
|
||||
|
||||
Reference in New Issue
Block a user