You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Allow multiple selection
This commit is contained in:
		| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -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 <div key={item.id + '_' + item.todo_completed} style={style}> | ||||
| 			{checkbox} | ||||
| 			<a | ||||
| 				data-id={item.id} | ||||
| 				className="list-item" | ||||
| 				onContextMenu={(event) => 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, | ||||
| 	}; | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										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') { | ||||
| 			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; | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user