@@ -8,50 +8,139 @@ 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 multiline = matchStr . indexOf ( '\n' ) != - 1
62
+ const beautifiedLine =
63
+ line . substring ( 0 , MAX_SEARCH_PREVIEW_LINE_LENGTH ) +
64
+ ( line . length > MAX_SEARCH_PREVIEW_LINE_LENGTH ? '...' : '' )
65
+ if ( multiline ) {
66
+ return (
67
+ < span >
68
+ < SearchMatchHighlight > { line } </ SearchMatchHighlight >
69
+ </ span >
70
+ )
71
+ } else {
72
+ return highlightMatchedTerm ( beautifiedLine , matchStr )
73
+ }
74
+ } ,
75
+ [ highlightMatchedTerm ]
76
+ )
77
+
25
78
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 }
79
+ < Container >
80
+ < MetaContainer onClick = { navigate } >
81
+ < div className = 'header' >
82
+ < div className = 'icon' >
83
+ < Icon path = { mdiTextBoxOutline } />
84
+ </ div >
85
+ < div className = 'title' > { note . title } </ div >
37
86
</ div >
38
- { note . tags . length > 0 && (
39
- < div className = 'tags ' >
40
- < Icon className = 'icon' path = { mdiTagMultiple } /> { ' ' }
41
- { note . tags . map ( ( tag ) => tag ) . join ( ', ' ) }
87
+ < div className = 'meta' >
88
+ < div className = 'folderPathname ' >
89
+ < Icon className = 'icon' path = { mdiFolder } />
90
+ { note . folderPathname }
42
91
</ div >
43
- ) }
44
- </ div >
92
+ { note . tags . length > 0 && (
93
+ < div className = 'tags' >
94
+ < Icon className = 'icon' path = { mdiTagMultiple } /> { ' ' }
95
+ { note . tags . map ( ( tag ) => tag ) . join ( ', ' ) }
96
+ </ div >
97
+ ) }
98
+ </ div >
99
+ </ MetaContainer >
100
+
101
+ < SearchResultContainer >
102
+ { searchResults . length > 0 &&
103
+ searchResults . map ( ( result ) => (
104
+ < SearchResultItem
105
+ className = {
106
+ selectedItemId == result . id ? 'search-result-selected' : ''
107
+ }
108
+ key = { getSearchResultKey ( note . _id , result . id ) }
109
+ onClick = { ( ) => updateSelectedItem ( note , result . id ) }
110
+ onDoubleClick = { ( ) =>
111
+ navigateToEditorFocused (
112
+ note . _id ,
113
+ result . lineNum - 1 ,
114
+ result . matchColumn
115
+ )
116
+ }
117
+ >
118
+ < SearchResultLeft title = { result . lineStr } >
119
+ { beautifyPreviewLine ( result . lineStr , result . matchStr ) }
120
+ </ SearchResultLeft >
121
+ < SearchResultRight > { result . lineNum } </ SearchResultRight >
122
+ </ SearchResultItem >
123
+ ) ) }
124
+ </ SearchResultContainer >
45
125
</ Container >
46
126
)
47
127
}
48
128
49
129
export default SearchModalNoteResultItem
50
130
51
- const Container = styled . div `
131
+ const Container = styled . div ``
132
+
133
+ const SearchResultContainer = styled . div `
134
+ padding: 10px;
135
+ cursor: pointer;
136
+ ${ borderBottom } ;
137
+ user-select: none;
138
+ `
139
+
140
+ const MetaContainer = styled . div `
52
141
padding: 10px;
53
142
cursor: pointer;
54
- ${ borderBottom }
143
+ ${ borderBottom } ;
55
144
user-select: none;
56
145
57
146
&:hover {
@@ -60,6 +149,7 @@ const Container = styled.div`
60
149
&:hover:active {
61
150
background-color: ${ ( { theme } ) => theme . navItemHoverActiveBackgroundColor } ;
62
151
}
152
+
63
153
& > .header {
64
154
font-size: 18px;
65
155
display: flex;
@@ -86,7 +176,7 @@ const Container = styled.div`
86
176
& > .folderPathname {
87
177
display: flex;
88
178
align-items: center;
89
- max-width: 150px ;
179
+ max-width: 350px ;
90
180
${ textOverflow }
91
181
&>.icon {
92
182
margin-right: 4px;
@@ -97,7 +187,7 @@ const Container = styled.div`
97
187
margin-left: 8px;
98
188
display: flex;
99
189
align-items: center;
100
- max-width: 150px ;
190
+ max-width: 350px ;
101
191
${ textOverflow }
102
192
&>.icon {
103
193
margin-right: 4px;
@@ -109,3 +199,50 @@ const Container = styled.div`
109
199
border-bottom: none;
110
200
}
111
201
`
202
+
203
+ const SearchResultItem = styled . div `
204
+ display: flex;
205
+ flex-direction: row;
206
+ width: 100%;
207
+ height: 100%;
208
+ justify-content: space-between;
209
+ overflow: hidden;
210
+
211
+ margin-top: 0.3em;
212
+
213
+ &.search-result-selected {
214
+ border-radius: 4px;
215
+ padding: 2px;
216
+ background-color: ${ ( { theme } ) =>
217
+ theme . searchItemSelectionBackgroundColor } ;
218
+ filter: brightness(
219
+ ${ ( { theme } ) => ( isColorBright ( theme . activeBackgroundColor ) ? 85 : 115 ) } %
220
+ );
221
+ }
222
+ }
223
+
224
+ &:hover {
225
+ border-radius: 4px;
226
+ background-color: ${ ( { theme } ) =>
227
+ theme . secondaryButtonHoverBackgroundColor } ;
228
+ filter: brightness(
229
+ ${ ( { theme } ) =>
230
+ isColorBright ( theme . secondaryButtonHoverBackgroundColor ) ? 85 : 115 } %
231
+ );
232
+ }
233
+ `
234
+
235
+ const SearchResultLeft = styled . div `
236
+ align-self: flex-start;
237
+ text-overflow: ellipsis;
238
+ white-space: nowrap;
239
+ overflow: hidden;
240
+
241
+ &:before {
242
+ content: attr(content);
243
+ }
244
+ `
245
+
246
+ const SearchResultRight = styled . div `
247
+ align-self: flex-end;
248
+ `
0 commit comments