mirror of
https://github.com/heyman/heynote.git
synced 2025-08-06 13:49:29 +02:00
268 lines
9.0 KiB
Vue
268 lines
9.0 KiB
Vue
<script>
|
|
import fuzzysort from 'fuzzysort'
|
|
|
|
import { mapState, mapActions } from 'pinia'
|
|
import { toRaw } from 'vue';
|
|
import { useNotesStore } from "../stores/notes-store"
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
selected: 0,
|
|
filter: "",
|
|
items: [],
|
|
}
|
|
},
|
|
|
|
async mounted() {
|
|
await this.updateNotes()
|
|
this.$refs.container.focus()
|
|
this.$refs.input.focus()
|
|
this.items = Object.entries(this.notes).map(([path, metadata]) => {
|
|
return {
|
|
"path": path,
|
|
"name": metadata?.name || path,
|
|
"folder": path.split("/").slice(0, -1).join("/"),
|
|
"scratch": path === "buffer-dev.txt",
|
|
}
|
|
})
|
|
if (this.items.length > 1) {
|
|
this.selected = 1
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
...mapState(useNotesStore, [
|
|
"notes",
|
|
"recentNotePaths",
|
|
]),
|
|
|
|
orderedItems() {
|
|
const sortKeys = Object.fromEntries(this.recentNotePaths.map((item, idx) => [item, idx]))
|
|
const getSortScore = (item) => sortKeys[item.path] !== undefined ? sortKeys[item.path] : 1000
|
|
const compareFunc = (a, b) => {
|
|
const sortScore = getSortScore(a) - getSortScore(b)
|
|
if (sortScore !== 0) {
|
|
// sort by recency first
|
|
return sortScore
|
|
} else {
|
|
// then notes in root
|
|
const aIsRoot = a.path.indexOf("/") === -1
|
|
const bIsRoot = b.path.indexOf("/") === -1
|
|
if (aIsRoot && !bIsRoot) {
|
|
return -1
|
|
} else if (!aIsRoot && bIsRoot) {
|
|
return 1
|
|
} else {
|
|
// last sort by path
|
|
return a.path.localeCompare(b.path)
|
|
}
|
|
}
|
|
}
|
|
return [...this.items].sort(compareFunc)
|
|
},
|
|
|
|
filteredItems() {
|
|
if (this.filter === "") {
|
|
return this.orderedItems
|
|
} else {
|
|
const searchResults = fuzzysort.go(this.filter, this.items, {
|
|
keys: ["name", "folder"],
|
|
})
|
|
return searchResults.map((result) => {
|
|
const obj = {...result.obj}
|
|
const nameHighlight = result[0].highlight("<b>", "</b>")
|
|
const folderHighlight = result[1].highlight("<b>", "</b>")
|
|
obj.name = nameHighlight !== "" ? nameHighlight : obj.name
|
|
obj.folder = folderHighlight !== "" ? folderHighlight : obj.folder
|
|
return obj
|
|
})
|
|
}
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
...mapActions(useNotesStore, [
|
|
"updateNotes",
|
|
]),
|
|
|
|
onKeydown(event) {
|
|
if (event.key === "ArrowDown") {
|
|
if (this.selected === this.filteredItems.length - 1) {
|
|
this.selected = 0
|
|
} else {
|
|
this.selected = Math.min(this.selected + 1, this.filteredItems.length - 1)
|
|
}
|
|
event.preventDefault()
|
|
if (this.selected === this.filteredItems.length - 1) {
|
|
this.$refs.container.scrollIntoView({block: "end"})
|
|
} else {
|
|
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
|
}
|
|
|
|
} else if (event.key === "ArrowUp") {
|
|
if (this.selected === 0) {
|
|
this.selected = this.filteredItems.length - 1
|
|
} else {
|
|
this.selected = Math.max(this.selected - 1, 0)
|
|
}
|
|
event.preventDefault()
|
|
if (this.selected === 0) {
|
|
this.$refs.container.scrollIntoView({block: "start"})
|
|
} else {
|
|
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
|
}
|
|
} else if (event.key === "Enter") {
|
|
this.selectItem(this.filteredItems[this.selected].path)
|
|
event.preventDefault()
|
|
} else if (event.key === "Escape") {
|
|
this.$emit("close")
|
|
event.preventDefault()
|
|
}
|
|
},
|
|
|
|
selectItem(path) {
|
|
this.$emit("openNote", path)
|
|
},
|
|
|
|
onInput(event) {
|
|
// reset selection
|
|
this.selected = 0
|
|
},
|
|
|
|
onFocusOut(event) {
|
|
let container = this.$refs.container
|
|
if (container !== event.relatedTarget && !container.contains(event.relatedTarget)) {
|
|
this.$emit("close")
|
|
}
|
|
},
|
|
|
|
getItemClass(item, idx) {
|
|
return {
|
|
"selected": idx === this.selected,
|
|
"scratch": item.scratch,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="scroller">
|
|
<form class="note-selector" tabindex="-1" @focusout="onFocusOut" ref="container">
|
|
<input
|
|
type="text"
|
|
ref="input"
|
|
@keydown="onKeydown"
|
|
@input="onInput"
|
|
v-model="filter"
|
|
/>
|
|
<ul class="items">
|
|
<li
|
|
v-for="item, idx in filteredItems"
|
|
:key="item.path"
|
|
:class="getItemClass(item, idx)"
|
|
@click="selectItem(item.path)"
|
|
ref="item"
|
|
>
|
|
<span class="name" v-html="item.name" />
|
|
<span class="path" v-html="item.folder" />
|
|
</li>
|
|
</ul>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="sass">
|
|
.scroller
|
|
//overflow: auto
|
|
//position: fixed
|
|
//top: 0
|
|
//left: 0
|
|
//bottom: 0
|
|
//right: 0
|
|
.note-selector
|
|
font-size: 13px
|
|
padding: 10px
|
|
//background: #48b57e
|
|
background: #efefef
|
|
position: absolute
|
|
top: 0
|
|
left: 50%
|
|
width: 420px
|
|
transform: translateX(-50%)
|
|
max-height: 100%
|
|
box-sizing: border-box
|
|
display: flex
|
|
flex-direction: column
|
|
border-radius: 0 0 5px 5px
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
|
+dark-mode
|
|
background: #151516
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.5)
|
|
+webapp-mobile
|
|
max-width: calc(100% - 80px)
|
|
|
|
input
|
|
background: #fff
|
|
padding: 4px 5px
|
|
border: 1px solid #ccc
|
|
box-sizing: border-box
|
|
border-radius: 2px
|
|
width: 100%
|
|
margin-bottom: 10px
|
|
&:focus
|
|
outline: none
|
|
border: 1px solid #fff
|
|
outline: 2px solid #48b57e
|
|
+dark-mode
|
|
background: #3b3b3b
|
|
color: rgba(255,255,255, 0.9)
|
|
border: 1px solid #5a5a5a
|
|
&:focus
|
|
border: 1px solid #3b3b3b
|
|
+webapp-mobile
|
|
font-size: 16px
|
|
max-width: 100%
|
|
|
|
.items
|
|
overflow-y: auto
|
|
> li
|
|
border-radius: 3px
|
|
padding: 5px 12px
|
|
cursor: pointer
|
|
display: flex
|
|
align-items: center
|
|
&:hover
|
|
background: #e2e2e2
|
|
&.selected
|
|
background: #48b57e
|
|
color: #fff
|
|
&.scratch
|
|
font-weight: 600
|
|
+dark-mode
|
|
color: rgba(255,255,255, 0.65)
|
|
&:hover
|
|
background: #29292a
|
|
&.selected
|
|
background: #1b6540
|
|
color: rgba(255,255,255, 0.87)
|
|
.name
|
|
margin-right: 12px
|
|
flex-shrink: 0
|
|
overflow: hidden
|
|
text-overflow: ellipsis
|
|
text-wrap: nowrap
|
|
::v-deep(b)
|
|
font-weight: 700
|
|
.path
|
|
opacity: 0.6
|
|
font-size: 12px
|
|
flex-shrink: 1
|
|
overflow: hidden
|
|
text-overflow: ellipsis
|
|
text-wrap: nowrap
|
|
::v-deep(b)
|
|
font-weight: 700
|
|
</style>
|