2020-10-18 22:52:10 +02:00
import * as React from 'react' ;
import { AppState } from '../app' ;
2020-11-07 17:59:37 +02:00
import CommandService , { SearchResult as CommandSearchResult } from '@joplin/lib/services/CommandService' ;
import KeymapService from '@joplin/lib/services/KeymapService' ;
import shim from '@joplin/lib/shim' ;
2020-10-18 22:52:10 +02:00
2019-04-01 21:43:13 +02:00
const { connect } = require ( 'react-redux' ) ;
2020-11-07 17:59:37 +02:00
const { _ } = require ( '@joplin/lib/locale' ) ;
const { themeStyle } = require ( '@joplin/lib/theme' ) ;
const SearchEngine = require ( '@joplin/lib/services/searchengine/SearchEngine' ) ;
const BaseModel = require ( '@joplin/lib/BaseModel' ) . default ;
const Tag = require ( '@joplin/lib/models/Tag' ) ;
const Folder = require ( '@joplin/lib/models/Folder' ) ;
const Note = require ( '@joplin/lib/models/Note' ) ;
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' ) ;
2020-11-07 17:59:37 +02:00
const { surroundKeywords , nextWhitespaceIndex , removeDiacritics } = require ( '@joplin/lib/string-utils.js' ) ;
const { mergeOverlappingIntervals } = require ( '@joplin/lib/ArrayUtils.js' ) ;
const markupLanguageUtils = require ( '@joplin/lib/markupLanguageUtils' ) . default ;
2020-10-18 22:52:10 +02:00
const PLUGIN_NAME = 'gotoAnything' ;
interface SearchResult {
id : string ,
title : string ,
parent_id : string ,
fields : string [ ] ,
fragments? : string ,
path? : string ,
type ? : number ,
}
interface Props {
themeId : number ,
dispatch : Function ,
folders : any [ ] ,
showCompletedTodos : boolean ,
userData : any ,
}
interface State {
query : string ,
results : SearchResult [ ] ,
selectedItemId : string ,
keywords : string [ ] ,
listType : number ,
showHelp : boolean ,
resultsInBody : boolean ,
}
2019-04-01 21:43:13 +02:00
class GotoAnything {
2020-10-18 22:52:10 +02:00
public dispatch :Function ;
public static Dialog :any ;
public static manifest :any ;
onTrigger ( event :any ) {
2019-04-01 21:43:13 +02:00
this . dispatch ( {
2020-10-09 19:35:46 +02:00
type : 'PLUGINLEGACY_DIALOG_SET' ,
2019-04-01 21:43:13 +02:00
open : true ,
pluginName : PLUGIN_NAME ,
2020-10-18 22:52:10 +02:00
userData : event.userData ,
2019-04-01 21:43:13 +02:00
} ) ;
}
}
2020-10-18 22:52:10 +02:00
class Dialog extends React . PureComponent < Props , State > {
private fuzzy_ :boolean ;
private styles_ :any ;
private inputRef :any ;
private itemListRef :any ;
private listUpdateIID_ :any ;
private markupToHtml_ :any ;
2019-04-01 21:43:13 +02:00
2020-10-18 22:52:10 +02:00
constructor ( props :Props ) {
super ( props ) ;
2019-04-01 21:43:13 +02:00
2020-09-15 15:01:07 +02:00
this . fuzzy_ = false ;
2020-10-18 22:52:10 +02:00
const startString = props ? . userData ? . startString ? props ? . userData ? . startString : '' ;
2019-04-01 21:43:13 +02:00
this . state = {
2020-10-18 22:52:10 +02:00
query : startString ,
2019-04-01 21:43:13 +02:00
results : [ ] ,
selectedItemId : null ,
keywords : [ ] ,
listType : BaseModel.TYPE_NOTE ,
showHelp : false ,
2020-04-14 00:10:59 +02:00
resultsInBody : false ,
2019-04-01 21:43:13 +02:00
} ;
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 ) ;
2020-04-05 09:55:00 +02:00
this . modalLayer_onClick = this . modalLayer_onClick . bind ( this ) ;
2020-10-18 22:52:10 +02:00
this . renderItem = this . renderItem . bind ( this ) ;
2019-04-01 21:43:13 +02:00
this . listItem_onClick = this . listItem_onClick . bind ( this ) ;
this . helpButton_onClick = this . helpButton_onClick . bind ( this ) ;
2020-10-18 22:52:10 +02:00
if ( startString ) this . scheduleListUpdate ( ) ;
2019-04-01 21:43:13 +02:00
}
style() {
2020-10-18 22:52:10 +02:00
const styleKey = [ this . props . themeId , this . state . listType , this . state . resultsInBody ? '1' : '0' ] . join ( '-' ) ;
2020-04-14 00:10:59 +02:00
if ( this . styles_ [ styleKey ] ) return this . styles_ [ styleKey ] ;
2019-04-01 21:43:13 +02:00
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-04-01 21:43:13 +02:00
2020-10-18 22:52:10 +02:00
let itemHeight = this . state . resultsInBody ? 84 : 64 ;
if ( this . state . listType === BaseModel . TYPE_COMMAND ) {
itemHeight = 40 ;
}
2020-04-14 00:10:59 +02:00
this . styles_ [ styleKey ] = {
2019-04-01 21:43:13 +02:00
dialogBox : Object.assign ( { } , theme . dialogBox , { minWidth : '50%' , maxWidth : '50%' } ) ,
input : Object.assign ( { } , theme . inputStyle , { flex : 1 } ) ,
2020-04-14 00:10:59 +02:00
row : {
overflow : 'hidden' ,
height : itemHeight ,
display : 'flex' ,
justifyContent : 'center' ,
flexDirection : 'column' ,
paddingLeft : 10 ,
paddingRight : 10 ,
borderBottomWidth : 1 ,
borderBottomStyle : 'solid' ,
borderBottomColor : theme.dividerColor ,
boxSizing : 'border-box' ,
} ,
2019-04-01 21:43:13 +02:00
help : Object.assign ( { } , theme . textStyle , { marginBottom : 10 } ) ,
2020-02-05 00:09:34 +02:00
inputHelpWrapper : { display : 'flex' , flexDirection : 'row' , alignItems : 'center' } ,
2019-04-01 21:43:13 +02:00
} ;
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 ,
2020-04-14 00:10:59 +02:00
marginBottom : this.state.resultsInBody ? 6 : 4 ,
2020-03-28 15:05:00 +02:00
color : theme.colorFaded ,
} ) ;
const rowFragmentsStyle = Object . assign ( { } , rowTextStyle , {
fontSize : rowTextStyle.fontSize * 1.2 ,
2020-04-14 00:10:59 +02:00
marginBottom : this.state.resultsInBody ? 8 : 6 ,
2019-04-01 21:43:13 +02:00
color : theme.colorFaded ,
} ) ;
2020-04-14 00:10:59 +02:00
this . styles_ [ styleKey ] . rowSelected = Object . assign ( { } , this . styles_ [ styleKey ] . row , { backgroundColor : theme.selectedColor } ) ;
this . styles_ [ styleKey ] . rowPath = rowTextStyle ;
this . styles_ [ styleKey ] . rowTitle = rowTitleStyle ;
this . styles_ [ styleKey ] . rowFragments = rowFragmentsStyle ;
this . styles_ [ styleKey ] . itemHeight = itemHeight ;
2019-04-01 21:43:13 +02:00
2020-04-14 00:10:59 +02:00
return this . styles_ [ styleKey ] ;
2019-04-01 21:43:13 +02:00
}
componentDidMount() {
document . addEventListener ( 'keydown' , this . onKeyDown ) ;
2020-09-21 18:31:25 +02:00
this . props . dispatch ( {
type : 'VISIBLE_DIALOGS_ADD' ,
name : 'gotoAnything' ,
} ) ;
2019-04-01 21:43:13 +02:00
}
componentWillUnmount() {
2020-10-09 19:35:46 +02:00
if ( this . listUpdateIID_ ) shim . clearTimeout ( this . listUpdateIID_ ) ;
2019-04-01 21:43:13 +02:00
document . removeEventListener ( 'keydown' , this . onKeyDown ) ;
2020-09-21 18:31:25 +02:00
this . props . dispatch ( {
type : 'VISIBLE_DIALOGS_REMOVE' ,
name : 'gotoAnything' ,
} ) ;
2019-04-01 21:43:13 +02:00
}
2020-10-18 22:52:10 +02:00
onKeyDown ( event :any ) {
2019-04-01 21:43:13 +02:00
if ( event . keyCode === 27 ) { // ESCAPE
this . props . dispatch ( {
pluginName : PLUGIN_NAME ,
2020-10-09 19:35:46 +02:00
type : 'PLUGINLEGACY_DIALOG_SET' ,
2019-04-01 21:43:13 +02:00
open : false ,
} ) ;
}
}
2020-10-18 22:52:10 +02:00
modalLayer_onClick ( event :any ) {
2020-04-12 10:59:00 +02:00
if ( event . currentTarget == event . target ) {
this . props . dispatch ( {
pluginName : PLUGIN_NAME ,
2020-10-09 19:35:46 +02:00
type : 'PLUGINLEGACY_DIALOG_SET' ,
2020-04-12 10:59:00 +02:00
open : false ,
} ) ;
}
2020-03-30 19:33:36 +02:00
}
2019-09-13 00:16:42 +02:00
helpButton_onClick() {
2019-04-01 21:43:13 +02:00
this . setState ( { showHelp : ! this . state . showHelp } ) ;
}
2020-10-18 22:52:10 +02:00
input_onChange ( event :any ) {
2019-04-01 21:43:13 +02:00
this . setState ( { query : event.target.value } ) ;
this . scheduleListUpdate ( ) ;
}
scheduleListUpdate() {
2020-10-09 19:35:46 +02:00
if ( this . listUpdateIID_ ) shim . clearTimeout ( this . listUpdateIID_ ) ;
2019-04-01 21:43:13 +02:00
2020-10-09 19:35:46 +02:00
this . listUpdateIID_ = shim . setTimeout ( async ( ) = > {
2019-04-01 21:43:13 +02:00
await this . updateList ( ) ;
this . listUpdateIID_ = null ;
2020-07-15 00:27:12 +02:00
} , 100 ) ;
2019-04-01 21:43:13 +02:00
}
2020-10-18 22:52:10 +02:00
makeSearchQuery ( query :string ) {
2019-04-01 21:43:13 +02:00
const output = [ ] ;
2020-04-14 00:10:59 +02:00
const splitted = query . split ( ' ' ) ;
2020-03-28 15:05:00 +02:00
2019-04-01 21:43:13 +02:00
for ( let i = 0 ; i < splitted . length ; i ++ ) {
const s = splitted [ i ] . trim ( ) ;
if ( ! s ) continue ;
2020-04-14 00:10:59 +02:00
output . push ( ` ${ s } * ` ) ;
2019-04-01 21:43:13 +02:00
}
return output . join ( ' ' ) ;
}
2020-10-18 22:52:10 +02:00
async keywords ( searchQuery :string ) {
2020-09-15 15:01:07 +02:00
const parsedQuery = await SearchEngine . instance ( ) . parseQuery ( searchQuery , this . fuzzy_ ) ;
2020-09-11 23:52:32 +02:00
return SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
2019-04-01 21:43:13 +02:00
}
2020-07-15 00:27:12 +02:00
markupToHtml() {
if ( this . markupToHtml_ ) return this . markupToHtml_ ;
this . markupToHtml_ = markupLanguageUtils . newMarkupToHtml ( ) ;
return this . markupToHtml_ ;
}
2019-04-01 21:43:13 +02:00
async updateList() {
2020-04-14 00:10:59 +02:00
let resultsInBody = false ;
2019-04-01 21:43:13 +02:00
if ( ! this . state . query ) {
this . setState ( { results : [ ] , keywords : [ ] } ) ;
} else {
2020-10-18 22:52:10 +02:00
let results :SearchResult [ ] = [ ] ;
2019-04-01 21:43:13 +02:00
let listType = null ;
let searchQuery = '' ;
2020-10-18 22:52:10 +02:00
let keywords = null ;
if ( this . state . query . indexOf ( ':' ) === 0 ) { // COMMANDS
const query = this . state . query . substr ( 1 ) ;
listType = BaseModel . TYPE_COMMAND ;
keywords = [ query ] ;
const commandResults = CommandService . instance ( ) . searchCommands ( query , true ) ;
results = commandResults . map ( ( result :CommandSearchResult ) = > {
return {
id : result.commandName ,
title : result.title ,
parent_id : null ,
fields : [ ] ,
type : BaseModel . TYPE_COMMAND ,
} ;
} ) ;
} else if ( this . state . query . indexOf ( '#' ) === 0 ) { // TAGS
2019-04-01 21:43:13 +02:00
listType = BaseModel . TYPE_TAG ;
2020-07-28 19:50:34 +02:00
searchQuery = ` * ${ this . state . query . split ( ' ' ) [ 0 ] . substr ( 1 ) . trim ( ) } * ` ;
results = await Tag . searchAllWithNotes ( { titlePattern : searchQuery } ) ;
2019-04-01 21:43:13 +02:00
} else if ( this . state . query . indexOf ( '@' ) === 0 ) { // FOLDERS
listType = BaseModel . TYPE_FOLDER ;
2019-09-19 23:51:18 +02:00
searchQuery = ` * ${ this . state . query . split ( ' ' ) [ 0 ] . substr ( 1 ) . trim ( ) } * ` ;
2019-04-01 21:43:13 +02:00
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 : '/' } ) ;
}
2020-04-14 00:10:59 +02:00
} else { // Note TITLE or BODY
2020-03-28 15:05:00 +02:00
listType = BaseModel . TYPE_NOTE ;
2020-04-14 00:10:59 +02:00
searchQuery = this . makeSearchQuery ( this . state . query ) ;
2020-09-15 15:01:07 +02:00
results = await SearchEngine . instance ( ) . search ( searchQuery , { fuzzy : this.fuzzy_ } ) ;
2020-03-28 15:05:00 +02:00
2020-10-18 22:52:10 +02:00
resultsInBody = ! ! results . find ( ( row :any ) = > row . fields . includes ( 'body' ) ) ;
2020-03-28 15:05:00 +02:00
2020-07-15 00:27:12 +02:00
if ( ! resultsInBody || this . state . query . length <= 1 ) {
2020-04-14 00:10:59 +02:00
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 } ) ;
}
} else {
const limit = 20 ;
2020-09-11 23:52:32 +02:00
const searchKeywords = await this . keywords ( searchQuery ) ;
2020-10-18 22:52:10 +02:00
const notes = await Note . byIds ( results . map ( ( result :any ) = > result . id ) . slice ( 0 , limit ) , { fields : [ 'id' , 'body' , 'markup_language' , 'is_todo' , 'todo_completed' ] } ) ;
// Can't make any sense of this code so...
// @ts-ignore
2020-07-15 00:27:12 +02:00
const notesById = notes . reduce ( ( obj , { id , body , markup_language } ) = > ( ( obj [ [ id ] ] = { id , body , markup_language } ) , obj ) , { } ) ;
2020-04-14 00:10:59 +02:00
for ( let i = 0 ; i < results . length ; i ++ ) {
const row = results [ i ] ;
const path = Folder . folderPathString ( this . props . folders , row . parent_id ) ;
if ( row . fields . includes ( 'body' ) ) {
let fragments = '...' ;
if ( i < limit ) { // Display note fragments of search keyword matches
const indices = [ ] ;
2020-07-15 00:27:12 +02:00
const note = notesById [ row . id ] ;
const body = this . markupToHtml ( ) . stripMarkup ( note . markup_language , note . body , { collapseWhiteSpaces : true } ) ;
2020-04-14 00:10:59 +02:00
// Iterate over all matches in the body for each search keyword
2020-06-02 17:57:24 +02:00
for ( let { valueRegex } of searchKeywords ) {
valueRegex = removeDiacritics ( valueRegex ) ;
for ( const match of removeDiacritics ( body ) . matchAll ( new RegExp ( valueRegex , 'ig' ) ) ) {
2020-04-14 00:10:59 +02:00
// Populate 'indices' with [begin index, end index] of each note fragment
// Begins at the regex matching index, ends at the next whitespace after seeking 15 characters to the right
indices . push ( [ match . index , nextWhitespaceIndex ( body , match . index + match [ 0 ] . length + 15 ) ] ) ;
if ( indices . length > 20 ) break ;
}
}
// Merge multiple overlapping fragments into a single fragment to prevent repeated content
// e.g. 'Joplin is a free, open source' and 'open source note taking application'
// will result in 'Joplin is a free, open source note taking application'
const mergedIndices = mergeOverlappingIntervals ( indices , 3 ) ;
2020-10-18 22:52:10 +02:00
fragments = mergedIndices . map ( ( f :any ) = > body . slice ( f [ 0 ] , f [ 1 ] ) ) . join ( ' ... ' ) ;
2020-04-14 00:10:59 +02:00
// Add trailing ellipsis if the final fragment doesn't end where the note is ending
2020-04-20 20:01:28 +02:00
if ( mergedIndices . length && mergedIndices [ mergedIndices . length - 1 ] [ 1 ] !== body . length ) fragments += ' ...' ;
2020-07-15 00:27:12 +02:00
2020-03-28 15:05:00 +02:00
}
2020-04-14 00:10:59 +02:00
results [ i ] = Object . assign ( { } , row , { path , fragments } ) ;
} else {
results [ i ] = Object . assign ( { } , row , { path : path , fragments : '' } ) ;
}
2020-03-28 15:05:00 +02:00
}
2020-08-01 19:17:40 +02:00
if ( ! this . props . showCompletedTodos ) {
2020-10-18 22:52:10 +02:00
results = results . filter ( ( row :any ) = > ! row . is_todo || ! row . todo_completed ) ;
2020-08-01 19:17:40 +02:00
}
2019-04-01 21:43:13 +02:00
}
}
2020-06-03 19:01:17 +02:00
// make list scroll to top in every search
this . itemListRef . current . makeItemIndexVisible ( 0 ) ;
2019-04-01 21:43:13 +02:00
this . setState ( {
listType : listType ,
results : results ,
2020-10-18 22:52:10 +02:00
keywords : keywords ? keywords : await this . keywords ( searchQuery ) ,
2020-06-03 19:01:17 +02:00
selectedItemId : results.length === 0 ? null : results [ 0 ] . id ,
2020-04-14 00:10:59 +02:00
resultsInBody : resultsInBody ,
2019-04-01 21:43:13 +02:00
} ) ;
}
}
2020-10-18 22:52:10 +02:00
async gotoItem ( item :any ) {
2019-04-01 21:43:13 +02:00
this . props . dispatch ( {
pluginName : PLUGIN_NAME ,
2020-10-09 19:35:46 +02:00
type : 'PLUGINLEGACY_DIALOG_SET' ,
2019-04-01 21:43:13 +02:00
open : false ,
} ) ;
2020-10-18 22:52:10 +02:00
if ( item . type === BaseModel . TYPE_COMMAND ) {
CommandService . instance ( ) . execute ( item . id ) ;
return ;
}
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 ,
} ) ;
2020-05-09 17:17:11 +02:00
2020-10-18 22:52:10 +02:00
CommandService . instance ( ) . scheduleExecute ( 'focusElement' , 'noteBody' ) ;
2019-04-01 21:43:13 +02:00
} 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 ,
} ) ;
}
}
2020-10-18 22:52:10 +02:00
listItem_onClick ( event :any ) {
2019-04-01 21:43:13 +02:00
const itemId = event . currentTarget . getAttribute ( 'data-id' ) ;
const parentId = event . currentTarget . getAttribute ( 'data-parent-id' ) ;
2020-10-24 12:46:02 +02:00
const itemType = Number ( event . currentTarget . getAttribute ( 'data-type' ) ) ;
2019-04-01 21:43:13 +02:00
this . gotoItem ( {
id : itemId ,
parent_id : parentId ,
2020-10-18 22:52:10 +02:00
type : itemType ,
2019-04-01 21:43:13 +02:00
} ) ;
}
2020-10-18 22:52:10 +02:00
renderItem ( item :SearchResult ) {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-04-01 21:43:13 +02:00
const style = this . style ( ) ;
const rowStyle = item . id === this . state . selectedItemId ? style.rowSelected : style.row ;
2020-03-28 15:05:00 +02:00
const titleHtml = item . fragments
? ` <span style="font-weight: bold; color: ${ theme . colorBright } ;"> ${ item . title } </span> `
2020-07-04 13:57:19 +02:00
: surroundKeywords ( this . state . keywords , item . title , ` <span style="font-weight: bold; color: ${ theme . colorBright } ;"> ` , '</span>' , { escapeHtml : true } ) ;
2019-04-01 21:43:13 +02:00
2020-07-04 13:57:19 +02:00
const fragmentsHtml = ! item . fragments ? null : surroundKeywords ( this . state . keywords , item . fragments , ` <span style="font-weight: bold; color: ${ theme . colorBright } ;"> ` , '</span>' , { escapeHtml : true } ) ;
2020-04-14 00:10:59 +02:00
const folderIcon = < i style = { { fontSize : theme.fontSize , marginRight : 2 } } className = "fa fa-book" / > ;
const pathComp = ! item . path ? null : < div style = { style . rowPath } > { folderIcon } { item . path } < / div > ;
2020-07-04 13:57:19 +02:00
const fragmentComp = ! fragmentsHtml ? null : < div style = { style . rowFragments } dangerouslySetInnerHTML = { { __html : ( fragmentsHtml ) } } > < / div > ;
2019-04-01 21:43:13 +02:00
return (
2020-10-18 22:52:10 +02:00
< div key = { item . id } style = { rowStyle } onClick = { this . listItem_onClick } data - id = { item . id } data - parent - id = { item . parent_id } data - type = { item . type } >
2020-02-05 00:09:34 +02:00
< div style = { style . rowTitle } dangerouslySetInnerHTML = { { __html : titleHtml } } > < / div >
2020-04-14 00:10:59 +02:00
{ fragmentComp }
2019-04-01 21:43:13 +02:00
{ pathComp }
< / div >
) ;
}
2020-10-18 22:52:10 +02:00
selectedItemIndex ( results :any [ ] = undefined , itemId :string = undefined ) {
2019-04-01 21:43:13 +02:00
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 ] ;
}
2020-10-18 22:52:10 +02:00
input_onKeyDown ( event :any ) {
2019-04-01 21:43:13 +02:00
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() {
2020-04-14 00:10:59 +02:00
const style = this . style ( ) ;
const itemListStyle = {
2019-04-01 21:43:13 +02:00
marginTop : 5 ,
2020-10-18 22:52:10 +02:00
height : Math.min ( style . itemHeight * this . state . results . length , 10 * style . itemHeight ) ,
2019-04-01 21:43:13 +02:00
} ;
return (
< ItemList
ref = { this . itemListRef }
2020-04-14 00:10:59 +02:00
itemHeight = { style . itemHeight }
2019-04-01 21:43:13 +02:00
items = { this . state . results }
2020-04-14 00:10:59 +02:00
style = { itemListStyle }
2020-10-18 22:52:10 +02:00
itemRenderer = { this . renderItem }
2019-04-01 21:43:13 +02:00
/ >
) ;
}
render() {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-04-01 21:43:13 +02:00
const style = this . style ( ) ;
2020-10-18 22:52:10 +02:00
const helpComp = ! this . state . showHelp ? null : < div style = { style . help } > { _ ( 'Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.' ) } < / div > ;
2019-04-01 21:43:13 +02:00
return (
2020-03-30 19:33:36 +02:00
< div onClick = { this . modalLayer_onClick } style = { theme . dialogModalLayer } >
2019-04-01 21:43:13 +02:00
< 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 >
) ;
}
}
2020-10-18 22:52:10 +02:00
const mapStateToProps = ( state :AppState ) = > {
2019-04-01 21:43:13 +02:00
return {
folders : state.folders ,
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2020-08-01 19:17:40 +02:00
showCompletedTodos : state.settings.showCompletedTodos ,
2020-09-06 14:07:00 +02:00
highlightedWords : state.highlightedWords ,
2019-04-01 21:43:13 +02:00
} ;
} ;
GotoAnything . Dialog = connect ( mapStateToProps ) ( Dialog ) ;
GotoAnything . manifest = {
name : PLUGIN_NAME ,
menuItems : [
{
name : 'main' ,
2020-10-31 14:46:55 +02:00
parent : 'go' ,
2019-04-01 21:43:13 +02:00
label : _ ( 'Goto Anything...' ) ,
2020-08-02 13:26:55 +02:00
accelerator : ( ) = > KeymapService . instance ( ) . getAccelerator ( 'gotoAnything' ) ,
2019-04-01 21:43:13 +02:00
screens : [ 'Main' ] ,
} ,
2020-10-18 22:52:10 +02:00
{
name : 'main' ,
parent : 'tools' ,
label : _ ( 'Command palette' ) ,
accelerator : ( ) = > KeymapService . instance ( ) . getAccelerator ( 'commandPalette' ) ,
screens : [ 'Main' ] ,
userData : {
startString : ':' ,
} ,
} ,
2019-04-01 21:43:13 +02:00
] ,
} ;
2020-10-18 22:52:10 +02:00
export default GotoAnything ;