2019-04-01 21:43:13 +02:00
const React = require ( 'react' ) ;
const { connect } = require ( 'react-redux' ) ;
const { _ } = require ( 'lib/locale.js' ) ;
const { themeStyle } = require ( '../theme.js' ) ;
const SearchEngine = require ( 'lib/services/SearchEngine' ) ;
const BaseModel = require ( 'lib/BaseModel' ) ;
const Tag = require ( 'lib/models/Tag' ) ;
2019-07-30 09:35:42 +02:00
const Folder = require ( 'lib/models/Folder' ) ;
2019-04-01 21:43:13 +02:00
const { ItemList } = require ( '../gui/ItemList.min' ) ;
2019-05-06 22:35:29 +02:00
const HelpButton = require ( '../gui/HelpButton.min' ) ;
2019-07-30 09:35:42 +02:00
const { surroundKeywords } = require ( 'lib/string-utils.js' ) ;
2019-04-01 21:43:13 +02:00
const PLUGIN _NAME = 'gotoAnything' ;
const itemHeight = 60 ;
class GotoAnything {
2019-09-13 00:16:42 +02:00
onTrigger ( ) {
2019-04-01 21:43:13 +02:00
this . dispatch ( {
type : 'PLUGIN_DIALOG_SET' ,
open : true ,
pluginName : PLUGIN _NAME ,
} ) ;
}
}
class Dialog extends React . PureComponent {
constructor ( ) {
super ( ) ;
this . state = {
query : '' ,
results : [ ] ,
selectedItemId : null ,
keywords : [ ] ,
listType : BaseModel . TYPE _NOTE ,
showHelp : false ,
} ;
this . styles _ = { } ;
this . inputRef = React . createRef ( ) ;
this . itemListRef = React . createRef ( ) ;
this . onKeyDown = this . onKeyDown . bind ( this ) ;
this . input _onChange = this . input _onChange . bind ( this ) ;
this . input _onKeyDown = this . input _onKeyDown . bind ( this ) ;
this . listItemRenderer = this . listItemRenderer . bind ( this ) ;
this . listItem _onClick = this . listItem _onClick . bind ( this ) ;
this . helpButton _onClick = this . helpButton _onClick . bind ( this ) ;
}
style ( ) {
if ( this . styles _ [ this . props . theme ] ) return this . styles _ [ this . props . theme ] ;
const theme = themeStyle ( this . props . theme ) ;
this . styles _ [ this . props . theme ] = {
dialogBox : Object . assign ( { } , theme . dialogBox , { minWidth : '50%' , maxWidth : '50%' } ) ,
input : Object . assign ( { } , theme . inputStyle , { flex : 1 } ) ,
row : { overflow : 'hidden' , height : itemHeight , display : 'flex' , justifyContent : 'center' , flexDirection : 'column' , paddingLeft : 10 , paddingRight : 10 } ,
help : Object . assign ( { } , theme . textStyle , { marginBottom : 10 } ) ,
inputHelpWrapper : { display : 'flex' , flexDirection : 'row' , alignItems : 'center' } ,
} ;
const rowTextStyle = {
fontSize : theme . fontSize ,
color : theme . color ,
fontFamily : theme . fontFamily ,
whiteSpace : 'nowrap' ,
opacity : 0.7 ,
userSelect : 'none' ,
} ;
const rowTitleStyle = Object . assign ( { } , rowTextStyle , {
fontSize : rowTextStyle . fontSize * 1.4 ,
marginBottom : 5 ,
color : theme . colorFaded ,
} ) ;
this . styles _ [ this . props . theme ] . rowSelected = Object . assign ( { } , this . styles _ [ this . props . theme ] . row , { backgroundColor : theme . selectedColor } ) ;
this . styles _ [ this . props . theme ] . rowPath = rowTextStyle ;
this . styles _ [ this . props . theme ] . rowTitle = rowTitleStyle ;
return this . styles _ [ this . props . theme ] ;
}
componentDidMount ( ) {
document . addEventListener ( 'keydown' , this . onKeyDown ) ;
}
componentWillUnmount ( ) {
if ( this . listUpdateIID _ ) clearTimeout ( this . listUpdateIID _ ) ;
document . removeEventListener ( 'keydown' , this . onKeyDown ) ;
}
onKeyDown ( event ) {
if ( event . keyCode === 27 ) { // ESCAPE
this . props . dispatch ( {
pluginName : PLUGIN _NAME ,
type : 'PLUGIN_DIALOG_SET' ,
open : false ,
} ) ;
}
}
2019-09-13 00:16:42 +02:00
helpButton _onClick ( ) {
2019-04-01 21:43:13 +02:00
this . setState ( { showHelp : ! this . state . showHelp } ) ;
}
input _onChange ( event ) {
this . setState ( { query : event . target . value } ) ;
this . scheduleListUpdate ( ) ;
}
scheduleListUpdate ( ) {
if ( this . listUpdateIID _ ) return ;
this . listUpdateIID _ = setTimeout ( async ( ) => {
await this . updateList ( ) ;
this . listUpdateIID _ = null ;
} , 10 ) ;
}
makeSearchQuery ( query ) {
const splitted = query . split ( ' ' ) ;
const output = [ ] ;
for ( let i = 0 ; i < splitted . length ; i ++ ) {
const s = splitted [ i ] . trim ( ) ;
if ( ! s ) continue ;
output . push ( 'title:' + s + '*' ) ;
}
return output . join ( ' ' ) ;
}
keywords ( searchQuery ) {
const parsedQuery = SearchEngine . instance ( ) . parseQuery ( searchQuery ) ;
return SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
}
async updateList ( ) {
if ( ! this . state . query ) {
this . setState ( { results : [ ] , keywords : [ ] } ) ;
} else {
let results = [ ] ;
let listType = null ;
let searchQuery = '' ;
if ( this . state . query . indexOf ( '#' ) === 0 ) { // TAGS
listType = BaseModel . TYPE _TAG ;
searchQuery = '*' + this . state . query . split ( ' ' ) [ 0 ] . substr ( 1 ) . trim ( ) + '*' ;
results = await Tag . searchAllWithNotes ( { titlePattern : searchQuery } ) ;
} else if ( this . state . query . indexOf ( '@' ) === 0 ) { // FOLDERS
listType = BaseModel . TYPE _FOLDER ;
searchQuery = '*' + this . state . query . split ( ' ' ) [ 0 ] . substr ( 1 ) . trim ( ) + '*' ;
results = await Folder . search ( { titlePattern : searchQuery } ) ;
for ( let i = 0 ; i < results . length ; i ++ ) {
const row = results [ i ] ;
const path = Folder . folderPathString ( this . props . folders , row . parent _id ) ;
results [ i ] = Object . assign ( { } , row , { path : path ? path : '/' } ) ;
}
} else { // NOTES
listType = BaseModel . TYPE _NOTE ;
searchQuery = this . makeSearchQuery ( this . state . query ) ;
results = await SearchEngine . instance ( ) . search ( searchQuery ) ;
for ( let i = 0 ; i < results . length ; i ++ ) {
const row = results [ i ] ;
const path = Folder . folderPathString ( this . props . folders , row . parent _id ) ;
results [ i ] = Object . assign ( { } , row , { path : path } ) ;
}
}
let selectedItemId = null ;
const itemIndex = this . selectedItemIndex ( results , this . state . selectedItemId ) ;
if ( itemIndex > 0 ) {
selectedItemId = this . state . selectedItemId ;
} else if ( results . length > 0 ) {
selectedItemId = results [ 0 ] . id ;
}
this . setState ( {
listType : listType ,
results : results ,
keywords : this . keywords ( searchQuery ) ,
selectedItemId : selectedItemId ,
} ) ;
}
}
2019-05-14 01:11:27 +02:00
async gotoItem ( item ) {
2019-04-01 21:43:13 +02:00
this . props . dispatch ( {
pluginName : PLUGIN _NAME ,
type : 'PLUGIN_DIALOG_SET' ,
open : false ,
} ) ;
2019-05-14 01:11:27 +02:00
if ( this . state . listType === BaseModel . TYPE _NOTE || this . state . listType === BaseModel . TYPE _FOLDER ) {
const folderPath = await Folder . folderPath ( this . props . folders , item . parent _id ) ;
for ( const folder of folderPath ) {
this . props . dispatch ( {
2019-07-30 09:35:42 +02:00
type : 'FOLDER_SET_COLLAPSED' ,
2019-05-14 01:11:27 +02:00
id : folder . id ,
collapsed : false ,
} ) ;
}
2019-07-30 09:35:42 +02:00
}
2019-05-14 01:11:27 +02:00
2019-04-01 21:43:13 +02:00
if ( this . state . listType === BaseModel . TYPE _NOTE ) {
this . props . dispatch ( {
2019-07-30 09:35:42 +02:00
type : 'FOLDER_AND_NOTE_SELECT' ,
2019-04-01 21:43:13 +02:00
folderId : item . parent _id ,
noteId : item . id ,
} ) ;
} else if ( this . state . listType === BaseModel . TYPE _TAG ) {
this . props . dispatch ( {
2019-07-30 09:35:42 +02:00
type : 'TAG_SELECT' ,
2019-04-01 21:43:13 +02:00
id : item . id ,
} ) ;
} else if ( this . state . listType === BaseModel . TYPE _FOLDER ) {
this . props . dispatch ( {
2019-07-30 09:35:42 +02:00
type : 'FOLDER_SELECT' ,
2019-04-01 21:43:13 +02:00
id : item . id ,
} ) ;
}
}
listItem _onClick ( event ) {
const itemId = event . currentTarget . getAttribute ( 'data-id' ) ;
const parentId = event . currentTarget . getAttribute ( 'data-parent-id' ) ;
this . gotoItem ( {
id : itemId ,
parent _id : parentId ,
} ) ;
}
listItemRenderer ( item ) {
const theme = themeStyle ( this . props . theme ) ;
const style = this . style ( ) ;
const rowStyle = item . id === this . state . selectedItemId ? style . rowSelected : style . row ;
const titleHtml = surroundKeywords ( this . state . keywords , item . title , '<span style="font-weight: bold; color: ' + theme . colorBright + ';">' , '</span>' ) ;
2019-07-30 09:35:42 +02:00
const pathComp = ! item . path ? null : < div style = { style . rowPath } > { item . path } < / div > ;
2019-04-01 21:43:13 +02:00
return (
< div key = { item . id } style = { rowStyle } onClick = { this . listItem _onClick } data - id = { item . id } data - parent - id = { item . parent _id } >
< div style = { style . rowTitle } dangerouslySetInnerHTML = { { _ _html : titleHtml } } > < / div >
{ pathComp }
< / div >
) ;
}
selectedItemIndex ( results , itemId ) {
if ( typeof results === 'undefined' ) results = this . state . results ;
if ( typeof itemId === 'undefined' ) itemId = this . state . selectedItemId ;
for ( let i = 0 ; i < results . length ; i ++ ) {
const r = results [ i ] ;
if ( r . id === itemId ) return i ;
}
return - 1 ;
}
selectedItem ( ) {
const index = this . selectedItemIndex ( ) ;
if ( index < 0 ) return null ;
return this . state . results [ index ] ;
}
input _onKeyDown ( event ) {
const keyCode = event . keyCode ;
if ( this . state . results . length > 0 && ( keyCode === 40 || keyCode === 38 ) ) { // DOWN / UP
event . preventDefault ( ) ;
const inc = keyCode === 38 ? - 1 : + 1 ;
let index = this . selectedItemIndex ( ) ;
if ( index < 0 ) return ; // Not possible, but who knows
2019-07-30 09:35:42 +02:00
2019-04-01 21:43:13 +02:00
index += inc ;
if ( index < 0 ) index = 0 ;
if ( index >= this . state . results . length ) index = this . state . results . length - 1 ;
const newId = this . state . results [ index ] . id ;
this . itemListRef . current . makeItemIndexVisible ( index ) ;
this . setState ( { selectedItemId : newId } ) ;
}
if ( keyCode === 13 ) { // ENTER
event . preventDefault ( ) ;
const item = this . selectedItem ( ) ;
if ( ! item ) return ;
this . gotoItem ( item ) ;
}
}
renderList ( ) {
const style = {
marginTop : 5 ,
height : Math . min ( itemHeight * this . state . results . length , 7 * itemHeight ) ,
} ;
return (
< ItemList
ref = { this . itemListRef }
itemHeight = { itemHeight }
items = { this . state . results }
style = { style }
itemRenderer = { this . listItemRenderer }
/ >
) ;
}
render ( ) {
const theme = themeStyle ( this . props . theme ) ;
const style = this . style ( ) ;
2019-07-30 09:35:42 +02:00
const helpComp = ! this . state . showHelp ? null : < div style = { style . help } > { _ ( 'Type a note title to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.' ) } < / div > ;
2019-04-01 21:43:13 +02:00
return (
< div style = { theme . dialogModalLayer } >
< div style = { style . dialogBox } >
{ helpComp }
< div style = { style . inputHelpWrapper } >
< input autoFocus type = "text" style = { style . input } ref = { this . inputRef } value = { this . state . query } onChange = { this . input _onChange } onKeyDown = { this . input _onKeyDown } / >
2019-05-06 22:35:29 +02:00
< HelpButton onClick = { this . helpButton _onClick } / >
2019-04-01 21:43:13 +02:00
< / div >
{ this . renderList ( ) }
< / div >
< / div >
) ;
}
}
const mapStateToProps = ( state ) => {
return {
folders : state . folders ,
theme : state . settings . theme ,
} ;
} ;
GotoAnything . Dialog = connect ( mapStateToProps ) ( Dialog ) ;
GotoAnything . manifest = {
name : PLUGIN _NAME ,
menuItems : [
{
name : 'main' ,
parent : 'tools' ,
label : _ ( 'Goto Anything...' ) ,
2019-04-20 13:02:43 +02:00
accelerator : 'CommandOrControl+G' ,
2019-04-01 21:43:13 +02:00
screens : [ 'Main' ] ,
} ,
] ,
} ;
2019-07-30 09:35:42 +02:00
module . exports = GotoAnything ;