1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Allow selecting, deleting and moving multiple notes

This commit is contained in:
Laurent Cozic 2017-11-23 18:47:51 +00:00
parent bcd5cd9110
commit acc4eb5d28
9 changed files with 233 additions and 881 deletions

View File

@ -270,6 +270,7 @@ class MainScreenComponent extends React.Component {
return ( return (
<div style={style}> <div style={style}>
<PromptDialog <PromptDialog
autocomplete={promptOptions && ('autocomplete' in promptOptions) ? promptOptions.autocomplete : null}
value={promptOptions && promptOptions.value ? promptOptions.value : ''} value={promptOptions && promptOptions.value ? promptOptions.value : ''}
theme={this.props.theme} theme={this.props.theme}
style={promptStyle} style={promptStyle}

View File

@ -87,7 +87,10 @@ class Dropdown extends React.Component {
let headerLabel = '...'; let headerLabel = '...';
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[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 = () => { const closeList = () => {

View File

@ -1,6 +1,6 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux'); 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 { Log } = require('lib/log.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { Checkbox } = require('lib/components/checkbox.js'); const { Checkbox } = require('lib/components/checkbox.js');
@ -41,13 +41,16 @@ class NoteItemComponent extends Component {
paddingRight: theme.marginRight, paddingRight: theme.marginRight,
paddingTop: theme.itemMarginTop, paddingTop: theme.itemMarginTop,
paddingBottom: theme.itemMarginBottom, paddingBottom: theme.itemMarginBottom,
backgroundColor: theme.backgroundColor, //backgroundColor: theme.backgroundColor,
}, },
listItemText: { listItemText: {
flex: 1, flex: 1,
color: theme.color, color: theme.color,
fontSize: theme.fontSize, fontSize: theme.fontSize,
}, },
selectionWrapper: {
backgroundColor: theme.backgroundColor,
},
}; };
styles.listItemWithCheckbox = Object.assign({}, styles.listItem); styles.listItemWithCheckbox = Object.assign({}, styles.listItem);
@ -59,6 +62,9 @@ class NoteItemComponent extends Component {
styles.listItemTextWithCheckbox.marginTop = styles.listItem.paddingTop - 1; styles.listItemTextWithCheckbox.marginTop = styles.listItem.paddingTop - 1;
styles.listItemTextWithCheckbox.marginBottom = styles.listItem.paddingBottom; 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); this.styles_[this.props.theme] = StyleSheet.create(styles);
return this.styles_[this.props.theme]; return this.styles_[this.props.theme];
} }
@ -76,10 +82,26 @@ class NoteItemComponent extends Component {
onPress() { onPress() {
if (!this.props.note) return; 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({ this.props.dispatch({
type: 'NAV_GO', type: 'NOTE_SELECTION_START',
routeName: 'Note', id: this.props.note.id,
noteId: this.props.note.id,
}); });
} }
@ -105,18 +127,23 @@ class NoteItemComponent extends Component {
const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem; const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem;
const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText; const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText;
const rootStyle = isTodo && checkboxChecked ? {opacity: 0.4} : {}; 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 ( return (
<TouchableHighlight onPress={() => this.onPress()} underlayColor="#0066FF" style={rootStyle}> <TouchableOpacity onPress={() => this.onPress()} style={rootStyle} onLongPress={() => this.onLongPress()}>
<View style={ listItemStyle }> <View style={ selectionWrapperStyle }>
<Checkbox <View style={ listItemStyle }>
style={checkboxStyle} <Checkbox
checked={checkboxChecked} style={checkboxStyle}
onChange={(checked) => this.todoCheckbox_change(checked)} checked={checkboxChecked}
/> onChange={(checked) => this.todoCheckbox_change(checked)}
<Text style={listItemTextStyle}>{note.title}</Text> />
<Text style={listItemTextStyle}>{note.title}</Text>
</View>
</View> </View>
</TouchableHighlight> </TouchableOpacity>
); );
} }
@ -126,6 +153,8 @@ const NoteItem = connect(
(state) => { (state) => {
return { return {
theme: state.settings.theme, theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
}; };
} }
)(NoteItemComponent) )(NoteItemComponent)

View File

@ -113,6 +113,7 @@ const NoteList = connect(
items: state.notes, items: state.notes,
notesSource: state.notesSource, notesSource: state.notesSource,
theme: state.settings.theme, theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
}; };
} }
)(NoteListComponent) )(NoteListComponent)

