2023-08-06 17:21:09 +01:00
import * as React from 'react' ;
2023-08-12 15:45:19 +01:00
import { _ } from '@joplin/lib/locale' ;
import { useMemo , useCallback , useState } from 'react' ;
2023-08-06 17:21:09 +01:00
import { AppState } from '../../app.reducer' ;
import BaseModel , { ModelType } from '@joplin/lib/BaseModel' ;
const { connect } = require ( 'react-redux' ) ;
2023-08-11 18:18:32 +01:00
import { ItemFlow , Props } from './utils/types' ;
2023-08-06 17:21:09 +01:00
import { itemIsReadOnlySync , ItemSlice } from '@joplin/lib/models/utils/readOnly' ;
2023-08-11 18:18:32 +01:00
import { FolderEntity } from '@joplin/lib/services/database/types' ;
2023-08-06 17:21:09 +01:00
import ItemChange from '@joplin/lib/models/ItemChange' ;
import { Size } from '@joplin/utils/types' ;
2023-08-11 18:18:32 +01:00
import defaultListRenderer from './utils/defaultListRenderer' ;
2023-08-11 18:52:10 +01:00
import NoteListItem from '../NoteListItem/NoteListItem' ;
2023-08-11 18:18:32 +01:00
import useRenderedNotes from './utils/useRenderedNote' ;
2023-08-11 18:34:12 +01:00
import useItemCss from './utils/useItemCss' ;
2023-08-11 19:19:17 +01:00
import useOnContextMenu from '../NoteListItem/utils/useOnContextMenu' ;
2023-08-12 15:45:19 +01:00
import useVisibleRange from './utils/useVisibleRange' ;
2023-08-06 17:21:09 +01:00
2023-08-11 10:57:27 +01:00
const NoteList = ( props : Props ) = > {
2023-08-12 15:45:19 +01:00
const [ scrollTop , setScrollTop ] = useState ( 0 ) ;
2023-08-11 18:09:12 +01:00
const listRenderer = defaultListRenderer ;
if ( listRenderer . flow !== ItemFlow . TopToBottom ) throw new Error ( 'Not implemented' ) ;
2023-08-06 17:21:09 +01:00
const itemSize : Size = useMemo ( ( ) = > {
2023-08-11 18:09:12 +01:00
return listRenderer . itemSize ;
} , [ listRenderer . itemSize ] ) ;
2023-08-06 17:21:09 +01:00
2023-08-12 15:45:19 +01:00
const [ startNoteIndex , endNoteIndex ] = useVisibleRange ( scrollTop , props . size , itemSize , props . notes . length ) ;
const renderedNotes = useRenderedNotes ( startNoteIndex , endNoteIndex , props . notes , props . selectedNoteIds , itemSize , listRenderer ) ;
2023-08-06 17:21:09 +01:00
const noteItemStyle = useMemo ( ( ) = > {
return {
width : 'auto' ,
height : itemSize.height ,
} ;
} , [ itemSize . height ] ) ;
const noteListStyle = useMemo ( ( ) = > {
return {
width : props.size.width ,
height : props.size.height ,
} ;
} , [ props . size ] ) ;
const onNoteClick = useCallback ( ( event : React.MouseEvent < HTMLDivElement > ) = > {
const noteId = event . currentTarget . getAttribute ( 'data-note-id' ) ;
if ( event . ctrlKey || event . metaKey ) {
event . preventDefault ( ) ;
props . dispatch ( {
type : 'NOTE_SELECT_TOGGLE' ,
id : noteId ,
} ) ;
} else if ( event . shiftKey ) {
event . preventDefault ( ) ;
props . dispatch ( {
type : 'NOTE_SELECT_EXTEND' ,
id : noteId ,
} ) ;
} else {
props . dispatch ( {
type : 'NOTE_SELECT' ,
id : noteId ,
} ) ;
}
} , [ props . dispatch ] ) ;
2023-08-11 18:34:12 +01:00
useItemCss ( listRenderer . itemCss ) ;
2023-08-11 18:09:12 +01:00
2023-08-11 19:19:17 +01:00
const onItemContextMenu = useOnContextMenu (
props . selectedNoteIds ,
props . selectedFolderId ,
props . notes ,
props . dispatch ,
props . watchedNoteFiles ,
props . plugins ,
props . customCss
) ;
2023-08-12 15:45:19 +01:00
const onScroll = useCallback ( ( event : any ) = > {
setScrollTop ( event . target . scrollTop ) ;
} , [ ] ) ;
const renderFiller = ( key : string , height : number ) = > {
return < div key = { key } style = { { height : height } } > < / div > ;
} ;
const renderEmptyList = ( ) = > {
if ( props . notes . length ) return null ;
return < div className = "emptylist" > { 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 > ;
} ;
2023-08-06 17:21:09 +01:00
const renderNotes = ( ) = > {
2023-08-12 15:45:19 +01:00
if ( ! props . notes . length ) return null ;
2023-08-06 17:21:09 +01:00
const output : JSX.Element [ ] = [ ] ;
2023-08-12 15:45:19 +01:00
output . push ( renderFiller ( 'top' , startNoteIndex * itemSize . height ) ) ;
for ( let i = startNoteIndex ; i <= endNoteIndex ; i ++ ) {
const note = props . notes [ i ] ;
const renderedNote = renderedNotes [ note . id ] ;
2023-08-06 17:21:09 +01:00
output . push (
2023-08-11 18:34:12 +01:00
< NoteListItem
2023-08-12 15:45:19 +01:00
key = { note . id }
2023-08-06 17:21:09 +01:00
onClick = { onNoteClick }
2023-08-11 18:09:12 +01:00
onChange = { listRenderer . onChange }
2023-08-12 15:45:19 +01:00
noteId = { note . id }
noteHtml = { renderedNote ? renderedNote . html : '' }
2023-08-11 18:34:12 +01:00
itemSize = { itemSize }
2023-08-06 17:21:09 +01:00
style = { noteItemStyle }
2023-08-11 19:19:17 +01:00
onContextMenu = { onItemContextMenu }
2023-08-06 17:21:09 +01:00
/ >
) ;
}
2023-08-12 15:45:19 +01:00
output . push ( renderFiller ( 'bottom' , ( props . notes . length - endNoteIndex - 1 ) * itemSize . height ) ) ;
2023-08-06 17:21:09 +01:00
return output ;
} ;
return (
2023-08-12 15:45:19 +01:00
< div className = "note-list" style = { noteListStyle } onScroll = { onScroll } >
{ renderEmptyList ( ) }
2023-08-06 17:21:09 +01:00
{ renderNotes ( ) }
< / div >
) ;
} ;
const mapStateToProps = ( state : AppState ) = > {
const selectedFolder : FolderEntity = state . notesParentType === 'Folder' ? BaseModel . byId ( state . folders , state . selectedFolderId ) : null ;
const userId = state . settings [ 'sync.userId' ] ;
return {
notes : state.notes ,
folders : state.folders ,
selectedNoteIds : state.selectedNoteIds ,
selectedFolderId : state.selectedFolderId ,
themeId : state.settings.theme ,
notesParentType : state.notesParentType ,
searches : state.searches ,
selectedSearchId : state.selectedSearchId ,
watchedNoteFiles : state.watchedNoteFiles ,
provisionalNoteIds : state.provisionalNoteIds ,
isInsertingNotes : state.isInsertingNotes ,
noteSortOrder : state.settings [ 'notes.sortOrder.field' ] ,
uncompletedTodosOnTop : state.settings.uncompletedTodosOnTop ,
showCompletedTodos : state.settings.showCompletedTodos ,
highlightedWords : state.highlightedWords ,
plugins : state.pluginService.plugins ,
customCss : state.customCss ,
focusedField : state.focusedField ,
parentFolderIsReadOnly : state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync ( ModelType . Folder , ItemChange . SOURCE_UNSPECIFIED , selectedFolder as ItemSlice , userId , state . shareService ) : false ,
} ;
} ;
2023-08-11 10:57:27 +01:00
export default connect ( mapStateToProps ) ( NoteList ) ;