2020-07-03 23:32:39 +02:00
const { ItemList } = require ( '../ItemList.min.js' ) ;
2017-11-05 02:17:48 +02:00
const React = require ( 'react' ) ;
const { connect } = require ( 'react-redux' ) ;
2017-11-10 22:11:48 +02:00
const { time } = require ( 'lib/time-utils.js' ) ;
2020-06-10 23:08:59 +02:00
const { themeStyle } = require ( 'lib/theme' ) ;
2017-12-14 22:21:36 +02:00
const BaseModel = require ( 'lib/BaseModel' ) ;
2017-11-08 19:51:55 +02:00
const { _ } = require ( 'lib/locale.js' ) ;
const { bridge } = require ( 'electron' ) . remote . require ( './bridge' ) ;
2020-07-03 23:32:39 +02:00
const eventManager = require ( 'lib/eventManager' ) ;
2020-08-08 01:13:21 +02:00
const SearchEngine = require ( 'lib/services/searchengine/SearchEngine' ) ;
2019-07-29 14:13:23 +02:00
const Note = require ( 'lib/models/Note' ) ;
2020-05-27 18:21:46 +02:00
const Setting = require ( 'lib/models/Setting' ) ;
2020-07-03 23:32:39 +02:00
const NoteListUtils = require ( '../utils/NoteListUtils' ) ;
const NoteListItem = require ( '../NoteListItem' ) . default ;
const CommandService = require ( 'lib/services/CommandService.js' ) . default ;
const commands = [
require ( './commands/focusElementNoteList' ) ,
] ;
2017-11-04 18:40:34 +02:00
class NoteListComponent extends React . Component {
2018-11-21 21:50:50 +02:00
constructor ( ) {
super ( ) ;
2020-07-03 23:32:39 +02:00
CommandService . instance ( ) . componentRegisterCommands ( this , commands ) ;
2020-05-27 18:21:46 +02:00
this . itemHeight = 34 ;
this . state = {
dragOverTargetNoteIndex : null ,
} ;
2019-01-25 21:59:36 +02:00
this . itemListRef = React . createRef ( ) ;
this . itemAnchorRefs _ = { } ;
2018-11-21 21:50:50 +02:00
this . itemRenderer = this . itemRenderer . bind ( this ) ;
2019-01-25 21:59:36 +02:00
this . onKeyDown = this . onKeyDown . bind ( this ) ;
2020-05-27 18:21:46 +02:00
this . noteItem _titleClick = this . noteItem _titleClick . bind ( this ) ;
this . noteItem _noteDragOver = this . noteItem _noteDragOver . bind ( this ) ;
this . noteItem _noteDrop = this . noteItem _noteDrop . bind ( this ) ;
this . noteItem _checkboxClick = this . noteItem _checkboxClick . bind ( this ) ;
this . noteItem _dragStart = this . noteItem _dragStart . bind ( this ) ;
this . onGlobalDrop _ = this . onGlobalDrop _ . bind ( this ) ;
this . registerGlobalDragEndEvent _ = this . registerGlobalDragEndEvent _ . bind ( this ) ;
this . unregisterGlobalDragEndEvent _ = this . unregisterGlobalDragEndEvent _ . bind ( this ) ;
2020-06-06 16:47:02 +02:00
this . itemContextMenu = this . itemContextMenu . bind ( this ) ;
2018-11-21 21:50:50 +02:00
}
2017-11-09 21:21:10 +02:00
style ( ) {
2020-05-27 18:21:46 +02:00
if ( this . styleCache _ && this . styleCache _ [ this . props . theme ] ) return this . styleCache _ [ this . props . theme ] ;
2017-11-09 21:21:10 +02:00
2020-05-27 18:21:46 +02:00
const theme = themeStyle ( this . props . theme ) ;
2019-11-06 23:46:26 +02:00
2020-03-14 01:46:14 +02:00
const style = {
2017-11-09 21:21:10 +02:00
root : {
backgroundColor : theme . backgroundColor ,
} ,
listItem : {
2020-05-27 18:21:46 +02:00
maxWidth : '100%' ,
height : this . itemHeight ,
2017-11-09 21:21:10 +02:00
boxSizing : 'border-box' ,
display : 'flex' ,
2017-11-10 22:11:48 +02:00
alignItems : 'stretch' ,
2017-11-09 21:21:10 +02:00
backgroundColor : theme . backgroundColor ,
2019-09-19 23:51:18 +02:00
borderBottom : ` 1px solid ${ theme . dividerColor } ` ,
2017-11-09 21:21:10 +02:00
} ,
listItemSelected : {
backgroundColor : theme . selectedColor ,
} ,
2017-11-10 22:11:48 +02:00
listItemTitle : {
fontFamily : theme . fontFamily ,
fontSize : theme . fontSize ,
textDecoration : 'none' ,
color : theme . color ,
cursor : 'default' ,
whiteSpace : 'nowrap' ,
flex : 1 ,
display : 'flex' ,
alignItems : 'center' ,
overflow : 'hidden' ,
} ,
2017-11-10 22:12:38 +02:00
listItemTitleCompleted : {
opacity : 0.5 ,
textDecoration : 'line-through' ,
} ,
2017-11-09 21:21:10 +02:00
} ;
2020-05-27 18:21:46 +02:00
this . styleCache _ = { } ;
this . styleCache _ [ this . props . theme ] = style ;
2017-11-09 21:21:10 +02:00
return style ;
}
2017-11-08 19:51:55 +02:00
itemContextMenu ( event ) {
2018-01-09 22:16:09 +02:00
const currentItemId = event . currentTarget . getAttribute ( 'data-id' ) ;
if ( ! currentItemId ) return ;
let noteIds = [ ] ;
if ( this . props . selectedNoteIds . indexOf ( currentItemId ) < 0 ) {
noteIds = [ currentItemId ] ;
} else {
noteIds = this . props . selectedNoteIds ;
}
2017-11-22 20:35:31 +02:00
if ( ! noteIds . length ) return ;
2017-11-08 19:51:55 +02:00
2019-01-29 20:02:34 +02:00
const menu = NoteListUtils . makeContextMenu ( noteIds , {
notes : this . props . notes ,
dispatch : this . props . dispatch ,
2020-01-06 23:16:39 +02:00
watchedNoteFiles : this . props . watchedNoteFiles ,
2019-01-29 20:02:34 +02:00
} ) ;
2017-11-10 22:34:36 +02:00
2017-11-08 19:51:55 +02:00
menu . popup ( bridge ( ) . window ( ) ) ;
}
2020-05-27 18:21:46 +02:00
onGlobalDrop _ ( ) {
this . unregisterGlobalDragEndEvent _ ( ) ;
this . setState ( { dragOverTargetNoteIndex : null } ) ;
}
2018-11-21 21:50:50 +02:00
2020-05-27 18:21:46 +02:00
registerGlobalDragEndEvent _ ( ) {
if ( this . globalDragEndEventRegistered _ ) return ;
this . globalDragEndEventRegistered _ = true ;
document . addEventListener ( 'dragend' , this . onGlobalDrop _ ) ;
}
2017-11-05 01:27:13 +02:00
2020-05-27 18:21:46 +02:00
unregisterGlobalDragEndEvent _ ( ) {
this . globalDragEndEventRegistered _ = false ;
document . removeEventListener ( 'dragend' , this . onGlobalDrop _ ) ;
}
2018-06-10 02:27:20 +02:00
2020-05-27 18:21:46 +02:00
dragTargetNoteIndex _ ( event ) {
return Math . abs ( Math . round ( ( event . clientY - this . itemListRef . current . offsetTop ( ) ) / this . itemHeight ) ) ;
}
2018-06-10 02:27:20 +02:00
2020-05-27 18:21:46 +02:00
noteItem _noteDragOver ( event ) {
if ( this . props . notesParentType !== 'Folder' ) return ;
2019-07-29 14:13:23 +02:00
2020-05-27 18:21:46 +02:00
const dt = event . dataTransfer ;
2017-11-22 21:20:19 +02:00
2020-05-27 18:21:46 +02:00
if ( dt . types . indexOf ( 'text/x-jop-note-ids' ) >= 0 ) {
event . preventDefault ( ) ;
const newIndex = this . dragTargetNoteIndex _ ( event ) ;
if ( this . state . dragOverTargetNoteIndex === newIndex ) return ;
this . registerGlobalDragEndEvent _ ( ) ;
this . setState ( { dragOverTargetNoteIndex : newIndex } ) ;
}
}
2017-11-10 22:11:48 +02:00
2020-05-27 18:21:46 +02:00
async noteItem _noteDrop ( event ) {
if ( this . props . notesParentType !== 'Folder' ) return ;
2017-11-10 22:11:48 +02:00
2020-05-27 18:21:46 +02:00
if ( this . props . noteSortOrder !== 'order' ) {
const doIt = await bridge ( ) . showConfirmMessageBox ( _ ( 'To manually sort the notes, the sort order must be changed to "%s" in the menu "%s" > "%s"' , _ ( 'Custom order' ) , _ ( 'View' ) , _ ( 'Sort notes by' ) ) , {
buttons : [ _ ( 'Do it now' ) , _ ( 'Cancel' ) ] ,
} ) ;
if ( ! doIt ) return ;
Setting . setValue ( 'notes.sortOrder.field' , 'order' ) ;
return ;
2018-03-20 01:04:48 +02:00
}
2020-05-27 18:21:46 +02:00
// TODO: check that parent type is folder
2018-01-12 21:58:01 +02:00
2020-05-27 18:21:46 +02:00
const dt = event . dataTransfer ;
this . unregisterGlobalDragEndEvent _ ( ) ;
this . setState ( { dragOverTargetNoteIndex : null } ) ;
2017-11-08 19:51:55 +02:00
2020-05-27 18:21:46 +02:00
const targetNoteIndex = this . dragTargetNoteIndex _ ( event ) ;
const noteIds = JSON . parse ( dt . getData ( 'text/x-jop-note-ids' ) ) ;
2018-12-14 00:57:14 +02:00
2020-05-27 18:21:46 +02:00
Note . insertNotesAt ( this . props . selectedFolderId , noteIds , targetNoteIndex ) ;
}
2018-12-14 00:57:14 +02:00
2020-05-27 18:21:46 +02:00
async noteItem _checkboxClick ( event , item ) {
const checked = event . target . checked ;
const newNote = {
id : item . id ,
todo _completed : checked ? time . unixMs ( ) : 0 ,
} ;
await Note . save ( newNote , { userSideValidation : true } ) ;
eventManager . emit ( 'todoToggle' , { noteId : item . id , note : newNote } ) ;
}
async noteItem _titleClick ( event , item ) {
if ( event . ctrlKey || event . metaKey ) {
event . preventDefault ( ) ;
this . props . dispatch ( {
type : 'NOTE_SELECT_TOGGLE' ,
id : item . id ,
} ) ;
} else if ( event . shiftKey ) {
event . preventDefault ( ) ;
this . props . dispatch ( {
type : 'NOTE_SELECT_EXTEND' ,
id : item . id ,
} ) ;
} else {
this . props . dispatch ( {
type : 'NOTE_SELECT' ,
id : item . id ,
} ) ;
}
}
noteItem _dragStart ( event ) {
let noteIds = [ ] ;
2018-03-20 01:04:48 +02:00
2020-05-27 18:21:46 +02:00
// Here there is two cases:
// - If multiple notes are selected, we drag the group
// - If only one note is selected, we drag the note that was clicked on (which might be different from the currently selected note)
if ( this . props . selectedNoteIds . length >= 2 ) {
noteIds = this . props . selectedNoteIds ;
2018-03-20 01:04:48 +02:00
} else {
2020-05-27 18:21:46 +02:00
const clickedNoteId = event . currentTarget . getAttribute ( 'data-id' ) ;
if ( clickedNoteId ) noteIds . push ( clickedNoteId ) ;
2018-03-20 01:04:48 +02:00
}
2020-05-27 18:21:46 +02:00
if ( ! noteIds . length ) return ;
event . dataTransfer . setDragImage ( new Image ( ) , 1 , 1 ) ;
event . dataTransfer . clearData ( ) ;
event . dataTransfer . setData ( 'text/x-jop-note-ids' , JSON . stringify ( noteIds ) ) ;
}
itemRenderer ( item , index ) {
const highlightedWords = ( ) => {
if ( this . props . notesParentType === 'Search' ) {
const query = BaseModel . byId ( this . props . searches , this . props . selectedSearchId ) ;
if ( query ) {
const parsedQuery = SearchEngine . instance ( ) . parseQuery ( query . query _pattern ) ;
return SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
}
}
return [ ] ;
2018-11-21 21:50:50 +02:00
} ;
2019-01-25 21:59:36 +02:00
if ( ! this . itemAnchorRefs _ [ item . id ] ) this . itemAnchorRefs _ [ item . id ] = React . createRef ( ) ;
const ref = this . itemAnchorRefs _ [ item . id ] ;
2020-05-27 18:21:46 +02:00
return < NoteListItem
ref = { ref }
key = { item . id }
style = { this . style ( this . props . theme ) }
item = { item }
index = { index }
theme = { this . props . theme }
width = { this . props . style . width }
dragItemIndex = { this . state . dragOverTargetNoteIndex }
highlightedWords = { highlightedWords ( ) }
isProvisional = { this . props . provisionalNoteIds . includes ( item . id ) }
isSelected = { this . props . selectedNoteIds . indexOf ( item . id ) >= 0 }
isWatched = { this . props . watchedNoteFiles . indexOf ( item . id ) < 0 }
itemCount = { this . props . notes . length }
onCheckboxClick = { this . noteItem _checkboxClick }
onDragStart = { this . noteItem _dragStart }
onNoteDragOver = { this . noteItem _noteDragOver }
onNoteDrop = { this . noteItem _noteDrop }
onTitleClick = { this . noteItem _titleClick }
2020-06-06 16:47:02 +02:00
onContextMenu = { this . itemContextMenu }
2020-05-27 18:21:46 +02:00
/ > ;
2017-11-04 18:40:34 +02:00
}
2019-01-25 21:59:36 +02:00
itemAnchorRef ( itemId ) {
if ( this . itemAnchorRefs _ [ itemId ] && this . itemAnchorRefs _ [ itemId ] . current ) return this . itemAnchorRefs _ [ itemId ] . current ;
return null ;
}
2019-09-13 00:16:42 +02:00
componentDidUpdate ( prevProps ) {
2019-01-29 20:32:52 +02:00
if ( prevProps . selectedNoteIds !== this . props . selectedNoteIds && this . props . selectedNoteIds . length === 1 ) {
const id = this . props . selectedNoteIds [ 0 ] ;
2020-03-11 19:08:35 +02:00
const doRefocus = this . props . notes . length < prevProps . notes . length ;
2019-01-29 20:32:52 +02:00
for ( let i = 0 ; i < this . props . notes . length ; i ++ ) {
if ( this . props . notes [ i ] . id === id ) {
this . itemListRef . current . makeItemIndexVisible ( i ) ;
2020-03-11 19:08:35 +02:00
if ( doRefocus ) {
const ref = this . itemAnchorRef ( id ) ;
if ( ref ) ref . focus ( ) ;
}
2019-01-29 20:32:52 +02:00
break ;
2019-07-29 14:13:23 +02:00
}
2019-01-29 20:32:52 +02:00
}
}
2019-01-26 20:04:32 +02:00
}
2020-02-06 11:38:33 +02:00
scrollNoteIndex _ ( keyCode , ctrlKey , metaKey , noteIndex ) {
if ( keyCode === 33 ) {
// Page Up
noteIndex -= ( this . itemListRef . current . visibleItemCount ( ) - 1 ) ;
} else if ( keyCode === 34 ) {
// Page Down
noteIndex += ( this . itemListRef . current . visibleItemCount ( ) - 1 ) ;
} else if ( ( keyCode === 35 && ctrlKey ) || ( keyCode === 40 && metaKey ) ) {
// CTRL+End, CMD+Down
noteIndex = this . props . notes . length - 1 ;
} else if ( ( keyCode === 36 && ctrlKey ) || ( keyCode === 38 && metaKey ) ) {
// CTRL+Home, CMD+Up
noteIndex = 0 ;
} else if ( keyCode === 38 && ! metaKey ) {
// Up
noteIndex -= 1 ;
} else if ( keyCode === 40 && ! metaKey ) {
// Down
noteIndex += 1 ;
}
if ( noteIndex < 0 ) noteIndex = 0 ;
if ( noteIndex > this . props . notes . length - 1 ) noteIndex = this . props . notes . length - 1 ;
return noteIndex ;
}
2019-01-26 17:15:16 +02:00
async onKeyDown ( event ) {
2019-01-25 21:59:36 +02:00
const keyCode = event . keyCode ;
const noteIds = this . props . selectedNoteIds ;
2019-01-26 17:15:16 +02:00
2020-03-11 19:08:35 +02:00
if ( noteIds . length > 0 && ( keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode == 36 ) ) {
2020-02-06 11:38:33 +02:00
// DOWN / UP / PAGEDOWN / PAGEUP / END / HOME
2019-01-25 21:59:36 +02:00
const noteId = noteIds [ 0 ] ;
let noteIndex = BaseModel . modelIndexById ( this . props . notes , noteId ) ;
2020-02-06 11:38:33 +02:00
noteIndex = this . scrollNoteIndex _ ( keyCode , event . ctrlKey , event . metaKey , noteIndex ) ;
2019-01-25 21:59:36 +02:00
const newSelectedNote = this . props . notes [ noteIndex ] ;
this . props . dispatch ( {
type : 'NOTE_SELECT' ,
id : newSelectedNote . id ,
} ) ;
this . itemListRef . current . makeItemIndexVisible ( noteIndex ) ;
2019-01-26 17:33:45 +02:00
this . focusNoteId _ ( newSelectedNote . id ) ;
2019-01-25 21:59:36 +02:00
event . preventDefault ( ) ;
}
2019-01-26 17:15:16 +02:00
2019-07-29 14:13:23 +02:00
if ( noteIds . length && ( keyCode === 46 || ( keyCode === 8 && event . metaKey ) ) ) {
// DELETE / CMD+Backspace
2019-01-26 17:15:16 +02:00
event . preventDefault ( ) ;
2019-01-29 20:02:34 +02:00
await NoteListUtils . confirmDeleteNotes ( noteIds ) ;
2019-01-26 17:15:16 +02:00
}
2019-01-26 17:33:45 +02:00
2019-07-29 14:13:23 +02:00
if ( noteIds . length && keyCode === 32 ) {
// SPACE
2019-01-26 17:33:45 +02:00
event . preventDefault ( ) ;
const notes = BaseModel . modelsByIds ( this . props . notes , noteIds ) ;
2020-05-21 10:14:33 +02:00
const todos = notes . filter ( n => ! ! n . is _todo ) ;
2019-01-26 17:33:45 +02:00
if ( ! todos . length ) return ;
for ( let i = 0 ; i < todos . length ; i ++ ) {
const toggledTodo = Note . toggleTodoCompleted ( todos [ i ] ) ;
await Note . save ( toggledTodo ) ;
}
this . focusNoteId _ ( todos [ 0 ] . id ) ;
}
2019-01-26 20:04:32 +02:00
2019-07-29 14:13:23 +02:00
if ( keyCode === 9 ) {
// TAB
2019-01-26 20:04:32 +02:00
event . preventDefault ( ) ;
if ( event . shiftKey ) {
2020-07-03 23:32:39 +02:00
CommandService . instance ( ) . execute ( 'focusElement' , { target : 'sideBar' } ) ;
2019-01-26 20:04:32 +02:00
} else {
2020-07-03 23:32:39 +02:00
CommandService . instance ( ) . execute ( 'focusElement' , { target : 'noteTitle' } ) ;
2019-01-26 20:04:32 +02:00
}
}
2020-02-04 23:55:06 +02:00
if ( event . keyCode === 65 && ( event . ctrlKey || event . metaKey ) ) {
// Ctrl+A key
event . preventDefault ( ) ;
this . props . dispatch ( {
type : 'NOTE_SELECT_ALL' ,
} ) ;
}
2019-01-26 17:33:45 +02:00
}
focusNoteId _ ( noteId ) {
// - We need to focus the item manually otherwise focus might be lost when the
// list is scrolled and items within it are being rebuilt.
// - We need to use an interval because when leaving the arrow pressed, the rendering
// of items might lag behind and so the ref is not yet available at this point.
if ( ! this . itemAnchorRef ( noteId ) ) {
if ( this . focusItemIID _ ) clearInterval ( this . focusItemIID _ ) ;
this . focusItemIID _ = setInterval ( ( ) => {
if ( this . itemAnchorRef ( noteId ) ) {
this . itemAnchorRef ( noteId ) . focus ( ) ;
2019-07-29 14:13:23 +02:00
clearInterval ( this . focusItemIID _ ) ;
2019-01-26 17:33:45 +02:00
this . focusItemIID _ = null ;
}
} , 10 ) ;
} else {
this . itemAnchorRef ( noteId ) . focus ( ) ;
}
2019-01-25 21:59:36 +02:00
}
componentWillUnmount ( ) {
if ( this . focusItemIID _ ) {
clearInterval ( this . focusItemIID _ ) ;
this . focusItemIID _ = null ;
}
2020-07-03 23:32:39 +02:00
CommandService . instance ( ) . componentUnregisterCommands ( commands ) ;
2019-01-25 21:59:36 +02:00
}
2017-11-04 18:40:34 +02:00
render ( ) {
2017-11-08 19:51:55 +02:00
const theme = themeStyle ( this . props . theme ) ;
2017-11-10 19:58:17 +02:00
const style = this . props . style ;
2019-07-29 14:13:23 +02:00
2020-05-27 18:21:46 +02:00
if ( ! this . props . notes . length ) {
2017-11-10 19:58:17 +02:00
const padding = 10 ;
2019-07-29 14:13:23 +02:00
const emptyDivStyle = Object . assign (
{
2019-09-19 23:51:18 +02:00
padding : ` ${ padding } px ` ,
2019-07-29 14:13:23 +02:00
fontSize : theme . fontSize ,
color : theme . color ,
backgroundColor : theme . backgroundColor ,
fontFamily : theme . fontFamily ,
} ,
style
) ;
2017-11-10 19:58:17 +02:00
emptyDivStyle . width = emptyDivStyle . width - padding * 2 ;
emptyDivStyle . height = emptyDivStyle . height - padding * 2 ;
2019-07-29 14:13:23 +02:00
return < div style = { emptyDivStyle } > { this . props . folders . length ? _ ( 'No notes in here. Create one by clicking on "New note".' ) : _ ( 'There is currently no notebook. Create one by clicking on "New notebook".' ) } < / div > ;
2017-11-10 19:58:17 +02:00
}
2017-11-08 19:51:55 +02:00
2020-05-27 18:21:46 +02:00
return < ItemList
ref = { this . itemListRef }
disabled = { this . props . isInsertingNotes }
itemHeight = { this . style ( this . props . theme ) . listItem . height }
className = { 'note-list' }
items = { this . props . notes }
style = { style }
itemRenderer = { this . itemRenderer }
onKeyDown = { this . onKeyDown }
/ > ;
2017-11-04 18:40:34 +02:00
}
}
2020-05-21 10:14:33 +02:00
const mapStateToProps = state => {
2017-11-04 18:40:34 +02:00
return {
2017-11-05 01:27:13 +02:00
notes : state . notes ,
2017-12-07 15:16:38 +02:00
folders : state . folders ,
2017-11-22 20:35:31 +02:00
selectedNoteIds : state . selectedNoteIds ,
2020-05-27 18:21:46 +02:00
selectedFolderId : state . selectedFolderId ,
2017-11-08 19:51:55 +02:00
theme : state . settings . theme ,
2018-03-20 01:04:48 +02:00
notesParentType : state . notesParentType ,
searches : state . searches ,
selectedSearchId : state . selectedSearchId ,
2018-11-21 21:50:50 +02:00
watchedNoteFiles : state . watchedNoteFiles ,
2020-02-29 14:39:15 +02:00
provisionalNoteIds : state . provisionalNoteIds ,
2020-05-27 18:21:46 +02:00
isInsertingNotes : state . isInsertingNotes ,
noteSortOrder : state . settings [ 'notes.sortOrder.field' ] ,
2017-11-04 18:40:34 +02:00
} ;
} ;
const NoteList = connect ( mapStateToProps ) ( NoteListComponent ) ;
2019-07-29 14:13:23 +02:00
module . exports = { NoteList } ;