View File

@ -8,6 +8,7 @@ const { ReportService } = require('lib/services/report.js');
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu'); const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { Setting } = require('lib/models/setting.js'); const { Setting } = require('lib/models/setting.js');
const { Note } = require('lib/models/note.js');
const { FileApi } = require('lib/file-api.js'); const { FileApi } = require('lib/file-api.js');
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js'); const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
const { reg } = require('lib/registry.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 { Dropdown } = require('lib/components/Dropdown.js');
const { time } = require('lib/time-utils'); const { time } = require('lib/time-utils');
const RNFS = require('react-native-fs'); 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 // Rather than applying a padding to the whole bar, it is applied to each
// individual component (button, picker, etc.) so that the touchable areas // individual component (button, picker, etc.) so that the touchable areas
@ -147,7 +150,6 @@ class ScreenHeaderComponent extends Component {
async backButton_press() { async backButton_press() {
await BackButtonService.back(); await BackButtonService.back();
//this.props.dispatch({ type: 'NAV_BACK' });
} }
searchButton_press() { 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) { menu_select(value) {
if (typeof(value) == 'function') { if (typeof(value) == 'function') {
value(); value();
@ -256,59 +270,93 @@ class ScreenHeaderComponent extends Component {
); );
} }
let key = 0; function deleteButton(styles, onPress) {
let menuOptionComponents = []; return (
for (let i = 0; i < this.props.menuOptions.length; i++) { <TouchableOpacity onPress={onPress}>
let o = this.props.menuOptions[i]; <View style={styles.iconButton}>
menuOptionComponents.push( <Icon name='md-trash' style={styles.topIcon} />
<MenuOption value={o.onPress} key={'menuOption_' + key++} style={this.styles().contextMenuItem}> </View>
<Text style={this.styles().contextMenuItemText}>{o.title}</Text> </TouchableOpacity>
</MenuOption>); );
} }
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) { if (menuOptionComponents.length) {
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>); menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
} }
menuOptionComponents.push( menuOptionComponents.push(
<MenuOption value={() => this.log_press()} key={'menuOption_log'} style={this.styles().contextMenuItem}> <MenuOption value={() => this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Log')}</Text> <Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
</MenuOption>); </MenuOption>);
} else {
menuOptionComponents.push( menuOptionComponents.push(
<MenuOption value={() => this.status_press()} key={'menuOption_status'} style={this.styles().contextMenuItem}> <MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Status')}</Text> <Text style={this.styles().contextMenuItemText}>{_('Delete')}</Text>
</MenuOption>); </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 createTitleComponent = () => {
const themeId = Setting.value('theme'); const themeId = Setting.value('theme');
const theme = themeStyle(themeId); 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 ( return (
<Dropdown <Dropdown
items={p.items} items={titlePickerItems(!!folderPickerOptions.mustSelect)}
itemHeight={35} itemHeight={35}
selectedValue={p.selectedValue} selectedValue={('selectedFolderId' in folderPickerOptions) ? folderPickerOptions.selectedFolderId : null}
itemListStyle={{ itemListStyle={{
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
}} }}
@ -320,7 +368,10 @@ class ScreenHeaderComponent extends Component {
color: theme.color, color: theme.color,
fontSize: theme.fontSize, 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 { } else {
@ -330,22 +381,32 @@ class ScreenHeaderComponent extends Component {
} }
const titleComp = createTitleComponent(); 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}> &#8942;</Text>
</MenuTrigger>
<MenuOptions>
{ menuOptionComponents }
</MenuOptions>
</Menu>
);
return ( return (
<View style={this.styles().container} > <View style={this.styles().container} >
{ sideMenuButton(this.styles(), () => this.sideMenuButton_press()) } { sideMenuComp }
{ backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack) } { backButtonComp }
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) } { saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
{ titleComp } { titleComp }
{ searchButton(this.styles(), () => this.searchButton_press()) } { searchButtonComp }
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}> { deleteButtonComp }
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}> { menuComp }
<Text style={this.styles().contextMenuTrigger}> &#8942;</Text> <DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</MenuTrigger>
<MenuOptions>
{ menuOptionComponents }
</MenuOptions>
</Menu>
</View> </View>
); );
} }
@ -361,8 +422,11 @@ const ScreenHeader = connect(
return { return {
historyCanGoBack: state.historyCanGoBack, historyCanGoBack: state.historyCanGoBack,
locale: state.settings.locale, locale: state.settings.locale,
folders: state.folders,
theme: state.settings.theme, theme: state.settings.theme,
showAdvancedOptions: state.settings.showAdvancedOptions, showAdvancedOptions: state.settings.showAdvancedOptions,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
}; };
} }
)(ScreenHeaderComponent) )(ScreenHeaderComponent)

