diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js
index 98424b6e46..c839f297e5 100644
--- a/CliClient/app/app-gui.js
+++ b/CliClient/app/app-gui.js
@@ -167,7 +167,7 @@ class AppGui {
});
this.rootWidget_.connect(noteList, (state) => {
return {
- selectedNoteId: state.selectedNoteId,
+ selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
items: state.notes,
};
});
@@ -181,7 +181,7 @@ class AppGui {
};
this.rootWidget_.connect(noteText, (state) => {
return {
- noteId: state.selectedNoteId,
+ noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
notes: state.notes,
};
});
@@ -195,7 +195,7 @@ class AppGui {
borderRightWidth: 1,
};
this.rootWidget_.connect(noteMetadata, (state) => {
- return { noteId: state.selectedNoteId };
+ return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
diff --git a/ElectronClient/app/gui/NoteList.jsx b/ElectronClient/app/gui/NoteList.jsx
index 5266b1f40d..fc2b8faf88 100644
--- a/ElectronClient/app/gui/NoteList.jsx
+++ b/ElectronClient/app/gui/NoteList.jsx
@@ -52,28 +52,30 @@ class NoteListComponent extends React.Component {
}
itemContextMenu(event) {
- const noteId = event.target.getAttribute('data-id');
- if (!noteId) throw new Error('No data-id on element');
+ const noteIds = this.props.selectedNoteIds;
+ if (!noteIds.length) return;
const menu = new Menu()
- menu.append(new MenuItem({label: _('Add or remove tags'), click: async () => {
+ menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'setTags',
- noteId: noteId,
+ noteId: noteIds[0],
});
}}));
- menu.append(new MenuItem({label: _('Switch between note and to-do'), click: async () => {
- const note = await Note.load(noteId);
- await Note.save(Note.toggleIsTodo(note));
+ menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
+ for (let i = 0; i < noteIds.length; i++) {
+ const note = await Note.load(noteIds[i]);
+ await Note.save(Note.toggleIsTodo(note));
+ }
}}));
menu.append(new MenuItem({label: _('Delete'), click: async () => {
- const ok = bridge().showConfirmMessageBox(_('Delete note?'));
+ const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
if (!ok) return;
- await Note.delete(noteId);
+ await Note.batchDelete(noteIds);
}}));
menu.popup(bridge().window());
@@ -81,10 +83,23 @@ class NoteListComponent extends React.Component {
itemRenderer(item, theme, width) {
const onTitleClick = async (event, item) => {
- this.props.dispatch({
- type: 'NOTE_SELECT',
- id: item.id,
- });
+ event.preventDefault();
+ if (event.ctrlKey) {
+ this.props.dispatch({
+ type: 'NOTE_SELECT_TOGGLE',
+ id: item.id,
+ });
+ } else if (event.shiftKey) {
+ this.props.dispatch({
+ type: 'NOTE_SELECT_EXTEND',
+ id: item.id,
+ });
+ } else {
+ this.props.dispatch({
+ type: 'NOTE_SELECT',
+ id: item.id,
+ });
+ }
}
const onCheckboxClick = async (event) => {
@@ -99,7 +114,7 @@ class NoteListComponent extends React.Component {
const hPadding = 10;
let style = Object.assign({ width: width }, this.style().listItem);
- if (this.props.selectedNoteId === item.id) style = Object.assign(style, this.style().listItemSelected);
+ if (this.props.selectedNoteIds.indexOf(item.id) >= 0) style = Object.assign(style, this.style().listItemSelected);
// Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows
// but don't know how it will look in other OSes.
@@ -118,7 +133,6 @@ class NoteListComponent extends React.Component {
return
{checkbox}
this.itemContextMenu(event)}
href="#"
@@ -164,7 +178,7 @@ class NoteListComponent extends React.Component {
const mapStateToProps = (state) => {
return {
notes: state.notes,
- selectedNoteId: state.selectedNoteId,
+ selectedNoteIds: state.selectedNoteIds,
theme: state.settings.theme,
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
};
diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx
index f5bbf40610..15ac652cdd 100644
--- a/ElectronClient/app/gui/NoteText.jsx
+++ b/ElectronClient/app/gui/NoteText.jsx
@@ -504,7 +504,7 @@ class NoteTextComponent extends React.Component {
const mapStateToProps = (state) => {
return {
- noteId: state.selectedNoteId,
+ noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,
diff --git a/ReactNativeClient/lib/ArrayUtils.js b/ReactNativeClient/lib/ArrayUtils.js
new file mode 100644
index 0000000000..74e0797f81
--- /dev/null
+++ b/ReactNativeClient/lib/ArrayUtils.js
@@ -0,0 +1,9 @@
+const ArrayUtils = {};
+
+ArrayUtils.unique = function(array) {
+ return array.filter(function(elem, index, self) {
+ return index === self.indexOf(elem);
+ });
+}
+
+module.exports = ArrayUtils;
\ No newline at end of file
diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js
index 40cb26c67d..8fba64f1c1 100644
--- a/ReactNativeClient/lib/BaseApplication.js
+++ b/ReactNativeClient/lib/BaseApplication.js
@@ -152,9 +152,6 @@ class BaseApplication {
if (parentType === 'Folder') {
parentId = state.selectedFolderId;
parentType = BaseModel.TYPE_FOLDER;
- } else if (parentType === 'Note') {
- parentId = state.selectedNoteId;
- parentType = BaseModel.TYPE_NOTE;
} else if (parentType === 'Tag') {
parentId = state.selectedTagId;
parentType = BaseModel.TYPE_TAG;
diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js
index d1e38d70f9..31c338b627 100644
--- a/ReactNativeClient/lib/components/screens/note.js
+++ b/ReactNativeClient/lib/components/screens/note.js
@@ -550,7 +550,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const NoteScreen = connect(
(state) => {
return {
- noteId: state.selectedNoteId,
+ noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,
diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js
index cd3589ab14..02d0b9e92b 100644
--- a/ReactNativeClient/lib/reducer.js
+++ b/ReactNativeClient/lib/reducer.js
@@ -1,5 +1,6 @@
const { Note } = require('lib/models/note.js');
const { Folder } = require('lib/models/folder.js');
+const ArrayUtils = require('lib/ArrayUtils.js');
const defaultState = {
notes: [],
@@ -8,7 +9,7 @@ const defaultState = {
folders: [],
tags: [],
searches: [],
- selectedNoteId: null,
+ selectedNoteIds: [],
selectedFolderId: null,
selectedTagId: null,
selectedSearchId: null,
@@ -33,7 +34,7 @@ function handleItemDelete(state, action) {
const map = {
'FOLDER_DELETE': ['folders', 'selectedFolderId'],
- 'NOTE_DELETE': ['notes', 'selectedNoteId'],
+ 'NOTE_DELETE': ['notes', 'selectedNoteIds'],
'TAG_DELETE': ['tags', 'selectedTagId'],
'SEARCH_DELETE': ['searches', 'selectedSearchId'],
};
@@ -60,10 +61,10 @@ function handleItemDelete(state, action) {
previousIndex = newItems.length - 1;
}
- const newIndex = previousIndex >= 0 ? newItems[previousIndex].id : null;
- newState[selectedItemKey] = newIndex;
+ const newId = previousIndex >= 0 ? newItems[previousIndex].id : null;
+ newState[selectedItemKey] = action.type === 'NOTE_DELETE' ? [newId] : newId;
- if (!newIndex && newState.notesParentType !== 'Folder') {
+ if (!newId && newState.notesParentType !== 'Folder') {
newState.notesParentType = 'Folder';
}
@@ -111,6 +112,51 @@ function defaultNotesParentType(state, exclusion) {
return newNotesParentType;
}
+function changeSelectedNotes(state, action) {
+ const noteIds = 'id' in action ? (action.id ? [action.id] : []) : action.ids;
+ let newState = Object.assign({}, state);
+
+ if (action.type === 'NOTE_SELECT') {
+ newState.selectedNoteIds = noteIds;
+ return newState;
+ }
+
+ if (action.type === 'NOTE_SELECT_ADD') {
+ if (!noteIds.length) return state;
+ newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
+ return newState;
+ }
+
+ if (action.type === 'NOTE_SELECT_REMOVE') {
+ if (!noteIds.length) return state; // Nothing to unselect
+ if (state.selectedNoteIds.length <= 1) return state; // Cannot unselect the last note
+
+ let newSelectedNoteIds = [];
+ for (let i = 0; i < newState.selectedNoteIds.length; i++) {
+ const id = newState.selectedNoteIds[i];
+ if (noteIds.indexOf(id) >= 0) continue;
+ newSelectedNoteIds.push(id);
+ }
+ newState.selectedNoteIds = newSelectedNoteIds;
+
+ return newState;
+ }
+
+ if (action.type === 'NOTE_SELECT_TOGGLE') {
+ if (!noteIds.length) return state;
+
+ if (newState.selectedNoteIds.indexOf(noteIds[0]) >= 0) {
+ newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_REMOVE', id: noteIds[0] });
+ } else {
+ newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_ADD', id: noteIds[0] });
+ }
+
+ return newState;
+ }
+
+ throw new Error('Unreachable');
+}
+
const reducer = (state = defaultState, action) => {
let newState = state;
@@ -118,9 +164,44 @@ const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'NOTE_SELECT':
+ case 'NOTE_SELECT_ADD':
+ case 'NOTE_SELECT_REMOVE':
+ case 'NOTE_SELECT_TOGGLE':
+
+ newState = changeSelectedNotes(state, action);
+ break;
+
+ case 'NOTE_SELECT_EXTEND':
newState = Object.assign({}, state);
- newState.selectedNoteId = action.id;
+
+ if (!newState.selectedNoteIds.length) {
+ newState.selectedNoteIds = [action.id];
+ } else {
+ const selectRangeId1 = state.selectedNoteIds[state.selectedNoteIds.length - 1];
+ const selectRangeId2 = action.id;
+ if (selectRangeId1 === selectRangeId2) return state;
+
+ let newSelectedNoteIds = state.selectedNoteIds.slice();
+ let selectionStarted = false;
+ for (let i = 0; i < state.notes.length; i++) {
+ const id = state.notes[i].id;
+
+ if (!selectionStarted && (id === selectRangeId1 || id === selectRangeId2)) {
+ selectionStarted = true;
+ if (newSelectedNoteIds.indexOf(id) < 0) newSelectedNoteIds.push(id);
+ continue;
+ } else if (selectionStarted && (id === selectRangeId1 || id === selectRangeId2)) {
+ if (newSelectedNoteIds.indexOf(id) < 0) newSelectedNoteIds.push(id);
+ break;
+ }
+
+ if (selectionStarted && newSelectedNoteIds.indexOf(id) < 0) {
+ newSelectedNoteIds.push(id);
+ }
+ }
+ newState.selectedNoteIds = newSelectedNoteIds;
+ }
break;
case 'FOLDER_SELECT':
@@ -208,7 +289,7 @@ const reducer = (state = defaultState, action) => {
newState.notes = newNotes;
if (noteFolderHasChanged) {
- newState.selectedNoteId = newNotes.length ? newNotes[0].id : null;
+ newState.selectedNoteIds = newNotes.length ? [newNotes[0].id] : null;
}
break;
diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js
index 3fc3e8cec6..a4a142e9e6 100644
--- a/ReactNativeClient/root.js
+++ b/ReactNativeClient/root.js
@@ -142,7 +142,7 @@ const appReducer = (state = appDefaultState, action) => {
newState = Object.assign({}, state);
if ('noteId' in action) {
- newState.selectedNoteId = action.noteId;
+ newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
}
if ('folderId' in action) {