Refactoring to Svelte - still a bit messy

Also, issues with eslint
This commit is contained in:
Simon Cambier
2022-04-16 14:59:02 +02:00
parent 2e5bd085bd
commit 75af87849b
7 changed files with 185 additions and 195 deletions

View File

@@ -13,6 +13,7 @@
"license": "MIT",
"devDependencies": {
"@tsconfig/svelte": "^3.0.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
@@ -35,6 +36,7 @@
"typescript": "^4.6.3"
},
"dependencies": {
"lodash-es": "^4.17.21",
"minisearch": "^5.0.0-beta1"
}
}

18
pnpm-lock.yaml generated
View File

@@ -2,6 +2,7 @@ lockfileVersion: 5.3
specifiers:
'@tsconfig/svelte': ^3.0.0
'@types/lodash-es': ^4.17.6
'@types/node': ^16.11.6
'@typescript-eslint/eslint-plugin': ^5.18.0
'@typescript-eslint/parser': ^5.18.0
@@ -15,6 +16,7 @@ specifiers:
eslint-plugin-node: 11.1.0
eslint-plugin-promise: 5.0.0
eslint-plugin-svelte3: ^3.4.1
lodash-es: ^4.17.21
minisearch: ^5.0.0-beta1
obsidian: latest
prettier: ^2.6.2
@@ -25,10 +27,12 @@ specifiers:
typescript: ^4.6.3
dependencies:
lodash-es: 4.17.21
minisearch: 5.0.0-beta1
devDependencies:
'@tsconfig/svelte': 3.0.0
'@types/lodash-es': 4.17.6
'@types/node': 16.11.26
'@typescript-eslint/eslint-plugin': 5.18.0_2e93aa916703472007e9b5dfec98785b
'@typescript-eslint/parser': 5.18.0_eslint@7.12.1+typescript@4.6.3
@@ -164,6 +168,16 @@ packages:
resolution: {integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=}
dev: true
/@types/lodash-es/4.17.6:
resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==}
dependencies:
'@types/lodash': 4.14.181
dev: true
/@types/lodash/4.14.181:
resolution: {integrity: sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==}
dev: true
/@types/node/16.11.26:
resolution: {integrity: sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==}
dev: true
@@ -1547,6 +1561,10 @@ packages:
path-exists: 3.0.0
dev: true
/lodash-es/4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
/lodash.merge/4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true

View File

@@ -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"
/>

View File

@@ -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,8 +39,7 @@ function getSuggestions(query: string): ResultNote[] {
// console.log(`Omnisearch - Results for "${query}"`)
// console.log(results)
const suggestions =
results.map(result => {
const suggestions = results.map((result) => {
let note = plugin.indexedNotes[result.id]
let basename = escapeHTML(note.basename)
let content = escapeHTML(note.content)
@@ -43,7 +47,7 @@ function getSuggestions(query: string): ResultNote[] {
// 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 reg = new RegExp(terms.map(escapeRegex).join("|"), "gi")
const matches = getAllIndexes(content, reg)
// If the body contains a searched term, find its position
@@ -54,9 +58,9 @@ function getSuggestions(query: string): ResultNote[] {
const from = Math.max(0, pos - surroundLen)
const to = Math.min(content.length - 1, pos + surroundLen)
content =
(from > 0 ? '…' : '') +
(from > 0 ? "…" : "") +
content.slice(from, to).trim() +
(to < content.length - 1 ? '…' : '')
(to < content.length - 1 ? "…" : "")
}
// console.log(matches)
@@ -74,22 +78,44 @@ function getSuggestions(query: string): ResultNote[] {
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">

View File

@@ -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}&nbsp;{nbMatches > 1 ? 'matches' : 'match'}
{note.matches.length}&nbsp;{note.matches.length > 1 ? "matches" : "match"}
</span>
<div class="omnisearch-result__body">
{@html content}
{@html note.content}
</div>
</div>

View File

@@ -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'
}
}

View File

@@ -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()