2017-11-04 18:40:34 +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' ) ;
2017-11-08 19:51:55 +02:00
const { themeStyle } = require ( '../theme.js' ) ;
2017-12-14 22:21:36 +02:00
const BaseModel = require ( 'lib/BaseModel' ) ;
2019-01-18 19:56:56 +02:00
const markJsUtils = require ( 'lib/markJsUtils' ) ;
2017-11-08 19:51:55 +02:00
const { _ } = require ( 'lib/locale.js' ) ;
const { bridge } = require ( 'electron' ) . remote . require ( './bridge' ) ;
const Menu = bridge ( ) . Menu ;
const MenuItem = bridge ( ) . MenuItem ;
2017-11-30 01:03:10 +02:00
const eventManager = require ( '../eventManager' ) ;
2018-03-01 22:14:06 +02:00
const InteropService = require ( 'lib/services/InteropService' ) ;
const InteropServiceHelper = require ( '../InteropServiceHelper.js' ) ;
2018-03-20 01:04:48 +02:00
const Search = require ( 'lib/models/Search' ) ;
2019-01-29 20:32:52 +02:00
const { stateUtils } = require ( 'lib/reducer' ) ;
2018-03-20 01:04:48 +02:00
const Mark = require ( 'mark.js/dist/mark.min.js' ) ;
2018-12-14 00:57:14 +02:00
const SearchEngine = require ( 'lib/services/SearchEngine' ) ;
2019-01-29 20:02:34 +02:00
const NoteListUtils = require ( './utils/NoteListUtils' ) ;
2019-01-18 19:56:56 +02:00
const { replaceRegexDiacritics , pregQuote } = require ( 'lib/string-utils' ) ;
2017-11-04 18:40:34 +02:00
class NoteListComponent extends React . Component {
2018-11-21 21:50:50 +02:00
constructor ( ) {
super ( ) ;
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 ) ;
2018-11-21 21:50:50 +02:00
}
2017-11-09 21:21:10 +02:00
style ( ) {
const theme = themeStyle ( this . props . theme ) ;
2017-11-10 22:12:38 +02:00
const itemHeight = 34 ;
2017-11-09 21:21:10 +02:00
let style = {
root : {
backgroundColor : theme . backgroundColor ,
} ,
listItem : {
height : itemHeight ,
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 ,
borderBottom : '1px solid ' + theme . dividerColor ,
} ,
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
} ;
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 ,
} ) ;
2017-11-10 22:34:36 +02:00
2017-11-08 19:51:55 +02:00
menu . popup ( bridge ( ) . window ( ) ) ;
}
2018-11-21 21:50:50 +02:00
itemRenderer ( item ) {
const theme = themeStyle ( this . props . theme ) ;
const width = this . props . style . width ;
2017-11-10 22:11:48 +02:00
const onTitleClick = async ( event , item ) => {
2019-01-29 20:02:34 +02:00
if ( event . ctrlKey || event . metaKey ) {
2017-11-22 21:20:19 +02:00
event . preventDefault ( ) ;
2017-11-22 20:35:31 +02:00
this . props . dispatch ( {
type : 'NOTE_SELECT_TOGGLE' ,
id : item . id ,
} ) ;
} else if ( event . shiftKey ) {
2017-11-22 21:20:19 +02:00
event . preventDefault ( ) ;
2017-11-22 20:35:31 +02:00
this . props . dispatch ( {
type : 'NOTE_SELECT_EXTEND' ,
id : item . id ,
} ) ;
} else {
this . props . dispatch ( {
type : 'NOTE_SELECT' ,
id : item . id ,
} ) ;
}
2017-11-05 01:27:13 +02:00
}
2017-11-22 21:20:19 +02:00
const onDragStart = ( event ) => {
2018-06-10 02:27:20 +02:00
let noteIds = [ ] ;
// 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 ;
} else {
const clickedNoteId = event . currentTarget . getAttribute ( 'data-id' ) ;
if ( clickedNoteId ) noteIds . push ( clickedNoteId ) ;
}
2017-11-22 21:20:19 +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 ) ) ;
}
2017-11-10 22:11:48 +02:00
const onCheckboxClick = async ( event ) => {
const checked = event . target . checked ;
const newNote = {
id : item . id ,
todo _completed : checked ? time . unixMs ( ) : 0 ,
}
2017-12-14 22:21:36 +02:00
await Note . save ( newNote , { userSideValidation : true } ) ;
2017-11-30 01:03:10 +02:00
eventManager . emit ( 'todoToggle' , { noteId : item . id } ) ;
2017-11-10 22:11:48 +02:00
}
2017-11-10 23:04:53 +02:00
const hPadding = 10 ;
2017-11-10 22:11:48 +02:00
2018-03-20 01:04:48 +02:00
let highlightedWords = [ ] ;
if ( this . props . notesParentType === 'Search' ) {
2018-12-14 00:57:14 +02:00
const query = BaseModel . byId ( this . props . searches , this . props . selectedSearchId ) ;
if ( query ) {
const parsedQuery = SearchEngine . instance ( ) . parseQuery ( query . query _pattern ) ;
highlightedWords = SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
}
2018-03-20 01:04:48 +02:00
}
2017-11-10 22:11:48 +02:00
let style = Object . assign ( { width : width } , this . style ( ) . listItem ) ;
2018-01-12 21:58:01 +02:00
if ( this . props . selectedNoteIds . indexOf ( item . id ) >= 0 ) {
style = Object . assign ( style , this . style ( ) . listItemSelected ) ;
}
2017-11-08 19:51:55 +02:00
2017-11-10 22:11:48 +02:00
// 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.
const checkbox = item . is _todo ?
2017-11-10 23:04:53 +02:00
< div style = { { display : 'flex' , height : style . height , alignItems : 'center' , paddingLeft : hPadding } } >
2017-11-10 22:11:48 +02:00
< input style = { { margin : 0 , marginBottom : 1 } } type = "checkbox" defaultChecked = { ! ! item . todo _completed } onClick = { ( event ) => { onCheckboxClick ( event , item ) } } / >
< / div >
: null ;
2017-11-10 22:12:38 +02:00
let listItemTitleStyle = Object . assign ( { } , this . style ( ) . listItemTitle ) ;
2017-11-10 23:04:53 +02:00
listItemTitleStyle . paddingLeft = ! checkbox ? hPadding : 4 ;
2017-11-10 22:12:38 +02:00
if ( item . is _todo && ! ! item . todo _completed ) listItemTitleStyle = Object . assign ( listItemTitleStyle , this . style ( ) . listItemTitleCompleted ) ;
2017-11-10 22:11:48 +02:00
2018-03-20 01:04:48 +02:00
let displayTitle = Note . displayTitle ( item ) ;
let titleComp = null ;
if ( highlightedWords . length ) {
const titleElement = document . createElement ( 'span' ) ;
titleElement . textContent = displayTitle ;
const mark = new Mark ( titleElement , {
exclude : [ 'img' ] ,
acrossElements : true ,
} ) ;
2018-12-14 00:57:14 +02:00
mark . unmark ( ) ;
for ( let i = 0 ; i < highlightedWords . length ; i ++ ) {
const w = highlightedWords [ i ] ;
2019-01-18 19:56:56 +02:00
markJsUtils . markKeyword ( mark , w , {
pregQuote : pregQuote ,
replaceRegexDiacritics : replaceRegexDiacritics ,
} ) ;
2018-12-14 00:57:14 +02:00
}
2018-03-20 01:04:48 +02:00
// Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement
// is a span tag that we created and that contains data that's been inserted as plain text
// with `textContent` so it cannot contain any XSS attacks. We use this feature because
// mark.js can only deal with DOM elements.
// https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
titleComp = < span dangerouslySetInnerHTML = { { _ _html : titleElement . outerHTML } } > < / span >
} else {
titleComp = < span > { displayTitle } < / span >
}
2018-11-21 21:50:50 +02:00
const watchedIconStyle = {
paddingRight : 4 ,
color : theme . color ,
} ;
const watchedIcon = this . props . watchedNoteFiles . indexOf ( item . id ) < 0 ? null : (
< i style = { watchedIconStyle } className = { "fa fa-external-link" } > < / i >
) ;
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 ] ;
2017-11-14 20:02:58 +02:00
// Need to include "todo_completed" in key so that checkbox is updated when
2018-03-20 01:04:48 +02:00
// item is changed via sync.
2017-11-14 20:02:58 +02:00
return < div key = { item . id + '_' + item . todo _completed } style = { style } >
2017-11-10 22:11:48 +02:00
{ checkbox }
< a
2019-01-25 21:59:36 +02:00
ref = { ref }
2017-11-10 22:11:48 +02:00
className = "list-item"
onContextMenu = { ( event ) => this . itemContextMenu ( event ) }
href = "#"
2017-11-22 21:20:19 +02:00
draggable = { true }
2017-11-10 22:11:48 +02:00
style = { listItemTitleStyle }
onClick = { ( event ) => { onTitleClick ( event , item ) } }
2017-11-22 21:20:19 +02:00
onDragStart = { ( event ) => onDragStart ( event ) }
2018-01-09 22:16:09 +02:00
data - id = { item . id }
2017-11-10 22:11:48 +02:00
>
2018-11-21 21:50:50 +02:00
{ watchedIcon }
2018-03-20 01:04:48 +02:00
{ titleComp }
2017-11-10 22:11:48 +02:00
< / a >
< / div >
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-01-26 20:04:32 +02:00
doCommand ( command ) {
if ( ! command ) return ;
let commandProcessed = true ;
if ( command . name === 'focusElement' && command . target === 'noteList' ) {
if ( this . props . selectedNoteIds . length ) {
const ref = this . itemAnchorRef ( this . props . selectedNoteIds [ 0 ] ) ;
if ( ref ) ref . focus ( ) ;
}
} else {
commandProcessed = false ;
}
if ( commandProcessed ) {
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : null ,
} ) ;
}
}
componentDidUpdate ( prevProps , prevState , snapshot ) {
if ( prevProps . windowCommand !== this . props . windowCommand ) {
this . doCommand ( this . props . windowCommand ) ;
}
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 ] ;
for ( let i = 0 ; i < this . props . notes . length ; i ++ ) {
if ( this . props . notes [ i ] . id === id ) {
this . itemListRef . current . makeItemIndexVisible ( i ) ;
break ;
}
}
}
2019-01-26 20:04:32 +02:00
}
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
2019-01-25 21:59:36 +02:00
if ( noteIds . length === 1 && ( keyCode === 40 || keyCode === 38 ) ) { // DOWN / UP
const noteId = noteIds [ 0 ] ;
let noteIndex = BaseModel . modelIndexById ( this . props . notes , noteId ) ;
const inc = keyCode === 38 ? - 1 : + 1 ;
noteIndex += inc ;
if ( noteIndex < 0 ) noteIndex = 0 ;
if ( noteIndex > this . props . notes . length - 1 ) noteIndex = this . props . notes . length - 1 ;
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-02-14 01:33:07 +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
if ( noteIds . length && keyCode === 32 ) { // SPACE
event . preventDefault ( ) ;
const notes = BaseModel . modelsByIds ( this . props . notes , noteIds ) ;
const todos = notes . filter ( n => ! ! n . is _todo ) ;
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
if ( keyCode === 9 ) { // TAB
event . preventDefault ( ) ;
if ( event . shiftKey ) {
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'focusElement' ,
target : 'sideBar' ,
} ) ;
} else {
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'focusElement' ,
target : 'noteTitle' ,
} ) ;
}
}
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 ( ) ;
clearInterval ( this . focusItemIID _ )
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 ;
}
}
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 ;
2018-01-12 21:58:01 +02:00
let notes = this . props . notes . slice ( ) ;
2019-02-16 03:12:43 +02:00
2018-01-12 21:58:01 +02:00
if ( ! notes . length ) {
2017-11-10 19:58:17 +02:00
const padding = 10 ;
const emptyDivStyle = Object . assign ( {
padding : padding + 'px' ,
fontSize : theme . fontSize ,
color : theme . color ,
backgroundColor : theme . backgroundColor ,
fontFamily : theme . fontFamily ,
} , style ) ;
emptyDivStyle . width = emptyDivStyle . width - padding * 2 ;
emptyDivStyle . height = emptyDivStyle . height - padding * 2 ;
2017-12-07 15:16:38 +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
2019-02-16 03:12:43 +02:00
return (
2017-11-04 21:46:37 +02:00
< ItemList
2019-01-25 21:59:36 +02:00
ref = { this . itemListRef }
2017-11-13 02:23:12 +02:00
itemHeight = { this . style ( ) . listItem . height }
2017-11-04 21:46:37 +02:00
className = { "note-list" }
2018-01-12 21:58:01 +02:00
items = { notes }
2019-02-16 03:12:43 +02:00
style = { style }
2018-11-21 21:50:50 +02:00
itemRenderer = { this . itemRenderer }
2019-01-25 21:59:36 +02:00
onKeyDown = { this . onKeyDown }
2019-02-16 03:12:43 +02:00
/ >
2017-11-04 18:40:34 +02:00
) ;
}
}
const mapStateToProps = ( state ) => {
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 ,
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 ,
2019-01-26 20:04:32 +02:00
windowCommand : state . windowCommand ,
2017-11-04 18:40:34 +02:00
} ;
} ;
const NoteList = connect ( mapStateToProps ) ( NoteListComponent ) ;
module . exports = { NoteList } ;