mirror of
https://github.com/heyman/heynote.git
synced 2024-12-22 23:00:38 +01:00
WIP: Create new node dialog
Implement folder selector UI element. Retrieve actual folder structure from Notes library.
This commit is contained in:
parent
9ee66743d7
commit
5e34656c1d
@ -67,7 +67,7 @@ export class FileLibrary {
|
||||
async getList() {
|
||||
console.log("Loading notes")
|
||||
const notes = {}
|
||||
const files = await this.jetpack.findAsync(this.basePath, {
|
||||
const files = await this.jetpack.findAsync(".", {
|
||||
matching: "*.txt",
|
||||
recursive: true,
|
||||
})
|
||||
@ -83,6 +83,15 @@ export class FileLibrary {
|
||||
return notes
|
||||
}
|
||||
|
||||
async getDirectoryList() {
|
||||
const directories = await this.jetpack.findAsync("", {
|
||||
files: false,
|
||||
directories: true,
|
||||
recursive: true,
|
||||
})
|
||||
return directories
|
||||
}
|
||||
|
||||
setupWatcher(win) {
|
||||
if (!this.watcher) {
|
||||
this.watcher = fs.watch(
|
||||
@ -189,6 +198,10 @@ export function setupFileLibraryEventHandlers(library, win) {
|
||||
return await library.getList()
|
||||
});
|
||||
|
||||
ipcMain.handle('buffer:getDirectoryList', async (event) => {
|
||||
return await library.getDirectoryList()
|
||||
});
|
||||
|
||||
ipcMain.handle('buffer:exists', async (event, path) => {
|
||||
return await library.exists(path)
|
||||
});
|
||||
|
@ -65,6 +65,10 @@ contextBridge.exposeInMainWorld("heynote", {
|
||||
return await ipcRenderer.invoke("buffer:getList")
|
||||
},
|
||||
|
||||
async getDirectoryList() {
|
||||
return await ipcRenderer.invoke("buffer:getDirectoryList")
|
||||
},
|
||||
|
||||
async load(path) {
|
||||
return await ipcRenderer.invoke("buffer:load", path)
|
||||
},
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useNotesStore } from "../stores/notes-store"
|
||||
|
||||
import FolderSelect from './form/FolderSelect.vue'
|
||||
import FolderSelector from './folder-selector/FolderSelector.vue'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@ -10,21 +10,60 @@
|
||||
name: "",
|
||||
filename: "",
|
||||
tags: [],
|
||||
directoryTree: null,
|
||||
parentPath: "",
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FolderSelect
|
||||
FolderSelector
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.updateNotes()
|
||||
this.$refs.nameInput.focus()
|
||||
this.updateNotes()
|
||||
|
||||
// build directory tree
|
||||
const directories = await window.heynote.buffer.getDirectoryList()
|
||||
const rootNode = {
|
||||
name: "Heynote Root",
|
||||
path: "",
|
||||
children: [],
|
||||
}
|
||||
const getNodeFromList = (list, part) => list.find(node => node.name === part)
|
||||
|
||||
directories.forEach((path) => {
|
||||
const parts = path.split("/")
|
||||
let currentLevel = rootNode
|
||||
let currentParts = []
|
||||
parts.forEach(part => {
|
||||
currentParts.push(part)
|
||||
let node = getNodeFromList(currentLevel.children, part)
|
||||
if (node) {
|
||||
currentLevel = node
|
||||
} else {
|
||||
node = {
|
||||
name: part,
|
||||
children: [],
|
||||
path: currentParts.join("/"),
|
||||
}
|
||||
currentLevel.children.push(node)
|
||||
currentLevel = node
|
||||
}
|
||||
})
|
||||
})
|
||||
//console.log("tree:", rootNode)
|
||||
this.directoryTree = rootNode
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useNotesStore, [
|
||||
"notes",
|
||||
"currentNotePath",
|
||||
]),
|
||||
|
||||
currentNoteDirectory() {
|
||||
return this.currentNotePath.split("/").slice(0, -1).join("/")
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -39,6 +78,15 @@
|
||||
}
|
||||
},
|
||||
|
||||
onInputKeydown(event) {
|
||||
// redirect arrow keys and page up/down to folder selector
|
||||
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
|
||||
if (redirectKeys.includes(event.key)) {
|
||||
this.$refs.folderSelect.$el.dispatchEvent(new KeyboardEvent("keydown", {key: event.key}))
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit(event) {
|
||||
event.preventDefault()
|
||||
console.log("Creating note", this.name)
|
||||
@ -60,10 +108,18 @@
|
||||
v-model="name"
|
||||
class="name-input"
|
||||
ref="nameInput"
|
||||
@keydown="onInputKeydown"
|
||||
/>
|
||||
|
||||
<label for="folder-select">Create in</label>
|
||||
<FolderSelect id="folder-select" />
|
||||
<FolderSelector
|
||||
v-if="directoryTree"
|
||||
:directoryTree="directoryTree"
|
||||
:selectedPath="currentNoteDirectory"
|
||||
id="folder-select"
|
||||
v-model="parentPath"
|
||||
ref="folderSelect"
|
||||
/>
|
||||
</div>
|
||||
<div class="bottom-bar">
|
||||
<button type="submit">Create Note</button>
|
||||
@ -84,12 +140,16 @@
|
||||
font-size: 13px
|
||||
//background: #48b57e
|
||||
background: #efefef
|
||||
width: 420px
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
border-radius: 0 0 5px 5px
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||
display: flex
|
||||
flex-direction: column
|
||||
max-height: 100%
|
||||
&:focus
|
||||
outline: none
|
||||
+dark-mode
|
||||
@ -101,6 +161,9 @@
|
||||
|
||||
.container
|
||||
padding: 10px
|
||||
min-height: 0
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
h1
|
||||
font-weight: bold
|
||||
@ -115,7 +178,7 @@
|
||||
|
||||
|
||||
.name-input
|
||||
width: 400px
|
||||
width: 100%
|
||||
background: #fff
|
||||
padding: 4px 5px
|
||||
border: 1px solid #ccc
|
||||
@ -138,8 +201,9 @@
|
||||
|
||||
.bottom-bar
|
||||
border-radius: 0 0 5px 5px
|
||||
background: #e3e3e3
|
||||
//background: #e3e3e3
|
||||
padding: 10px
|
||||
padding-top: 0
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
+dark-mode
|
||||
@ -155,3 +219,4 @@
|
||||
outline-color: #48b57e
|
||||
|
||||
</style>
|
||||
./folder-selector/FolderSelector.vue
|
96
src/components/folder-selector/FolderItem.vue
Normal file
96
src/components/folder-selector/FolderItem.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: String,
|
||||
level: Number,
|
||||
selected: Boolean,
|
||||
newFolder: Boolean,
|
||||
},
|
||||
|
||||
watch: {
|
||||
selected() {
|
||||
if (this.selected) {
|
||||
// scrollIntoViewIfNeeded is not supported in all browsers
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded
|
||||
if (this.$el.scrollIntoViewIfNeeded) {
|
||||
this.$el.scrollIntoViewIfNeeded({
|
||||
behavior: "auto",
|
||||
block: "nearest",
|
||||
})
|
||||
} else {
|
||||
this.$el.scrollIntoView({
|
||||
behavior: "auto",
|
||||
block: "nearest",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
className() {
|
||||
return {
|
||||
folder: true,
|
||||
selected: this.selected,
|
||||
new: this.newFolder,
|
||||
}
|
||||
},
|
||||
|
||||
style() {
|
||||
return {
|
||||
"--indent-level": this.level,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="className"
|
||||
:style="style"
|
||||
>
|
||||
<span class="name">{{ name }}</span>
|
||||
<button class="new-folder" tabindex="-1" @click="$emit('new-folder')">New folder (+)</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.folder
|
||||
padding: 3px 6px
|
||||
font-size: 13px
|
||||
padding-left: calc(6px + var(--indent-level) * 16px)
|
||||
display: flex
|
||||
scroll-margin-top: 5px
|
||||
scroll-margin-bottom: 5px
|
||||
&:hover
|
||||
background: #f1f1f1
|
||||
&.selected
|
||||
background: #48b57e
|
||||
color: #fff
|
||||
&:hover
|
||||
background: #40a773
|
||||
.new-folder
|
||||
display: block
|
||||
color: rgba(255,255,255, 0.9)
|
||||
&.new
|
||||
font-style: italic
|
||||
color: rgba(0,0,0, 0.5)
|
||||
&.selected
|
||||
color: rgba(255,255,255, 0.8)
|
||||
|
||||
|
||||
.name
|
||||
flex-grow: 1
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
.new-folder
|
||||
background: rgba(0,0,0, 0.15)
|
||||
border: none
|
||||
border-radius: 2px
|
||||
font-size: 10px
|
||||
display: none
|
||||
flex-shrink: 0
|
||||
cursor: pointer
|
||||
</style>
|
231
src/components/folder-selector/FolderSelector.vue
Normal file
231
src/components/folder-selector/FolderSelector.vue
Normal file
@ -0,0 +1,231 @@
|
||||
<script>
|
||||
import FolderItem from "./FolderItem.vue"
|
||||
import NewFolderItem from "./NewFolderItem.vue"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
directoryTree: Object,
|
||||
selectedPath: String,
|
||||
},
|
||||
|
||||
components: {
|
||||
FolderItem,
|
||||
NewFolderItem,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
tree: this.directoryTree,
|
||||
selected: 0,
|
||||
filter: "",
|
||||
filterSearchStart: 0,
|
||||
filterTimeout: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.selected = this.listItems.findIndex(item => item.path === this.selectedPath)
|
||||
},
|
||||
|
||||
watch: {
|
||||
directoryTree(newVal) {
|
||||
this.tree = newVal
|
||||
},
|
||||
|
||||
selected() {
|
||||
this.$emit("update:modelValue", this.listItems[this.selected].path)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
listItems() {
|
||||
const items = []
|
||||
const getListItems = (node, level) => {
|
||||
items.push({
|
||||
name: node.name,
|
||||
level: level,
|
||||
path: node.path,
|
||||
type: "folder",
|
||||
createNewFolder: node.createNewFolder,
|
||||
newFolder: node.newFolder,
|
||||
})
|
||||
if (node.createNewFolder) {
|
||||
items.push({
|
||||
level: level + 1,
|
||||
type: "new-folder",
|
||||
path: node.path,
|
||||
})
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
getListItems(child, level + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
getListItems(this.tree, 0)
|
||||
return items
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown(event) {
|
||||
//console.log("Keydown", event.key)
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
this.$emit("click")
|
||||
} else if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
this.selected = Math.min(this.selected + 1, this.listItems.length - 1)
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
this.selected = Math.max(this.selected - 1, 0)
|
||||
} else if (event.key === "+") {
|
||||
event.preventDefault()
|
||||
this.newFolderDialog(this.listItems[this.selected].path)
|
||||
} else if (event.key === "-") {
|
||||
event.preventDefault()
|
||||
this.removeNewFolder(this.listItems[this.selected].path)
|
||||
} else if (event.key === "PageDown") {
|
||||
event.preventDefault()
|
||||
this.selected = Math.min(this.selected + this.pageCount(), this.listItems.length - 1)
|
||||
} else if (event.key === "PageUp") {
|
||||
event.preventDefault()
|
||||
this.selected = Math.max(this.selected - this.pageCount(), 0)
|
||||
} else {
|
||||
if (event.key.length === 1) {
|
||||
this.filter += event.key
|
||||
if (this.filter === "") {
|
||||
this.filterSearchStart = this.selected
|
||||
}
|
||||
let idx = this.listItems.findIndex((item, idx) => idx > this.filterSearchStart && item.name.toLowerCase().startsWith(this.filter))
|
||||
if (idx === -1) {
|
||||
idx = this.listItems.findIndex((item, idx) => idx < this.filterSearchStart && item.name.toLowerCase().startsWith(this.filter))
|
||||
}
|
||||
if (idx !== -1) {
|
||||
this.selected = idx
|
||||
this.scheduleFilterReset()
|
||||
} else {
|
||||
this.filter = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
scheduleFilterReset() {
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout)
|
||||
this.filterTimeout = null
|
||||
}
|
||||
this.filterTimeout = setTimeout(() => {
|
||||
this.filter = ""
|
||||
this.filterSearchStart = 0
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
newFolderDialog(parentPath) {
|
||||
//console.log("Create new folder in", parentPath)
|
||||
const node = this.getNode(parentPath)
|
||||
node.createNewFolder = true
|
||||
},
|
||||
|
||||
createNewFolder(parentPath, name) {
|
||||
//console.log("Create new folder", name, "in", parentPath)
|
||||
const node = this.getNode(parentPath)
|
||||
node.createNewFolder = false
|
||||
node.children.unshift({
|
||||
name: name,
|
||||
path: parentPath === "" ? name : parentPath + "/" + name,
|
||||
children: [],
|
||||
newFolder: true,
|
||||
})
|
||||
this.selected++
|
||||
this.$refs.container.focus()
|
||||
},
|
||||
|
||||
cancelNewFolder(path) {
|
||||
//console.log("Cancel new folder in", path)
|
||||
const node = this.getNode(path)
|
||||
node.createNewFolder = false
|
||||
this.$refs.container.focus()
|
||||
},
|
||||
|
||||
removeNewFolder(path) {
|
||||
//console.log("Remove newly created folder:", path)
|
||||
const node = this.getNode(path)
|
||||
if (node.newFolder && path) {
|
||||
const parentPath = path.split("/").slice(0, -1).join("/")
|
||||
const parent = this.getNode(parentPath)
|
||||
parent.children = parent.children.filter(child => child.path !== path)
|
||||
this.selected--
|
||||
}
|
||||
this.$refs.container.focus()
|
||||
},
|
||||
|
||||
getNode(path) {
|
||||
const getNodeFromList = (list, part) => list.find(node => node.name === part)
|
||||
const parts = path.split("/")
|
||||
let currentLevel = this.tree
|
||||
for (const part of parts) {
|
||||
const node = getNodeFromList(currentLevel.children, part)
|
||||
if (!node) {
|
||||
return currentLevel
|
||||
}
|
||||
currentLevel = node
|
||||
}
|
||||
return currentLevel
|
||||
},
|
||||
|
||||
pageCount() {
|
||||
return Math.max(1, Math.floor(this.$refs.container.clientHeight / 24) - 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="folder-select-container"
|
||||
ref="container"
|
||||
@keydown="onKeyDown"
|
||||
@click.stop.prevent="()=>{}"
|
||||
>
|
||||
<!--<div class="folder root selected">
|
||||
Heynote Root
|
||||
</div>
|
||||
<div class="folder indent">New Folder…</div>-->
|
||||
|
||||
<template v-for="(item, idx) in listItems">
|
||||
<FolderItem
|
||||
v-if="item.type === 'folder'"
|
||||
:name="item.name"
|
||||
:level="item.level"
|
||||
:selected="idx === selected && !item.createNewFolder"
|
||||
:newFolder="item.newFolder"
|
||||
@click="selected = idx"
|
||||
@new-folder="newFolderDialog(item.path)"
|
||||
/>
|
||||
<NewFolderItem
|
||||
v-else-if="item.type === 'new-folder'"
|
||||
:parentPath="item.path"
|
||||
:level="item.level"
|
||||
@cancel="() => cancelNewFolder(item.path)"
|
||||
@create-folder="createNewFolder"
|
||||
/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.folder-select-container
|
||||
width: 100%
|
||||
overflow-y: auto
|
||||
background: #fff
|
||||
border: 1px solid #ccc
|
||||
border-radius: 2px
|
||||
padding: 5px 5px
|
||||
text-align: left
|
||||
&:focus, &:focus-within
|
||||
outline: none
|
||||
border: 1px solid #fff
|
||||
outline: 2px solid #48b57e
|
||||
</style>
|
108
src/components/folder-selector/NewFolderItem.vue
Normal file
108
src/components/folder-selector/NewFolderItem.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<script>
|
||||
import sanitizeFilename from "./sanitize-filename.js"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
parentPath: String,
|
||||
level: Number,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: "",
|
||||
eventTriggered: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
|
||||
computed: {
|
||||
className() {
|
||||
return {
|
||||
folder: true,
|
||||
selected: true
|
||||
}
|
||||
},
|
||||
|
||||
style() {
|
||||
return {
|
||||
"--indent-level": this.level,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown(event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.finish()
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.name = ""
|
||||
this.finish()
|
||||
}
|
||||
},
|
||||
|
||||
finish() {
|
||||
if (this.eventTriggered) {
|
||||
return
|
||||
}
|
||||
this.eventTriggered = true
|
||||
if (this.name === "") {
|
||||
this.$emit("cancel")
|
||||
} else {
|
||||
this.$emit("create-folder", this.parentPath, sanitizeFilename(this.name, "_"))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="className"
|
||||
:style="style"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="name"
|
||||
ref="input"
|
||||
placeholder="New folder name"
|
||||
maxlength="60"
|
||||
@keydown.stop="onKeyDown"
|
||||
@blur="finish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.folder
|
||||
padding: 3px 6px
|
||||
font-size: 13px
|
||||
padding-left: calc(0px + var(--indent-level) * 16px)
|
||||
display: flex
|
||||
background: #f1f1f1
|
||||
&:hover
|
||||
background: #f1f1f1
|
||||
|
||||
|
||||
input
|
||||
width: 100%
|
||||
background: #fff
|
||||
border: none
|
||||
border-radius: 2px
|
||||
font-size: 13px
|
||||
height: 16px
|
||||
padding: 2px 4px
|
||||
font-style: italic
|
||||
border: 2px solid #48b57e
|
||||
&:focus
|
||||
outline: none
|
||||
&::placeholder
|
||||
font-size: 12px
|
||||
|
||||
</style>
|
14
src/components/folder-selector/sanitize-filename.js
Normal file
14
src/components/folder-selector/sanitize-filename.js
Normal file
@ -0,0 +1,14 @@
|
||||
const illegalRe = /[\/\?<>\\:\*\|"]/g;
|
||||
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||
const reservedRe = /^\.+$/;
|
||||
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
const windowsTrailingRe = /[\. ]+$/;
|
||||
|
||||
export default function sanitizeFilename(input, replacement) {
|
||||
return input.trim()
|
||||
.replace(illegalRe, replacement)
|
||||
.replace(controlRe, replacement)
|
||||
.replace(reservedRe, replacement)
|
||||
.replace(windowsReservedRe, replacement)
|
||||
.replace(windowsTrailingRe, replacement)
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tree: Array,
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown(event) {
|
||||
console.log("Keydown", event.key)
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
this.$emit("click")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="folder-select-container"
|
||||
@keydown="onKeyDown"
|
||||
@click.stop.prevent="()=>{}"
|
||||
>
|
||||
<div class="folder root selected">
|
||||
Heynote Root
|
||||
</div>
|
||||
<div class="folder indent">New Folder…</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.folder-select-container
|
||||
width: 100%
|
||||
background: #fff
|
||||
border: 1px solid #ccc
|
||||
border-radius: 2px
|
||||
padding: 5px 5px
|
||||
text-align: left
|
||||
&:focus
|
||||
outline: none
|
||||
border: 1px solid #fff
|
||||
outline: 2px solid #48b57e
|
||||
|
||||
.folder
|
||||
padding: 3px 6px
|
||||
font-size: 13px
|
||||
&.selected
|
||||
background: #48b57e
|
||||
color: #fff
|
||||
&.indent
|
||||
padding-left: 16px
|
||||
</style>
|
@ -103,6 +103,10 @@ const Heynote = {
|
||||
return [{"path":"buffer.txt", "metadata":{}}]
|
||||
},
|
||||
|
||||
async getDirectoryList() {
|
||||
return []
|
||||
},
|
||||
|
||||
async close(path) {
|
||||
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user