First commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<span class="suggestion-flair" aria-label="Not created yet, select to create"
|
||||
><svg viewBox="0 0 100 100" class="add-note-glyph" width="16" height="16"
|
||||
><path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
d="M23.3,6.7c-3.7,0-6.7,3-6.7,6.7v73.3c0,3.7,3,6.7,6.7,6.7h28.4c-3.2-4.8-5.1-10.5-5.1-16.7c0-16.6,13.4-30,30-30 c2.3,0,4.5,0.3,6.7,0.8V31.7c0-0.9-0.3-1.7-1-2.4L60.7,7.6c-0.6-0.6-1.5-1-2.4-1L23.3,6.7z M56.7,13L77,33.3H60 c-1.8,0-3.3-1.5-3.3-3.3L56.7,13z M76.7,53.3c-12.9,0-23.3,10.4-23.3,23.3S63.8,100,76.7,100S100,89.6,100,76.7 S89.6,53.3,76.7,53.3z M76.7,63.3c1.8,0,3.3,1.5,3.3,3.3v6.7h6.7c1.8,0,3.3,1.5,3.3,3.3c0,1.8-1.5,3.3-3.3,3.3H80v6.7 c0,1.8-1.5,3.3-3.3,3.3c-1.8,0-3.3-1.5-3.3-3.3V80h-6.7c-1.8,0-3.3-1.5-3.3-3.3s1.5-3.3,3.3-3.3h6.7v-6.7 C73.3,64.8,74.8,63.3,76.7,63.3L76.7,63.3z" /></svg
|
||||
></span>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { debounce, Platform } from 'obsidian'
|
||||
import { toggleInputComposition } from '../globals'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import type LocatorPlugin from '../main'
|
||||
import { wait } from '../tools/utils'
|
||||
|
||||
export let initialValue = ''
|
||||
export let placeholder = ''
|
||||
export let plugin: LocatorPlugin
|
||||
let initialSet = false
|
||||
let value = ''
|
||||
let elInput: HTMLInputElement
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export function setInputValue(v: string): void {
|
||||
value = v
|
||||
}
|
||||
|
||||
function watchInitialValue(v: string): void {
|
||||
if (v && !initialSet && !value) {
|
||||
initialSet = true
|
||||
value = v
|
||||
selectInput()
|
||||
}
|
||||
}
|
||||
|
||||
$: watchInitialValue(initialValue)
|
||||
|
||||
function selectInput(_?: HTMLElement): void {
|
||||
tick()
|
||||
.then(async () => {
|
||||
if (Platform.isMobileApp) await wait(200)
|
||||
elInput.focus()
|
||||
return tick()
|
||||
})
|
||||
.then(async () => {
|
||||
if (Platform.isMobileApp) await wait(200)
|
||||
elInput.select()
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedOnInput = debounce(() => {
|
||||
// If typing a query and not executing it,
|
||||
// the next time we open the modal, the search field will be empty
|
||||
plugin.searchHistory.addToHistory('')
|
||||
dispatch('input', value)
|
||||
}, 300)
|
||||
</script>
|
||||
|
||||
<div class="omnisearch-input-container">
|
||||
<div class="omnisearch-input-field">
|
||||
<input
|
||||
bind:this="{elInput}"
|
||||
bind:value="{value}"
|
||||
class="prompt-input"
|
||||
on:compositionend="{_ => toggleInputComposition(false)}"
|
||||
on:compositionstart="{_ => toggleInputComposition(true)}"
|
||||
on:input="{debouncedOnInput}"
|
||||
placeholder="{placeholder}"
|
||||
spellcheck="false"
|
||||
type="text"
|
||||
use:selectInput />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<div class="prompt-results" on:mousedown={e => e.preventDefault()}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script lang="ts">
|
||||
import InputSearch from './InputSearch.svelte'
|
||||
import {
|
||||
Action,
|
||||
eventBus,
|
||||
excerptAfter,
|
||||
type ResultNote,
|
||||
type SearchMatch,
|
||||
} from '../globals'
|
||||
import { getCtrlKeyLabel, loopIndex } from '../tools/utils'
|
||||
import { onDestroy, onMount, tick } from 'svelte'
|
||||
import { MarkdownView, Platform } from 'obsidian'
|
||||
import ModalContainer from './ModalContainer.svelte'
|
||||
import {
|
||||
LocatorInFileModal,
|
||||
LocatorVaultModal,
|
||||
} from '../components/modals'
|
||||
import ResultItemInFile from './ResultItemInFile.svelte'
|
||||
import { Query } from '../search/query'
|
||||
import { openNote } from '../tools/notes'
|
||||
import type LocatorPlugin from '../main'
|
||||
|
||||
export let plugin: LocatorPlugin
|
||||
export let modal: LocatorInFileModal
|
||||
export let parent: LocatorVaultModal | null = null
|
||||
export let singleFilePath = ''
|
||||
export let previousQuery: string | undefined
|
||||
|
||||
let searchQuery: string
|
||||
let groupedOffsets: number[] = []
|
||||
let selectedIndex = 0
|
||||
let note: ResultNote | undefined
|
||||
let query: Query
|
||||
|
||||
$: searchQuery = previousQuery ?? ''
|
||||
|
||||
onMount(() => {
|
||||
eventBus.enable('infile')
|
||||
|
||||
eventBus.on('infile', Action.Enter, openSelection)
|
||||
eventBus.on('infile', Action.OpenInNewPane, openSelectionInNewTab)
|
||||
eventBus.on('infile', Action.ArrowUp, () => moveIndex(-1))
|
||||
eventBus.on('infile', Action.ArrowDown, () => moveIndex(1))
|
||||
eventBus.on('infile', Action.Tab, switchToVaultModal)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
eventBus.disable('infile')
|
||||
})
|
||||
|
||||
$: (async () => {
|
||||
if (searchQuery) {
|
||||
query = new Query(searchQuery, {
|
||||
ignoreDiacritics: plugin.settings.ignoreDiacritics,
|
||||
ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics,
|
||||
})
|
||||
note =
|
||||
(
|
||||
await plugin.searchEngine.getSuggestions(query, {
|
||||
singleFilePath,
|
||||
})
|
||||
)[0] ?? null
|
||||
}
|
||||
selectedIndex = 0
|
||||
await scrollIntoView()
|
||||
})()
|
||||
|
||||
$: {
|
||||
if (note) {
|
||||
let groups = getGroups(note.matches)
|
||||
|
||||
// If there are quotes in the search,
|
||||
// only show results that match at least one of the quotes
|
||||
const exactTerms = query.getExactTerms()
|
||||
if (exactTerms.length) {
|
||||
groups = groups.filter(group =>
|
||||
exactTerms.every(exact =>
|
||||
group.some(match => match.match.includes(exact))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
groupedOffsets = groups.map(group => Math.round(group.first()!.offset))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group together close matches to reduce the number of results
|
||||
*/
|
||||
function getGroups(matches: SearchMatch[]): SearchMatch[][] {
|
||||
const groups: SearchMatch[][] = []
|
||||
let lastOffset = -1
|
||||
let count = 0 // Avoid infinite loops
|
||||
while (++count < 100) {
|
||||
const group = getGroupedMatches(matches, lastOffset, excerptAfter)
|
||||
if (!group.length) break
|
||||
lastOffset = group.last()!.offset
|
||||
groups.push(group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
function getGroupedMatches(
|
||||
matches: SearchMatch[],
|
||||
offsetFrom: number,
|
||||
maxLen: number
|
||||
): SearchMatch[] {
|
||||
const first = matches.find(m => m.offset > offsetFrom)
|
||||
if (!first) return []
|
||||
return matches.filter(
|
||||
m => m.offset > offsetFrom && m.offset <= first.offset + maxLen
|
||||
)
|
||||
}
|
||||
|
||||
function moveIndex(dir: 1 | -1): void {
|
||||
selectedIndex = loopIndex(selectedIndex + dir, groupedOffsets.length)
|
||||
scrollIntoView()
|
||||
}
|
||||
|
||||
async function scrollIntoView(): Promise<void> {
|
||||
await tick()
|
||||
const elem = document.querySelector(`[data-result-id="${selectedIndex}"]`)
|
||||
elem?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
|
||||
async function openSelectionInNewTab(): Promise<void> {
|
||||
return openSelection(true)
|
||||
}
|
||||
|
||||
async function openSelection(newTab = false): Promise<void> {
|
||||
if (note) {
|
||||
modal.close()
|
||||
if (parent) parent.close()
|
||||
|
||||
// Open (or switch focus to) the note
|
||||
const reg = plugin.textProcessor.stringsToRegex(note.foundWords)
|
||||
reg.exec(note.content)
|
||||
await openNote(plugin.app, note, reg.lastIndex, newTab)
|
||||
|
||||
// Move cursor to the match
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
if (!view) {
|
||||
// Not an editable document, so no cursor to place
|
||||
return
|
||||
// throw new Error('OmniSearch - No active MarkdownView')
|
||||
}
|
||||
|
||||
const offset = groupedOffsets[selectedIndex] ?? 0
|
||||
const pos = view.editor.offsetToPos(offset)
|
||||
pos.ch = 0
|
||||
view.editor.setCursor(pos)
|
||||
view.editor.scrollIntoView({
|
||||
from: { line: pos.line - 10, ch: 0 },
|
||||
to: { line: pos.line + 10, ch: 0 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function switchToVaultModal(): void {
|
||||
new LocatorVaultModal(plugin, searchQuery ?? previousQuery).open()
|
||||
modal.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputSearch
|
||||
plugin="{plugin}"
|
||||
on:input="{e => (searchQuery = e.detail)}"
|
||||
placeholder="Locator - File"
|
||||
initialValue="{previousQuery}">
|
||||
<div class="omnisearch-input-container__buttons">
|
||||
{#if Platform.isMobile}
|
||||
<button on:click="{switchToVaultModal}">Vault search</button>
|
||||
{/if}
|
||||
</div>
|
||||
</InputSearch>
|
||||
|
||||
<ModalContainer>
|
||||
{#if groupedOffsets.length && note}
|
||||
{#each groupedOffsets as offset, i}
|
||||
<ResultItemInFile
|
||||
{plugin}
|
||||
offset="{offset}"
|
||||
note="{note}"
|
||||
index="{i}"
|
||||
selected="{i === selectedIndex}"
|
||||
on:mousemove="{_e => (selectedIndex = i)}"
|
||||
on:click="{evt => openSelection(evt.ctrlKey)}"
|
||||
on:auxclick="{evt => {
|
||||
if (evt.button == 1) openSelection(true)
|
||||
}}" />
|
||||
{/each}
|
||||
{:else}
|
||||
<div style="text-align: center;">
|
||||
We found 0 results for your search here.
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContainer>
|
||||
|
||||
<div class="prompt-instructions">
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">↑↓</span><span>to navigate</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">↵</span><span>to open</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">tab</span>
|
||||
<span>to switch to Vault Search</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">esc</span>
|
||||
{#if !!parent}
|
||||
<span>to go back to Vault Search</span>
|
||||
{:else}
|
||||
<span>to close</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{getCtrlKeyLabel()} ↵</span>
|
||||
<span>to open in a new pane</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,428 @@
|
||||
<script lang="ts">
|
||||
import { MarkdownView, Notice, Platform, TFile } from 'obsidian'
|
||||
import { onDestroy, onMount, tick } from 'svelte'
|
||||
import InputSearch from './InputSearch.svelte'
|
||||
import ModalContainer from './ModalContainer.svelte'
|
||||
import {
|
||||
eventBus,
|
||||
indexingStep,
|
||||
IndexingStepType,
|
||||
type ResultNote,
|
||||
SPACE_OR_PUNCTUATION,
|
||||
Action,
|
||||
} from '../globals'
|
||||
import { createNote, openNote } from '../tools/notes'
|
||||
import {
|
||||
getCtrlKeyLabel,
|
||||
getAltKeyLabel,
|
||||
getExtension,
|
||||
isFilePDF,
|
||||
loopIndex,
|
||||
} from '../tools/utils'
|
||||
import {
|
||||
LocatorInFileModal,
|
||||
type LocatorVaultModal,
|
||||
} from '../components/modals'
|
||||
import ResultItemVault from './ResultItemVault.svelte'
|
||||
import { Query } from '../search/query'
|
||||
import { cancelable, CancelablePromise } from 'cancelable-promise'
|
||||
import { debounce } from 'lodash-es'
|
||||
import type LocatorPlugin from '../main'
|
||||
import LazyLoader from './lazy-loader/LazyLoader.svelte'
|
||||
|
||||
let {
|
||||
modal,
|
||||
previousQuery,
|
||||
plugin,
|
||||
}: {
|
||||
modal: LocatorVaultModal
|
||||
previousQuery?: string | undefined
|
||||
plugin: LocatorPlugin
|
||||
} = $props()
|
||||
|
||||
let selectedIndex = $state(0)
|
||||
let historySearchIndex = 0
|
||||
let searchQuery = $state(previousQuery ?? '')
|
||||
let resultNotes: ResultNote[] = $state([])
|
||||
let query: Query
|
||||
let indexingStepDesc = $state('')
|
||||
let searching = $state(true)
|
||||
let refInput: InputSearch | undefined
|
||||
let openInNewPaneKey: string = $state('')
|
||||
let openInCurrentPaneKey: string = $state('')
|
||||
let createInNewPaneKey: string = $state('')
|
||||
let createInCurrentPaneKey: string = $state('')
|
||||
let openInNewLeafKey: string = `${getCtrlKeyLabel()} ${getAltKeyLabel()} ↵`
|
||||
|
||||
const selectedNote = $derived(resultNotes[selectedIndex])
|
||||
|
||||
$effect(() => {
|
||||
if (plugin.settings.openInNewPane) {
|
||||
openInNewPaneKey = '↵'
|
||||
openInCurrentPaneKey = getCtrlKeyLabel() + ' ↵'
|
||||
createInNewPaneKey = 'Shift ↵'
|
||||
createInCurrentPaneKey = getCtrlKeyLabel() + ' Shift ↵'
|
||||
} else {
|
||||
openInNewPaneKey = getCtrlKeyLabel() + ' ↵'
|
||||
openInCurrentPaneKey = '↵'
|
||||
createInNewPaneKey = getCtrlKeyLabel() + ' Shift ↵'
|
||||
createInCurrentPaneKey = 'Shift ↵'
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (searchQuery) {
|
||||
updateResultsDebounced()
|
||||
} else {
|
||||
searching = false
|
||||
resultNotes = []
|
||||
}
|
||||
})
|
||||
$effect(() => {
|
||||
switch ($indexingStep) {
|
||||
case IndexingStepType.LoadingCache:
|
||||
indexingStepDesc = 'Loading cache...'
|
||||
break
|
||||
case IndexingStepType.ReadingFiles:
|
||||
indexingStepDesc = 'Reading files...'
|
||||
break
|
||||
case IndexingStepType.IndexingFiles:
|
||||
indexingStepDesc = 'Indexing files...'
|
||||
break
|
||||
case IndexingStepType.WritingCache:
|
||||
updateResultsDebounced()
|
||||
indexingStepDesc = 'Updating cache...'
|
||||
break
|
||||
default:
|
||||
updateResultsDebounced()
|
||||
indexingStepDesc = ''
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
eventBus.enable('vault')
|
||||
eventBus.on('vault', Action.Enter, openNoteAndCloseModal)
|
||||
eventBus.on('vault', Action.OpenInBackground, openNoteInBackground)
|
||||
eventBus.on('vault', Action.CreateNote, createNoteAndCloseModal)
|
||||
eventBus.on('vault', Action.OpenInNewPane, openNoteInNewPane)
|
||||
eventBus.on('vault', Action.InsertLink, insertLink)
|
||||
eventBus.on('vault', Action.Tab, switchToInFileModal)
|
||||
eventBus.on('vault', Action.ArrowUp, () => moveIndex(-1))
|
||||
eventBus.on('vault', Action.ArrowDown, () => moveIndex(1))
|
||||
eventBus.on('vault', Action.PrevSearchHistory, prevSearchHistory)
|
||||
eventBus.on('vault', Action.NextSearchHistory, nextSearchHistory)
|
||||
eventBus.on('vault', Action.OpenInNewLeaf, openNoteInNewLeaf)
|
||||
await plugin.notesIndexer.refreshIndex()
|
||||
await updateResultsDebounced()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
eventBus.disable('vault')
|
||||
})
|
||||
|
||||
async function prevSearchHistory() {
|
||||
// Filter out the empty string, if it's there
|
||||
const history = (await plugin.searchHistory.getHistory()).filter(s => s)
|
||||
if (++historySearchIndex >= history.length) {
|
||||
historySearchIndex = 0
|
||||
}
|
||||
searchQuery = history[historySearchIndex]
|
||||
refInput?.setInputValue(searchQuery ?? '')
|
||||
}
|
||||
|
||||
async function nextSearchHistory() {
|
||||
const history = (await plugin.searchHistory.getHistory()).filter(s => s)
|
||||
if (--historySearchIndex < 0) {
|
||||
historySearchIndex = history.length ? history.length - 1 : 0
|
||||
}
|
||||
searchQuery = history[historySearchIndex]
|
||||
refInput?.setInputValue(searchQuery ?? '')
|
||||
}
|
||||
|
||||
let cancelableQuery: CancelablePromise<ResultNote[]> | null = null
|
||||
async function updateResults() {
|
||||
searching = true
|
||||
// If search is already in progress, cancel it and start a new one
|
||||
if (cancelableQuery) {
|
||||
cancelableQuery.cancel()
|
||||
cancelableQuery = null
|
||||
}
|
||||
query = new Query(searchQuery, {
|
||||
ignoreDiacritics: plugin.settings.ignoreDiacritics,
|
||||
ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics,
|
||||
})
|
||||
cancelableQuery = cancelable(
|
||||
new Promise(resolve => {
|
||||
resolve(plugin.searchEngine.getSuggestions(query))
|
||||
})
|
||||
)
|
||||
resultNotes = await cancelableQuery
|
||||
selectedIndex = 0
|
||||
await scrollIntoView()
|
||||
searching = false
|
||||
}
|
||||
|
||||
// Debounce this function to avoid multiple calls caused by Svelte reactivity
|
||||
const updateResultsDebounced = debounce(updateResults, 0)
|
||||
|
||||
function onClick(evt?: MouseEvent | KeyboardEvent) {
|
||||
if (!selectedNote) return
|
||||
if (evt?.ctrlKey) {
|
||||
openNoteInNewPane()
|
||||
} else {
|
||||
openNoteAndCloseModal()
|
||||
}
|
||||
modal.close()
|
||||
}
|
||||
|
||||
function openNoteAndCloseModal(): void {
|
||||
if (!selectedNote) return
|
||||
openSearchResult(selectedNote)
|
||||
modal.close()
|
||||
}
|
||||
|
||||
function openNoteInBackground(): void {
|
||||
if (!selectedNote) return
|
||||
openSearchResult(selectedNote, true)
|
||||
}
|
||||
|
||||
function openNoteInNewPane(): void {
|
||||
if (!selectedNote) return
|
||||
openSearchResult(selectedNote, true)
|
||||
modal.close()
|
||||
}
|
||||
|
||||
function openNoteInNewLeaf(): void {
|
||||
if (!selectedNote) return
|
||||
openSearchResult(selectedNote, true, true)
|
||||
modal.close()
|
||||
}
|
||||
|
||||
function saveCurrentQuery() {
|
||||
if (searchQuery) {
|
||||
plugin.searchHistory.addToHistory(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
function openSearchResult(
|
||||
note: ResultNote,
|
||||
newPane = false,
|
||||
newLeaf = false
|
||||
) {
|
||||
saveCurrentQuery()
|
||||
const offset = note.matches?.[0]?.offset ?? 0
|
||||
openNote(plugin.app, note, offset, newPane, newLeaf)
|
||||
}
|
||||
|
||||
async function onClickCreateNote(_e: MouseEvent) {
|
||||
await createNoteAndCloseModal()
|
||||
}
|
||||
|
||||
async function createNoteAndCloseModal(opt?: {
|
||||
newLeaf: boolean
|
||||
}): Promise<void> {
|
||||
if (searchQuery) {
|
||||
try {
|
||||
await createNote(plugin.app, searchQuery, opt?.newLeaf)
|
||||
} catch (e) {
|
||||
new Notice((e as Error).message)
|
||||
return
|
||||
}
|
||||
modal.close()
|
||||
}
|
||||
}
|
||||
|
||||
function insertLink(): void {
|
||||
if (!selectedNote) return
|
||||
const file = plugin.app.vault
|
||||
.getMarkdownFiles()
|
||||
.find(f => f.path === selectedNote.path)
|
||||
const active = plugin.app.workspace.getActiveFile()
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
if (!view?.editor) {
|
||||
new Notice('Locator - Error - No active editor', 3000)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate link
|
||||
let link: string
|
||||
if (file && active) {
|
||||
link = plugin.app.fileManager.generateMarkdownLink(
|
||||
file,
|
||||
active.path,
|
||||
'',
|
||||
selectedNote.displayTitle
|
||||
)
|
||||
} else {
|
||||
const maybeDisplayTitle =
|
||||
selectedNote.displayTitle === '' ? '' : `|${selectedNote.displayTitle}`
|
||||
link = `[[${selectedNote.basename}.${getExtension(
|
||||
selectedNote.path
|
||||
)}${maybeDisplayTitle}]]`
|
||||
}
|
||||
|
||||
// Inject link
|
||||
const cursor = view.editor.getCursor()
|
||||
view.editor.replaceRange(link, cursor, cursor)
|
||||
cursor.ch += link.length
|
||||
view.editor.setCursor(cursor)
|
||||
|
||||
modal.close()
|
||||
}
|
||||
|
||||
function switchToInFileModal(): void {
|
||||
// Do nothing if the selectedNote is a PDF,
|
||||
// or if there is 0 match (e.g indexing in progress)
|
||||
if (
|
||||
selectedNote &&
|
||||
(isFilePDF(selectedNote?.path) || !selectedNote?.matches.length)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
saveCurrentQuery()
|
||||
modal.close()
|
||||
|
||||
if (selectedNote) {
|
||||
// Open in-file modal for selected search result
|
||||
const file = plugin.app.vault.getAbstractFileByPath(selectedNote.path)
|
||||
if (file && file instanceof TFile) {
|
||||
new LocatorInFileModal(plugin, file, searchQuery).open()
|
||||
}
|
||||
} else {
|
||||
// Open in-file modal for active file
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
if (view?.file) {
|
||||
new LocatorInFileModal(plugin, view.file, searchQuery).open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveIndex(dir: 1 | -1): void {
|
||||
selectedIndex = loopIndex(selectedIndex + dir, resultNotes.length)
|
||||
scrollIntoView()
|
||||
}
|
||||
|
||||
async function scrollIntoView(): Promise<void> {
|
||||
await tick()
|
||||
if (selectedNote) {
|
||||
const elem = activeWindow.document.querySelector(
|
||||
`[data-result-id="${selectedNote.path}"]`
|
||||
)
|
||||
elem?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputSearch
|
||||
bind:this={refInput}
|
||||
{plugin}
|
||||
initialValue={searchQuery}
|
||||
on:input={e => (searchQuery = e.detail)}
|
||||
placeholder="Locator - Vault">
|
||||
<div class="omnisearch-input-container__buttons">
|
||||
{#if plugin.settings.showCreateButton}
|
||||
<button on:click={onClickCreateNote}>Create note</button>
|
||||
{/if}
|
||||
{#if Platform.isMobile}
|
||||
<button on:click={switchToInFileModal}>In-File search</button>
|
||||
{/if}
|
||||
</div>
|
||||
</InputSearch>
|
||||
|
||||
{#if indexingStepDesc}
|
||||
<div style="text-align: center; color: var(--text-accent); margin-top: 10px">
|
||||
⏳ Work in progress: {indexingStepDesc}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ModalContainer>
|
||||
{#each resultNotes as result, i}
|
||||
<LazyLoader
|
||||
height={100}
|
||||
offset={500}
|
||||
keep={true}
|
||||
fadeOption={{ delay: 0, duration: 0 }}>
|
||||
<ResultItemVault
|
||||
{plugin}
|
||||
selected={i === selectedIndex}
|
||||
note={result}
|
||||
on:mousemove={_ => (selectedIndex = i)}
|
||||
on:click={onClick}
|
||||
on:auxclick={evt => {
|
||||
if (evt.button == 1) openNoteInNewPane()
|
||||
}} />
|
||||
</LazyLoader>
|
||||
{/each}
|
||||
<div style="text-align: center;">
|
||||
{#if !resultNotes.length && searchQuery && !searching}
|
||||
We found 0 results for your search here.
|
||||
{#if plugin.settings.simpleSearch && searchQuery
|
||||
.split(SPACE_OR_PUNCTUATION)
|
||||
.some(w => w.length < 3)}
|
||||
<br />
|
||||
<span style="color: var(--text-accent); font-size: small">
|
||||
You have enabled "Simpler Search" in the settings, try to type more
|
||||
characters.
|
||||
</span>
|
||||
{/if}
|
||||
{:else if searching}
|
||||
Searching...
|
||||
{/if}
|
||||
</div>
|
||||
</ModalContainer>
|
||||
|
||||
<div class="prompt-instructions">
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">↑↓</span><span>to navigate</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{getAltKeyLabel()} ↑↓</span>
|
||||
<span>to cycle history</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{openInCurrentPaneKey}</span>
|
||||
<span>to open</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">Tab</span>
|
||||
<span>to switch to In-File Search</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{openInNewPaneKey}</span>
|
||||
<span>to open in a new pane</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{openInNewLeafKey}</span>
|
||||
<span>to open in a new split</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{getCtrlKeyLabel()} o</span>
|
||||
<span>to open in the background</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{createInCurrentPaneKey}</span>
|
||||
<span>to create</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{createInNewPaneKey}</span>
|
||||
<span>to create in a new pane</span>
|
||||
</div>
|
||||
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{getAltKeyLabel()} ↵</span>
|
||||
<span>to insert a link</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">{getCtrlKeyLabel()} g</span>
|
||||
<span>to toggle excerpts</span>
|
||||
</div>
|
||||
<div class="prompt-instruction">
|
||||
<span class="prompt-instruction-command">Esc</span><span>to close</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import GlyphAddNote from './GlyphAddNote.svelte'
|
||||
|
||||
export let id: string
|
||||
export let selected = false
|
||||
export let glyph = false
|
||||
export let cssClass = ''
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-result-id={id}
|
||||
class="suggestion-item omnisearch-result {cssClass}"
|
||||
class:is-selected={selected}
|
||||
on:mousemove
|
||||
on:click
|
||||
on:keypress
|
||||
on:auxclick>
|
||||
{#if glyph}
|
||||
<GlyphAddNote />
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { ResultNote } from '../globals'
|
||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||
import type LocatorPlugin from '../main'
|
||||
|
||||
export let plugin: LocatorPlugin
|
||||
export let offset: number
|
||||
export let note: ResultNote
|
||||
export let index = 0
|
||||
export let selected = false
|
||||
|
||||
$: cleanedContent = plugin.textProcessor.makeExcerpt(note?.content ?? '', offset)
|
||||
</script>
|
||||
|
||||
<ResultItemContainer
|
||||
id="{index.toString()}"
|
||||
on:auxclick
|
||||
on:click
|
||||
on:mousemove
|
||||
selected="{selected}">
|
||||
<div class="omnisearch-result__body">
|
||||
{@html plugin.textProcessor.highlightText(cleanedContent, note.matches)}
|
||||
</div>
|
||||
</ResultItemContainer>
|
||||
@@ -0,0 +1,227 @@
|
||||
<script lang="ts">
|
||||
import { showExcerpt } from '../settings/index'
|
||||
import type { ResultNote } from '../globals'
|
||||
import {
|
||||
getExtension,
|
||||
isFileCanvas,
|
||||
isFileExcalidraw,
|
||||
isFileImage,
|
||||
isFilePDF,
|
||||
pathWithoutFilename,
|
||||
} from '../tools/utils'
|
||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||
import type LocatorPlugin from '../main'
|
||||
import { setIcon, TFile } from 'obsidian'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
// Import icon utility functions
|
||||
import {
|
||||
loadIconData,
|
||||
initializeIconPacks,
|
||||
getIconNameForPath,
|
||||
loadIconSVG,
|
||||
getDefaultIconSVG,
|
||||
} from '../tools/icon-utils'
|
||||
|
||||
export let selected = false
|
||||
export let note: ResultNote
|
||||
export let plugin: LocatorPlugin
|
||||
|
||||
let imagePath: string | null = null
|
||||
let title = ''
|
||||
let notePath = ''
|
||||
let iconData = {}
|
||||
let folderIconSVG: string | null = null
|
||||
let fileIconSVG: string | null = null
|
||||
let prefixToIconPack: { [prefix: string]: string } = {}
|
||||
let iconsPath: string
|
||||
let iconDataLoaded = false // Flag to indicate iconData is loaded
|
||||
|
||||
// Initialize icon data and icon packs once when the component mounts
|
||||
onMount(async () => {
|
||||
iconData = await loadIconData(plugin)
|
||||
const iconPacks = await initializeIconPacks(plugin)
|
||||
prefixToIconPack = iconPacks.prefixToIconPack
|
||||
iconsPath = iconPacks.iconsPath
|
||||
iconDataLoaded = true // Set the flag after iconData is loaded
|
||||
})
|
||||
|
||||
// Reactive statement to call loadIcons() whenever the note changes and iconData is loaded
|
||||
$: if (note && note.path && iconDataLoaded) {
|
||||
;(async () => {
|
||||
// Update title and notePath before loading icons
|
||||
title = note.displayTitle || note.basename
|
||||
notePath = pathWithoutFilename(note.path)
|
||||
await loadIcons()
|
||||
})()
|
||||
}
|
||||
|
||||
async function loadIcons() {
|
||||
// Load folder icon
|
||||
const folderIconName = getIconNameForPath(notePath, iconData)
|
||||
if (folderIconName) {
|
||||
folderIconSVG = await loadIconSVG(
|
||||
folderIconName,
|
||||
plugin,
|
||||
iconsPath,
|
||||
prefixToIconPack
|
||||
)
|
||||
} else {
|
||||
// Fallback to default folder icon
|
||||
folderIconSVG = getDefaultIconSVG('folder', plugin)
|
||||
}
|
||||
|
||||
// Load file icon
|
||||
const fileIconName = getIconNameForPath(note.path, iconData)
|
||||
if (fileIconName) {
|
||||
fileIconSVG = await loadIconSVG(
|
||||
fileIconName,
|
||||
plugin,
|
||||
iconsPath,
|
||||
prefixToIconPack
|
||||
)
|
||||
} else {
|
||||
// Fallback to default icons based on file type
|
||||
fileIconSVG = getDefaultIconSVG(note.path, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
// Svelte action to render SVG content with dynamic updates
|
||||
function renderSVG(node: HTMLElement, svgContent: string) {
|
||||
node.innerHTML = svgContent
|
||||
return {
|
||||
update(newSvgContent: string) {
|
||||
node.innerHTML = newSvgContent
|
||||
},
|
||||
destroy() {
|
||||
node.innerHTML = ''
|
||||
},
|
||||
}
|
||||
}
|
||||
let elFolderPathIcon: HTMLElement | null = null
|
||||
let elFilePathIcon: HTMLElement | null = null
|
||||
let elEmbedIcon: HTMLElement | null = null
|
||||
|
||||
$: {
|
||||
imagePath = null
|
||||
if (isFileImage(note.path)) {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(note.path)
|
||||
if (file instanceof TFile) {
|
||||
imagePath = plugin.app.vault.getResourcePath(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: matchesTitle = plugin.textProcessor.getMatches(title, note.foundWords)
|
||||
$: matchesNotePath = plugin.textProcessor.getMatches(
|
||||
notePath,
|
||||
note.foundWords
|
||||
)
|
||||
$: cleanedContent = plugin.textProcessor.makeExcerpt(
|
||||
note.content,
|
||||
note.matches[0]?.offset ?? -1
|
||||
)
|
||||
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
|
||||
$: {
|
||||
title = note.displayTitle || note.basename
|
||||
notePath = pathWithoutFilename(note.path)
|
||||
|
||||
// Icons
|
||||
if (elFolderPathIcon) {
|
||||
setIcon(elFolderPathIcon, 'folder-open')
|
||||
}
|
||||
if (elFilePathIcon) {
|
||||
if (isFileImage(note.path)) {
|
||||
setIcon(elFilePathIcon, 'image')
|
||||
} else if (isFilePDF(note.path)) {
|
||||
setIcon(elFilePathIcon, 'file-text')
|
||||
} else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) {
|
||||
setIcon(elFilePathIcon, 'layout-dashboard')
|
||||
} else {
|
||||
setIcon(elFilePathIcon, 'file')
|
||||
}
|
||||
}
|
||||
if (elEmbedIcon) {
|
||||
setIcon(elEmbedIcon, 'corner-down-right')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ResultItemContainer
|
||||
glyph="{glyph}"
|
||||
id="{note.path}"
|
||||
cssClass=" {note.isEmbed ? 'omnisearch-result__embed' : ''}"
|
||||
on:auxclick
|
||||
on:click
|
||||
on:mousemove
|
||||
selected="{selected}">
|
||||
<div>
|
||||
<div class="omnisearch-result__title-container">
|
||||
<span class="omnisearch-result__title">
|
||||
{#if note.isEmbed}
|
||||
<span
|
||||
bind:this="{elEmbedIcon}"
|
||||
title="The document above is embedded in this note"></span>
|
||||
{:else}
|
||||
<!-- File Icon -->
|
||||
{#if fileIconSVG}
|
||||
<span class="omnisearch-result__icon" use:renderSVG="{fileIconSVG}"
|
||||
></span>
|
||||
{/if}
|
||||
{/if}
|
||||
<span>
|
||||
{@html plugin.textProcessor.highlightText(title, matchesTitle)}
|
||||
</span>
|
||||
{#if !note.displayTitle}
|
||||
<span class="omnisearch-result__extension">
|
||||
.{getExtension(note.path)}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Counter -->
|
||||
{#if note.matches.length > 0}
|
||||
<span class="omnisearch-result__counter">
|
||||
{note.matches.length} {note.matches.length > 1
|
||||
? 'matches'
|
||||
: 'match'}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Folder path -->
|
||||
{#if notePath}
|
||||
<div class="omnisearch-result__folder-path">
|
||||
<!-- Folder Icon -->
|
||||
{#if folderIconSVG}
|
||||
<span class="omnisearch-result__icon" use:renderSVG="{folderIconSVG}"
|
||||
></span>
|
||||
{/if}
|
||||
<span>
|
||||
{@html plugin.textProcessor.highlightText(notePath, matchesNotePath)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Do not display the excerpt for embedding references -->
|
||||
{#if !note.isEmbed}
|
||||
<div style="display: flex; flex-direction: row;">
|
||||
{#if $showExcerpt}
|
||||
<div class="omnisearch-result__body">
|
||||
{@html plugin.textProcessor.highlightText(
|
||||
cleanedContent,
|
||||
note.matches
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Image -->
|
||||
{#if imagePath}
|
||||
<div class="omnisearch-result__image-container">
|
||||
<img style="width: 100px" src="{imagePath}" alt="" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ResultItemContainer>
|
||||
@@ -0,0 +1,208 @@
|
||||
<div use:load class={rootClass} style="height: {rootInitialHeight}">
|
||||
{#if loaded}
|
||||
<div
|
||||
in:fade={fadeOption || {}}
|
||||
class={contentClass}
|
||||
style={contentStyle}
|
||||
>
|
||||
<slot>Lazy load content</slot>
|
||||
</div>
|
||||
{#if !contentShow && placeholder}
|
||||
<Placeholder {placeholder} {placeholderProps} />
|
||||
{/if}
|
||||
{:else if placeholder}
|
||||
<Placeholder {placeholder} {placeholderProps} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// https://github.com/leafOfTree/svelte-lazy
|
||||
import { fade } from 'svelte/transition';
|
||||
import Placeholder from './Placeholder.svelte';
|
||||
export let keep = false;
|
||||
export let height = 0;
|
||||
export let offset = 150;
|
||||
export let fadeOption = {
|
||||
delay: 0,
|
||||
duration: 400,
|
||||
};
|
||||
export let resetHeightDelay = 0;
|
||||
export let onload = null;
|
||||
export let placeholder = null;
|
||||
export let placeholderProps = null;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
|
||||
const rootClass = 'svelte-lazy'
|
||||
+ (className ? ' ' + className : '');
|
||||
const contentClass = 'svelte-lazy-content';
|
||||
const rootInitialHeight = getStyleHeight();
|
||||
let loaded = false;
|
||||
|
||||
let contentShow = true;
|
||||
$: contentStyle = !contentShow ? 'display: none' : '';
|
||||
|
||||
function load(node) {
|
||||
setHeight(node);
|
||||
const handler = createHandler(node);
|
||||
addListeners(handler);
|
||||
setTimeout(() => {
|
||||
handler();
|
||||
});
|
||||
const observer = observeNode(node, handler);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
removeListeners(handler);
|
||||
observer.unobserve(node);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHandler(node) {
|
||||
const handler = throttle(e => {
|
||||
const nodeTop = node.getBoundingClientRect().top;
|
||||
const nodeBottom = node.getBoundingClientRect().bottom;
|
||||
const expectedTop = getContainerHeight(e) + offset;
|
||||
|
||||
if (nodeTop <= expectedTop && nodeBottom > 0) {
|
||||
loadNode(node);
|
||||
} else if (!keep) {
|
||||
unload(node)
|
||||
}
|
||||
}, 200);
|
||||
return handler;
|
||||
}
|
||||
|
||||
function observeNode(node, handler) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadNode(node);
|
||||
}
|
||||
})
|
||||
observer.observe(node);
|
||||
return observer;
|
||||
}
|
||||
|
||||
function unload(node) {
|
||||
setHeight(node);
|
||||
loaded = false
|
||||
}
|
||||
|
||||
function loadNode(node, handler) {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
resetHeight(node);
|
||||
if (onload) {
|
||||
onload(node);
|
||||
}
|
||||
}
|
||||
|
||||
function addListeners(handler) {
|
||||
document.addEventListener('scroll', handler, true);
|
||||
window.addEventListener('resize', handler);
|
||||
}
|
||||
|
||||
function removeListeners(handler) {
|
||||
document.removeEventListener('scroll', handler, true);
|
||||
window.removeEventListener('resize', handler);
|
||||
}
|
||||
|
||||
function getStyleHeight() {
|
||||
return (typeof height === 'number')
|
||||
? height + 'px'
|
||||
: height;
|
||||
}
|
||||
|
||||
function setHeight(node) {
|
||||
if (height) {
|
||||
node.style.height = getStyleHeight();
|
||||
}
|
||||
}
|
||||
|
||||
function resetHeight(node) {
|
||||
setTimeout(() => {
|
||||
const isLoading = checkImgLoadingStatus(node);
|
||||
if (!isLoading) {
|
||||
node.style.height = 'auto';
|
||||
}
|
||||
// Add a delay to wait for remote resources like images to load
|
||||
}, resetHeightDelay);
|
||||
}
|
||||
|
||||
function checkImgLoadingStatus(node) {
|
||||
const img = node.querySelector('img');
|
||||
if (!img) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!img.complete) {
|
||||
contentShow = false;
|
||||
|
||||
node.addEventListener('load', () => {
|
||||
// Use auto height if loading successfully
|
||||
contentShow = true;
|
||||
node.style.height = 'auto';
|
||||
}, { capture: true, once: true });
|
||||
|
||||
node.addEventListener('error', () => {
|
||||
// Show content with fixed height if there is error
|
||||
contentShow = true;
|
||||
}, { capture: true, once: true });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (img.naturalHeight === 0) {
|
||||
// Use fixed height if img has zero height
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getContainerHeight(e) {
|
||||
if (e?.target?.getBoundingClientRect) {
|
||||
return e.target.getBoundingClientRect().bottom;
|
||||
} else {
|
||||
return window.innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// From underscore souce code
|
||||
function throttle(func, wait, options) {
|
||||
let context, args, result;
|
||||
let timeout = null;
|
||||
let previous = 0;
|
||||
if (!options) options = {};
|
||||
const later = function() {
|
||||
previous = options.leading === false ? 0 : new Date();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
};
|
||||
|
||||
return function(event) {
|
||||
const now = new Date();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
const remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
{#if placeholder}
|
||||
<div class={placeholderClass}>
|
||||
{#if typeof placeholder === 'string'}
|
||||
<div>{placeholder}</div>
|
||||
{:else if ['function', 'object'].includes(typeof placeholder)}
|
||||
<svelte:component this={placeholder} {...placeholderProps} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<script>
|
||||
export let placeholder = null;
|
||||
export let placeholderProps = null;
|
||||
const placeholderClass = 'svelte-lazy-placeholder';
|
||||
</script>
|
||||
@@ -0,0 +1,223 @@
|
||||
import { MarkdownView, Modal, TFile } from 'obsidian'
|
||||
import type { Modifier } from 'obsidian'
|
||||
import ModalVault from './ModalVault.svelte'
|
||||
import ModalInFile from './ModalInFile.svelte'
|
||||
import { Action, eventBus, EventNames, isInputComposition } from '../globals'
|
||||
import type LocatorPlugin from '../main'
|
||||
import { mount, unmount } from 'svelte'
|
||||
|
||||
abstract class LocatorModal extends Modal {
|
||||
protected constructor(plugin: LocatorPlugin) {
|
||||
super(plugin.app)
|
||||
const settings = plugin.settings
|
||||
|
||||
// Remove all the default modal's children
|
||||
// so that we can more easily customize it
|
||||
// const closeEl = this.containerEl.find('.modal-close-button')
|
||||
this.modalEl.replaceChildren()
|
||||
// this.modalEl.append(closeEl)
|
||||
this.modalEl.addClass('locator-modal', 'prompt')
|
||||
this.modalEl.removeClass('modal')
|
||||
this.modalEl.tabIndex = -1
|
||||
|
||||
// Setup events that can be listened through the event bus
|
||||
|
||||
// #region Up/Down navigation
|
||||
|
||||
this.scope.register([], 'ArrowDown', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.ArrowDown)
|
||||
})
|
||||
this.scope.register([], 'ArrowUp', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.ArrowUp)
|
||||
})
|
||||
|
||||
// Ctrl+j/k
|
||||
for (const key of [
|
||||
{ k: 'J', dir: 'down' },
|
||||
{ k: 'K', dir: 'up' },
|
||||
] as const) {
|
||||
for (const modifier of ['Ctrl', 'Mod'] as const) {
|
||||
this.scope.register([modifier], key.k, _e => {
|
||||
if (settings.vimLikeNavigationShortcut) {
|
||||
// e.preventDefault()
|
||||
eventBus.emit('arrow-' + key.dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+n/p
|
||||
for (const key of [
|
||||
{ k: 'N', dir: 'down' },
|
||||
{ k: 'P', dir: 'up' },
|
||||
] as const) {
|
||||
for (const modifier of ['Ctrl', 'Mod'] as const) {
|
||||
this.scope.register([modifier], key.k, _e => {
|
||||
if (settings.vimLikeNavigationShortcut) {
|
||||
// e.preventDefault()
|
||||
eventBus.emit('arrow-' + key.dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion Up/Down navigation
|
||||
|
||||
let openInCurrentPaneKey: Modifier[]
|
||||
let openInNewPaneKey: Modifier[]
|
||||
let createInCurrentPaneKey: Modifier[]
|
||||
let createInNewPaneKey: Modifier[]
|
||||
let openInNewLeafKey: Modifier[] = ['Mod', 'Alt']
|
||||
if (settings.openInNewPane) {
|
||||
openInCurrentPaneKey = ['Mod']
|
||||
openInNewPaneKey = []
|
||||
createInCurrentPaneKey = ['Mod', 'Shift']
|
||||
createInNewPaneKey = ['Shift']
|
||||
} else {
|
||||
openInCurrentPaneKey = []
|
||||
openInNewPaneKey = ['Mod']
|
||||
createInCurrentPaneKey = ['Shift']
|
||||
createInNewPaneKey = ['Mod', 'Shift']
|
||||
}
|
||||
|
||||
// Open in new pane
|
||||
this.scope.register(openInNewPaneKey, 'Enter', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.OpenInNewPane)
|
||||
})
|
||||
|
||||
// Open in a new leaf
|
||||
this.scope.register(openInNewLeafKey, 'Enter', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.OpenInNewLeaf)
|
||||
})
|
||||
|
||||
// Insert link
|
||||
this.scope.register(['Alt'], 'Enter', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.InsertLink)
|
||||
})
|
||||
|
||||
// Create a new note
|
||||
this.scope.register(createInCurrentPaneKey, 'Enter', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.CreateNote)
|
||||
})
|
||||
this.scope.register(createInNewPaneKey, 'Enter', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.CreateNote, { newLeaf: true })
|
||||
})
|
||||
|
||||
// Open in current pane
|
||||
this.scope.register(openInCurrentPaneKey, 'Enter', e => {
|
||||
if (!isInputComposition()) {
|
||||
// Check if the user is still typing
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.Enter)
|
||||
}
|
||||
})
|
||||
|
||||
// Open in background
|
||||
this.scope.register(['Mod'], 'O', e => {
|
||||
if (!isInputComposition()) {
|
||||
// Check if the user is still typing
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.OpenInBackground)
|
||||
}
|
||||
})
|
||||
|
||||
this.scope.register([], 'Tab', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.Tab) // Switch context
|
||||
})
|
||||
|
||||
// Search history
|
||||
this.scope.register(['Alt'], 'ArrowDown', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.NextSearchHistory)
|
||||
})
|
||||
this.scope.register(['Alt'], 'ArrowUp', e => {
|
||||
e.preventDefault()
|
||||
eventBus.emit(Action.PrevSearchHistory)
|
||||
})
|
||||
|
||||
// Context
|
||||
this.scope.register(['Mod'], 'G', _e => {
|
||||
eventBus.emit(EventNames.ToggleExcerpts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class LocatorVaultModal extends LocatorModal {
|
||||
/**
|
||||
* Instanciate the Locator vault modal
|
||||
* @param plugin
|
||||
* @param query The query to pre-fill the search field with
|
||||
*/
|
||||
constructor(plugin: LocatorPlugin, query?: string) {
|
||||
super(plugin)
|
||||
|
||||
// Selected text in the editor
|
||||
const selectedText = plugin.app.workspace
|
||||
.getActiveViewOfType(MarkdownView)
|
||||
?.editor.getSelection()
|
||||
|
||||
plugin.searchHistory.getHistory().then(history => {
|
||||
// Previously searched query (if enabled in settings)
|
||||
const previous = plugin.settings.showPreviousQueryResults
|
||||
? history[0]
|
||||
: null
|
||||
|
||||
// Instantiate and display the Svelte component
|
||||
const cmp = mount(ModalVault, {
|
||||
target: this.modalEl,
|
||||
props: {
|
||||
plugin,
|
||||
modal: this,
|
||||
previousQuery: query || selectedText || previous || '',
|
||||
},
|
||||
})
|
||||
|
||||
this.onClose = () => {
|
||||
// Since the component is manually created,
|
||||
// we also need to manually destroy it
|
||||
unmount(cmp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class LocatorInFileModal extends LocatorModal {
|
||||
constructor(
|
||||
plugin: LocatorPlugin,
|
||||
file: TFile,
|
||||
searchQuery: string = '',
|
||||
parent?: LocatorModal
|
||||
) {
|
||||
super(plugin)
|
||||
|
||||
const cmp = mount(ModalInFile, {
|
||||
target: this.modalEl,
|
||||
props: {
|
||||
plugin,
|
||||
modal: this,
|
||||
singleFilePath: file.path,
|
||||
parent: parent,
|
||||
previousQuery: searchQuery,
|
||||
},
|
||||
})
|
||||
|
||||
if (parent) {
|
||||
// Hide the parent vault modal, and show it back when this one is closed
|
||||
parent.containerEl.toggleVisibility(false)
|
||||
}
|
||||
this.onClose = () => {
|
||||
if (parent) {
|
||||
parent.containerEl.toggleVisibility(true)
|
||||
}
|
||||
unmount(cmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user