View File

@ -448,15 +448,6 @@ class NoteScreenComponent extends BaseScreenComponent {
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} /> 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(); const actionButtonComp = renderActionButton();
let showSaveButton = this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_; let showSaveButton = this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
@ -506,19 +497,10 @@ class NoteScreenComponent extends BaseScreenComponent {
return ( return (
<View style={this.rootStyle(this.props.theme).root}> <View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader <ScreenHeader
titlePicker={{ folderPickerOptions={{
items: titlePickerItems(), enabled: true,
selectedValue: folder ? folder.id : null, selectedFolderId: folder ? folder.id : null,
onValueChange: async (itemValue, itemIndex) => { 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); if (note.id) await Note.moveToFolder(note.id, itemValue);
note.parent_id = itemValue; note.parent_id = itemValue;
@ -529,7 +511,7 @@ class NoteScreenComponent extends BaseScreenComponent {
note: note, note: note,
folder: folder, folder: folder,
}); });
} },
}} }}
menuOptions={this.menuOptions()} menuOptions={this.menuOptions()}
showSaveButton={showSaveButton} showSaveButton={showSaveButton}

View File

@ -142,10 +142,34 @@ class NotesScreenComponent extends BaseScreenComponent {
let title = parent ? parent.title : null; let title = parent ? parent.title : null;
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId(); const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
const thisComp = this;
return ( return (
<View style={rootStyle}> <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}}/> <NoteList style={{flex: 1}}/>
<ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton> <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/> <DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
@ -160,6 +184,7 @@ const NotesScreen = connect(
folders: state.folders, folders: state.folders,
tags: state.tags, tags: state.tags,
selectedFolderId: state.selectedFolderId, selectedFolderId: state.selectedFolderId,
selectedNoteIds: state.selectedNoteIds,
selectedTagId: state.selectedTagId, selectedTagId: state.selectedTagId,
notesParentType: state.notesParentType, notesParentType: state.notesParentType,
notes: state.notes, notes: state.notes,
@ -167,6 +192,7 @@ const NotesScreen = connect(
notesSource: state.notesSource, notesSource: state.notesSource,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop, uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
theme: state.settings.theme, theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
}; };
} }
)(NotesScreenComponent) )(NotesScreenComponent)

View File

