mirror of
https://github.com/heyman/heynote.git
synced 2024-12-23 07:08:49 +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() {
|
async getList() {
|
||||||
console.log("Loading notes")
|
console.log("Loading notes")
|
||||||
const notes = {}
|
const notes = {}
|
||||||
const files = await this.jetpack.findAsync(this.basePath, {
|
const files = await this.jetpack.findAsync(".", {
|
||||||
matching: "*.txt",
|
matching: "*.txt",
|
||||||
recursive: true,
|
recursive: true,
|
||||||
})
|
})
|
||||||
@ -83,6 +83,15 @@ export class FileLibrary {
|
|||||||
return notes
|
return notes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDirectoryList() {
|
||||||
|
const directories = await this.jetpack.findAsync("", {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
return directories
|
||||||
|
}
|
||||||
|
|
||||||
setupWatcher(win) {
|
setupWatcher(win) {
|
||||||
if (!this.watcher) {
|
if (!this.watcher) {
|
||||||
this.watcher = fs.watch(
|
this.watcher = fs.watch(
|
||||||
@ -189,6 +198,10 @@ export function setupFileLibraryEventHandlers(library, win) {
|
|||||||
return await library.getList()
|
return await library.getList()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:getDirectoryList', async (event) => {
|
||||||
|
return await library.getDirectoryList()
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('buffer:exists', async (event, path) => {
|
ipcMain.handle('buffer:exists', async (event, path) => {
|
||||||
return await library.exists(path)
|
return await library.exists(path)
|
||||||
});
|
});
|
||||||
|
@ -65,6 +65,10 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
return await ipcRenderer.invoke("buffer:getList")
|
return await ipcRenderer.invoke("buffer:getList")
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getDirectoryList() {
|
||||||
|
return await ipcRenderer.invoke("buffer:getDirectoryList")
|
||||||
|
},
|
||||||
|
|
||||||
async load(path) {
|
async load(path) {
|
||||||
return await ipcRenderer.invoke("buffer:load", path)
|
return await ipcRenderer.invoke("buffer:load", path)
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { mapState, mapActions } from 'pinia'
|
import { mapState, mapActions } from 'pinia'
|
||||||
import { useNotesStore } from "../stores/notes-store"
|
import { useNotesStore } from "../stores/notes-store"
|
||||||
|
|
||||||
import FolderSelect from './form/FolderSelect.vue'
|
import FolderSelector from './folder-selector/FolderSelector.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@ -10,21 +10,60 @@
|
|||||||
name: "",
|
name: "",
|
||||||
filename: "",
|
filename: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
directoryTree: null,
|
||||||
|
parentPath: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
FolderSelect
|
FolderSelector
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.updateNotes()
|
|
||||||
this.$refs.nameInput.focus()
|
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: {
|
computed: {
|
||||||
...mapState(useNotesStore, [
|
...mapState(useNotesStore, [
|
||||||
"notes",
|
"notes",
|
||||||
|
"currentNotePath",
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
currentNoteDirectory() {
|
||||||
|
return this.currentNotePath.split("/").slice(0, -1).join("/")
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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) {
|
onSubmit(event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
console.log("Creating note", this.name)
|
console.log("Creating note", this.name)
|
||||||
@ -60,10 +108,18 @@
|
|||||||
v-model="name"
|
v-model="name"
|
||||||
class="name-input"
|
class="name-input"
|
||||||
ref="nameInput"
|
ref="nameInput"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="folder-select">Create in</label>
|
<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>
|
||||||
<div class="bottom-bar">
|
<div class="bottom-bar">
|
||||||
<button type="submit">Create Note</button>
|
<button type="submit">Create Note</button>
|
||||||
@ -84,12 +140,16 @@
|
|||||||
font-size: 13px
|
font-size: 13px
|
||||||
//background: #48b57e
|
//background: #48b57e
|
||||||
background: #efefef
|
background: #efefef
|
||||||
|
width: 420px
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 0
|
top: 0
|
||||||
left: 50%
|
left: 50%
|
||||||
transform: translateX(-50%)
|
transform: translateX(-50%)
|
||||||
border-radius: 0 0 5px 5px
|
border-radius: 0 0 5px 5px
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
max-height: 100%
|
||||||
&:focus
|
&:focus
|
||||||
outline: none
|
outline: none
|
||||||
+dark-mode
|
+dark-mode
|
||||||
@ -101,6 +161,9 @@
|
|||||||
|
|
||||||
.container
|
.container
|
||||||
padding: 10px
|
padding: 10px
|
||||||
|
min-height: 0
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
h1
|
h1
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
@ -115,7 +178,7 @@
|
|||||||
|
|
||||||
|
|
||||||
.name-input
|
.name-input
|
||||||
width: 400px
|
width: 100%
|
||||||
background: #fff
|
background: #fff
|
||||||
padding: 4px 5px
|
padding: 4px 5px
|
||||||
border: 1px solid #ccc
|
border: 1px solid #ccc
|
||||||
@ -138,8 +201,9 @@
|
|||||||
|
|
||||||
.bottom-bar
|
.bottom-bar
|
||||||
border-radius: 0 0 5px 5px
|
border-radius: 0 0 5px 5px
|
||||||
background: #e3e3e3
|
//background: #e3e3e3
|
||||||
padding: 10px
|
padding: 10px
|
||||||
|
padding-top: 0
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: flex-end
|
justify-content: flex-end
|
||||||
+dark-mode
|
+dark-mode
|
||||||
@ -155,3 +219,4 @@
|
|||||||
outline-color: #48b57e
|
outline-color: #48b57e
|
||||||
|
|
||||||
</style>
|
</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":{}}]
|
return [{"path":"buffer.txt", "metadata":{}}]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getDirectoryList() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
|
||||||
async close(path) {
|
async close(path) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user