find feature

This commit is contained in:
nicwands
2026-03-26 16:33:03 -04:00
parent 2ab76684cc
commit 86146f1ecf
4 changed files with 26 additions and 99 deletions

View File

@@ -56,6 +56,9 @@ onMounted(async () => {
content: note.content || [], content: note.content || [],
onUpdate: onUpdate, onUpdate: onUpdate,
}) })
// Clear any highlights from find feature
editor.value.chain().selectAll().unsetHighlight().run()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.value?.destroy?.() editor.value?.destroy?.()
@@ -152,10 +155,10 @@ defineExpose({
background: currentColor; background: currentColor;
} }
} }
mark { .highlighted {
background: var(--theme-accent); background: var(--theme-accent);
color: var(--theme-bg); color: var(--theme-bg);
padding: 0 0.2em; border-radius: 0.2em;
} }
ul[data-type='taskList'] { ul[data-type='taskList'] {

View File

@@ -1,34 +1,20 @@
<template> <template>
<div v-if="visible" class="note-find"> <div v-if="visible" class="note-find">
<div class="find-bar"> <div class="find-bar">
<!-- <input
@keydown.enter="findNext"
@keydown.escape="close"
ref="inputRef"
/> -->
<search-input <search-input
v-model="searchQuery" v-model="searchQuery"
placeholder="Find..." placeholder="Find..."
ref="inputRef" ref="inputRef"
@keydown.escape="close"
/> />
<span class="match-count">
{{ currentMatch + 1 }} / {{ matches.length }}
</span>
<button @click="findPrev" :disabled="matches.length === 0">
</button>
<button @click="findNext" :disabled="matches.length === 0">
</button>
<button @click="close"></button> <button @click="close"></button>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch, nextTick } from 'vue' import { ref, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
import SearchInput from '@/components/SearchInput.vue' import SearchInput from '@/components/SearchInput.vue'
import { Highlight } from '@tiptap/extension-highlight'
const props = defineProps({ const props = defineProps({
editor: { editor: {
@@ -45,7 +31,6 @@ const emit = defineEmits(['close'])
const searchQuery = ref('') const searchQuery = ref('')
const matches = ref([]) const matches = ref([])
const currentMatch = ref(0)
const inputRef = ref(null) const inputRef = ref(null)
const findInDocument = () => { const findInDocument = () => {
@@ -78,82 +63,25 @@ const findInDocument = () => {
}) })
matches.value = foundMatches matches.value = foundMatches
currentMatch.value = 0 highlightMatches()
if (foundMatches.length > 0) {
highlightMatch(0)
scrollToMatch(0)
}
} }
const highlightMatch = (index) => { const highlightMatches = () => {
if (!props.editor || !matches.value[index]) return if (!props.editor || !matches.value) return
const { from, to } = matches.value[index]
matches.value.forEach(({ from, to }) => {
props.editor props.editor
.chain() .chain()
.setTextSelection({ from, to }) .setTextSelection({ from, to })
.setHighlight({ color: 'var(--theme-accent)' }) .setHighlight({ color: 'var(--theme-accent)' })
.run() .run()
})
inputRef.value?.focus()
} }
const clearHighlights = () => { const clearHighlights = () => {
if (!props.editor) return if (!props.editor) return
const { state } = props.editor props.editor.chain().selectAll().unsetHighlight().run()
const { doc } = state
doc.descendants((node, pos) => {
if (node.marks && node.marks.length > 0) {
node.marks.forEach((mark) => {
if (mark.type.name === 'highlight') {
props.editor.chain().focus().unsetHighlight().run()
}
})
}
})
props.editor.commands.unsetHighlight()
}
const scrollToMatch = (index) => {
if (!props.editor || !matches.value[index]) return
const { from } = matches.value[index]
props.editor.commands.setTextSelection({ from, to: from })
const dom = props.editor.view.dom
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
range.commonAncestorContainer.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
}
const findNext = () => {
if (matches.value.length === 0) return
clearHighlights()
currentMatch.value = (currentMatch.value + 1) % matches.value.length
highlightMatch(currentMatch.value)
scrollToMatch(currentMatch.value)
}
const findPrev = () => {
if (matches.value.length === 0) return
clearHighlights()
currentMatch.value =
(currentMatch.value - 1 + matches.value.length) % matches.value.length
highlightMatch(currentMatch.value)
scrollToMatch(currentMatch.value)
} }
const close = () => { const close = () => {
@@ -170,27 +98,25 @@ watch(
await nextTick() await nextTick()
inputRef.value?.focus() inputRef.value?.focus()
if (props.editor && searchQuery.value) { if (props.editor && searchQuery.value) {
console.log('visible change')
findInDocument() findInDocument()
} }
} }
}, },
) )
watch( watch(
() => props.editor, () => props.editor,
(newEditor) => { (newEditor) => {
if (newEditor && props.visible && searchQuery.value) { if (newEditor && props.visible && searchQuery.value) {
console.log('editor change')
findInDocument() findInDocument()
} }
}, },
) )
watch(searchQuery, () => { watch(searchQuery, () => {
console.log('search change')
findInDocument() findInDocument()
}) })
onMounted(() => clearHighlights())
onBeforeUnmount(() => clearHighlights())
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -13,12 +13,6 @@
> >
Italic Italic
</button> </button>
<button
@click="editor.chain().focus().toggleHighlight().run()"
:class="{ active: editor.isActive('highlight') }"
>
Highlight
</button>
</div> </div>
</bubble-menu> </bubble-menu>
</template> </template>

View File

@@ -33,7 +33,11 @@ export const extensions = [
}), }),
TaskList, TaskList,
TaskItem, TaskItem,
Highlight, Highlight.configure({
HTMLAttributes: {
class: 'highlighted',
},
}),
Markdown, Markdown,
CodeBlockLowlight.configure({ CodeBlockLowlight.configure({
lowlight, lowlight,