Refactoring to Svelte - still a bit messy
Also, issues with eslint
This commit is contained in:
@@ -1,19 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { searchQuery } from './stores'
|
||||
import { createEventDispatcher, onMount, tick } from "svelte"
|
||||
// import { throttle } from "lodash-es"
|
||||
import { searchQuery, selectedNote } from "./stores"
|
||||
|
||||
let input: HTMLInputElement
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(async () => {
|
||||
await tick()
|
||||
input.focus()
|
||||
})
|
||||
|
||||
selectedNote.subscribe((note) => {
|
||||
const elem = document.querySelector(`[data-note-id="${note?.path}"]`)
|
||||
elem?.scrollIntoView({ behavior: "auto", block: "nearest" })
|
||||
})
|
||||
|
||||
// const throttledMoveNoteSelection = throttle(moveNoteSelection, 75)
|
||||
function moveNoteSelection(ev: KeyboardEvent): void {
|
||||
switch (ev.key) {
|
||||
case "ArrowDown":
|
||||
selectedNote.next()
|
||||
ev.preventDefault()
|
||||
break
|
||||
case "ArrowUp":
|
||||
selectedNote.previous()
|
||||
ev.preventDefault()
|
||||
break
|
||||
case "ArrowLeft":
|
||||
break
|
||||
case "ArrowRight":
|
||||
break
|
||||
|
||||
case "Enter":
|
||||
dispatch("enter", $selectedNote)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={input}
|
||||
bind:value={$searchQuery}
|
||||
on:keydown={moveNoteSelection}
|
||||
type="text"
|
||||
class="prompt-input"
|
||||
placeholder="Type to search through your notes"
|
||||
spellcheck="false"
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
<script lang="ts">
|
||||
import CmpInput from './CmpInput.svelte'
|
||||
import CmpNoteResult from './CmpNoteResult.svelte'
|
||||
import type { ResultNote } from './globals'
|
||||
import type OmnisearchPlugin from './main'
|
||||
import { resultNotes, searchQuery, selectedNoteId } from './stores'
|
||||
import { escapeHTML, escapeRegex, getAllIndexes, highlighter } from './utils'
|
||||
import { MarkdownView, TFile } from "obsidian"
|
||||
import { tick } from "svelte"
|
||||
import CmpInput from "./CmpInput.svelte"
|
||||
import CmpNoteResult from "./CmpNoteResult.svelte"
|
||||
import type { ResultNote } from "./globals"
|
||||
import type OmnisearchPlugin from "./main"
|
||||
import type { OmnisearchModal } from "./modal"
|
||||
import { resultNotes, searchQuery, selectedNote } from "./stores"
|
||||
import { escapeHTML, escapeRegex, getAllIndexes, highlighter } from "./utils"
|
||||
|
||||
export let plugin: OmnisearchPlugin
|
||||
export let modal: OmnisearchModal
|
||||
|
||||
searchQuery.subscribe(q => {
|
||||
searchQuery.subscribe(async (q) => {
|
||||
const results = getSuggestions(q)
|
||||
resultNotes.set(results)
|
||||
if (results.length) {
|
||||
selectedNoteId.set(results[0].path)
|
||||
await tick()
|
||||
selectedNote.set(results[0])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,8 +25,8 @@ function getSuggestions(query: string): ResultNote[] {
|
||||
const results = plugin.minisearch
|
||||
.search(query, {
|
||||
prefix: true,
|
||||
fuzzy: term => (term.length > 4 ? 0.2 : false),
|
||||
combineWith: 'AND',
|
||||
fuzzy: (term) => (term.length > 4 ? 0.2 : false),
|
||||
combineWith: "AND",
|
||||
boost: {
|
||||
basename: 2,
|
||||
headings1: 1.5,
|
||||
@@ -34,62 +39,83 @@ function getSuggestions(query: string): ResultNote[] {
|
||||
// console.log(`Omnisearch - Results for "${query}"`)
|
||||
// console.log(results)
|
||||
|
||||
const suggestions =
|
||||
results.map(result => {
|
||||
let note = plugin.indexedNotes[result.id]
|
||||
let basename = escapeHTML(note.basename)
|
||||
let content = escapeHTML(note.content)
|
||||
const suggestions = results.map((result) => {
|
||||
let note = plugin.indexedNotes[result.id]
|
||||
let basename = escapeHTML(note.basename)
|
||||
let content = escapeHTML(note.content)
|
||||
|
||||
// Sort the terms from smaller to larger
|
||||
// and highlight them in the title and body
|
||||
const terms = result.terms.sort((a, b) => a.length - b.length)
|
||||
const reg = new RegExp(terms.map(escapeRegex).join('|'), 'gi')
|
||||
const matches = getAllIndexes(content, reg)
|
||||
// Sort the terms from smaller to larger
|
||||
// and highlight them in the title and body
|
||||
const terms = result.terms.sort((a, b) => a.length - b.length)
|
||||
const reg = new RegExp(terms.map(escapeRegex).join("|"), "gi")
|
||||
const matches = getAllIndexes(content, reg)
|
||||
|
||||
// If the body contains a searched term, find its position
|
||||
// and trim the text around it
|
||||
const pos = content.toLowerCase().indexOf(result.terms[0])
|
||||
const surroundLen = 180
|
||||
if (pos > -1) {
|
||||
const from = Math.max(0, pos - surroundLen)
|
||||
const to = Math.min(content.length - 1, pos + surroundLen)
|
||||
content =
|
||||
(from > 0 ? '…' : '') +
|
||||
content.slice(from, to).trim() +
|
||||
(to < content.length - 1 ? '…' : '')
|
||||
}
|
||||
// If the body contains a searched term, find its position
|
||||
// and trim the text around it
|
||||
const pos = content.toLowerCase().indexOf(result.terms[0])
|
||||
const surroundLen = 180
|
||||
if (pos > -1) {
|
||||
const from = Math.max(0, pos - surroundLen)
|
||||
const to = Math.min(content.length - 1, pos + surroundLen)
|
||||
content =
|
||||
(from > 0 ? "…" : "") +
|
||||
content.slice(from, to).trim() +
|
||||
(to < content.length - 1 ? "…" : "")
|
||||
}
|
||||
|
||||
// console.log(matches)
|
||||
content = content.replace(reg, highlighter)
|
||||
basename = basename.replace(reg, highlighter)
|
||||
// console.log(matches)
|
||||
content = content.replace(reg, highlighter)
|
||||
basename = basename.replace(reg, highlighter)
|
||||
|
||||
const resultNote: ResultNote = {
|
||||
content,
|
||||
basename,
|
||||
path: note.path,
|
||||
matches,
|
||||
occurence: 0,
|
||||
}
|
||||
|
||||
return resultNote
|
||||
})
|
||||
const resultNote: ResultNote = {
|
||||
content,
|
||||
basename,
|
||||
path: note.path,
|
||||
matches,
|
||||
occurence: 0,
|
||||
}
|
||||
|
||||
return resultNote
|
||||
})
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
async function onChooseSuggestion(item: ResultNote): Promise<void> {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(item.path) as TFile
|
||||
// const fileCache = this.app.metadataCache.getFileCache(file)
|
||||
// console.log(fileCache)
|
||||
const content = (await plugin.app.vault.cachedRead(file)).toLowerCase()
|
||||
const offset = content.indexOf(
|
||||
item.matches[item.occurence].match.toLowerCase()
|
||||
)
|
||||
await plugin.app.workspace.openLinkText(item.path, "")
|
||||
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
if (!view) {
|
||||
throw new Error("OmniSearch - No active MarkdownView")
|
||||
}
|
||||
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 openNote(event: CustomEvent<ResultNote>): void {
|
||||
onChooseSuggestion(event.detail)
|
||||
modal.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<CmpInput />
|
||||
<CmpInput on:enter={openNote} />
|
||||
|
||||
<div class="prompt-results">
|
||||
{#each $resultNotes as result (result.path)}
|
||||
<CmpNoteResult
|
||||
selected={result.path === $selectedNoteId}
|
||||
id={result.path}
|
||||
title={result.basename}
|
||||
content={result.content}
|
||||
nbMatches={result.matches.length}
|
||||
/>
|
||||
<CmpNoteResult selected={result === $selectedNote} note={result} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="prompt-instructions">
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
export let title = ''
|
||||
export let content = ''
|
||||
export let id = ''
|
||||
export let nbMatches = 0
|
||||
import type { ResultNote } from "./globals"
|
||||
|
||||
export let selected = false
|
||||
export let note: ResultNote
|
||||
</script>
|
||||
|
||||
<div data-note-id={id} class="suggestion-item omnisearch-result"
|
||||
class:is-selected="{selected}">
|
||||
<div
|
||||
data-note-id={note.path}
|
||||
class="suggestion-item omnisearch-result"
|
||||
class:is-selected={selected}
|
||||
>
|
||||
<span class="omnisearch-result__title">
|
||||
{@html title}
|
||||
{@html note.basename}
|
||||
</span>
|
||||
<span class="omnisearch-result__counter">
|
||||
{nbMatches} {nbMatches > 1 ? 'matches' : 'match'}
|
||||
{note.matches.length} {note.matches.length > 1 ? "matches" : "match"}
|
||||
</span>
|
||||
<div class="omnisearch-result__body">
|
||||
{@html content}
|
||||
{@html note.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
141
src/modal.ts
141
src/modal.ts
@@ -1,142 +1,25 @@
|
||||
import { MarkdownView, Modal, TFile } from 'obsidian'
|
||||
import type { ResultNote } from './globals'
|
||||
import type OmnisearchPlugin from './main'
|
||||
import CmpNoteResult from './CmpNoteResult.svelte'
|
||||
import CmpModal from './CmpModal.svelte'
|
||||
import { escapeHTML, escapeRegex, getAllIndexes, highlighter } from './utils'
|
||||
import { selectedNoteId } from './stores'
|
||||
import { Modal } from "obsidian";
|
||||
import type OmnisearchPlugin from "./main";
|
||||
import CmpModal from "./CmpModal.svelte";
|
||||
|
||||
export class OmnisearchModal extends Modal {
|
||||
private plugin: OmnisearchPlugin
|
||||
private mutationObserver?: MutationObserver
|
||||
private cmp: CmpModal
|
||||
|
||||
constructor(plugin: OmnisearchPlugin) {
|
||||
super(plugin.app)
|
||||
this.plugin = plugin
|
||||
this.modalEl.addClass('omnisearch-modal', 'prompt')
|
||||
this.modalEl.replaceChildren() // Remove all the default Modal's children
|
||||
super(plugin.app);
|
||||
this.modalEl.addClass("omnisearch-modal", "prompt");
|
||||
this.modalEl.replaceChildren(); // Remove all the default Modal's children
|
||||
|
||||
this.cmp = new CmpModal({
|
||||
new CmpModal({
|
||||
target: this.modalEl,
|
||||
props: {
|
||||
modal: this,
|
||||
plugin,
|
||||
},
|
||||
})
|
||||
|
||||
// this.modalEl.addClass('omnisearch-modal')
|
||||
|
||||
// this.setPlaceholder('Type to search through your notes')
|
||||
|
||||
// this.setInstructions([
|
||||
// { command: '↑↓', purpose: 'to navigate' },
|
||||
// { command: '↵', purpose: 'to open' },
|
||||
// { command: 'ctrl ↵', purpose: 'to open in a new pane' },
|
||||
// { command: 'shift ↵', purpose: 'to create' },
|
||||
// { command: 'esc', purpose: 'to dismiss' },
|
||||
// ])
|
||||
});
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.style.border = '1px solid red'
|
||||
this.modalEl.style.border = '1px solid blue'
|
||||
this.contentEl.style.border = '1px solid green'
|
||||
// this.inputEl.focus()
|
||||
// this.inputEl.onkeydown = this.onKeydown.bind(this)
|
||||
// Reload last search, if any
|
||||
// if (this.plugin.lastSearch) {
|
||||
// const event = new Event('input', {
|
||||
// bubbles: true,
|
||||
// cancelable: true,
|
||||
// })
|
||||
// // this.inputEl.value = this.plugin.lastSearch
|
||||
// // this.inputEl.dispatchEvent(event)
|
||||
// // this.inputEl.select()
|
||||
// // this.inputEl.spellcheck = false
|
||||
// }
|
||||
|
||||
// this.setupObserver(this.modalEl)
|
||||
}
|
||||
|
||||
|
||||
// async onKeydown(ev: KeyboardEvent): Promise<void> {
|
||||
// if (ev.key === 'ArrowRight') {
|
||||
// console.log('TODO: open in-note search')
|
||||
// return
|
||||
// }
|
||||
// const noteId = get(selectedNoteId)
|
||||
// if (ev.key !== 'Enter' || !noteId) return
|
||||
|
||||
// if (ev.ctrlKey || ev.metaKey) {
|
||||
// // Open in a new pane
|
||||
// await this.app.workspace.openLinkText(noteId, '', true)
|
||||
// }
|
||||
// else if (ev.shiftKey) {
|
||||
// // Create a note
|
||||
// try {
|
||||
// const file = await this.app.vault.create(
|
||||
// this.inputEl.value + '.md',
|
||||
// '# ' + this.inputEl.value,
|
||||
// )
|
||||
// await this.app.workspace.openLinkText(file.path, '')
|
||||
// }
|
||||
// catch (e) {
|
||||
// if (e instanceof Error && e.message === 'File already exists.') {
|
||||
// // Open the existing file instead of creating it
|
||||
// await this.app.workspace.openLinkText(this.inputEl.value, '')
|
||||
// }
|
||||
// else {
|
||||
// console.error(e)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// this.close()
|
||||
// }
|
||||
|
||||
/**
|
||||
* Observes the modal element to keep track of which search result is currently selected
|
||||
* @param modalEl
|
||||
*/
|
||||
setupObserver(modalEl: HTMLElement): void {
|
||||
this.mutationObserver = new MutationObserver(events => {
|
||||
const record = events.find(event =>
|
||||
(event.target as HTMLDivElement).classList.contains('is-selected'),
|
||||
)
|
||||
const id =
|
||||
(record?.target?.firstChild as HTMLElement)?.getAttribute(
|
||||
'data-note-id',
|
||||
) ?? null
|
||||
if (id) {
|
||||
selectedNoteId.set(id)
|
||||
}
|
||||
})
|
||||
this.mutationObserver.observe(modalEl, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
})
|
||||
}
|
||||
|
||||
async onChooseSuggestion(item: ResultNote): Promise<void> {
|
||||
const file = this.app.vault.getAbstractFileByPath(item.path) as TFile
|
||||
// const fileCache = this.app.metadataCache.getFileCache(file)
|
||||
// console.log(fileCache)
|
||||
const content = (await this.app.vault.cachedRead(file)).toLowerCase()
|
||||
const offset = content.indexOf(
|
||||
item.matches[item.occurence].match.toLowerCase(),
|
||||
)
|
||||
await this.app.workspace.openLinkText(item.path, '')
|
||||
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
if (!view) {
|
||||
throw new Error('OmniSearch - No active MarkdownView')
|
||||
}
|
||||
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 },
|
||||
})
|
||||
// this.containerEl.style.border = '1px solid red'
|
||||
// this.modalEl.style.border = '1px solid blue'
|
||||
// this.contentEl.style.border = '1px solid green'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { get, writable } from 'svelte/store'
|
||||
import type { ResultNote } from './globals'
|
||||
|
||||
export const selectedNoteId = writable<string>('')
|
||||
// export const selectedNoteId = writable<string>('')
|
||||
export const searchQuery = writable<string>('')
|
||||
export const resultNotes = writable<ResultNote[]>([])
|
||||
|
||||
function createSelectedNote() {
|
||||
const { subscribe, set, update } = writable<ResultNote|null>(null)
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
next: () =>
|
||||
update(v => {
|
||||
const notes = get(resultNotes)
|
||||
let id = notes.findIndex(n => n.path === v?.path)
|
||||
if (!notes.length) return null
|
||||
if (id === -1) return notes[0]
|
||||
id = id < notes.length - 1 ? id + 1 : 0
|
||||
return notes[id]
|
||||
}),
|
||||
previous: () =>
|
||||
update(v => {
|
||||
const notes = get(resultNotes)
|
||||
let id = notes.findIndex(n => n.path === v?.path)
|
||||
if (!notes.length) return null
|
||||
if (id === -1) return notes[0]
|
||||
id = id > 0 ? id - 1 : notes.length - 1
|
||||
return notes[id]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const selectedNote = createSelectedNote()
|
||||
|
||||
Reference in New Issue
Block a user