You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-06-15 23:00:36 +02:00
Allow multiple selection
This commit is contained in:
@ -167,7 +167,7 @@ class AppGui {
|
|||||||
});
|
});
|
||||||
this.rootWidget_.connect(noteList, (state) => {
|
this.rootWidget_.connect(noteList, (state) => {
|
||||||
return {
|
return {
|
||||||
selectedNoteId: state.selectedNoteId,
|
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||||
items: state.notes,
|
items: state.notes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -181,7 +181,7 @@ class AppGui {
|
|||||||
};
|
};
|
||||||
this.rootWidget_.connect(noteText, (state) => {
|
this.rootWidget_.connect(noteText, (state) => {
|
||||||
return {
|
return {
|
||||||
noteId: state.selectedNoteId,
|
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||||
notes: state.notes,
|
notes: state.notes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -195,7 +195,7 @@ class AppGui {
|
|||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
};
|
};
|
||||||
this.rootWidget_.connect(noteMetadata, (state) => {
|
this.rootWidget_.connect(noteMetadata, (state) => {
|
||||||
return { noteId: state.selectedNoteId };
|
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
|
||||||
});
|
});
|
||||||
noteMetadata.hide();
|
noteMetadata.hide();
|
||||||
|
|
||||||
|
@ -52,28 +52,30 @@ class NoteListComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemContextMenu(event) {
|
itemContextMenu(event) {
|
||||||
const noteId = event.target.getAttribute('data-id');
|
const noteIds = this.props.selectedNoteIds;
|
||||||
if (!noteId) throw new Error('No data-id on element');
|
if (!noteIds.length) return;
|
||||||
|
|
||||||
const menu = new Menu()
|
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({
|
this.props.dispatch({
|
||||||
type: 'WINDOW_COMMAND',
|
type: 'WINDOW_COMMAND',
|
||||||
name: 'setTags',
|
name: 'setTags',
|
||||||
noteId: noteId,
|
noteId: noteIds[0],
|
||||||
});
|
});
|
||||||
}}));
|
}}));
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Switch between note and to-do'), click: async () => {
|
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
||||||
const note = await Note.load(noteId);
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
await Note.save(Note.toggleIsTodo(note));
|
const note = await Note.load(noteIds[i]);
|
||||||
|
await Note.save(Note.toggleIsTodo(note));
|
||||||
|
}
|
||||||
}}));
|
}}));
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
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;
|
if (!ok) return;
|
||||||
await Note.delete(noteId);
|
await Note.batchDelete(noteIds);
|
||||||
}}));
|
}}));
|
||||||
|
|
||||||
menu.popup(bridge().window());
|
menu.popup(bridge().window());
|
||||||
@ -81,10 +83,23 @@ class NoteListComponent extends React.Component {
|
|||||||
|
|
||||||
itemRenderer(item, theme, width) {
|
itemRenderer(item, theme, width) {
|
||||||
const onTitleClick = async (event, item) => {
|
const onTitleClick = async (event, item) => {
|
||||||
this.props.dispatch({
|
event.preventDefault();
|
||||||
type: 'NOTE_SELECT',
|
if (event.ctrlKey) {
|
||||||
id: item.id,
|
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) => {
|
const onCheckboxClick = async (event) => {
|
||||||
@ -99,7 +114,7 @@ class NoteListComponent extends React.Component {
|
|||||||
const hPadding = 10;
|
const hPadding = 10;
|
||||||
|
|
||||||
let style = Object.assign({ width: width }, this.style().listItem);
|
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
|
// 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.
|
// but don't know how it will look in other OSes.
|
||||||
@ -118,7 +133,6 @@ class NoteListComponent extends React.Component {
|
|||||||
return <div key={item.id + '_' + item.todo_completed} style={style}>
|
return <div key={item.id + '_' + item.todo_completed} style={style}>
|
||||||
{checkbox}
|
{checkbox}
|
||||||
<a
|
<a
|
||||||
data-id={item.id}
|
|
||||||
className="list-item"
|
className="list-item"
|
||||||
onContextMenu={(event) => this.itemContextMenu(event)}
|
onContextMenu={(event) => this.itemContextMenu(event)}
|
||||||
href="#"
|
href="#"
|
||||||
@ -164,7 +178,7 @@ class NoteListComponent extends React.Component {
|
|||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return {
|
return {
|
||||||
notes: state.notes,
|
notes: state.notes,
|
||||||
selectedNoteId: state.selectedNoteId,
|
selectedNoteIds: state.selectedNoteIds,
|
||||||
theme: state.settings.theme,
|
theme: state.settings.theme,
|
||||||
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||||
};
|
};
|
||||||
|
@ -504,7 +504,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return {
|
return {
|
||||||
noteId: state.selectedNoteId,
|
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||||
folderId: state.selectedFolderId,
|
folderId: state.selectedFolderId,
|
||||||
itemType: state.selectedItemType,
|
itemType: state.selectedItemType,
|
||||||
folders: state.folders,
|
folders: state.folders,
|
||||||
|
9
ReactNativeClient/lib/ArrayUtils.js
Normal file
9
ReactNativeClient/lib/ArrayUtils.js
Normal file
@ -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;
|
@ -152,9 +152,6 @@ class BaseApplication {
|
|||||||
if (parentType === 'Folder') {
|
if (parentType === 'Folder') {
|
||||||
parentId = state.selectedFolderId;
|
parentId = state.selectedFolderId;
|
||||||
parentType = BaseModel.TYPE_FOLDER;
|
parentType = BaseModel.TYPE_FOLDER;
|
||||||
} else if (parentType === 'Note') {
|
|
||||||
parentId = state.selectedNoteId;
|
|
||||||
parentType = BaseModel.TYPE_NOTE;
|
|
||||||
} else if (parentType === 'Tag') {
|
} else if (parentType === 'Tag') {
|
||||||
parentId = state.selectedTagId;
|
parentId = state.selectedTagId;
|
||||||
parentType = BaseModel.TYPE_TAG;
|
parentType = BaseModel.TYPE_TAG;
|
||||||
|
@ -550,7 +550,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
const NoteScreen = connect(
|
const NoteScreen = connect(
|
||||||
(state) => {
|
(state) => {
|
||||||
return {
|
return {
|
||||||
noteId: state.selectedNoteId,
|
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||||
folderId: state.selectedFolderId,
|
folderId: state.selectedFolderId,
|
||||||
itemType: state.selectedItemType,
|
itemType: state.selectedItemType,
|
||||||
folders: state.folders,
|
folders: state.folders,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const { Note } = require('lib/models/note.js');
|
const { Note } = require('lib/models/note.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const { Folder } = require('lib/models/folder.js');
|
||||||
|
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
notes: [],
|
notes: [],
|
||||||
@ -8,7 +9,7 @@ const defaultState = {
|
|||||||
folders: [],
|
folders: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
searches: [],
|
searches: [],
|
||||||
selectedNoteId: null,
|
selectedNoteIds: [],
|
||||||
selectedFolderId: null,
|
selectedFolderId: null,
|
||||||
selectedTagId: null,
|
selectedTagId: null,
|
||||||
selectedSearchId: null,
|
selectedSearchId: null,
|
||||||
@ -33,7 +34,7 @@ function handleItemDelete(state, action) {
|
|||||||
|
|
||||||
const map = {
|
const map = {
|
||||||
'FOLDER_DELETE': ['folders', 'selectedFolderId'],
|
'FOLDER_DELETE': ['folders', 'selectedFolderId'],
|
||||||
'NOTE_DELETE': ['notes', 'selectedNoteId'],
|
'NOTE_DELETE': ['notes', 'selectedNoteIds'],
|
||||||
'TAG_DELETE': ['tags', 'selectedTagId'],
|
'TAG_DELETE': ['tags', 'selectedTagId'],
|
||||||
'SEARCH_DELETE': ['searches', 'selectedSearchId'],
|
'SEARCH_DELETE': ['searches', 'selectedSearchId'],
|
||||||
};
|
};
|
||||||
@ -60,10 +61,10 @@ function handleItemDelete(state, action) {
|
|||||||
previousIndex = newItems.length - 1;
|
previousIndex = newItems.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIndex = previousIndex >= 0 ? newItems[previousIndex].id : null;
|
const newId = previousIndex >= 0 ? newItems[previousIndex].id : null;
|
||||||
newState[selectedItemKey] = newIndex;
|
newState[selectedItemKey] = action.type === 'NOTE_DELETE' ? [newId] : newId;
|
||||||
|
|
||||||
if (!newIndex && newState.notesParentType !== 'Folder') {
|
if (!newId && newState.notesParentType !== 'Folder') {
|
||||||
newState.notesParentType = 'Folder';
|
newState.notesParentType = 'Folder';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +112,51 @@ function defaultNotesParentType(state, exclusion) {
|
|||||||
return newNotesParentType;
|
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) => {
|
const reducer = (state = defaultState, action) => {
|
||||||
let newState = state;
|
let newState = state;
|
||||||
|
|
||||||
@ -118,9 +164,44 @@ const reducer = (state = defaultState, action) => {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
|
||||||
case 'NOTE_SELECT':
|
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 = 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;
|
break;
|
||||||
|
|
||||||
case 'FOLDER_SELECT':
|
case 'FOLDER_SELECT':
|
||||||
@ -208,7 +289,7 @@ const reducer = (state = defaultState, action) => {
|
|||||||
newState.notes = newNotes;
|
newState.notes = newNotes;
|
||||||
|
|
||||||
if (noteFolderHasChanged) {
|
if (noteFolderHasChanged) {
|
||||||
newState.selectedNoteId = newNotes.length ? newNotes[0].id : null;
|
newState.selectedNoteIds = newNotes.length ? [newNotes[0].id] : null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ const appReducer = (state = appDefaultState, action) => {
|
|||||||
newState = Object.assign({}, state);
|
newState = Object.assign({}, state);
|
||||||
|
|
||||||
if ('noteId' in action) {
|
if ('noteId' in action) {
|
||||||
newState.selectedNoteId = action.noteId;
|
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('folderId' in action) {
|
if ('folderId' in action) {
|
||||||
|
Reference in New Issue
Block a user