-
- {note.folderPathname}
+
+
+
+
+
+
+
+ {titleIsEmpty
+ ? 'Untitled'
+ : titleSearchResult != null
+ ? highlightMatchedTerm(note.title, titleSearchResult)
+ : note.title}
+
- {note.tags.length > 0 && (
-
-
{' '}
- {note.tags.map((tag) => tag).join(', ')}
+
+
+
+ {note.folderPathname}
- )}
-
+ {note.tags.length > 0 && (
+
+ {' '}
+ {note.tags.map((tag) => {
+ const matchedString = searchedTagNameMatchStringMap.get(tag)
+ if (matchedString == null) {
+ return (
+
+ {tag}
+
+ )
+ }
+ return (
+
+ {highlightMatchedTerm(tag, matchedString)}
+
+ )
+ })}
+
+ )}
+
+
+
+ {searchResults.length > 0 && (
+ <>
+
+
+ {searchResults.map((result) => (
+
+ updateSelectedItemAndFocus(event.target, note, result.id)
+ }
+ onDoubleClick={() =>
+ navigateToEditorFocused(
+ note._id,
+ result.lineNumber - 1,
+ result.matchColumn
+ )
+ }
+ >
+
+
+ {beautifyPreviewLine(result.lineString, result.matchString)}
+
+
+
+ {result.lineNumber}
+
+
+ ))}
+
+ >
+ )}
)
}
@@ -49,9 +195,29 @@ const SearchModalNoteResultItem = ({
export default SearchModalNoteResultItem
const Container = styled.div`
- padding: 10px;
+ ${borderBottom};
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .separator {
+ border: none;
+
+ ${borderBottom};
+ margin: 0 10px 2px;
+ }
+`
+
+const SearchResultContainer = styled.div`
+ padding: 0 5px;
+ margin-bottom: 5px;
+ cursor: pointer;
+ user-select: none;
+`
+
+const MetaContainer = styled.div`
+ padding: 10px 10px;
cursor: pointer;
- ${borderBottom}
user-select: none;
&:hover {
@@ -60,21 +226,26 @@ const Container = styled.div`
&:hover:active {
background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor};
}
+
& > .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}
}
& > .title {
flex: 1;
+ font-size: 18px;
${textOverflow}
+ &.empty {
+ color: ${({ theme }) => theme.disabledUiTextColor};
+ }
}
}
& > .meta {
@@ -86,7 +257,7 @@ const Container = styled.div`
& > .folderPathname {
display: flex;
align-items: center;
- max-width: 150px;
+ max-width: 350px;
${textOverflow}
&>.icon {
margin-right: 4px;
@@ -97,15 +268,62 @@ const Container = styled.div`
margin-left: 8px;
display: flex;
align-items: center;
- max-width: 150px;
+ max-width: 350px;
${textOverflow}
&>.icon {
margin-right: 4px;
flex-shrink: 0;
}
+ & > .tags__item {
+ margin-right: 5px;
+ &:not(:last-child)::after {
+ content: ',';
+ }
+ }
}
}
&:last-child {
border-bottom: none;
}
`
+
+const SearchResultItem = styled.div`
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ height: 100%;
+ justify-content: space-between;
+ overflow: hidden;
+ padding: 3px 5px;
+ border-radius: 4px;
+ margin-bottom: 2px;
+ &:last-child {
+ margin-bottom: 10px;
+ }
+
+ &.selected {
+ color: ${({ theme }) => theme.searchItemSelectionTextColor};
+ background-color: ${({ theme }) =>
+ theme.searchItemSelectionBackgroundColor};
+ }
+ &.selected:hover {
+ background-color: ${({ theme }) =>
+ theme.searchItemSelectionHoverBackgroundColor};
+ }
+
+ &:hover {
+ background-color: ${({ theme }) =>
+ theme.secondaryButtonHoverBackgroundColor};
+ }
+`
+
+const SearchResultLeft = styled.div`
+ align-self: flex-start;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+`
+
+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..d9e2fa321b 100644
--- a/src/components/organisms/SearchModal.tsx
+++ b/src/components/organisms/SearchModal.tsx
@@ -6,29 +6,51 @@ 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 { mdiMagnify } from '@mdi/js'
+import {
+ border,
+ borderBottom,
+ borderTop,
+ flexCenter,
+ textOverflow,
+} from '../../lib/styled/styleFunctions'
+import { mdiMagnify, mdiClose, mdiTextBoxOutline } 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,
+ TagSearchResult,
+} from '../../lib/search/search'
+import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor'
+import CodeMirror from 'codemirror'
+import { BaseTheme } from '../../lib/styled/BaseTheme'
+import cc from 'classcat'
interface SearchModalProps {
storage: NoteStorage
}
const SearchModal = ({ storage }: SearchModalProps) => {
+ const [noteToSearchResultMap] = useState({})
+ const [selectedNote, setSelectedNote] = useState(null)
+ 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 +58,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 +91,55 @@ 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)
+
+ 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,
+ })
+ }
+ })
+
+ setSelectedItemId('')
+ setResultList(searchResultData)
setSearching(false)
},
- 200,
+ SEARCH_DEBOUNCE_TIMEOUT,
[storage.noteMap, searchValue]
)
@@ -81,8 +154,7 @@ const SearchModal = ({ storage }: SearchModalProps) => {
)
const handleSearchInputKeyDown = useCallback(
- (event: KeyboardEvent) => {
- console.log(event.key)
+ (event: KeyboardEvent) => {
if (event.key === 'Escape') {
// TODO: Focus back after modal closed
toggleShowSearchModal()
@@ -91,16 +163,135 @@ 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 ? 'marked selected' : 'marked',
+ })
+
+ if (!MERGE_SAME_LINE_RESULTS_INTO_ONE) {
+ currentItemId++
+ }
+ }
+ }
+ },
+ [getSearchRegex]
+ )
+
+ const focusEditorOnSelectedItem = useCallback(
+ (
+ editor: CodeMirror.EditorFromTextArea,
+ searchResults: SearchResult[],
+ selectedIdx: number
+ ) => {
+ if (selectedIdx >= searchResults.length) {
+ console.warn(
+ 'Cannot focus editor on selected idx.',
+ selectedIdx,
+ searchResults.length
+ )
+ return
+ }
+ const focusLocation = {
+ line: searchResults[selectedIdx].lineNumber - 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 == 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].matchString, selectedItemIdNum)
+ if (selectedItemIdNum != -1) {
+ focusEditorOnSelectedItem(codeMirror, searchResults, selectedItemIdNum)
+ }
+ },
+ [
+ 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])
+
+ const closePreview = useCallback(() => {
+ setSelectedItemId('')
+ }, [])
+
return (
-
-
+
+
-
@@ -112,13 +303,58 @@ const SearchModal = ({ storage }: SearchModalProps) => {
resultList.map((result) => {
return (
)
})}
+ {selectedItemId &&
+ selectedNote != null &&
+ !searching &&
+ resultList.length > 0 && (
+
+
+
+
+
+ {selectedNote.title.trim().length > 0
+ ? selectedNote.title
+ : 'Untitled'}
+
+
+
+
+
+
+ )}
@@ -127,42 +363,52 @@ 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;
left: 0;
bottom: 0;
right: 0;
- -webkit-app-region: drag;
& > .container {
position: relative;
margin: 50px auto 0;
background-color: ${({ theme }) => theme.navBackgroundColor};
- width: 400px;
+ width: calc(100% -15px);
+ max-width: 720px;
+ overflow: hidden;
z-index: 6002;
- ${border}
+ ${border};
border-radius: 10px;
- max-height: 360px;
+ max-height: calc(100% - 100px);
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 {
- flex: 1;
overflow-x: hidden;
overflow-y: auto;
+ flex: 1;
& > .searching {
text-align: center;
color: ${({ theme }) => theme.disabledUiTextColor};
@@ -187,3 +433,89 @@ const Container = styled.div`
background-color: rgba(0, 0, 0, 0.4);
}
`
+
+const EditorPreview = styled.div`
+ .marked {
+ background-color: ${({ theme }) =>
+ theme.searchHighlightSubtleBackgroundColor};
+ color: ${({ theme }) => theme.searchHighlightTextColor} !important;
+ padding: 3px;
+ }
+
+ .marked + .marked {
+ margin-left: -3px;
+ padding-left: 0;
+ }
+
+ .selected {
+ background-color: ${({ theme }) => theme.searchHighlightBackgroundColor};
+ }
+
+ background-color: ${({ theme }) => theme.navBackgroundColor};
+
+ ${borderTop};
+ width: 100%;
+ flex: 1;
+ flex-shrink: 0;
+ height: 324px;
+ display: flex;
+ overflow: hidden;
+ flex-direction: column;
+ & > .preview-control {
+ flex-shrink: 0;
+ display: flex;
+ ${borderBottom}
+ & > .preview-control__location {
+ overflow: hidden;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ padding-left: 10px;
+ & > .label {
+ flex: 1;
+ ${textOverflow}
+ &.empty {
+ color: ${({ theme }) => theme.disabledUiTextColor};
+ }
+ }
+ & > .icon {
+ flex-shrink: 0;
+ 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;
+ overflow-y: auto;
+ }
+ .CodeMirror {
+ 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..a730efe772
--- /dev/null
+++ b/src/lib/search/search.ts
@@ -0,0 +1,91 @@
+import { NoteDoc } from '../db/types'
+import { EditorPosition } from '../CodeMirror'
+
+export interface SearchResult {
+ id: string
+ lineString: string
+ matchString: string
+ matchColumn: number
+ matchLength: number
+ lineNumber: number
+}
+
+export interface TagSearchResult {
+ tagName: string
+ matchString: string
+}
+
+export interface NoteSearchData {
+ titleSearchResult: string | null
+ tagSearchResults: TagSearchResult[]
+ 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 currentPosition = 0
+ let lineColumn = 0
+ let lineNum = 0
+
+ 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
+ }
+ }
+ return {
+ line: lineNum,
+ ch: lineColumn,
+ }
+}
+
+export function getMatchData(text: string, searchTerm: RegExp): SearchResult[] {
+ const data: SearchResult[] = []
+
+ let resultId = 0
+ const lines: string[] = text.split('\n')
+
+ 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 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 == previousLineNumber) {
+ continue
+ } else {
+ previousLineNumber = pos.line
+ }
+ }
+ data.push({
+ id: `${resultId++}`,
+ lineString: lines[pos.line],
+ lineNumber: pos.line + 1,
+ matchLength: matchStr.length,
+ matchColumn: pos.ch,
+ matchString: 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..e987552409 100644
--- a/src/lib/styled/BaseTheme.ts
+++ b/src/lib/styled/BaseTheme.ts
@@ -59,4 +59,12 @@ export interface BaseTheme {
// Input
inputBackground: string
+
+ // Search Highlight
+ searchHighlightBackgroundColor: string
+ searchHighlightSubtleBackgroundColor: string
+ searchItemSelectionTextColor: string
+ searchItemSelectionBackgroundColor: string
+ searchItemSelectionHoverBackgroundColor: string
+ searchHighlightTextColor: string
}
diff --git a/src/themes/dark.ts b/src/themes/dark.ts
index b6fd12c7ec..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)'
@@ -82,4 +83,12 @@ export const darkTheme: BaseTheme = {
// Input
inputBackground: light12Color,
+
+ // Search Highlight
+ 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 099375de31..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)'
@@ -84,4 +84,12 @@ export const legacyTheme: BaseTheme = {
// Input
inputBackground: dark12Color,
+
+ // Search Highlight
+ 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 fbd4411297..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'
@@ -91,4 +91,12 @@ export const lightTheme: BaseTheme = {
// Input
inputBackground: '#fff',
+
+ // Search Highlight
+ 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 1591f29c62..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)'
@@ -85,4 +86,12 @@ export const sepiaTheme: BaseTheme = {
// Input
inputBackground: dark12Color,
+
+ // Search Highlight
+ 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 004660d33f..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)'
@@ -82,4 +83,12 @@ export const solarizedDarkTheme: BaseTheme = {
// Input
inputBackground: light12Color,
+
+ // Search Highlight
+ searchHighlightBackgroundColor: '#ffc107',
+ searchHighlightSubtleBackgroundColor: '#ffdb70',
+ searchItemSelectionTextColor: light100Color,
+ searchItemSelectionBackgroundColor: primaryColor,
+ searchItemSelectionHoverBackgroundColor: primaryDarkerColor,
+ searchHighlightTextColor: dark87Color,
}