Skip to content

Commit 01a2bf7

Browse files
committed
Add initial global search setup
Add search functionality and result data structures Add preview of found items, highlighted style, the rows of found items Add note preview Add selection of items in search Add text highlight in editor preview Add editor preview focus Add editor preview while clicking on found item Add better ID management and state update fix Add some stability updates (max searched content, max result line length) Add double click on search item to lead to focused item in editor Add focusLine number for editor to route params Add better colors across themes for search Fix multiline search Add all previous features in multiline search Focus at correct item on multiple items (local column) Focus at correct editor position (line and column) via route hash
1 parent ca2ebab commit 01a2bf7

19 files changed

+704
-76
lines changed

src/components/PreferencesModal/styled.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
tableStyle,
88
disabledUiTextColor,
99
PrimaryTextColor,
10+
searchMatchHighlightStyle,
1011
} from '../../lib/styled/styleFunctions'
1112

1213
export const Section = styled.section`
@@ -43,7 +44,7 @@ export const SectionControl = styled.div`
4344
`
4445

4546
export const SectionSelect = styled.select`
46-
${selectStyle}
47+
${selectStyle};
4748
padding: 0 16px;
4849
width: 200px;
4950
height: 40px;
@@ -56,7 +57,7 @@ export const SectionSelect = styled.select`
5657
`
5758

5859
export const SectionPrimaryButton = styled.button`
59-
${primaryButtonStyle}
60+
${primaryButtonStyle};
6061
padding: 0 16px;
6162
height: 40px;
6263
border-radius: 2px;
@@ -66,7 +67,7 @@ export const SectionPrimaryButton = styled.button`
6667
`
6768

6869
export const SectionSecondaryButton = styled.button`
69-
${secondaryButtonStyle}
70+
${secondaryButtonStyle};
7071
padding: 0 16px;
7172
height: 40px;
7273
border-radius: 2px;
@@ -75,7 +76,7 @@ export const SectionSecondaryButton = styled.button`
7576
`
7677

7778
export const SectionInput = styled.input`
78-
${inputStyle}
79+
${inputStyle};
7980
padding: 0 16px;
8081
width: 200px;
8182
height: 40px;
@@ -96,11 +97,24 @@ export const TopMargin = styled.div`
9697
`
9798

9899
export const DeleteStorageButton = styled.button`
99-
${secondaryButtonStyle}
100+
${secondaryButtonStyle};
100101
padding: 0 16px;
101102
height: 40px;
102103
border-radius: 2px;
103104
cursor: pointer;
104105
vertical-align: middle;
105106
align-items: center;
106107
`
108+
109+
export const SectionListSelect = styled.div`
110+
${selectStyle};
111+
padding: 0 16px;
112+
width: 200px;
113+
height: 40px;
114+
border-radius: 2px;
115+
font-size: 14px;
116+
`
117+
118+
export const SearchMatchHighlight = styled.span`
119+
${searchMatchHighlightStyle}
120+
`

src/components/molecules/SearchModalNoteResultItem.tsx

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,50 +8,130 @@ import {
88
borderBottom,
99
textOverflow,
1010
} 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'
1119

1220
interface SearchModalNoteResultItemProps {
1321
note: NoteDoc
22+
selectedItemId: string
23+
searchResults: SearchResult[]
1424
navigateToNote: (noteId: string) => void
25+
updateSelectedItem: (note: NoteDoc, selectedId: string) => void
26+
navigateToEditorFocused: (
27+
noteId: string,
28+
lineNum: number,
29+
lineColumn?: number
30+
) => void
1531
}
1632

1733
const SearchModalNoteResultItem = ({
1834
note,
35+
searchResults,
1936
navigateToNote,
37+
selectedItemId,
38+
updateSelectedItem,
39+
navigateToEditorFocused,
2040
}: SearchModalNoteResultItemProps) => {
2141
const navigate = useCallback(() => {
2242
navigateToNote(note._id)
2343
}, [navigateToNote, note._id])
2444

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+
2569
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>
3777
</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}
4282
</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>
45116
</Container>
46117
)
47118
}
48119

49120
export default SearchModalNoteResultItem
50121

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`
52132
padding: 10px;
53133
cursor: pointer;
54-
${borderBottom}
134+
${borderBottom};
55135
user-select: none;
56136
57137
&:hover {
@@ -60,6 +140,7 @@ const Container = styled.div`
60140
&:hover:active {
61141
background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor};
62142
}
143+
63144
& > .header {
64145
font-size: 18px;
65146
display: flex;
@@ -86,7 +167,7 @@ const Container = styled.div`
86167
& > .folderPathname {
87168
display: flex;
88169
align-items: center;
89-
max-width: 150px;
170+
max-width: 350px;
90171
${textOverflow}
91172
&>.icon {
92173
margin-right: 4px;
@@ -97,7 +178,7 @@ const Container = styled.div`
97178
margin-left: 8px;
98179
display: flex;
99180
align-items: center;
100-
max-width: 150px;
181+
max-width: 350px;
101182
${textOverflow}
102183
&>.icon {
103184
margin-right: 4px;
@@ -109,3 +190,50 @@ const Container = styled.div`
109190
border-bottom: none;
110191
}
111192
`
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+
`

src/components/organisms/NoteDetail.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type NoteDetailProps = {
3434
props: Partial<NoteDocEditibleProps>
3535
) => Promise<void | NoteDoc>
3636
viewMode: ViewModeType
37+
initialCursorPosition: EditorPosition
3738
addAttachments(storageId: string, files: File[]): Promise<Attachment[]>
3839
}
3940

@@ -73,6 +74,12 @@ class NoteDetail extends React.Component<NoteDetailProps, NoteDetailState> {
7374
codeMirror?: CodeMirror.EditorFromTextArea
7475
codeMirrorRef = (codeMirror: CodeMirror.EditorFromTextArea) => {
7576
this.codeMirror = codeMirror
77+
78+
// Update cursor if needed
79+
if (this.props.initialCursorPosition) {
80+
this.codeMirror.focus()
81+
this.codeMirror.setCursor(this.props.initialCursorPosition)
82+
}
7683
}
7784

7885
static getDerivedStateFromProps(
@@ -86,8 +93,10 @@ class NoteDetail extends React.Component<NoteDetailProps, NoteDetailState> {
8693
prevNoteId: note._id,
8794
content: note.content,
8895
currentCursor: {
89-
line: 0,
90-
ch: 0,
96+
line: props.initialCursorPosition
97+
? props.initialCursorPosition.line
98+
: 0,
99+
ch: props.initialCursorPosition ? props.initialCursorPosition.ch : 0,
91100
},
92101
currentSelections: [
93102
{
@@ -285,13 +294,13 @@ class NoteDetail extends React.Component<NoteDetailProps, NoteDetailState> {
285294
}
286295

287296
render() {
288-
const { note, storage, viewMode } = this.props
297+
const { note, storage, viewMode, initialCursorPosition } = this.props
289298
const { currentCursor, currentSelections } = this.state
290299

291300
const codeEditor = (
292301
<CustomizedCodeEditor
293302
className='editor'
294-
key={note._id}
303+
key={note._id + initialCursorPosition.line}
295304
codeMirrorRef={this.codeMirrorRef}
296305
value={this.state.content}
297306
onChange={this.updateContent}

0 commit comments

Comments
 (0)