1
0
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:
Laurent Cozic
2017-11-22 18:35:31 +00:00
parent 3e1ea0eb0a
commit e4d48f43d6
8 changed files with 133 additions and 32 deletions

View File

@ -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();

View File

@ -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++) {
const note = await Note.load(noteIds[i]);
await Note.save(Note.toggleIsTodo(note)); 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,11 +83,24 @@ class NoteListComponent extends React.Component {
itemRenderer(item, theme, width) { itemRenderer(item, theme, width) {
const onTitleClick = async (event, item) => { const onTitleClick = async (event, item) => {
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({ this.props.dispatch({
type: 'NOTE_SELECT', type: 'NOTE_SELECT',
id: item.id, id: item.id,
}); });
} }
}
const onCheckboxClick = async (event) => { const onCheckboxClick = async (event) => {
const checked = event.target.checked; const checked = event.target.checked;
@ -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,
}; };

View File

@ -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,

View 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;

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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) {