From ee9fb64a877298aaddaa77af5dd3307c332abe10 Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Tue, 1 Dec 2020 22:21:14 +0100 Subject: [PATCH 01/13] 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 --- src/components/PreferencesModal/styled.tsx | 24 +- .../molecules/SearchModalNoteResultItem.tsx | 179 +++++++++-- src/components/organisms/NoteDetail.tsx | 17 +- .../organisms/NoteStorageNavigator.tsx | 10 +- src/components/organisms/SearchModal.tsx | 279 +++++++++++++++--- src/components/pages/NotePage.tsx | 23 +- src/components/pages/WikiNotePage.tsx | 35 ++- src/lib/colors.ts | 40 +++ src/lib/keyboard.ts | 11 + src/lib/search/search.ts | 87 ++++++ src/lib/storageRouter.ts | 19 +- src/lib/string.ts | 8 + src/lib/styled/BaseTheme.ts | 4 + src/lib/styled/styleFunctions.ts | 31 ++ src/themes/dark.ts | 4 + src/themes/legacy.ts | 4 + src/themes/light.ts | 4 + src/themes/sepia.ts | 4 + src/themes/solarizedDark.ts | 4 + 19 files changed, 712 insertions(+), 75 deletions(-) create mode 100644 src/lib/colors.ts create mode 100644 src/lib/search/search.ts diff --git a/src/components/PreferencesModal/styled.tsx b/src/components/PreferencesModal/styled.tsx index 91929b210e..83696b0929 100644 --- a/src/components/PreferencesModal/styled.tsx +++ b/src/components/PreferencesModal/styled.tsx @@ -7,6 +7,7 @@ import { tableStyle, disabledUiTextColor, PrimaryTextColor, + searchMatchHighlightStyle, } from '../../lib/styled/styleFunctions' export const Section = styled.section` @@ -43,7 +44,7 @@ export const SectionControl = styled.div` ` export const SectionSelect = styled.select` - ${selectStyle} + ${selectStyle}; padding: 0 16px; width: 200px; height: 40px; @@ -56,7 +57,7 @@ export const SectionSelect = styled.select` ` export const SectionPrimaryButton = styled.button` - ${primaryButtonStyle} + ${primaryButtonStyle}; padding: 0 16px; height: 40px; border-radius: 2px; @@ -66,7 +67,7 @@ export const SectionPrimaryButton = styled.button` ` export const SectionSecondaryButton = styled.button` - ${secondaryButtonStyle} + ${secondaryButtonStyle}; padding: 0 16px; height: 40px; border-radius: 2px; @@ -75,7 +76,7 @@ export const SectionSecondaryButton = styled.button` ` export const SectionInput = styled.input` - ${inputStyle} + ${inputStyle}; padding: 0 16px; width: 200px; height: 40px; @@ -96,7 +97,7 @@ export const TopMargin = styled.div` ` export const DeleteStorageButton = styled.button` - ${secondaryButtonStyle} + ${secondaryButtonStyle}; padding: 0 16px; height: 40px; border-radius: 2px; @@ -104,3 +105,16 @@ export const DeleteStorageButton = styled.button` vertical-align: middle; align-items: center; ` + +export const SectionListSelect = styled.div` + ${selectStyle}; + padding: 0 16px; + width: 200px; + height: 40px; + border-radius: 2px; + font-size: 14px; +` + +export const SearchMatchHighlight = styled.span` + ${searchMatchHighlightStyle} +` diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index 4595124196..df9e194ea0 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -8,50 +8,139 @@ import { borderBottom, textOverflow, } from '../../lib/styled/styleFunctions' +import { + getSearchResultKey, + MAX_SEARCH_PREVIEW_LINE_LENGTH, + SearchResult, +} from '../../lib/search/search' +import { isColorBright } from '../../lib/colors' +import { SearchMatchHighlight } from '../PreferencesModal/styled' +import { escapeRegExp } from '../../lib/string' interface SearchModalNoteResultItemProps { note: NoteDoc + selectedItemId: string + searchResults: SearchResult[] navigateToNote: (noteId: string) => void + updateSelectedItem: (note: NoteDoc, selectedId: string) => void + navigateToEditorFocused: ( + noteId: string, + lineNum: number, + lineColumn?: number + ) => void } const SearchModalNoteResultItem = ({ note, + searchResults, navigateToNote, + selectedItemId, + updateSelectedItem, + navigateToEditorFocused, }: SearchModalNoteResultItemProps) => { const navigate = useCallback(() => { navigateToNote(note._id) }, [navigateToNote, note._id]) + const highlightMatchedTerm = useCallback((line, matchStr) => { + const parts = line.split(new RegExp(`(${escapeRegExp(matchStr)})`, 'gi')) + return ( + + {parts.map((part: string, i: number) => + part.toLowerCase() === matchStr.toLowerCase() ? ( + {matchStr} + ) : ( + part + ) + )} + + ) + }, []) + const beautifyPreviewLine = useCallback( + (line, matchStr) => { + const multiline = matchStr.indexOf('\n') != -1 + const beautifiedLine = + line.substring(0, MAX_SEARCH_PREVIEW_LINE_LENGTH) + + (line.length > MAX_SEARCH_PREVIEW_LINE_LENGTH ? '...' : '') + if (multiline) { + return ( + + {line} + + ) + } else { + return highlightMatchedTerm(beautifiedLine, matchStr) + } + }, + [highlightMatchedTerm] + ) + return ( - -
-
- -
-
{note.title}
-
-
-
- - {note.folderPathname} + + +
+
+ +
+
{note.title}
- {note.tags.length > 0 && ( -
- {' '} - {note.tags.map((tag) => tag).join(', ')} +
+
+ + {note.folderPathname}
- )} -
+ {note.tags.length > 0 && ( +
+ {' '} + {note.tags.map((tag) => tag).join(', ')} +
+ )} +
+
+ + + {searchResults.length > 0 && + searchResults.map((result) => ( + updateSelectedItem(note, result.id)} + onDoubleClick={() => + navigateToEditorFocused( + note._id, + result.lineNum - 1, + result.matchColumn + ) + } + > + + {beautifyPreviewLine(result.lineStr, result.matchStr)} + + {result.lineNum} + + ))} +
) } export default SearchModalNoteResultItem -const Container = styled.div` +const Container = styled.div`` + +const SearchResultContainer = styled.div` + padding: 10px; + cursor: pointer; + ${borderBottom}; + user-select: none; +` + +const MetaContainer = styled.div` padding: 10px; cursor: pointer; - ${borderBottom} + ${borderBottom}; user-select: none; &:hover { @@ -60,6 +149,7 @@ const Container = styled.div` &:hover:active { background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor}; } + & > .header { font-size: 18px; display: flex; @@ -86,7 +176,7 @@ const Container = styled.div` & > .folderPathname { display: flex; align-items: center; - max-width: 150px; + max-width: 350px; ${textOverflow} &>.icon { margin-right: 4px; @@ -97,7 +187,7 @@ const Container = styled.div` margin-left: 8px; display: flex; align-items: center; - max-width: 150px; + max-width: 350px; ${textOverflow} &>.icon { margin-right: 4px; @@ -109,3 +199,50 @@ const Container = styled.div` border-bottom: none; } ` + +const SearchResultItem = styled.div` + display: flex; + flex-direction: row; + width: 100%; + height: 100%; + justify-content: space-between; + overflow: hidden; + + margin-top: 0.3em; + + &.search-result-selected { + border-radius: 4px; + padding: 2px; + background-color: ${({ theme }) => + theme.searchItemSelectionBackgroundColor}; + filter: brightness( + ${({ theme }) => (isColorBright(theme.activeBackgroundColor) ? 85 : 115)}% + ); + } + } + + &:hover { + border-radius: 4px; + background-color: ${({ theme }) => + theme.secondaryButtonHoverBackgroundColor}; + filter: brightness( + ${({ theme }) => + isColorBright(theme.secondaryButtonHoverBackgroundColor) ? 85 : 115}% + ); + } +` + +const SearchResultLeft = styled.div` + align-self: flex-start; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + &:before { + content: attr(content); + } +` + +const SearchResultRight = styled.div` + align-self: flex-end; +` diff --git a/src/components/organisms/NoteDetail.tsx b/src/components/organisms/NoteDetail.tsx index 290a6a06e6..745530db56 100644 --- a/src/components/organisms/NoteDetail.tsx +++ b/src/components/organisms/NoteDetail.tsx @@ -34,6 +34,7 @@ type NoteDetailProps = { props: Partial ) => Promise viewMode: ViewModeType + initialCursorPosition: EditorPosition addAttachments(storageId: string, files: File[]): Promise } @@ -73,6 +74,12 @@ class NoteDetail extends React.Component { codeMirror?: CodeMirror.EditorFromTextArea codeMirrorRef = (codeMirror: CodeMirror.EditorFromTextArea) => { this.codeMirror = codeMirror + + // Update cursor if needed + if (this.props.initialCursorPosition) { + this.codeMirror.focus() + this.codeMirror.setCursor(this.props.initialCursorPosition) + } } static getDerivedStateFromProps( @@ -86,8 +93,10 @@ class NoteDetail extends React.Component { prevNoteId: note._id, content: note.content, currentCursor: { - line: 0, - ch: 0, + line: props.initialCursorPosition + ? props.initialCursorPosition.line + : 0, + ch: props.initialCursorPosition ? props.initialCursorPosition.ch : 0, }, currentSelections: [ { @@ -285,13 +294,13 @@ class NoteDetail extends React.Component { } render() { - const { note, storage, viewMode } = this.props + const { note, storage, viewMode, initialCursorPosition } = this.props const { currentCursor, currentSelections } = this.state const codeEditor = ( theme.secondaryButtonLabelColor}; background-color: ${({ theme }) => theme.secondaryButtonBackgroundColor}; border: none; @@ -345,7 +344,7 @@ const SearchButton = styled.button` & > .icon { width: 24px; height: 24px; - ${flexCenter} + ${flexCenter}; flex-shrink: 0; } & > .label { @@ -357,7 +356,7 @@ const SearchButton = styled.button` display: none; font-size: 12px; margin-left: 5px; - ${textOverflow} + ${textOverflow}; align-items: center; flex-shrink: 0; } @@ -366,7 +365,6 @@ const SearchButton = styled.button` const NewNoteButton = styled.button` margin: 8px 8px; height: 34px; - padding: 0; color: ${({ theme }) => theme.primaryButtonLabelColor}; background-color: ${({ theme }) => theme.primaryButtonBackgroundColor}; border: none; @@ -387,7 +385,7 @@ const NewNoteButton = styled.button` & > .icon { width: 24px; height: 24px; - ${flexCenter} + ${flexCenter}; flex-shrink: 0; } & > .label { @@ -398,7 +396,7 @@ const NewNoteButton = styled.button` display: none; font-size: 12px; margin-left: 5px; - ${textOverflow} + ${textOverflow}; align-items: center; & > .icon { flex-shrink: 0; diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 475931227d..b63e2dfa2b 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -6,29 +6,47 @@ import React, { KeyboardEvent, } from 'react' import styled from '../../lib/styled' -import { NoteStorage, NoteDoc } from '../../lib/db/types' +import { NoteDoc, NoteStorage } from '../../lib/db/types' import { useEffectOnce, useDebounce } from 'react-use' -import { values } from '../../lib/db/utils' +import { excludeNoteIdPrefix, values } from '../../lib/db/utils' import { escapeRegExp } from '../../lib/string' import { useSearchModal } from '../../lib/searchModal' -import { border, borderBottom } from '../../lib/styled/styleFunctions' +import { + border, + borderBottom, + searchMatchHighlightEditorStyle, +} from '../../lib/styled/styleFunctions' import { mdiMagnify } from '@mdi/js' import Icon from '../atoms/Icon' import SearchModalNoteResultItem from '../molecules/SearchModalNoteResultItem' import { useStorageRouter } from '../../lib/storageRouter' +import { + getMatchData, + getSearchResultKey, + NoteSearchData, + SearchResult, + SEARCH_DEBOUNCE_TIMEOUT, + MERGE_SAME_LINE_RESULTS_INTO_ONE, +} from '../../lib/search/search' +import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' +import CodeMirror from 'codemirror' +import { BaseTheme } from '../../lib/styled/BaseTheme' interface SearchModalProps { storage: NoteStorage } const SearchModal = ({ storage }: SearchModalProps) => { + const [noteToSearchResultMap] = useState({}) + const [selectedNote, setSelectedNote] = useState({ _id: '', content: '' }) + const [selectedItemId, setSelectedItemId] = useState('') const [searchValue, setSearchValue] = useState('') - const [resultList, setResultList] = useState([]) + const [resultList, setResultList] = useState([]) const [searching, setSearching] = useState(false) const { toggleShowSearchModal } = useSearchModal() - const searchInputRef = useRef(null) + const searchTextAreaRef = useRef(null) - const updateSearchValue: ChangeEventHandler = useCallback( + const updateSearchValue: ChangeEventHandler = useCallback( (event) => { setSearchValue(event.target.value) setSearching(true) @@ -36,17 +54,31 @@ const SearchModal = ({ storage }: SearchModalProps) => { [] ) - const focusInput = useCallback(() => { - if (searchInputRef.current == null) { + const focusTextAreaInput = useCallback(() => { + if (searchTextAreaRef.current == null) { return } - searchInputRef.current.focus() + searchTextAreaRef.current.focus() }, []) useEffectOnce(() => { - focusInput() + focusTextAreaInput() }) + const getSearchRegex = useCallback((rawSearch) => { + return new RegExp(escapeRegExp(rawSearch), 'gim') + }, []) + + const { navigateToNoteWithEditorFocus: _navFocusEditor } = useStorageRouter() + + const navFocusEditor = useCallback( + (noteId: string, lineNum: number, lineColumn = 0) => { + toggleShowSearchModal() + _navFocusEditor(storage.id, noteId, '/', `${lineNum},${lineColumn}`) + }, + [toggleShowSearchModal, _navFocusEditor, storage.id] + ) + useDebounce( () => { if (searchValue.trim() === '') { @@ -55,18 +87,33 @@ const SearchModal = ({ storage }: SearchModalProps) => { return } const notes = values(storage.noteMap) - const regex = new RegExp(escapeRegExp(searchValue), 'i') - const filteredNotes = notes.filter( - (note) => - !note.trashed && - (note.tags.join().match(regex) || - note.title.match(regex) || - note.content.match(regex)) - ) - setResultList(filteredNotes) + const regex = getSearchRegex(searchValue) + // todo: [komediruzecki-01/12/2020] Here we could have buttons (toggles) for content/title/tag search! (by tag color?) + // for now, it's only content search + const searchResultData: NoteSearchData[] = [] + notes.forEach((note) => { + if (note.trashed) { + return + } + const matchDataContent = getMatchData(note.content, regex) + // todo: [komediruzecki-04/12/2020] Use title and tag search to find those elements too, once found + // we can highlight them too + // const matchDataTitle = getMatchData(note.title, regex) + // const matchDataTags = getMatchData(note.tags.join(), regex) + if (matchDataContent && matchDataContent.length > 0) { + const noteResultKey = excludeNoteIdPrefix(note._id) + noteToSearchResultMap[noteResultKey] = matchDataContent + searchResultData.push({ + note: note, + results: matchDataContent, + }) + } + }) + + setResultList(searchResultData) setSearching(false) }, - 200, + SEARCH_DEBOUNCE_TIMEOUT, [storage.noteMap, searchValue] ) @@ -81,7 +128,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { ) const handleSearchInputKeyDown = useCallback( - (event: KeyboardEvent) => { + (event: KeyboardEvent) => { console.log(event.key) if (event.key === 'Escape') { // TODO: Focus back after modal closed @@ -91,16 +138,134 @@ const SearchModal = ({ storage }: SearchModalProps) => { [toggleShowSearchModal] ) + const updateSelectedItems = useCallback((note: NoteDoc, itemId: string) => { + setSelectedItemId(itemId) + setSelectedNote(note) + }, []) + + const addMarkers = useCallback( + (codeEditor, searchValue, selectedItemId = -1) => { + if (codeEditor) { + const cursor = codeEditor.getSearchCursor(getSearchRegex(searchValue)) + let first = true + let from, to + let currentItemId = 0 + let previousLine = -1 + let lineChanged = false + while (cursor.findNext()) { + from = cursor.from() + to = cursor.to() + + if (first) { + previousLine = from.line + first = false + } + + lineChanged = from.line != previousLine + previousLine = from.line + if (MERGE_SAME_LINE_RESULTS_INTO_ONE) { + if (lineChanged) { + currentItemId++ + } + } + + codeEditor.markText(from, to, { + className: + currentItemId == selectedItemId + ? 'codeMirrorSelectedTextStyle' + : 'codeMirrorMarkedTextStyle', + }) + + if (!MERGE_SAME_LINE_RESULTS_INTO_ONE) { + currentItemId++ + } + } + } + }, + [getSearchRegex] + ) + + const focusEditorOnSelectedItem = useCallback( + ( + editor: CodeMirror.EditorFromTextArea, + searchResults: SearchResult[], + selectedIdx: number + ) => { + if (selectedIdx >= searchResults.length) { + console.log( + 'Cannot focus editor on selected idx.', + selectedIdx, + searchResults.length + ) + return + } + const focusLocation = { + line: searchResults[selectedIdx].lineNum - 1, + ch: + searchResults[selectedIdx].matchColumn + + searchResults[selectedIdx].matchLength, + } + editor.focus() + editor.setCursor(focusLocation) + + // Un-focus to searching + focusTextAreaInput() + }, + [focusTextAreaInput] + ) + + const updateCodeMirrorMarks = useCallback( + (codeMirror: CodeMirror.EditorFromTextArea) => { + if (codeMirror) { + // Get search result from selected note + if (selectedNote != null && selectedNote._id && selectedItemId != '') { + const noteResultKey = excludeNoteIdPrefix(selectedNote._id) + const searchResults: SearchResult[] = + noteToSearchResultMap[noteResultKey] + if (searchResults.length > 0) { + const selectedItemIdNum = + selectedItemId && !Number.isNaN(parseInt(selectedItemId)) + ? parseInt(selectedItemId) + : -1 + addMarkers(codeMirror, searchResults[0].matchStr, selectedItemIdNum) + if (selectedItemIdNum != -1) { + focusEditorOnSelectedItem( + codeMirror, + searchResults, + selectedItemIdNum + ) + } + } + } + } else { + console.log('code mirror was null, cannot highlight text') + } + }, + [ + addMarkers, + focusEditorOnSelectedItem, + noteToSearchResultMap, + selectedItemId, + selectedNote, + ] + ) + + const textAreaRows = useCallback(() => { + const searchNumLines = searchValue ? searchValue.split('\n').length : 0 + return searchNumLines == 0 || searchNumLines == 1 ? 1 : searchNumLines + 1 + }, [searchValue]) + return ( - -
+ +
-
@@ -112,13 +277,34 @@ const SearchModal = ({ storage }: SearchModalProps) => { resultList.map((result) => { return ( ) })}
+ {selectedItemId && + selectedNote._id && + !searching && + resultList.length > 0 && ( + + + + )}
@@ -127,7 +313,11 @@ const SearchModal = ({ storage }: SearchModalProps) => { export default SearchModal -const Container = styled.div` +interface TextAreaProps { + numRows: number +} + +const Container = styled.div` z-index: 6000; position: fixed; top: 0; @@ -140,23 +330,28 @@ const Container = styled.div` position: relative; margin: 50px auto 0; background-color: ${({ theme }) => theme.navBackgroundColor}; - width: 400px; + width: 45%; z-index: 6002; - ${border} + ${border}; border-radius: 10px; - max-height: 360px; + max-height: 65%; display: flex; flex-direction: column; & > .search { padding: 10px; display: flex; - align-items: center; + align-items: ${({ numRows }) => (numRows == 1 ? 'center' : 'self-start')}; ${borderBottom}; - input { + textarea { flex: 1; background-color: transparent; border: none; color: ${({ theme }) => theme.uiTextColor}; + + resize: none; + max-height: 4em; + min-height: 1em; + height: unset; } } & > .list { @@ -187,3 +382,21 @@ const Container = styled.div` background-color: rgba(0, 0, 0, 0.4); } ` + +const EditorPreview = styled.div` + .codeMirrorMarkedTextStyle { + ${searchMatchHighlightEditorStyle}; + opacity: 0.9; + } + + .codeMirrorSelectedTextStyle { + ${searchMatchHighlightEditorStyle}; + filter: brightness(125%); + } + + background-color: ${({ theme }) => theme.navBackgroundColor}; + z-index: 6002; + + width: 100%; + height: 100%; +` diff --git a/src/components/pages/NotePage.tsx b/src/components/pages/NotePage.tsx index 869c41fbc1..abbd715a43 100644 --- a/src/components/pages/NotePage.tsx +++ b/src/components/pages/NotePage.tsx @@ -31,6 +31,7 @@ import NotePageToolbar from '../organisms/NotePageToolbar' import SearchModal from '../organisms/SearchModal' import { useSearchModal } from '../../lib/searchModal' import styled from '../../lib/styled' +import { getNumberFromStr } from '../../lib/string' interface NotePageProps { storage: NoteStorage @@ -49,7 +50,7 @@ const NotePage = ({ storage }: NotePageProps) => { | StorageTrashCanRouteParams | StorageTagsRouteParams const { noteId } = routeParams - const { push } = useRouter() + const { push, hash } = useRouter() const currentPathnameWithoutNoteId = usePathnameWithoutNoteId() const { preferences, setPreferences } = usePreferences() const noteSorting = preferences['general.noteSorting'] @@ -87,6 +88,25 @@ const NotePage = ({ storage }: NotePageProps) => { [updateNote, report] ) + const getCurrentPositionFromRoute = useCallback(() => { + let focusLine = 0 + let focusColumn = 0 + if (hash.startsWith('#L')) { + const focusData = hash.substring(2).split(',') + if (focusData.length == 2) { + focusLine = getNumberFromStr(focusData[0]) + focusColumn = getNumberFromStr(focusData[1]) + } else if (focusData.length == 1) { + focusLine = getNumberFromStr(focusData[0]) + } + } + + return { + line: focusLine, + ch: focusColumn, + } + }, [hash]) + const notes = useMemo((): NoteDoc[] => { switch (routeParams.name) { case 'storages.notes': @@ -254,6 +274,7 @@ const NotePage = ({ storage }: NotePageProps) => { updateNote={updateNoteAndReport} addAttachments={addAttachments} viewMode={generalStatus.noteViewMode} + initialCursorPosition={getCurrentPositionFromRoute()} /> ) } diff --git a/src/components/pages/WikiNotePage.tsx b/src/components/pages/WikiNotePage.tsx index c67b19c358..b764463275 100644 --- a/src/components/pages/WikiNotePage.tsx +++ b/src/components/pages/WikiNotePage.tsx @@ -3,7 +3,12 @@ import { NoteStorage } from '../../lib/db/types' import StorageLayout from '../atoms/StorageLayout' import NotePageToolbar from '../organisms/NotePageToolbar' import NoteDetail from '../organisms/NoteDetail' -import { useRouteParams } from '../../lib/routeParams' +import { + StorageNotesRouteParams, + StorageTagsRouteParams, + StorageTrashCanRouteParams, + useRouteParams, +} from '../../lib/routeParams' import { useGeneralStatus, ViewModeType } from '../../lib/generalStatus' import { useDb } from '../../lib/db' import FolderDetail from '../organisms/FolderDetail' @@ -12,13 +17,19 @@ import TrashDetail from '../organisms/TrashDetail' import SearchModal from '../organisms/SearchModal' import { useSearchModal } from '../../lib/searchModal' import styled from '../../lib/styled' +import { useRouter } from '../../lib/router' +import { getNumberFromStr } from '../../lib/string' interface WikiNotePageProps { storage: NoteStorage } const WikiNotePage = ({ storage }: WikiNotePageProps) => { - const routeParams = useRouteParams() + const routeParams = useRouteParams() as + | StorageNotesRouteParams + | StorageTrashCanRouteParams + | StorageTagsRouteParams + const { hash } = useRouter() const { generalStatus, setGeneralStatus } = useGeneralStatus() const noteViewMode = generalStatus.noteViewMode @@ -77,6 +88,25 @@ const WikiNotePage = ({ storage }: WikiNotePageProps) => { const { showSearchModal } = useSearchModal() + const getCurrentPositionFromRoute = useCallback(() => { + let focusLine = 0 + let focusColumn = 0 + if (hash.startsWith('#L')) { + const focusData = hash.substring(2).split(',') + if (focusData.length == 2) { + focusLine = getNumberFromStr(focusData[0]) + focusColumn = getNumberFromStr(focusData[1]) + } else if (focusData.length == 1) { + focusLine = getNumberFromStr(focusData[0]) + } + } + + return { + line: focusLine, + ch: focusColumn, + } + }, [hash]) + return ( {showSearchModal && } @@ -108,6 +138,7 @@ const WikiNotePage = ({ storage }: WikiNotePageProps) => { updateNote={updateNote} addAttachments={addAttachments} viewMode={noteViewMode} + initialCursorPosition={getCurrentPositionFromRoute()} /> )}
diff --git a/src/lib/colors.ts b/src/lib/colors.ts new file mode 100644 index 0000000000..ade087d3c0 --- /dev/null +++ b/src/lib/colors.ts @@ -0,0 +1,40 @@ +interface RGBColor { + r: number + g: number + b: number +} + +export function convertHexStringToRgbString(hex: string): RGBColor { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : { + r: 255, + g: 255, + b: 255, + } +} + +const brightnessDefaultThreshold = 110 + +export function getColorBrightness(color: RGBColor | string) { + if (color == '') { + return 0 + } + if (typeof color === 'string') { + color = convertHexStringToRgbString(color) + } + const brightness = (color.r * 299 + color.g * 587 + color.b * 114) / 1000 + return brightness +} + +export function isColorBright( + color: RGBColor | string, + threshold: number = brightnessDefaultThreshold +) { + return getColorBrightness(color) > threshold +} diff --git a/src/lib/keyboard.ts b/src/lib/keyboard.ts index 3ed55ba4af..68dab85e98 100644 --- a/src/lib/keyboard.ts +++ b/src/lib/keyboard.ts @@ -22,3 +22,14 @@ export function isWithGeneralCtrlKey( return event.ctrlKey } } + +export function isWithGeneralCtrlShiftKeys( + event: KeyboardEvent | React.KeyboardEvent +) { + switch (osName) { + case 'macos': + return event.metaKey && event.shiftKey + default: + return event.ctrlKey && event.shiftKey + } +} diff --git a/src/lib/search/search.ts b/src/lib/search/search.ts new file mode 100644 index 0000000000..e7b14cce95 --- /dev/null +++ b/src/lib/search/search.ts @@ -0,0 +1,87 @@ +import { NoteDoc } from '../db/types' +import { EditorPosition } from '../CodeMirror' + +export interface SearchResult { + id: string + lineStr: string + matchStr: string + matchColumn: number + matchLength: number + lineNum: number +} + +export interface NoteSearchData { + results: SearchResult[] + note: NoteDoc +} + +const SEARCH_MEGABYTES_PER_NOTE = 30 +export const MAX_SEARCH_PREVIEW_LINE_LENGTH = 10000 +export const MAX_SEARCH_CONTENT_LENGTH_PER_NOTE = + SEARCH_MEGABYTES_PER_NOTE * 10e6 +export const SEARCH_DEBOUNCE_TIMEOUT = 350 +export const MERGE_SAME_LINE_RESULTS_INTO_ONE = true + +export function getSearchResultKey(noteId: string, searchItemId: string) { + return `${noteId}${searchItemId}` +} + +function getMatchDataFromGlobalColumn( + lines: string[], + position: number +): EditorPosition { + let current_position = 0 + let lineColumn = 0 + let lineNum = 0 + + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + current_position += lines[lineIdx].length + 1 + if (current_position > position) { + lineNum = lineIdx + lineColumn = position - (current_position - (lines[lineIdx].length + 1)) + break + } + } + return { + line: lineNum, + ch: lineColumn, + } +} + +export function getMatchData(text: string, searchTerm: RegExp) { + const data: SearchResult[] = [] + + // Split text + let resultId = 0 + const lines: string[] = text.split('\n') + + // Use only first N lines + if (text.length > MAX_SEARCH_CONTENT_LENGTH_PER_NOTE) { + text = text.substring(0, MAX_SEARCH_PREVIEW_LINE_LENGTH) + } + const matches: IterableIterator = text.matchAll(searchTerm) + + let previousLineNum = 0 + for (const match of matches) { + const matchStr = match[0] + const matchIndex: number = match.index ? match.index : 0 + const pos = getMatchDataFromGlobalColumn(lines, matchIndex) + if (MERGE_SAME_LINE_RESULTS_INTO_ONE) { + if (pos.line == previousLineNum) { + // same result at a line, skip this one + continue + } else { + previousLineNum = pos.line + } + } + data.push({ + id: `${resultId++}`, + lineStr: lines[pos.line], + lineNum: pos.line + 1, + matchLength: matchStr.length, + matchColumn: pos.ch, + matchStr: matchStr, + }) + } + return data +} diff --git a/src/lib/storageRouter.ts b/src/lib/storageRouter.ts index edccdacbcf..8fe5ea2eb7 100644 --- a/src/lib/storageRouter.ts +++ b/src/lib/storageRouter.ts @@ -26,17 +26,29 @@ function useStorageRouterStore() { [activeStorageId, push] ) - const navigateToNote = useCallback( - (storageId: string, noteId: string, noteFolderPathname = '/') => { + const navigateToNoteWithEditorFocus = useCallback( + ( + storageId: string, + noteId: string, + noteFolderPathname = '/', + focusEditorPosition = '0,0' + ) => { push( `/app/storages/${storageId}/notes${ noteFolderPathname === '/' ? '' : noteFolderPathname - }/${noteId}` + }/${noteId}/#L${focusEditorPosition}` ) }, [push] ) + const navigateToNote = useCallback( + (storageId: string, noteId: string, noteFolderPathname = '/') => { + navigateToNoteWithEditorFocus(storageId, noteId, noteFolderPathname) + }, + [navigateToNoteWithEditorFocus] + ) + useEffect(() => { if (activeStorageId != null) { lastStoragePathnameMapRef.current.set(activeStorageId, pathname) @@ -46,6 +58,7 @@ function useStorageRouterStore() { return { navigate, navigateToNote, + navigateToNoteWithEditorFocus, } } diff --git a/src/lib/string.ts b/src/lib/string.ts index 94bb66699b..709401c9e8 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -23,3 +23,11 @@ export function filenamify(value: string) { export function getHexatrigesimalString(value: number) { return value.toString(36) } + +export function getNumberFromStr(str: string): number { + if (!Number.isNaN(parseInt(str))) { + return parseInt(str) + } else { + return 0 + } +} diff --git a/src/lib/styled/BaseTheme.ts b/src/lib/styled/BaseTheme.ts index a9e128102e..d8d354bda3 100644 --- a/src/lib/styled/BaseTheme.ts +++ b/src/lib/styled/BaseTheme.ts @@ -59,4 +59,8 @@ export interface BaseTheme { // Input inputBackground: string + + // Search Highlight + searchHighlightBackgroundColor: string + searchItemSelectionBackgroundColor: string } diff --git a/src/lib/styled/styleFunctions.ts b/src/lib/styled/styleFunctions.ts index 59e100b63e..6b83d82f67 100644 --- a/src/lib/styled/styleFunctions.ts +++ b/src/lib/styled/styleFunctions.ts @@ -1,4 +1,5 @@ import { BaseTheme } from './BaseTheme' +import { isColorBright } from '../colors' interface StyledProps { theme: BaseTheme @@ -199,3 +200,33 @@ export const flexCenter = () => `display: flex; align-items: center; justify-content: center; ` + +export const searchMatchHighlightStyle = ({ theme }: StyledProps) => ` +background-color: ${theme.searchHighlightBackgroundColor}; +color: ${ + isColorBright(theme.searchHighlightBackgroundColor) ? '#000000' : '#FFF' +}; + +border-radius: 4px; +padding: 1px 2px; +` + +export const searchMatchHighlightEditorStyle = ({ theme }: StyledProps) => ` +background-color: ${theme.searchHighlightBackgroundColor}; +color: ${ + isColorBright(theme.searchHighlightBackgroundColor) ? '#000000' : '#FFF' +}; +&.cm-variable { + color: ${ + isColorBright(theme.searchHighlightBackgroundColor) ? '#000000' : '#FFF' + }; +} +// better to remove, because if any border radius is added +// the searched elements all include their radius and it looks blocky instead of one +// search string (anything separated with space, or other character is one block) +// border-radius: 4px; +padding-left: 0.1em; +padding-right: 0.1em; +padding-bottom: 0.05em; +padding-top: 0.1em; +` diff --git a/src/themes/dark.ts b/src/themes/dark.ts index b6fd12c7ec..3580285918 100644 --- a/src/themes/dark.ts +++ b/src/themes/dark.ts @@ -82,4 +82,8 @@ export const darkTheme: BaseTheme = { // Input inputBackground: light12Color, + + // Search Highlight + searchHighlightBackgroundColor: '#1362ac', + searchItemSelectionBackgroundColor: '#942fca', } diff --git a/src/themes/legacy.ts b/src/themes/legacy.ts index 099375de31..b576d465ed 100644 --- a/src/themes/legacy.ts +++ b/src/themes/legacy.ts @@ -84,4 +84,8 @@ export const legacyTheme: BaseTheme = { // Input inputBackground: dark12Color, + + // Search Highlight + searchHighlightBackgroundColor: '#1362ac', + searchItemSelectionBackgroundColor: '#942fca', } diff --git a/src/themes/light.ts b/src/themes/light.ts index fbd4411297..f6e5fa5572 100644 --- a/src/themes/light.ts +++ b/src/themes/light.ts @@ -91,4 +91,8 @@ export const lightTheme: BaseTheme = { // Input inputBackground: '#fff', + + // Search Highlight + searchHighlightBackgroundColor: '#1362ac', + searchItemSelectionBackgroundColor: '#45c4c0', } diff --git a/src/themes/sepia.ts b/src/themes/sepia.ts index 1591f29c62..f8873857b2 100644 --- a/src/themes/sepia.ts +++ b/src/themes/sepia.ts @@ -85,4 +85,8 @@ export const sepiaTheme: BaseTheme = { // Input inputBackground: dark12Color, + + // Search Highlight + searchHighlightBackgroundColor: '#1362ac', + searchItemSelectionBackgroundColor: '#45c4c0', } diff --git a/src/themes/solarizedDark.ts b/src/themes/solarizedDark.ts index 004660d33f..cb86e2be33 100644 --- a/src/themes/solarizedDark.ts +++ b/src/themes/solarizedDark.ts @@ -82,4 +82,8 @@ export const solarizedDarkTheme: BaseTheme = { // Input inputBackground: light12Color, + + // Search Highlight + searchHighlightBackgroundColor: '#1362ac', + searchItemSelectionBackgroundColor: '#942fca', } From 14e09244e6e9d948b8e5af8c522a49677128a0fb Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Sat, 12 Dec 2020 12:49:42 +0100 Subject: [PATCH 02/13] Fix initial focus preview out of focus if selection is below preview Add auto focus (scrollIntoView) if user selects the item below editor preview pane --- .../molecules/SearchModalNoteResultItem.tsx | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index df9e194ea0..0a9afa7939 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -75,6 +75,30 @@ const SearchModalNoteResultItem = ({ [highlightMatchedTerm] ) + const updateSelectedItemAndFocus = useCallback( + (target, note, id) => { + { + updateSelectedItem(note, id) + + setTimeout(() => { + if (target) { + target.scrollIntoView( + { + // todo: [komediruzecki-12/12/2020] Smooth looks nice, + // do we want instant (as now) or slowly auto scrolling to element? + behavior: 'auto', + block: 'nearest', + inline: 'nearest', + }, + 20 + ) + } + }) + } + }, + [updateSelectedItem] + ) + return ( @@ -106,7 +130,9 @@ const SearchModalNoteResultItem = ({ selectedItemId == result.id ? 'search-result-selected' : '' } key={getSearchResultKey(note._id, result.id)} - onClick={() => updateSelectedItem(note, result.id)} + onClick={(event: MouseEvent) => + updateSelectedItemAndFocus(event.target, note, result.id) + } onDoubleClick={() => navigateToEditorFocused( note._id, From 1f19c89fb6ff089983be5fec7f1766b5a7f45ab0 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 21 Dec 2020 18:29:22 +0900 Subject: [PATCH 03/13] Refactor SearchModal --- src/components/organisms/SearchModal.tsx | 48 ++++++++++++------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index b63e2dfa2b..f757ca94e4 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -129,7 +129,6 @@ const SearchModal = ({ storage }: SearchModalProps) => { const handleSearchInputKeyDown = useCallback( (event: KeyboardEvent) => { - console.log(event.key) if (event.key === 'Escape') { // TODO: Focus back after modal closed toggleShowSearchModal() @@ -192,7 +191,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { selectedIdx: number ) => { if (selectedIdx >= searchResults.length) { - console.log( + console.warn( 'Cannot focus editor on selected idx.', selectedIdx, searchResults.length @@ -216,29 +215,28 @@ const SearchModal = ({ storage }: SearchModalProps) => { const updateCodeMirrorMarks = useCallback( (codeMirror: CodeMirror.EditorFromTextArea) => { - if (codeMirror) { - // Get search result from selected note - if (selectedNote != null && selectedNote._id && selectedItemId != '') { - const noteResultKey = excludeNoteIdPrefix(selectedNote._id) - const searchResults: SearchResult[] = - noteToSearchResultMap[noteResultKey] - if (searchResults.length > 0) { - const selectedItemIdNum = - selectedItemId && !Number.isNaN(parseInt(selectedItemId)) - ? parseInt(selectedItemId) - : -1 - addMarkers(codeMirror, searchResults[0].matchStr, selectedItemIdNum) - if (selectedItemIdNum != -1) { - focusEditorOnSelectedItem( - codeMirror, - searchResults, - selectedItemIdNum - ) - } - } - } - } else { - console.log('code mirror was null, cannot highlight text') + if (codeMirror == null) { + console.warn('code mirror was null, cannot highlight text') + return + } + + if (selectedNote?._id == null || selectedItemId == null) { + return + } + + const noteResultKey = excludeNoteIdPrefix(selectedNote._id) + const searchResults: SearchResult[] = noteToSearchResultMap[noteResultKey] + if (searchResults.length === 0) { + return + } + + const selectedItemIdNum = + selectedItemId && !Number.isNaN(parseInt(selectedItemId)) + ? parseInt(selectedItemId) + : -1 + addMarkers(codeMirror, searchResults[0].matchStr, selectedItemIdNum) + if (selectedItemIdNum != -1) { + focusEditorOnSelectedItem(codeMirror, searchResults, selectedItemIdNum) } }, [ From 14209c70bbfc4f07150a9edd98c74185f243060f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 21 Dec 2020 18:57:13 +0900 Subject: [PATCH 04/13] Improve style of search modal --- .../molecules/SearchModalNoteResultItem.tsx | 19 +++++++------------ src/components/organisms/SearchModal.tsx | 18 +++++++++++------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index 0a9afa7939..ef7426f618 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -13,7 +13,6 @@ import { MAX_SEARCH_PREVIEW_LINE_LENGTH, SearchResult, } from '../../lib/search/search' -import { isColorBright } from '../../lib/colors' import { SearchMatchHighlight } from '../PreferencesModal/styled' import { escapeRegExp } from '../../lib/string' @@ -154,12 +153,16 @@ const SearchModalNoteResultItem = ({ export default SearchModalNoteResultItem -const Container = styled.div`` +const Container = styled.div` + ${borderBottom}; + &:last-child { + border-bottom: none; + } +` const SearchResultContainer = styled.div` padding: 10px; cursor: pointer; - ${borderBottom}; user-select: none; ` @@ -233,28 +236,20 @@ const SearchResultItem = styled.div` height: 100%; justify-content: space-between; overflow: hidden; + padding: 2px 4px; margin-top: 0.3em; &.search-result-selected { border-radius: 4px; - padding: 2px; background-color: ${({ theme }) => theme.searchItemSelectionBackgroundColor}; - filter: brightness( - ${({ theme }) => (isColorBright(theme.activeBackgroundColor) ? 85 : 115)}% - ); - } } &:hover { border-radius: 4px; background-color: ${({ theme }) => theme.secondaryButtonHoverBackgroundColor}; - filter: brightness( - ${({ theme }) => - isColorBright(theme.secondaryButtonHoverBackgroundColor) ? 85 : 115}% - ); } ` diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index f757ca94e4..2a3068aed6 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -15,6 +15,7 @@ import { border, borderBottom, searchMatchHighlightEditorStyle, + borderTop, } from '../../lib/styled/styleFunctions' import { mdiMagnify } from '@mdi/js' import Icon from '../atoms/Icon' @@ -110,6 +111,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { } }) + setSelectedItemId('') setResultList(searchResultData) setSearching(false) }, @@ -299,7 +301,6 @@ const SearchModal = ({ storage }: SearchModalProps) => { codeMirrorRef={updateCodeMirrorMarks} value={selectedNote.content} readonly={true} - // todo: [komediruzecki-04/12/2020] Maybe implement onChange and update the content directly? /> )} @@ -322,17 +323,17 @@ const Container = styled.div` left: 0; bottom: 0; right: 0; - -webkit-app-region: drag; & > .container { position: relative; margin: 50px auto 0; background-color: ${({ theme }) => theme.navBackgroundColor}; - width: 45%; + width: calc(100% -15px); + max-width: 720px; z-index: 6002; ${border}; border-radius: 10px; - max-height: 65%; + max-height: calc(100% - 100px); display: flex; flex-direction: column; & > .search { @@ -353,9 +354,9 @@ const Container = styled.div` } } & > .list { - flex: 1; overflow-x: hidden; overflow-y: auto; + flex: 1; & > .searching { text-align: center; color: ${({ theme }) => theme.disabledUiTextColor}; @@ -393,8 +394,11 @@ const EditorPreview = styled.div` } background-color: ${({ theme }) => theme.navBackgroundColor}; - z-index: 6002; + ${borderTop}; width: 100%; - height: 100%; + flex: 1; + .CodeMirror { + height: 100%; + } ` From 2809ab1cc17a08610a2502acbf76643fafafbe61 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 21 Dec 2020 19:19:16 +0900 Subject: [PATCH 05/13] Implement preview control --- src/components/organisms/SearchModal.tsx | 73 ++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 2a3068aed6..11e54388fe 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -16,8 +16,9 @@ import { borderBottom, searchMatchHighlightEditorStyle, borderTop, + flexCenter, } from '../../lib/styled/styleFunctions' -import { mdiMagnify } from '@mdi/js' +import { mdiMagnify, mdiClose, mdiTextBoxOutline } from '@mdi/js' import Icon from '../atoms/Icon' import SearchModalNoteResultItem from '../molecules/SearchModalNoteResultItem' import { useStorageRouter } from '../../lib/storageRouter' @@ -39,7 +40,7 @@ interface SearchModalProps { const SearchModal = ({ storage }: SearchModalProps) => { const [noteToSearchResultMap] = useState({}) - const [selectedNote, setSelectedNote] = useState({ _id: '', content: '' }) + const [selectedNote, setSelectedNote] = useState(null) const [selectedItemId, setSelectedItemId] = useState('') const [searchValue, setSearchValue] = useState('') const [resultList, setResultList] = useState([]) @@ -255,6 +256,10 @@ const SearchModal = ({ storage }: SearchModalProps) => { return searchNumLines == 0 || searchNumLines == 1 ? 1 : searchNumLines + 1 }, [searchValue]) + const closePreview = useCallback(() => { + setSelectedItemId('') + }, []) + return (
@@ -280,7 +285,9 @@ const SearchModal = ({ storage }: SearchModalProps) => { key={result.note._id} note={result.note} selectedItemId={ - selectedNote._id == result.note._id ? selectedItemId : '-1' + selectedNote != null && selectedNote._id == result.note._id + ? selectedItemId + : '-1' } searchResults={result.results} updateSelectedItem={updateSelectedItems} @@ -291,10 +298,22 @@ const SearchModal = ({ storage }: SearchModalProps) => { })}
{selectedItemId && - selectedNote._id && + selectedNote != null && !searching && resultList.length > 0 && ( +
+
+ + {selectedNote.title} +
+ +
.preview-control { + flex-shrink: 0; + display: flex; + ${borderBottom} + & > .preview-control__location { + flex: 1; + display: flex; + align-items: center; + padding: 0 5px; + & > .icon { + margin-right: 2px; + } + } + & > .preview-control__close-button { + flex-shrink: 0; + + height: 24px; + width: 24px; + box-sizing: border-box; + font-size: 18px; + outline: none; + padding: 0 5px; + + background-color: transparent; + ${flexCenter} + + border: none; + cursor: pointer; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.navItemColor}; + &:hover { + color: ${({ theme }) => theme.navButtonHoverColor}; + } + + &:active, + &.active { + color: ${({ theme }) => theme.navButtonActiveColor}; + } + } + } + & > .editor { + flex: 1; + } .CodeMirror { height: 100%; } From 24e68f3247c797a41d82cda25ecbf038d9a69015 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 21 Dec 2020 19:55:47 +0900 Subject: [PATCH 06/13] Adjust layout style --- src/components/organisms/SearchModal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 11e54388fe..582181463d 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -417,7 +417,10 @@ const EditorPreview = styled.div` ${borderTop}; width: 100%; flex: 1; + flex-shrink: 0; + height: 324px; display: flex; + overflow: hidden; flex-direction: column; & > .preview-control { flex-shrink: 0; @@ -462,6 +465,7 @@ const EditorPreview = styled.div` } & > .editor { flex: 1; + overflow-y: auto; } .CodeMirror { height: 100%; From 833a6a381d5fa0ca869caa99d2cc1980253e60db Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 22 Dec 2020 06:05:02 +0900 Subject: [PATCH 07/13] Style search modal note result item component --- src/components/PreferencesModal/styled.tsx | 6 ++- .../molecules/SearchModalNoteResultItem.tsx | 38 ++++++++++--------- src/components/organisms/SearchModal.tsx | 23 ++++++----- src/lib/styled/BaseTheme.ts | 4 ++ src/lib/styled/styleFunctions.ts | 31 --------------- src/themes/dark.ts | 9 ++++- src/themes/legacy.ts | 10 +++-- src/themes/light.ts | 10 +++-- src/themes/sepia.ts | 9 ++++- src/themes/solarizedDark.ts | 9 ++++- 10 files changed, 77 insertions(+), 72 deletions(-) diff --git a/src/components/PreferencesModal/styled.tsx b/src/components/PreferencesModal/styled.tsx index 83696b0929..b8cd6c7e04 100644 --- a/src/components/PreferencesModal/styled.tsx +++ b/src/components/PreferencesModal/styled.tsx @@ -7,7 +7,6 @@ import { tableStyle, disabledUiTextColor, PrimaryTextColor, - searchMatchHighlightStyle, } from '../../lib/styled/styleFunctions' export const Section = styled.section` @@ -116,5 +115,8 @@ export const SectionListSelect = styled.div` ` export const SearchMatchHighlight = styled.span` - ${searchMatchHighlightStyle} + background-color: ${({ theme }) => theme.searchHighlightBackgroundColor}; + color: ${({ theme }) => theme.searchHighlightTextColor}; + + padding: 2px; ` diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index ef7426f618..9c2c641d73 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -125,9 +125,7 @@ const SearchModalNoteResultItem = ({ {searchResults.length > 0 && searchResults.map((result) => ( updateSelectedItemAndFocus(event.target, note, result.id) @@ -141,9 +139,13 @@ const SearchModalNoteResultItem = ({ } > - {beautifyPreviewLine(result.lineStr, result.matchStr)} + + {beautifyPreviewLine(result.lineStr, result.matchStr)} + - {result.lineNum} + + {result.lineNum} + ))} @@ -161,13 +163,13 @@ const Container = styled.div` ` const SearchResultContainer = styled.div` - padding: 10px; + padding: 5px; cursor: pointer; user-select: none; ` const MetaContainer = styled.div` - padding: 10px; + padding: 5px 10px; cursor: pointer; ${borderBottom}; user-select: none; @@ -236,18 +238,24 @@ const SearchResultItem = styled.div` height: 100%; justify-content: space-between; overflow: hidden; - padding: 2px 4px; - - margin-top: 0.3em; + padding: 3px 5px; + border-radius: 4px; + margin-bottom: 2px; + &:last-child { + margin-bottom: 0; + } - &.search-result-selected { - border-radius: 4px; + &.selected { + color: ${({ theme }) => theme.searchItemSelectionTextColor}; background-color: ${({ theme }) => theme.searchItemSelectionBackgroundColor}; } + &.selected:hover { + background-color: ${({ theme }) => + theme.searchItemSelectionHoverBackgroundColor}; + } &:hover { - border-radius: 4px; background-color: ${({ theme }) => theme.secondaryButtonHoverBackgroundColor}; } @@ -258,10 +266,6 @@ const SearchResultLeft = styled.div` text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - - &:before { - content: attr(content); - } ` const SearchResultRight = styled.div` diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 582181463d..c01051f595 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -14,7 +14,6 @@ import { useSearchModal } from '../../lib/searchModal' import { border, borderBottom, - searchMatchHighlightEditorStyle, borderTop, flexCenter, } from '../../lib/styled/styleFunctions' @@ -173,9 +172,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { codeEditor.markText(from, to, { className: - currentItemId == selectedItemId - ? 'codeMirrorSelectedTextStyle' - : 'codeMirrorMarkedTextStyle', + currentItemId == selectedItemId ? 'marked selected' : 'marked', }) if (!MERGE_SAME_LINE_RESULTS_INTO_ONE) { @@ -402,14 +399,20 @@ const Container = styled.div` ` const EditorPreview = styled.div` - .codeMirrorMarkedTextStyle { - ${searchMatchHighlightEditorStyle}; - opacity: 0.9; + .marked { + background-color: ${({ theme }) => + theme.searchHighlightSubtleBackgroundColor}; + color: ${({ theme }) => theme.searchHighlightTextColor} !important; + padding: 3px; } - .codeMirrorSelectedTextStyle { - ${searchMatchHighlightEditorStyle}; - filter: brightness(125%); + .marked + .marked { + margin-left: -3px; + padding-left: 0; + } + + .selected { + background-color: ${({ theme }) => theme.searchHighlightBackgroundColor}; } background-color: ${({ theme }) => theme.navBackgroundColor}; diff --git a/src/lib/styled/BaseTheme.ts b/src/lib/styled/BaseTheme.ts index d8d354bda3..e987552409 100644 --- a/src/lib/styled/BaseTheme.ts +++ b/src/lib/styled/BaseTheme.ts @@ -62,5 +62,9 @@ export interface BaseTheme { // Search Highlight searchHighlightBackgroundColor: string + searchHighlightSubtleBackgroundColor: string + searchItemSelectionTextColor: string searchItemSelectionBackgroundColor: string + searchItemSelectionHoverBackgroundColor: string + searchHighlightTextColor: string } diff --git a/src/lib/styled/styleFunctions.ts b/src/lib/styled/styleFunctions.ts index 6b83d82f67..59e100b63e 100644 --- a/src/lib/styled/styleFunctions.ts +++ b/src/lib/styled/styleFunctions.ts @@ -1,5 +1,4 @@ import { BaseTheme } from './BaseTheme' -import { isColorBright } from '../colors' interface StyledProps { theme: BaseTheme @@ -200,33 +199,3 @@ export const flexCenter = () => `display: flex; align-items: center; justify-content: center; ` - -export const searchMatchHighlightStyle = ({ theme }: StyledProps) => ` -background-color: ${theme.searchHighlightBackgroundColor}; -color: ${ - isColorBright(theme.searchHighlightBackgroundColor) ? '#000000' : '#FFF' -}; - -border-radius: 4px; -padding: 1px 2px; -` - -export const searchMatchHighlightEditorStyle = ({ theme }: StyledProps) => ` -background-color: ${theme.searchHighlightBackgroundColor}; -color: ${ - isColorBright(theme.searchHighlightBackgroundColor) ? '#000000' : '#FFF' -}; -&.cm-variable { - color: ${ - isColorBright(theme.searchHighlightBackgroundColor) ? '#000000' : '#FFF' - }; -} -// better to remove, because if any border radius is added -// the searched elements all include their radius and it looks blocky instead of one -// search string (anything separated with space, or other character is one block) -// border-radius: 4px; -padding-left: 0.1em; -padding-right: 0.1em; -padding-bottom: 0.05em; -padding-top: 0.1em; -` diff --git a/src/themes/dark.ts b/src/themes/dark.ts index 3580285918..5b7d1446b6 100644 --- a/src/themes/dark.ts +++ b/src/themes/dark.ts @@ -6,6 +6,7 @@ const primaryColor = '#5580DC' const primaryDarkerColor = '#4070D8' const dangerColor = '#DC3545' +const dark87Color = '#212121' const dark54Color = 'rgba(0,0,0,0.54)' const dark26Color = 'rgba(0,0,0,0.26)' const light70Color = 'rgba(255,255,255,0.7)' @@ -84,6 +85,10 @@ export const darkTheme: BaseTheme = { inputBackground: light12Color, // Search Highlight - searchHighlightBackgroundColor: '#1362ac', - searchItemSelectionBackgroundColor: '#942fca', + searchHighlightBackgroundColor: '#ffc107', + searchHighlightSubtleBackgroundColor: '#ffdb70', + searchItemSelectionTextColor: light100Color, + searchItemSelectionBackgroundColor: primaryColor, + searchItemSelectionHoverBackgroundColor: primaryDarkerColor, + searchHighlightTextColor: dark87Color, } diff --git a/src/themes/legacy.ts b/src/themes/legacy.ts index b576d465ed..31e1868334 100644 --- a/src/themes/legacy.ts +++ b/src/themes/legacy.ts @@ -6,7 +6,7 @@ const primaryColor = '#5580DC' const primaryDarkerColor = '#4070D8' const dangerColor = '#DC3545' -const dark87Color = 'rgba(0,0,0,0.87)' +const dark87Color = '#212121' const dark54Color = 'rgba(0,0,0,0.54)' const dark26Color = 'rgba(0,0,0,0.26)' const dark12Color = 'rgba(0,0,0,0.12)' @@ -86,6 +86,10 @@ export const legacyTheme: BaseTheme = { inputBackground: dark12Color, // Search Highlight - searchHighlightBackgroundColor: '#1362ac', - searchItemSelectionBackgroundColor: '#942fca', + searchHighlightBackgroundColor: '#ffc107', + searchHighlightSubtleBackgroundColor: '#ffdb70', + searchItemSelectionTextColor: light100Color, + searchItemSelectionBackgroundColor: primaryColor, + searchItemSelectionHoverBackgroundColor: primaryDarkerColor, + searchHighlightTextColor: dark87Color, } diff --git a/src/themes/light.ts b/src/themes/light.ts index f6e5fa5572..a3efe6d036 100644 --- a/src/themes/light.ts +++ b/src/themes/light.ts @@ -6,7 +6,7 @@ const primaryColor = '#5580DC' const primaryDarkerColor = '#4070D8' const dangerColor = '#DC3545' -const dark87Color = 'rgba(0,0,0,0.87)' +const dark87Color = '#212121' const dark54Color = 'rgba(0,0,0,0.54)' const dark26Color = 'rgba(0,0,0,0.26)' const dark12Color = '#bbb' @@ -93,6 +93,10 @@ export const lightTheme: BaseTheme = { inputBackground: '#fff', // Search Highlight - searchHighlightBackgroundColor: '#1362ac', - searchItemSelectionBackgroundColor: '#45c4c0', + searchHighlightBackgroundColor: '#ffc107', + searchHighlightSubtleBackgroundColor: '#ffdb70', + searchItemSelectionTextColor: light100Color, + searchItemSelectionBackgroundColor: primaryColor, + searchItemSelectionHoverBackgroundColor: primaryDarkerColor, + searchHighlightTextColor: dark87Color, } diff --git a/src/themes/sepia.ts b/src/themes/sepia.ts index f8873857b2..5a92af470a 100644 --- a/src/themes/sepia.ts +++ b/src/themes/sepia.ts @@ -10,6 +10,7 @@ const dangerColor = '#DC3545' const dark54Color = 'rgba(0,0,0,0.54)' const dark26Color = 'rgba(0,0,0,0.26)' const dark12Color = 'rgba(0,0,0,0.12)' +const dark87Color = '#212121' const light100Color = '#FFF' const light30Color = 'rgba(255,255,255,0.3)' @@ -87,6 +88,10 @@ export const sepiaTheme: BaseTheme = { inputBackground: dark12Color, // Search Highlight - searchHighlightBackgroundColor: '#1362ac', - searchItemSelectionBackgroundColor: '#45c4c0', + searchHighlightBackgroundColor: '#ffc107', + searchHighlightSubtleBackgroundColor: '#ffdb70', + searchItemSelectionTextColor: light100Color, + searchItemSelectionBackgroundColor: primaryColor, + searchItemSelectionHoverBackgroundColor: primaryDarkerColor, + searchHighlightTextColor: dark87Color, } diff --git a/src/themes/solarizedDark.ts b/src/themes/solarizedDark.ts index cb86e2be33..a598194309 100644 --- a/src/themes/solarizedDark.ts +++ b/src/themes/solarizedDark.ts @@ -6,6 +6,7 @@ const primaryColor = '#34a198' const primaryDarkerColor = '#2e8e86' const dangerColor = '#DC3545' +const dark87Color = '#212121' const dark26Color = 'rgba(0,0,0,0.26)' const light70Color = 'rgba(255,255,255,0.7)' const light30Color = 'rgba(255,255,255,0.3)' @@ -84,6 +85,10 @@ export const solarizedDarkTheme: BaseTheme = { inputBackground: light12Color, // Search Highlight - searchHighlightBackgroundColor: '#1362ac', - searchItemSelectionBackgroundColor: '#942fca', + searchHighlightBackgroundColor: '#ffc107', + searchHighlightSubtleBackgroundColor: '#ffdb70', + searchItemSelectionTextColor: light100Color, + searchItemSelectionBackgroundColor: primaryColor, + searchItemSelectionHoverBackgroundColor: primaryDarkerColor, + searchHighlightTextColor: dark87Color, } From 2370c1b44b74f0099911eee1ff0a7f999981e9fd Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 22 Dec 2020 06:20:27 +0900 Subject: [PATCH 08/13] Fix first line search bug --- src/lib/search/search.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/lib/search/search.ts b/src/lib/search/search.ts index e7b14cce95..af1ebde215 100644 --- a/src/lib/search/search.ts +++ b/src/lib/search/search.ts @@ -30,15 +30,15 @@ function getMatchDataFromGlobalColumn( lines: string[], position: number ): EditorPosition { - let current_position = 0 + let currentPosition = 0 let lineColumn = 0 let lineNum = 0 - for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { - current_position += lines[lineIdx].length + 1 - if (current_position > position) { - lineNum = lineIdx - lineColumn = position - (current_position - (lines[lineIdx].length + 1)) + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + currentPosition += lines[lineIndex].length + 1 + if (currentPosition > position) { + lineNum = lineIndex + lineColumn = position - (currentPosition - (lines[lineIndex].length + 1)) break } } @@ -61,17 +61,16 @@ export function getMatchData(text: string, searchTerm: RegExp) { } const matches: IterableIterator = text.matchAll(searchTerm) - let previousLineNum = 0 + let previousLineNumber = -1 for (const match of matches) { const matchStr = match[0] const matchIndex: number = match.index ? match.index : 0 const pos = getMatchDataFromGlobalColumn(lines, matchIndex) if (MERGE_SAME_LINE_RESULTS_INTO_ONE) { - if (pos.line == previousLineNum) { - // same result at a line, skip this one + if (pos.line == previousLineNumber) { continue } else { - previousLineNum = pos.line + previousLineNumber = pos.line } } data.push({ From f2310f2fa93271b90b1e42f4b4f2f7c69d2641ce Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 22 Dec 2020 06:22:11 +0900 Subject: [PATCH 09/13] Discard comments in lib/search --- src/lib/search/search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/search/search.ts b/src/lib/search/search.ts index af1ebde215..0d10d895e1 100644 --- a/src/lib/search/search.ts +++ b/src/lib/search/search.ts @@ -51,11 +51,9 @@ function getMatchDataFromGlobalColumn( export function getMatchData(text: string, searchTerm: RegExp) { const data: SearchResult[] = [] - // Split text let resultId = 0 const lines: string[] = text.split('\n') - // Use only first N lines if (text.length > MAX_SEARCH_CONTENT_LENGTH_PER_NOTE) { text = text.substring(0, MAX_SEARCH_PREVIEW_LINE_LENGTH) } From 7efd4b34ee7e9cdf9cf6f8ae301cb55cccfe549c Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 22 Dec 2020 06:31:52 +0900 Subject: [PATCH 10/13] Apply overflow to container to trim coners of preview --- src/components/organisms/SearchModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index c01051f595..59ace82f93 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -346,6 +346,7 @@ const Container = styled.div` background-color: ${({ theme }) => theme.navBackgroundColor}; width: calc(100% -15px); max-width: 720px; + overflow: hidden; z-index: 6002; ${border}; border-radius: 10px; From b8f5eeb4eac1fa841beb3b356d74e11bbe58584d Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 22 Dec 2020 06:50:42 +0900 Subject: [PATCH 11/13] Fix style issues - title overflow in preview - show placeholder when title is empty --- .../molecules/SearchModalNoteResultItem.tsx | 16 +++++++++---- src/components/organisms/SearchModal.tsx | 24 +++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index 9c2c641d73..3ebcdbcf54 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -15,6 +15,7 @@ import { } from '../../lib/search/search' import { SearchMatchHighlight } from '../PreferencesModal/styled' import { escapeRegExp } from '../../lib/string' +import cc from 'classcat' interface SearchModalNoteResultItemProps { note: NoteDoc @@ -98,6 +99,8 @@ const SearchModalNoteResultItem = ({ [updateSelectedItem] ) + const titleIsEmpty = note.title.trim().length === 0 + return ( @@ -105,7 +108,9 @@ const SearchModalNoteResultItem = ({
-
{note.title}
+
+ {titleIsEmpty ? 'Untitled' : note.title} +
@@ -182,13 +187,13 @@ const MetaContainer = styled.div` } & > .header { - font-size: 18px; + font-size: 15px; display: flex; align-items: center; margin-bottom: 5px; & > .icon { - width: 18px; - height: 18px; + width: 15px; + height: 15px; margin-right: 4px; ${flexCenter} } @@ -196,6 +201,9 @@ const MetaContainer = styled.div` & > .title { flex: 1; ${textOverflow} + &.empty { + color: ${({ theme }) => theme.disabledUiTextColor}; + } } } & > .meta { diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 59ace82f93..6e75dab27c 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -16,6 +16,7 @@ import { borderBottom, borderTop, flexCenter, + textOverflow, } from '../../lib/styled/styleFunctions' import { mdiMagnify, mdiClose, mdiTextBoxOutline } from '@mdi/js' import Icon from '../atoms/Icon' @@ -32,6 +33,7 @@ import { import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' import CodeMirror from 'codemirror' import { BaseTheme } from '../../lib/styled/BaseTheme' +import cc from 'classcat' interface SearchModalProps { storage: NoteStorage @@ -302,7 +304,16 @@ const SearchModal = ({ storage }: SearchModalProps) => {
- {selectedNote.title} + + {selectedNote.title.trim().length > 0 + ? selectedNote.title + : 'Untitled'} +
- - {searchResults.length > 0 && - searchResults.map((result) => ( - - updateSelectedItemAndFocus(event.target, note, result.id) - } - onDoubleClick={() => - navigateToEditorFocused( - note._id, - result.lineNum - 1, - result.matchColumn - ) - } - > - - - {beautifyPreviewLine(result.lineStr, result.matchStr)} - - - - {result.lineNum} - - - ))} - + {searchResults.length > 0 && ( + <> +
+ + {searchResults.map((result) => ( + + updateSelectedItemAndFocus(event.target, note, result.id) + } + onDoubleClick={() => + navigateToEditorFocused( + note._id, + result.lineNum - 1, + result.matchColumn + ) + } + > + + + {beautifyPreviewLine(result.lineStr, result.matchStr)} + + + + {result.lineNum} + + + ))} + + + )} ) } @@ -165,18 +169,25 @@ const Container = styled.div` &:last-child { border-bottom: none; } + + .separator { + border: none; + + ${borderBottom}; + margin: 0 10px 2px; + } ` const SearchResultContainer = styled.div` - padding: 5px; + padding: 0 5px; + margin-bottom: 5px; cursor: pointer; user-select: none; ` const MetaContainer = styled.div` - padding: 5px 10px; + padding: 10px 10px; cursor: pointer; - ${borderBottom}; user-select: none; &:hover { @@ -200,6 +211,7 @@ const MetaContainer = styled.div` & > .title { flex: 1; + font-size: 18px; ${textOverflow} &.empty { color: ${({ theme }) => theme.disabledUiTextColor}; @@ -250,7 +262,7 @@ const SearchResultItem = styled.div` border-radius: 4px; margin-bottom: 2px; &:last-child { - margin-bottom: 0; + margin-bottom: 10px; } &.selected { From 4f2bcde407805d576e4128a8dd006af7f0197881 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 22 Dec 2020 19:29:45 +0900 Subject: [PATCH 13/13] Implement tag and title search --- .../molecules/SearchModalNoteResultItem.tsx | 54 +++++++++++++++---- src/components/organisms/SearchModal.tsx | 38 ++++++++++--- src/lib/search/search.ts | 21 +++++--- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index d38c3ddf79..b48bb9c15f 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useMemo } from 'react' import styled from '../../lib/styled' import { NoteDoc } from '../../lib/db/types' import Icon from '../atoms/Icon' @@ -12,6 +12,7 @@ import { getSearchResultKey, MAX_SEARCH_PREVIEW_LINE_LENGTH, SearchResult, + TagSearchResult, } from '../../lib/search/search' import { SearchMatchHighlight } from '../PreferencesModal/styled' import { escapeRegExp } from '../../lib/string' @@ -19,6 +20,8 @@ import cc from 'classcat' interface SearchModalNoteResultItemProps { note: NoteDoc + titleSearchResult: string | null + tagSearchResults: TagSearchResult[] selectedItemId: string searchResults: SearchResult[] navigateToNote: (noteId: string) => void @@ -32,6 +35,8 @@ interface SearchModalNoteResultItemProps { const SearchModalNoteResultItem = ({ note, + titleSearchResult, + tagSearchResults, searchResults, navigateToNote, selectedItemId, @@ -42,7 +47,7 @@ const SearchModalNoteResultItem = ({ navigateToNote(note._id) }, [navigateToNote, note._id]) - const highlightMatchedTerm = useCallback((line, matchStr) => { + const highlightMatchedTerm = useCallback((line: string, matchStr: string) => { const parts = line.split(new RegExp(`(${escapeRegExp(matchStr)})`, 'gi')) return ( @@ -57,7 +62,7 @@ const SearchModalNoteResultItem = ({ ) }, []) const beautifyPreviewLine = useCallback( - (line, matchStr) => { + (line: string, matchStr: string) => { const multiline = matchStr.indexOf('\n') != -1 const beautifiedLine = line.substring(0, MAX_SEARCH_PREVIEW_LINE_LENGTH) + @@ -101,6 +106,13 @@ const SearchModalNoteResultItem = ({ const titleIsEmpty = note.title.trim().length === 0 + const searchedTagNameMatchStringMap = useMemo(() => { + return tagSearchResults.reduce>((map, searchResult) => { + map.set(searchResult.tagName, searchResult.matchString) + return map + }, new Map()) + }, [tagSearchResults]) + return ( @@ -109,7 +121,11 @@ const SearchModalNoteResultItem = ({
- {titleIsEmpty ? 'Untitled' : note.title} + {titleIsEmpty + ? 'Untitled' + : titleSearchResult != null + ? highlightMatchedTerm(note.title, titleSearchResult) + : note.title}
@@ -120,7 +136,21 @@ const SearchModalNoteResultItem = ({ {note.tags.length > 0 && (
{' '} - {note.tags.map((tag) => tag).join(', ')} + {note.tags.map((tag) => { + const matchedString = searchedTagNameMatchStringMap.get(tag) + if (matchedString == null) { + return ( + + {tag} + + ) + } + return ( + + {highlightMatchedTerm(tag, matchedString)} + + ) + })}
)}
@@ -140,18 +170,18 @@ const SearchModalNoteResultItem = ({ onDoubleClick={() => navigateToEditorFocused( note._id, - result.lineNum - 1, + result.lineNumber - 1, result.matchColumn ) } > - + - {beautifyPreviewLine(result.lineStr, result.matchStr)} + {beautifyPreviewLine(result.lineString, result.matchString)} - {result.lineNum} + {result.lineNumber} ))} @@ -244,6 +274,12 @@ const MetaContainer = styled.div` margin-right: 4px; flex-shrink: 0; } + & > .tags__item { + margin-right: 5px; + &:not(:last-child)::after { + content: ','; + } + } } } &:last-child { diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 6e75dab27c..d9e2fa321b 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -29,6 +29,7 @@ import { SearchResult, SEARCH_DEBOUNCE_TIMEOUT, MERGE_SAME_LINE_RESULTS_INTO_ONE, + TagSearchResult, } from '../../lib/search/search' import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' import CodeMirror from 'codemirror' @@ -99,14 +100,35 @@ const SearchModal = ({ storage }: SearchModalProps) => { return } const matchDataContent = getMatchData(note.content, regex) - // todo: [komediruzecki-04/12/2020] Use title and tag search to find those elements too, once found - // we can highlight them too - // const matchDataTitle = getMatchData(note.title, regex) - // const matchDataTags = getMatchData(note.tags.join(), regex) - if (matchDataContent && matchDataContent.length > 0) { + + const titleMatchResult = note.title.match(regex) + + const titleSearchResult = + titleMatchResult != null ? titleMatchResult[0] : null + const tagSearchResults = note.tags.reduce( + (searchResults, tagName) => { + const matchResult = tagName.match(regex) + if (matchResult != null) { + searchResults.push({ + tagName, + matchString: matchResult[0], + }) + } + return searchResults + }, + [] + ) + + if ( + titleSearchResult || + tagSearchResults.length > 0 || + matchDataContent.length > 0 + ) { const noteResultKey = excludeNoteIdPrefix(note._id) noteToSearchResultMap[noteResultKey] = matchDataContent searchResultData.push({ + titleSearchResult, + tagSearchResults, note: note, results: matchDataContent, }) @@ -201,7 +223,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { return } const focusLocation = { - line: searchResults[selectedIdx].lineNum - 1, + line: searchResults[selectedIdx].lineNumber - 1, ch: searchResults[selectedIdx].matchColumn + searchResults[selectedIdx].matchLength, @@ -236,7 +258,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { selectedItemId && !Number.isNaN(parseInt(selectedItemId)) ? parseInt(selectedItemId) : -1 - addMarkers(codeMirror, searchResults[0].matchStr, selectedItemIdNum) + addMarkers(codeMirror, searchResults[0].matchString, selectedItemIdNum) if (selectedItemIdNum != -1) { focusEditorOnSelectedItem(codeMirror, searchResults, selectedItemIdNum) } @@ -288,6 +310,8 @@ const SearchModal = ({ storage }: SearchModalProps) => { ? selectedItemId : '-1' } + titleSearchResult={result.titleSearchResult} + tagSearchResults={result.tagSearchResults} searchResults={result.results} updateSelectedItem={updateSelectedItems} navigateToNote={navigateToNote} diff --git a/src/lib/search/search.ts b/src/lib/search/search.ts index 0d10d895e1..a730efe772 100644 --- a/src/lib/search/search.ts +++ b/src/lib/search/search.ts @@ -3,14 +3,21 @@ import { EditorPosition } from '../CodeMirror' export interface SearchResult { id: string - lineStr: string - matchStr: string + lineString: string + matchString: string matchColumn: number matchLength: number - lineNum: number + lineNumber: number +} + +export interface TagSearchResult { + tagName: string + matchString: string } export interface NoteSearchData { + titleSearchResult: string | null + tagSearchResults: TagSearchResult[] results: SearchResult[] note: NoteDoc } @@ -48,7 +55,7 @@ function getMatchDataFromGlobalColumn( } } -export function getMatchData(text: string, searchTerm: RegExp) { +export function getMatchData(text: string, searchTerm: RegExp): SearchResult[] { const data: SearchResult[] = [] let resultId = 0 @@ -73,11 +80,11 @@ export function getMatchData(text: string, searchTerm: RegExp) { } data.push({ id: `${resultId++}`, - lineStr: lines[pos.line], - lineNum: pos.line + 1, + lineString: lines[pos.line], + lineNumber: pos.line + 1, matchLength: matchStr.length, matchColumn: pos.ch, - matchStr: matchStr, + matchString: matchStr, }) } return data