@ -2087,795 +2087,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
}, },
"fsevents": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
"integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
"optional": true,
"requires": {
"nan": "2.8.0",
"node-pre-gyp": "0.6.39"
},
"dependencies": {
"abbrev": {
"version": "1.1.0",
"bundled": true,
"optional": true
},
"ajv": {
"version": "4.11.8",
"bundled": true,
"optional": true,
"requires": {
"co": "4.6.0",
"json-stable-stringify": "1.0.1"
}
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
},
"aproba": {
"version": "1.1.1",
"bundled": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.4",
"bundled": true,
"optional": true,
"requires": {
"delegates": "1.0.0",
"readable-stream": "2.2.9"
}
},
"asn1": {
"version": "0.2.3",
"bundled": true,
"optional": true
},
"assert-plus": {
"version": "0.2.0",
"bundled": true,
"optional": true
},
"asynckit": {
"version": "0.4.0",
"bundled": true,
"optional": true
},
"aws-sign2": {
"version": "0.6.0",
"bundled": true,
"optional": true
},
"aws4": {
"version": "1.6.0",
"bundled": true,
"optional": true
},
"balanced-match": {
"version": "0.4.2",
"bundled": true
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"bundled": true,
"optional": true,
"requires": {
"tweetnacl": "0.14.5"
}
},
"block-stream": {
"version": "0.0.9",
"bundled": true,
"requires": {
"inherits": "2.0.3"
}
},
"boom": {
"version": "2.10.1",
"bundled": true,
"requires": {
"hoek": "2.16.3"
}
},
"brace-expansion": {
"version": "1.1.7",
"bundled": true,
"requires": {
"balanced-match": "0.4.2",
"concat-map": "0.0.1"
}
},
"buffer-shims": {
"version": "1.0.0",
"bundled": true
},
"caseless": {
"version": "0.12.0",
"bundled": true,
"optional": true
},
"co": {
"version": "4.6.0",
"bundled": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
},
"combined-stream": {
"version": "1.0.5",
"bundled": true,
"requires": {
"delayed-stream": "1.0.0"
}
},
"concat-map": {
"version": "0.0.1",
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true
},
"cryptiles": {
"version": "2.0.5",
"bundled": true,
"requires": {
"boom": "2.10.1"
}
},
"dashdash": {
"version": "1.14.1",
"bundled": true,
"optional": true,
"requires": {
"assert-plus": "1.0.0"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"optional": true
}
}
},
"debug": {
"version": "2.6.8",
"bundled": true,
"optional": true,
"requires": {
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.4.2",
"bundled": true,
"optional": true
},
"delayed-stream": {
"version": "1.0.0",
"bundled": true
},
"delegates": {
"version": "1.0.0",
"bundled": true,
"optional": true
},
"detect-libc": {
"version": "1.0.2",
"bundled": true,
"optional": true
},
"ecc-jsbn": {
"version": "0.1.1",
"bundled": true,
"optional": true,
"requires": {
"jsbn": "0.1.1"
}
},
"extend": {
"version": "3.0.1",
"bundled": true,
"optional": true
},
"extsprintf": {
"version": "1.0.2",
"bundled": true
},
"forever-agent": {
"version": "0.6.1",
"bundled": true,
"optional": true
},
"form-data": {
"version": "2.1.4",
"bundled": true,
"optional": true,
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.5",
"mime-types": "2.1.15"
}
},
"fs.realpath": {
"version": "1.0.0",
"bundled": true
},
"fstream": {
"version": "1.0.11",
"bundled": true,
"requires": {
"graceful-fs": "4.1.11",
"inherits": "2.0.3",
"mkdirp": "0.5.1",
"rimraf": "2.6.1"
}
},
"fstream-ignore": {
"version": "1.0.5",
"bundled": true,
"optional": true,
"requires": {
"fstream": "1.0.11",
"inherits": "2.0.3",
"minimatch": "3.0.4"
}
},
"gauge": {
"version": "2.7.4",
"bundled": true,
"optional": true,
"requires": {
"aproba": "1.1.1",
"console-control-strings": "1.1.0",
"has-unicode": "2.0.1",
"object-assign": "4.1.1",
"signal-exit": "3.0.2",
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
"wide-align": "1.1.2"
}
},
"getpass": {
"version": "0.1.7",
"bundled": true,
"optional": true,
"requires": {
"assert-plus": "1.0.0"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"optional": true
}
}
},
"glob": {
"version": "7.1.2",
"bundled": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"graceful-fs": {
"version": "4.1.11",
"bundled": true
},
"har-schema": {
"version": "1.0.5",
"bundled": true,
"optional": true
},
"har-validator": {
"version": "4.2.1",
"bundled": true,
"optional": true,
"requires": {
"ajv": "4.11.8",
"har-schema": "1.0.5"
}
},
"has-unicode": {
"version": "2.0.1",
"bundled": true,
"optional": true
},
"hawk": {
"version": "3.1.3",
"bundled": true,
"requires": {
"boom": "2.10.1",
"cryptiles": "2.0.5",
"hoek": "2.16.3",
"sntp": "1.0.9"
}
},
"hoek": {
"version": "2.16.3",
"bundled": true
},
"http-signature": {
"version": "1.1.1",
"bundled": true,
"optional": true,
"requires": {
"assert-plus": "0.2.0",
"jsprim": "1.4.0",
"sshpk": "1.13.0"
}
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"bundled": true
},
"ini": {
"version": "1.3.4",
"bundled": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"requires": {
"number-is-nan": "1.0.1"
}
},
"is-typedarray": {
"version": "1.0.0",
"bundled": true,
"optional": true
},
"isarray": {
"version": "1.0.0",
"bundled": true
},
"isstream": {
"version": "0.1.2",
"bundled": true,
"optional": true
},
"jodid25519": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"jsbn": "0.1.1"
}
},
"jsbn": {
"version": "0.1.1",
"bundled": true,
"optional": true
},
"json-schema": {
"version": "0.2.3",
"bundled": true,
"optional": true
},
"json-stable-stringify": {
"version": "1.0.1",
"bundled": true,
"optional": true,
"requires": {
"jsonify": "0.0.0"
}
},
"json-stringify-safe": {
"version": "5.0.1",
"bundled": true,
"optional": true
},
"jsonify": {
"version": "0.0.0",
"bundled": true,
"optional": true
},
"jsprim": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.0.2",
"json-schema": "0.2.3",
"verror": "1.3.6"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"optional": true
}
}
},
"mime-db": {
"version": "1.27.0",
"bundled": true
},
"mime-types": {
"version": "2.1.15",
"bundled": true,
"requires": {
"mime-db": "1.27.0"
}
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"requires": {
"brace-expansion": "1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.0.0",
"bundled": true,
"optional": true
},
"node-pre-gyp": {
"version": "0.6.39",
"bundled": true,
"optional": true,
"requires": {
"detect-libc": "1.0.2",
"hawk": "3.1.3",
"mkdirp": "0.5.1",
"nopt": "4.0.1",
"npmlog": "4.1.0",
"rc": "1.2.1",
"request": "2.81.0",
"rimraf": "2.6.1",
"semver": "5.3.0",
"tar": "2.2.1",
"tar-pack": "3.4.0"
}
},
"nopt": {
"version": "4.0.1",
"bundled": true,
"optional": true,
"requires": {
"abbrev": "1.1.0",
"osenv": "0.1.4"
}
},
"npmlog": {
"version": "4.1.0",
"bundled": true,
"optional": true,
"requires": {
"are-we-there-yet": "1.1.4",
"console-control-strings": "1.1.0",
"gauge": "2.7.4",
"set-blocking": "2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
},
"oauth-sign": {
"version": "0.8.2",
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
"bundled": true,
"optional": true
},
"once": {
"version": "1.4.0",
"bundled": true,
"requires": {
"wrappy": "1.0.2"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"optional": true
},
"osenv": {
"version": "0.1.4",
"bundled": true,
"optional": true,
"requires": {
"os-homedir": "1.0.2",
"os-tmpdir": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true
},
"performance-now": {
"version": "0.2.0",
"bundled": true,
"optional": true
},
"process-nextick-args": {
"version": "1.0.7",
"bundled": true
},
"punycode": {
"version": "1.4.1",
"bundled": true,
"optional": true
},
"qs": {
"version": "6.4.0",
"bundled": true,
"optional": true
},
"rc": {
"version": "1.2.1",
"bundled": true,
"optional": true,
"requires": {
"deep-extend": "0.4.2",
"ini": "1.3.4",
"minimist": "1.2.0",
"strip-json-comments": "2.0.1"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"bundled": true,
"optional": true
}
}
},
"readable-stream": {
"version": "2.2.9",
"bundled": true,
"requires": {
"buffer-shims": "1.0.0",
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"string_decoder": "1.0.1",
"util-deprecate": "1.0.2"
}
},
"request": {
"version": "2.81.0",
"bundled": true,
"optional": true,
"requires": {
"aws-sign2": "0.6.0",
"aws4": "1.6.0",
"caseless": "0.12.0",
"combined-stream": "1.0.5",
"extend": "3.0.1",
"forever-agent": "0.6.1",
"form-data": "2.1.4",
"har-validator": "4.2.1",
"hawk": "3.1.3",
"http-signature": "1.1.1",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
"mime-types": "2.1.15",
"oauth-sign": "0.8.2",
"performance-now": "0.2.0",
"qs": "6.4.0",
"safe-buffer": "5.0.1",
"stringstream": "0.0.5",
"tough-cookie": "2.3.2",
"tunnel-agent": "0.6.0",
"uuid": "3.0.1"
}
},
"rimraf": {
"version": "2.6.1",
"bundled": true,
"requires": {
"glob": "7.1.2"
}
},
"safe-buffer": {
"version": "5.0.1",
"bundled": true
},
"semver": {
"version": "5.3.0",
"bundled": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"bundled": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"bundled": true,
"optional": true
},
"sntp": {
"version": "1.0.9",
"bundled": true,
"requires": {
"hoek": "2.16.3"
}
},
"sshpk": {
"version": "1.13.0",
"bundled": true,
"optional": true,
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"bcrypt-pbkdf": "1.0.1",
"dashdash": "1.14.1",
"ecc-jsbn": "0.1.1",
"getpass": "0.1.7",
"jodid25519": "1.0.2",
"jsbn": "0.1.1",
"tweetnacl": "0.14.5"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"optional": true
}
}
},
"string-width": {
"version": "1.0.2",
"bundled": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"stringstream": {
"version": "0.0.5",
"bundled": true,
"optional": true
},
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"requires": {
"ansi-regex": "2.1.1"
}
},
"strip-json-comments": {
"version": "2.0.1",
"bundled": true,
"optional": true
},
"tar": {
"version": "2.2.1",
"bundled": true,
"requires": {
"block-stream": "0.0.9",
"fstream": "1.0.11",
"inherits": "2.0.3"
}
},
"tar-pack": {
"version": "3.4.0",
"bundled": true,
"optional": true,
"requires": {
"debug": "2.6.8",
"fstream": "1.0.11",
"fstream-ignore": "1.0.5",
"once": "1.4.0",
"readable-stream": "2.2.9",
"rimraf": "2.6.1",
"tar": "2.2.1",
"uid-number": "0.0.6"
}
},
"tough-cookie": {
"version": "2.3.2",
"bundled": true,
"optional": true,
"requires": {
"punycode": "1.4.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"bundled": true,
"optional": true
},
"uid-number": {
"version": "0.0.6",
"bundled": true,
"optional": true
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true
},
"uuid": {
"version": "3.0.1",
"bundled": true,
"optional": true
},
"verror": {
"version": "1.3.6",
"bundled": true,
"optional": true,
"requires": {
"extsprintf": "1.0.2"
}
},
"wide-align": {
"version": "1.1.2",
"bundled": true,
"optional": true,
"requires": {
"string-width": "1.0.2"
}
},
"wrappy": {
"version": "1.0.2",
"bundled": true
}
}
},
"gauge": { "gauge": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz",
@ -4759,12 +3970,6 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
}, },
"nan": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
"integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
"optional": true
},
"natural-compare": { "natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -6022,7 +5227,6 @@
"anymatch": "1.3.2", "anymatch": "1.3.2",
"exec-sh": "0.2.1", "exec-sh": "0.2.1",
"fb-watchman": "2.0.0", "fb-watchman": "2.0.0",
"fsevents": "1.1.3",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"minimist": "1.2.0", "minimist": "1.2.0",
"walker": "1.0.7", "walker": "1.0.7",

View File

@ -81,6 +81,7 @@ const appDefaultState = Object.assign({}, defaultState, {
routeName: 'Welcome', routeName: 'Welcome',
params: {}, params: {},
}, },
noteSelectionEnabled: false,
}); });
const appReducer = (state = appDefaultState, action) => { const appReducer = (state = appDefaultState, action) => {
@ -187,6 +188,41 @@ const appReducer = (state = appDefaultState, action) => {
newState.sideMenuOpenPercent = action.value; newState.sideMenuOpenPercent = action.value;
break; break;
case 'NOTE_SELECTION_TOGGLE':
newState = Object.assign({}, state);
const noteId = action.id;
const newSelectedNoteIds = state.selectedNoteIds.slice();
const existingIndex = state.selectedNoteIds.indexOf(noteId);
if (existingIndex >= 0) {
newSelectedNoteIds.splice(existingIndex, 1);
} else {
newSelectedNoteIds.push(noteId);
}
newState.selectedNoteIds = newSelectedNoteIds;
newState.noteSelectionEnabled = !!newSelectedNoteIds.length;
break;
case 'NOTE_SELECTION_START':
if (!state.noteSelectionEnabled) {
newState = Object.assign({}, state);
newState.noteSelectionEnabled = true;
newState.selectedNoteIds = [action.id];
}
break;
case 'NOTE_SELECTION_END':
newState = Object.assign({}, state);
newState.noteSelectionEnabled = false;
newState.selectedNoteIds = [];
break;
} }
} catch (error) { } catch (error) {
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action); error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
@ -344,6 +380,11 @@ class AppComponent extends React.Component {
} }
async backButtonHandler() { async backButtonHandler() {
if (this.props.noteSelectionEnabled) {
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
return true;
}
if (this.props.showSideMenu) { if (this.props.showSideMenu) {
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
return true; return true;
@ -414,6 +455,7 @@ const mapStateToProps = (state) => {
showSideMenu: state.showSideMenu, showSideMenu: state.showSideMenu,
syncStarted: state.syncStarted, syncStarted: state.syncStarted,
appState: state.appState, appState: state.appState,
noteSelectionEnabled: state.noteSelectionEnabled,
}; };
}; };