@@ -8,50 +8,130 @@ import {
8
8
borderBottom ,
9
9
textOverflow ,
10
10
} from '../../lib/styled/styleFunctions'
11
+ import {
12
+ getSearchResultKey ,
13
+ MAX_SEARCH_PREVIEW_LINE_LENGTH ,
14
+ SearchResult ,
15
+ } from '../../lib/search/search'
16
+ import { isColorBright } from '../../lib/colors'
17
+ import { SearchMatchHighlight } from '../PreferencesModal/styled'
18
+ import { escapeRegExp } from '../../lib/string'
11
19
12
20
interface SearchModalNoteResultItemProps {
13
21
note : NoteDoc
22
+ selectedItemId : string
23
+ searchResults : SearchResult [ ]
14
24
navigateToNote : ( noteId : string ) => void
25
+ updateSelectedItem : ( note : NoteDoc , selectedId : string ) => void
26
+ navigateToEditorFocused : (
27
+ noteId : string ,
28
+ lineNum : number ,
29
+ lineColumn ?: number
30
+ ) => void
15
31
}
16
32
17
33
const SearchModalNoteResultItem = ( {
18
34
note,
35
+ searchResults,
19
36
navigateToNote,
37
+ selectedItemId,
38
+ updateSelectedItem,
39
+ navigateToEditorFocused,
20
40
} : SearchModalNoteResultItemProps ) => {
21
41
const navigate = useCallback ( ( ) => {
22
42
navigateToNote ( note . _id )
23
43
} , [ navigateToNote , note . _id ] )
24
44
45
+ const highlightMatchedTerm = useCallback ( ( line , matchStr ) => {
46
+ const parts = line . split ( new RegExp ( `(${ escapeRegExp ( matchStr ) } )` , 'gi' ) )
47
+ return (
48
+ < span >
49
+ { parts . map ( ( part : string , i : number ) =>
50
+ part . toLowerCase ( ) === matchStr . toLowerCase ( ) ? (
51
+ < SearchMatchHighlight key = { i } > { matchStr } </ SearchMatchHighlight >
52
+ ) : (
53
+ part
54
+ )
55
+ ) }
56
+ </ span >
57
+ )
58
+ } , [ ] )
59
+ const beautifyPreviewLine = useCallback (
60
+ ( line , matchStr ) => {
61
+ const beautifiedLine =
62
+ line . substring ( 0 , MAX_SEARCH_PREVIEW_LINE_LENGTH ) +
63
+ ( line . length > MAX_SEARCH_PREVIEW_LINE_LENGTH ? '...' : '' )
64
+ return highlightMatchedTerm ( beautifiedLine , matchStr )
65
+ } ,
66
+ [ highlightMatchedTerm ]
67
+ )
68
+
25
69
return (
26
- < Container onClick = { navigate } >
27
- < div className = 'header' >
28
- < div className = 'icon' >
29
- < Icon path = { mdiTextBoxOutline } />
30
- </ div >
31
- < div className = 'title' > { note . title } </ div >
32
- </ div >
33
- < div className = 'meta' >
34
- < div className = 'folderPathname' >
35
- < Icon className = 'icon' path = { mdiFolder } />
36
- { note . folderPathname }
70
+ < Container >
71
+ < MetaContainer onClick = { navigate } >
72
+ < div className = 'header' >
73
+ < div className = 'icon' >
74
+ < Icon path = { mdiTextBoxOutline } />
75
+ </ div >
76
+ < div className = 'title' > { note . title } </ div >
37
77
</ div >
38
- { note . tags . length > 0 && (
39
- < div className = 'tags ' >
40
- < Icon className = 'icon' path = { mdiTagMultiple } /> { ' ' }
41
- { note . tags . map ( ( tag ) => tag ) . join ( ', ' ) }
78
+ < div className = 'meta' >
79
+ < div className = 'folderPathname ' >
80
+ < Icon className = 'icon' path = { mdiFolder } />
81
+ { note . folderPathname }
42
82
</ div >
43
- ) }
44
- </ div >
83
+ { note . tags . length > 0 && (
84
+ < div className = 'tags' >
85
+ < Icon className = 'icon' path = { mdiTagMultiple } /> { ' ' }
86
+ { note . tags . map ( ( tag ) => tag ) . join ( ', ' ) }
87
+ </ div >
88
+ ) }
89
+ </ div >
90
+ </ MetaContainer >
91
+
92
+ < SearchResultContainer >
93
+ { searchResults . length > 0 &&
94
+ searchResults . map ( ( result ) => (
95
+ < SearchResultItem
96
+ className = {
97
+ selectedItemId == result . id ? 'search-result-selected' : ''
98
+ }
99
+ key = { getSearchResultKey ( note . _id , result . id ) }
100
+ onClick = { ( ) => updateSelectedItem ( note , result . id ) }
101
+ onDoubleClick = { ( ) =>
102
+ navigateToEditorFocused (
103
+ note . _id ,
104
+ result . lineNum - 1 ,
105
+ result . matchColumn
106
+ )
107
+ }
108
+ >
109
+ < SearchResultLeft title = { result . lineStr } >
110
+ { beautifyPreviewLine ( result . lineStr , result . matchStr ) }
111
+ </ SearchResultLeft >
112
+ < SearchResultRight > { result . lineNum } </ SearchResultRight >
113
+ </ SearchResultItem >
114
+ ) ) }
115
+ </ SearchResultContainer >
45
116
</ Container >
46
117
)
47
118
}
48
119
49
120
export default SearchModalNoteResultItem
50
121
51
- const Container = styled . div `
122
+ const Container = styled . div ``
123
+
124
+ const SearchResultContainer = styled . div `
125
+ padding: 10px;
126
+ cursor: pointer;
127
+ ${ borderBottom } ;
128
+ user-select: none;
129
+ `
130
+
131
+ const MetaContainer = styled . div `
52
132
padding: 10px;
53
133
cursor: pointer;
54
- ${ borderBottom }
134
+ ${ borderBottom } ;
55
135
user-select: none;
56
136
57
137
&:hover {
@@ -60,6 +140,7 @@ const Container = styled.div`
60
140
&:hover:active {
61
141
background-color: ${ ( { theme } ) => theme . navItemHoverActiveBackgroundColor } ;
62
142
}
143
+
63
144
& > .header {
64
145
font-size: 18px;
65
146
display: flex;
@@ -86,7 +167,7 @@ const Container = styled.div`
86
167
& > .folderPathname {
87
168
display: flex;
88
169
align-items: center;
89
- max-width: 150px ;
170
+ max-width: 350px ;
90
171
${ textOverflow }
91
172
&>.icon {
92
173
margin-right: 4px;
@@ -97,7 +178,7 @@ const Container = styled.div`
97
178
margin-left: 8px;
98
179
display: flex;
99
180
align-items: center;
100
- max-width: 150px ;
181
+ max-width: 350px ;
101
182
${ textOverflow }
102
183
&>.icon {
103
184
margin-right: 4px;
@@ -109,3 +190,50 @@ const Container = styled.div`
109
190
border-bottom: none;
110
191
}
111
192
`
193
+
194
+ const SearchResultItem = styled . div `
195
+ display: flex;
196
+ flex-direction: row;
197
+ width: 100%;
198
+ height: 100%;
199
+ justify-content: space-between;
200
+ overflow: hidden;
201
+
202
+ margin-top: 0.3em;
203
+
204
+ &.search-result-selected {
205
+ border-radius: 4px;
206
+ padding: 2px;
207
+ background-color: ${ ( { theme } ) =>
208
+ theme . searchItemSelectionBackgroundColor } ;
209
+ filter: brightness(
210
+ ${ ( { theme } ) => ( isColorBright ( theme . activeBackgroundColor ) ? 85 : 115 ) } %
211
+ );
212
+ }
213
+ }
214
+
215
+ &:hover {
216
+ border-radius: 4px;
217
+ background-color: ${ ( { theme } ) =>
218
+ theme . secondaryButtonHoverBackgroundColor } ;
219
+ filter: brightness(
220
+ ${ ( { theme } ) =>
221
+ isColorBright ( theme . secondaryButtonHoverBackgroundColor ) ? 85 : 115 } %
222
+ );
223
+ }
224
+ `
225
+
226
+ const SearchResultLeft = styled . div `
227
+ align-self: flex-start;
228
+ text-overflow: ellipsis;
229
+ white-space: nowrap;
230
+ overflow: hidden;
231
+
232
+ &:before {
233
+ content: attr(content);
234
+ }
235
+ `
236
+
237
+ const SearchResultRight = styled . div `
238
+ align-self: flex-end;
239
+ `
0 commit comments