2020-10-18 22:52:10 +02:00
import * as React from 'react' ;
2021-09-04 19:11:29 +02:00
import { AppState } from '../app.reducer' ;
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' ;
2019-04-01 21:43:13 +02:00
const { connect } = require ( 'react-redux' ) ;
2023-01-29 15:11:23 +02:00
import { _ } from '@joplin/lib/locale' ;
import { themeStyle } from '@joplin/lib/theme' ;
2024-01-05 17:03:23 +02:00
import SearchEngine from '@joplin/lib/services/search/SearchEngine' ;
import gotoAnythingStyleQuery from '@joplin/lib/services/search/gotoAnythingStyleQuery' ;
2023-12-13 21:24:58 +02:00
import BaseModel , { ModelType } from '@joplin/lib/BaseModel' ;
2021-01-22 19:41:11 +02:00
import Tag from '@joplin/lib/models/Tag' ;
import Folder from '@joplin/lib/models/Folder' ;
import Note from '@joplin/lib/models/Note' ;
2023-01-29 15:11:23 +02:00
import ItemList from '../gui/ItemList' ;
2023-01-19 19:19:06 +02:00
import HelpButton from '../gui/HelpButton' ;
2020-11-07 17:59:37 +02:00
const { surroundKeywords , nextWhitespaceIndex , removeDiacritics } = require ( '@joplin/lib/string-utils.js' ) ;
2022-05-26 16:57:44 +02:00
import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils' ;
2021-01-27 19:42:58 +02:00
import markupLanguageUtils from '../utils/markupLanguageUtils' ;
2021-04-06 22:21:24 +02:00
import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand' ;
2023-07-27 17:05:56 +02:00
import Logger from '@joplin/utils/Logger' ;
2023-12-13 21:24:58 +02:00
import { MarkupLanguage , MarkupToHtml } from '@joplin/renderer' ;
import Resource from '@joplin/lib/models/Resource' ;
import { NoteEntity , ResourceEntity } from '@joplin/lib/services/database/types' ;
2021-08-05 15:40:54 +02:00
const logger = Logger . create ( 'GotoAnything' ) ;
2020-10-18 22:52:10 +02:00
const PLUGIN_NAME = 'gotoAnything' ;
2023-12-13 21:24:58 +02:00
interface GotoAnythingSearchResult {
2020-11-12 21:29:22 +02:00
id : string ;
title : string ;
parent_id : string ;
fields : string [ ] ;
fragments? : string ;
path? : string ;
type ? : number ;
2023-12-13 21:24:58 +02:00
item_id? : string ;
item_type? : ModelType ;
2020-10-18 22:52:10 +02:00
}
interface Props {
2020-11-12 21:29:22 +02:00
themeId : number ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
dispatch : Function ;
folders : any [ ] ;
showCompletedTodos : boolean ;
userData : any ;
2020-10-18 22:52:10 +02:00
}
interface State {
2020-11-12 21:29:22 +02:00
query : string ;
2023-12-13 21:24:58 +02:00
results : GotoAnythingSearchResult [ ] ;
2020-11-12 21:29:22 +02:00
selectedItemId : string ;
keywords : string [ ] ;
listType : number ;
showHelp : boolean ;
resultsInBody : boolean ;
2021-06-10 11:46:41 +02:00
commandArgs : string [ ] ;
}
interface CommandQuery {
name : string ;
args : string [ ] ;
2020-10-18 22:52:10 +02:00
}
2019-04-01 21:43:13 +02:00
2023-12-13 21:24:58 +02:00
const getContentMarkupLanguageAndBody = ( result : GotoAnythingSearchResult , notesById : Record < string , NoteEntity > , resources : ResourceEntity [ ] ) = > {
if ( result . item_type === ModelType . Resource ) {
const resource = resources . find ( r = > r . id === result . item_id ) ;
if ( ! resource ) {
logger . warn ( 'Could not find resources associated with result:' , result ) ;
return { markupLanguage : MarkupLanguage.Markdown , content : '' } ;
} else {
return { markupLanguage : MarkupLanguage.Markdown , content : resource.ocr_text } ;
}
} else { // a note
const note = notesById [ result . id ] ;
return { markupLanguage : note.markup_language , content : note.body } ;
}
} ;
// A result row contains an `id` property (the note ID) and, if the current row
// is a resource, an `item_id` property, which is the resource ID. In that case,
// the row also has an `id` property, which is the note that contains the
// resource.
//
// It means a result set may include multiple results with the same `id`
// property, if it contains one or more resources that are in a note that's
// already in the result set. For that reason, when we need a unique ID for the
// result, we use this function - which returns either the item_id, if present,
// or the note ID.
const getResultId = ( result : GotoAnythingSearchResult ) = > {
return result . item_id ? result.item_id : result.id ;
} ;
2019-04-01 21:43:13 +02:00
class GotoAnything {
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
public dispatch : Function ;
public static Dialog : any ;
public static manifest : any ;
2020-10-18 22:52:10 +02:00
2023-03-06 16:22:01 +02:00
public 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 > {
2020-11-12 21:13:28 +02:00
private styles_ : any ;
private inputRef : any ;
private itemListRef : any ;
private listUpdateIID_ : any ;
2022-01-15 18:53:24 +02:00
private markupToHtml_ : MarkupToHtml ;
2021-06-27 15:14:11 +02:00
private userCallback_ : any = null ;
2019-04-01 21:43:13 +02:00
2023-03-06 16:22:01 +02:00
public constructor ( props : Props ) {
2020-10-18 22:52:10 +02:00
super ( props ) ;
2019-04-01 21:43:13 +02:00
2020-10-18 22:52:10 +02:00
const startString = props ? . userData ? . startString ? props ? . userData ? . startString : '' ;
2021-06-27 15:14:11 +02:00
this . userCallback_ = props ? . userData ? . callback ;
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 ,
2021-06-10 11:46:41 +02:00
commandArgs : [ ] ,
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
}
2023-03-06 16:22:01 +02:00
public 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 ] = {
2023-06-01 13:02:36 +02:00
dialogBox : { . . . theme . dialogBox , minWidth : '50%' , maxWidth : '50%' } ,
input : { . . . 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' ,
} ,
2023-06-01 13:02:36 +02:00
help : { . . . 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
} ;
2021-10-24 20:12:25 +02:00
delete this . styles_ [ styleKey ] . dialogBox . maxHeight ;
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' ,
} ;
2023-06-01 13:02:36 +02:00
const rowTitleStyle = { . . . rowTextStyle , fontSize : rowTextStyle.fontSize * 1.4 ,
2020-04-14 00:10:59 +02:00
marginBottom : this.state.resultsInBody ? 6 : 4 ,
2023-06-01 13:02:36 +02:00
color : theme.colorFaded } ;
2020-03-28 15:05:00 +02:00
2023-06-01 13:02:36 +02:00
const rowFragmentsStyle = { . . . rowTextStyle , fontSize : rowTextStyle.fontSize * 1.2 ,
2020-04-14 00:10:59 +02:00
marginBottom : this.state.resultsInBody ? 8 : 6 ,
2023-06-01 13:02:36 +02:00
color : theme.colorFaded } ;
2019-04-01 21:43:13 +02:00
2023-06-01 13:02:36 +02:00
this . styles_ [ styleKey ] . rowSelected = { . . . this . styles_ [ styleKey ] . row , backgroundColor : theme.selectedColor } ;
2020-04-14 00:10:59 +02:00
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
}
2023-03-06 16:22:01 +02:00
public componentDidMount() {
2019-04-01 21:43:13 +02:00
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
}
2023-03-06 16:22:01 +02:00
public 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
}
2023-03-06 16:22:01 +02:00
public 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 ,
} ) ;
}
}
2023-03-06 16:22:01 +02:00
private modalLayer_onClick ( event : any ) {
2022-07-23 11:33:12 +02:00
if ( event . currentTarget === event . target ) {
2020-04-12 10:59:00 +02:00
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
}
2023-03-06 16:22:01 +02:00
private helpButton_onClick() {
2019-04-01 21:43:13 +02:00
this . setState ( { showHelp : ! this . state . showHelp } ) ;
}
2023-03-06 16:22:01 +02:00
private input_onChange ( event : any ) {
2019-04-01 21:43:13 +02:00
this . setState ( { query : event.target.value } ) ;
this . scheduleListUpdate ( ) ;
}
2023-03-06 16:22:01 +02:00
public 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
}
2023-03-06 16:22:01 +02:00
public async keywords ( searchQuery : string ) {
2021-03-11 00:27:45 +02:00
const parsedQuery = await SearchEngine . instance ( ) . parseQuery ( searchQuery ) ;
2020-09-11 23:52:32 +02:00
return SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
2019-04-01 21:43:13 +02:00
}
2023-03-06 16:22:01 +02:00
public markupToHtml() {
2020-07-15 00:27:12 +02:00
if ( this . markupToHtml_ ) return this . markupToHtml_ ;
2021-05-19 15:00:16 +02:00
this . markupToHtml_ = markupLanguageUtils . newMarkupToHtml ( ) ;
2020-07-15 00:27:12 +02:00
return this . markupToHtml_ ;
}
2021-06-10 11:46:41 +02:00
private parseCommandQuery ( query : string ) : CommandQuery {
const fullQuery = query ;
const splitted = fullQuery . split ( /\s+/ ) ;
return {
name : splitted.length ? splitted [ 0 ] : '' ,
args : splitted.slice ( 1 ) ,
} ;
}
2023-03-06 16:22:01 +02:00
public 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 {
2023-12-13 21:24:58 +02:00
let results : GotoAnythingSearchResult [ ] = [ ] ;
2019-04-01 21:43:13 +02:00
let listType = null ;
let searchQuery = '' ;
2020-10-18 22:52:10 +02:00
let keywords = null ;
2021-06-10 11:46:41 +02:00
let commandArgs : string [ ] = [ ] ;
2020-10-18 22:52:10 +02:00
if ( this . state . query . indexOf ( ':' ) === 0 ) { // COMMANDS
2021-06-10 11:46:41 +02:00
const commandQuery = this . parseCommandQuery ( this . state . query . substr ( 1 ) ) ;
2020-10-18 22:52:10 +02:00
listType = BaseModel . TYPE_COMMAND ;
2021-06-10 11:46:41 +02:00
keywords = [ commandQuery . name ] ;
commandArgs = commandQuery . args ;
2020-10-18 22:52:10 +02:00
2021-06-10 11:46:41 +02:00
const commandResults = CommandService . instance ( ) . searchCommands ( commandQuery . name , true ) ;
2020-10-18 22:52:10 +02:00
2020-11-12 21:13:28 +02:00
results = commandResults . map ( ( result : CommandSearchResult ) = > {
2020-10-18 22:52:10 +02:00
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 ) ;
2023-06-01 13:02:36 +02:00
results [ i ] = { . . . row , path : path ? path : '/' } ;
2019-04-01 21:43:13 +02:00
}
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 ;
2022-12-30 16:12:07 +02:00
searchQuery = gotoAnythingStyleQuery ( this . state . query ) ;
2023-10-25 15:09:26 +02:00
results = ( await SearchEngine . instance ( ) . search ( searchQuery ) ) as any [ ] ;
2020-03-28 15:05:00 +02:00
2020-11-12 21:13:28 +02:00
resultsInBody = ! ! results . find ( ( row : any ) = > row . fields . includes ( 'body' ) ) ;
2020-03-28 15:05:00 +02:00
2023-12-13 21:24:58 +02:00
const resourceIds = results . filter ( r = > r . item_type === ModelType . Resource ) . map ( r = > r . item_id ) ;
const resources = await Resource . resourceOcrTextsByIds ( resourceIds ) ;
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 ) ;
2023-06-01 13:02:36 +02:00
results [ i ] = { . . . row , path : path } ;
2020-04-14 00:10:59 +02:00
}
} else {
const limit = 20 ;
2020-09-11 23:52:32 +02:00
const searchKeywords = await this . keywords ( searchQuery ) ;
2024-02-16 19:40:51 +02:00
// Note: any filtering must be done **before** fetching the notes, because we're
// going to apply a limit to the number of fetched notes.
// https://github.com/laurent22/joplin/issues/9944
if ( ! this . props . showCompletedTodos ) {
results = results . filter ( ( row : any ) = > ! row . is_todo || ! row . todo_completed ) ;
}
2020-11-12 21:13:28 +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' ] } ) ;
2020-10-18 22:52:10 +02:00
// Can't make any sense of this code so...
2023-06-30 11:22:47 +02:00
const notesById = notes . reduce ( ( obj , { id , body , markup_language } ) = > ( ( obj [ [ id ] as any ] = { id , body , markup_language } ) , obj ) , { } ) ;
2020-04-14 00:10:59 +02:00
2021-09-04 15:26:29 +02:00
// Filter out search results that are associated with non-existing notes.
// https://github.com/laurent22/joplin/issues/5417
results = results . filter ( r = > ! ! notesById [ r . id ] ) ;
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
2023-12-13 21:24:58 +02:00
const { markupLanguage , content } = getContentMarkupLanguageAndBody (
row ,
notesById ,
resources ,
) ;
2020-04-14 00:10:59 +02:00
const indices = [ ] ;
2023-12-13 21:24:58 +02:00
const body = this . markupToHtml ( ) . stripMarkup ( markupLanguage , content , { 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-11-12 21:13:28 +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-03-28 15:05:00 +02:00
}
2023-06-01 13:02:36 +02:00
results [ i ] = { . . . row , path , fragments } ;
2020-04-14 00:10:59 +02:00
} else {
2023-06-01 13:02:36 +02:00
results [ i ] = { . . . row , path : path , fragments : '' } ;
2020-04-14 00:10:59 +02:00
}
2020-03-28 15:05:00 +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
2021-08-05 15:40:54 +02:00
this . 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 ) ,
2023-12-13 21:24:58 +02:00
selectedItemId : results.length === 0 ? null : getResultId ( results [ 0 ] ) ,
2020-04-14 00:10:59 +02:00
resultsInBody : resultsInBody ,
2021-06-10 11:46:41 +02:00
commandArgs : commandArgs ,
2019-04-01 21:43:13 +02:00
} ) ;
}
}
2021-08-05 15:40:54 +02:00
private makeItemIndexVisible ( index : number ) {
// Looks like it's not always defined
// https://github.com/laurent22/joplin/issues/5184#issuecomment-879714850
if ( ! this . itemListRef || ! this . itemListRef . current ) {
logger . warn ( 'Trying to set item index but the item list is not defined. Index: ' , index ) ;
return ;
}
this . itemListRef . current . makeItemIndexVisible ( index ) ;
}
2023-03-06 16:22:01 +02:00
public 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 ,
} ) ;
2021-06-27 15:14:11 +02:00
if ( this . userCallback_ ) {
2021-08-18 12:54:28 +02:00
logger . info ( 'gotoItem: user callback' , item ) ;
2021-06-27 15:14:11 +02:00
this . userCallback_ . resolve ( {
type : this . state . listType ,
item : { . . . item } ,
} ) ;
return ;
}
2020-10-18 22:52:10 +02:00
if ( item . type === BaseModel . TYPE_COMMAND ) {
2021-08-18 12:54:28 +02:00
logger . info ( 'gotoItem: execute command' , item ) ;
2021-06-10 11:46:41 +02:00
void CommandService . instance ( ) . execute ( item . id , . . . item . commandArgs ) ;
2021-04-06 22:21:24 +02:00
void focusEditorIfEditorCommand ( item . id , CommandService . instance ( ) ) ;
2020-10-18 22:52:10 +02:00
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 ) {
2021-08-18 12:54:28 +02:00
logger . info ( 'gotoItem: note' , item ) ;
2019-04-01 21:43:13 +02:00
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 ) {
2021-08-18 12:54:28 +02:00
logger . info ( 'gotoItem: tag' , item ) ;
2019-04-01 21:43:13 +02:00
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 ) {
2021-08-18 12:54:28 +02:00
logger . info ( 'gotoItem: folder' , item ) ;
2019-04-01 21:43:13 +02:00
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 ,
} ) ;
}
}
2023-03-06 16:22:01 +02:00
private 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
2020-11-25 16:40:25 +02:00
void this . gotoItem ( {
2019-04-01 21:43:13 +02:00
id : itemId ,
parent_id : parentId ,
2020-10-18 22:52:10 +02:00
type : itemType ,
2021-06-10 11:46:41 +02:00
commandArgs : this.state.commandArgs ,
2019-04-01 21:43:13 +02:00
} ) ;
}
2023-12-13 21:24:58 +02:00
public renderItem ( item : GotoAnythingSearchResult ) {
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 ( ) ;
2023-12-13 21:24:58 +02:00
const isSelected = getResultId ( item ) === this . state . selectedItemId ;
2021-05-17 20:33:44 +02:00
const rowStyle = isSelected ? style.rowSelected : style.row ;
2020-03-28 15:05:00 +02:00
const titleHtml = item . fragments
2021-12-28 11:57:34 +02:00
? ` <span style="font-weight: bold; color: ${ theme . color } ;"> ${ item . title } </span> `
: surroundKeywords ( this . state . keywords , item . title , ` <span style="font-weight: bold; color: ${ theme . searchMarkerColor } ; background-color: ${ theme . searchMarkerBackgroundColor } "> ` , '</span>' , { escapeHtml : true } ) ;
2019-04-01 21:43:13 +02:00
2021-12-28 11:57:34 +02:00
const fragmentsHtml = ! item . fragments ? null : surroundKeywords ( this . state . keywords , item . fragments , ` <span style="color: ${ theme . searchMarkerColor } ; background-color: ${ theme . searchMarkerBackgroundColor } "> ` , '</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 (
2023-12-13 21:24:58 +02:00
< div key = { getResultId ( item ) } className = { isSelected ? 'selected' : null } 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 >
) ;
}
2023-03-06 16:22:01 +02:00
public 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 ] ;
2023-12-13 21:24:58 +02:00
if ( getResultId ( r ) === itemId ) return i ;
2019-04-01 21:43:13 +02:00
}
return - 1 ;
}
2023-03-06 16:22:01 +02:00
public selectedItem() {
2019-04-01 21:43:13 +02:00
const index = this . selectedItemIndex ( ) ;
if ( index < 0 ) return null ;
2021-06-10 11:46:41 +02:00
return { . . . this . state . results [ index ] , commandArgs : this.state.commandArgs } ;
2019-04-01 21:43:13 +02:00
}
2023-03-06 16:22:01 +02:00
private 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 ;
2023-12-13 21:24:58 +02:00
const newId = getResultId ( this . state . results [ index ] ) ;
2019-04-01 21:43:13 +02:00
2021-08-05 15:40:54 +02:00
this . makeItemIndexVisible ( index ) ;
2019-04-01 21:43:13 +02:00
this . setState ( { selectedItemId : newId } ) ;
}
if ( keyCode === 13 ) { // ENTER
event . preventDefault ( ) ;
const item = this . selectedItem ( ) ;
if ( ! item ) return ;
2020-11-25 16:40:25 +02:00
void this . gotoItem ( item ) ;
2019-04-01 21:43:13 +02:00
}
}
2021-10-24 20:12:25 +02:00
private calculateMaxHeight ( itemHeight : number ) {
const maxItemCount = Math . floor ( ( 0.7 * window . innerHeight ) / itemHeight ) ;
return maxItemCount * itemHeight ;
}
2023-03-06 16:22:01 +02:00
public 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 ,
2021-10-24 20:12:25 +02:00
height : Math.min ( style . itemHeight * this . state . results . length , this . calculateMaxHeight ( 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
/ >
) ;
}
2023-03-06 16:22:01 +02:00
public 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 ( ) ;
2021-05-17 20:33:44 +02:00
const helpComp = ! this . state . showHelp ? null : < div className = "help-text" 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 (
2021-05-17 20:33:44 +02:00
< div className = "modal-layer" onClick = { this . modalLayer_onClick } style = { theme . dialogModalLayer } >
< div className = "modal-dialog" style = { style . dialogBox } >
2019-04-01 21:43:13 +02:00
{ helpComp }
< div style = { style . inputHelpWrapper } >
2021-04-06 22:21:24 +02:00
< input autoFocus type = "text" style = { style . input } ref = { this . inputRef } value = { this . state . query } onChange = { this . input_onChange } onKeyDown = { this . input_onKeyDown } / >
< HelpButton onClick = { this . helpButton_onClick } / >
2019-04-01 21:43:13 +02:00
< / div >
{ this . renderList ( ) }
< / div >
< / div >
) ;
}
}
2020-11-12 21:13:28 +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 : [
{
2021-08-18 12:54:28 +02:00
id : 'gotoAnything' ,
2019-04-01 21:43:13 +02:00
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
{
2021-08-18 12:54:28 +02:00
id : 'commandPalette' ,
2020-10-18 22:52:10 +02:00
name : 'main' ,
parent : 'tools' ,
label : _ ( 'Command palette' ) ,
accelerator : ( ) = > KeymapService . instance ( ) . getAccelerator ( 'commandPalette' ) ,
screens : [ 'Main' ] ,
userData : {
startString : ':' ,
} ,
} ,
2021-06-27 15:14:11 +02:00
{
id : 'controlledApi' ,
} ,
2019-04-01 21:43:13 +02:00
] ,
} ;
2020-10-18 22:52:10 +02:00
export default GotoAnything ;