Bugfixes on tooltip z-indexing. Search using breadcrumbs in table. Link fixes.
This commit is contained in:
parent
e3af21af91
commit
1f24313d23
|
@ -187,10 +187,10 @@ table {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
nav {
|
header nav.headermain {
|
||||||
/* Position so that tooltips can appear on top of other positioned elements */
|
/* Position so that tooltips can appear on top of other positioned elements */
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
height: calc(100svh - var(--header-height));
|
height: calc(100svh - var(--header-height));
|
||||||
|
@ -198,7 +198,7 @@ main {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
[data-tooltip]:hover:after {
|
[data-tooltip]:hover:after {
|
||||||
z-index: 1000;
|
z-index: 101;
|
||||||
content: attr(data-tooltip);
|
content: attr(data-tooltip);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
|
@ -41,19 +41,12 @@ const props = defineProps<{
|
||||||
|
|
||||||
const longest = ref<Array<string>>([])
|
const longest = ref<Array<string>>([])
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const longcut = longest.value.slice(0, props.path.length)
|
|
||||||
const same = longcut.every((value, index) => value === props.path[index])
|
|
||||||
if (!same) longest.value = props.path
|
|
||||||
else if (props.path.length > longcut.length) {
|
|
||||||
longest.value = longcut.concat(props.path.slice(longcut.length))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isCurrent = (index: number) => index == props.path.length ? 'location' : undefined
|
const isCurrent = (index: number) => index == props.path.length ? 'location' : undefined
|
||||||
|
|
||||||
const navigate = (index: number) => {
|
const navigate = (index: number) => {
|
||||||
links[index].focus()
|
const link = links[index]
|
||||||
|
if (!link) throw Error(`No link at index ${index} (path: ${props.path})`)
|
||||||
|
link.focus()
|
||||||
router.replace(`/${longest.value.slice(0, index).join('/')}`)
|
router.replace(`/${longest.value.slice(0, index).join('/')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +55,18 @@ const move = (dir: number) => {
|
||||||
if (index < 0 || index > longest.value.length) return
|
if (index < 0 || index > longest.value.length) return
|
||||||
navigate(index)
|
navigate(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const longcut = longest.value.slice(0, props.path.length)
|
||||||
|
const same = longcut.every((value, index) => value === props.path[index])
|
||||||
|
if (!same) longest.value = props.path
|
||||||
|
else if (props.path.length > longcut.length) {
|
||||||
|
longest.value = longcut.concat(props.path.slice(longcut.length))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watchEffect(() => {
|
||||||
|
if (links.length) navigate(props.path.length)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -56,9 +56,14 @@
|
||||||
<td class="size right">{{ editing.sizedisp }}</td>
|
<td class="size right">{{ editing.sizedisp }}</td>
|
||||||
<td class="menu"></td>
|
<td class="menu"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<template
|
||||||
v-for="doc of sorted(props.documents as FolderDocument[])"
|
v-for="doc of sorted(props.documents as FolderDocument[])"
|
||||||
:key="doc.key"
|
:key="doc.key">
|
||||||
|
<tr v-if="doc.loc !== prevloc && ((prevloc = doc.loc) || true)">
|
||||||
|
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr
|
||||||
:id="`file-${doc.key}`"
|
:id="`file-${doc.key}`"
|
||||||
:class="{
|
:class="{
|
||||||
file: doc.type === 'file',
|
file: doc.type === 'file',
|
||||||
|
@ -92,13 +97,14 @@
|
||||||
"
|
"
|
||||||
/></template>
|
/></template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="loc" v-if="doc.loc.join('/') !== props.path.join('/')">{{ doc.loc.join('/') + '/'}}</span>
|
|
||||||
<a
|
<a
|
||||||
:href="url_for(doc)"
|
:href="url_for(doc)"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@contextmenu.prevent
|
@contextmenu.prevent
|
||||||
@focus.stop="cursor = doc"
|
@focus.stop="cursor = doc"
|
||||||
@blur="ev => { if (!editing) cursor = null }"
|
@blur="ev => { if (!editing) cursor = null }"
|
||||||
|
@keyup.left="router.back()"
|
||||||
|
@keyup.right.stop="ev => { if (doc.type === 'folder') (ev.target as HTMLElement).click() }"
|
||||||
>{{ doc.name }}</a
|
>{{ doc.name }}</a
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
@ -126,13 +132,14 @@
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-else class="empty-container">Nothing to see here</div>
|
<div v-else class="empty-container">Nothing to see here</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watchEffect } from 'vue'
|
import { ref, computed, watchEffect, onBeforeUpdate } from 'vue'
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
import type { Document, FolderDocument } from '@/repositories/Document'
|
import type { Document, FolderDocument } from '@/repositories/Document'
|
||||||
import FileRenameInput from './FileRenameInput.vue'
|
import FileRenameInput from './FileRenameInput.vue'
|
||||||
|
@ -147,15 +154,12 @@ const props = withDefaults(
|
||||||
}>(),
|
}>(),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const linkBasePath = computed(() => props.path.join('/'))
|
const url_for = (doc: FolderDocument) => {
|
||||||
const filesBasePath = computed(() => `/files/${linkBasePath.value}`)
|
const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
const url_for = (doc: FolderDocument) =>
|
return doc.type === 'folder' ? `#/${p}/` : `/files/${p}`
|
||||||
doc.type === 'folder'
|
}
|
||||||
? `#${linkBasePath.value}/${doc.name}/`
|
|
||||||
: `${filesBasePath.value}/${doc.name}`
|
|
||||||
const cursor = ref<FolderDocument | null>(null)
|
const cursor = ref<FolderDocument | null>(null)
|
||||||
// File rename
|
// File rename
|
||||||
const editing = ref<FolderDocument | null>(null)
|
const editing = ref<FolderDocument | null>(null)
|
||||||
|
@ -174,7 +178,7 @@ const rename = (doc: FolderDocument, newName: string) => {
|
||||||
control.send(
|
control.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
op: 'rename',
|
op: 'rename',
|
||||||
path: `${decodeURIComponent(linkBasePath.value)}/${oldName}`,
|
path: `${doc.loc}/${oldName}`,
|
||||||
to: newName
|
to: newName
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -185,6 +189,7 @@ defineExpose({
|
||||||
newFolder() {
|
newFolder() {
|
||||||
const now = Date.now() / 1000
|
const now = Date.now() / 1000
|
||||||
editing.value = {
|
editing.value = {
|
||||||
|
loc,
|
||||||
key: 'new',
|
key: 'new',
|
||||||
name: 'New Folder',
|
name: 'New Folder',
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
|
@ -252,12 +257,18 @@ defineExpose({
|
||||||
scrolltimer = null
|
scrolltimer = null
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
if (moveto === N) focusBreadcrumb()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const focusBreadcrumb = () => {
|
||||||
|
const el = document.querySelector('.breadcrumb') as HTMLElement | null
|
||||||
|
if (el) el.focus()
|
||||||
|
}
|
||||||
let scrolltimer: any = null
|
let scrolltimer: any = null
|
||||||
let scrolltr: any = null
|
let scrolltr: any = null
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
if (cursor.value && cursor.value !== editing.value) editing.value = null
|
||||||
|
if (editing.value) cursor.value = editing.value
|
||||||
if (cursor.value) {
|
if (cursor.value) {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${cursor.value.key} .name a`
|
`#file-${cursor.value.key} .name a`
|
||||||
|
@ -265,7 +276,13 @@ watchEffect(() => {
|
||||||
if (a) a.focus()
|
if (a) a.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const mkdir = (doc: FolderDocument, name: string) => {
|
watchEffect(() => {
|
||||||
|
if (!props.documents.length && cursor.value) {
|
||||||
|
cursor.value = null
|
||||||
|
focusBreadcrumb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mkdir = (doc: Document, name: string) => {
|
||||||
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
||||||
const msg = JSON.parse(ev.data)
|
const msg = JSON.parse(ev.data)
|
||||||
if ('error' in msg) {
|
if ('error' in msg) {
|
||||||
|
@ -273,14 +290,14 @@ const mkdir = (doc: FolderDocument, name: string) => {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
} else {
|
} else {
|
||||||
console.log('mkdir', msg)
|
console.log('mkdir', msg)
|
||||||
router.push(`/${linkBasePath.value}/${name}/`)
|
router.push(`/${doc.loc}/${name}/`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
control.onopen = () => {
|
control.onopen = () => {
|
||||||
control.send(
|
control.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
op: 'mkdir',
|
op: 'mkdir',
|
||||||
path: `${decodeURIComponent(linkBasePath.value)}/${name}`
|
path: `${doc.loc}/${name}`
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -332,10 +349,11 @@ const allSelected = computed({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
watchEffect(() => {
|
|
||||||
if (cursor.value && cursor.value !== editing.value) editing.value = null
|
const loc = computed(() => props.path.join('/'))
|
||||||
if (editing.value) cursor.value = editing.value
|
let prevloc = ''
|
||||||
})
|
onBeforeUpdate(() => { prevloc = loc.value })
|
||||||
|
|
||||||
const contextMenu = (ev: Event, doc: Document) => {
|
const contextMenu = (ev: Event, doc: Document) => {
|
||||||
cursor.value = doc
|
cursor.value = doc
|
||||||
console.log('Context menu', ev, doc)
|
console.log('Context menu', ev, doc)
|
||||||
|
@ -475,4 +493,7 @@ tbody .selection input {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
.loc {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,22 +12,17 @@ const toggleSearchInput = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const input = search.value
|
const input = search.value
|
||||||
if (input) input.focus()
|
if (input) input.focus()
|
||||||
else if (searchButton.value) searchButton.value.blur()
|
//else if (searchButton.value) document.querySelector('.breadcrumb')!.focus()
|
||||||
executeSearch()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSearch = () => {
|
|
||||||
documentStore.setFilter(search.value?.value ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
toggleSearchInput
|
toggleSearchInput
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav>
|
<nav class="headermain">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<UploadButton />
|
<UploadButton />
|
||||||
<SvgButton
|
<SvgButton
|
||||||
|
|
|
@ -5,7 +5,7 @@ import createWebSocket from './WS'
|
||||||
export type FUID = string
|
export type FUID = string
|
||||||
|
|
||||||
export type Document = {
|
export type Document = {
|
||||||
loc: string[]
|
loc: string
|
||||||
name: string
|
name: string
|
||||||
key: FUID
|
key: FUID
|
||||||
type: 'folder' | 'file'
|
type: 'folder' | 'file'
|
||||||
|
|
|
@ -60,7 +60,7 @@ export const useDocumentStore = defineStore({
|
||||||
updateRoot(root: DirEntry | null = null) {
|
updateRoot(root: DirEntry | null = null) {
|
||||||
root ??= this.root
|
root ??= this.root
|
||||||
// Transform tree data to flat documents array
|
// Transform tree data to flat documents array
|
||||||
let loc = [] as Array<string>
|
let loc = ""
|
||||||
const mapper = ([name, attr]: [string, FileEntry | DirEntry]) => ({
|
const mapper = ([name, attr]: [string, FileEntry | DirEntry]) => ({
|
||||||
loc,
|
loc,
|
||||||
name,
|
name,
|
||||||
|
@ -75,7 +75,7 @@ export const useDocumentStore = defineStore({
|
||||||
for (let doc; (doc = queue.shift()) !== undefined;) {
|
for (let doc; (doc = queue.shift()) !== undefined;) {
|
||||||
docs.push(doc)
|
docs.push(doc)
|
||||||
if ("dir" in doc) {
|
if ("dir" in doc) {
|
||||||
loc = [...doc.loc, doc.name]
|
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
queue.push(...Object.entries(doc.dir).map(mapper))
|
queue.push(...Object.entries(doc.dir).map(mapper))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,16 +88,6 @@ export const useDocumentStore = defineStore({
|
||||||
this.root = root
|
this.root = root
|
||||||
this.document = docs
|
this.document = docs
|
||||||
},
|
},
|
||||||
find(query: string): Document[] {
|
|
||||||
const needle = needleFormat(query)
|
|
||||||
return this.document.filter(doc => localeIncludes(doc.haystack, needle))
|
|
||||||
},
|
|
||||||
directory(loc: string[]) {
|
|
||||||
const ret = this.document.filter(
|
|
||||||
doc => doc.loc.length === loc.length && doc.loc.every((e, i) => e === loc[i])
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
},
|
|
||||||
updateUploadingDocuments(key: number, progress: number) {
|
updateUploadingDocuments(key: number, progress: number) {
|
||||||
for (const d of this.uploadingDocuments) {
|
for (const d of this.uploadingDocuments) {
|
||||||
if (d.key === key) d.progress = progress
|
if (d.key === key) d.progress = progress
|
||||||
|
@ -129,12 +119,19 @@ export const useDocumentStore = defineStore({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
mainDocument(): Document[] {
|
|
||||||
return this.document
|
|
||||||
},
|
|
||||||
isUserLogged(): boolean {
|
isUserLogged(): boolean {
|
||||||
return this.user.isLoggedIn
|
return this.user.isLoggedIn
|
||||||
},
|
},
|
||||||
|
recentDocuments(): Document[] {
|
||||||
|
const ret = [...this.document]
|
||||||
|
ret.sort((a, b) => b.mtime - a.mtime)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
largeDocuments(): Document[] {
|
||||||
|
const ret = [...this.document]
|
||||||
|
ret.sort((a, b) => b.size - a.size)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
selectedFiles(): SelectedItems {
|
selectedFiles(): SelectedItems {
|
||||||
function traverseDir(data: DirEntry | FileEntry, path: string, relpath: string) {
|
function traverseDir(data: DirEntry | FileEntry, path: string, relpath: string) {
|
||||||
if (!('dir' in data)) return
|
if (!('dir' in data)) return
|
||||||
|
|
|
@ -3,25 +3,50 @@
|
||||||
ref="fileExplorer"
|
ref="fileExplorer"
|
||||||
:key="Router.currentRoute.value.path"
|
:key="Router.currentRoute.value.path"
|
||||||
:path="props.path"
|
:path="props.path"
|
||||||
:documents="
|
:documents="documents"
|
||||||
documentStore.search ?
|
|
||||||
documentStore.find(documentStore.search) :
|
|
||||||
documentStore.directory(props.path)
|
|
||||||
"
|
|
||||||
v-if="props.path"
|
v-if="props.path"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watchEffect, ref } from 'vue'
|
import { watchEffect, ref, computed } from 'vue'
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
import Router from '@/router/index'
|
import Router from '@/router/index'
|
||||||
|
import { needleFormat, localeIncludes, collator } from '@/utils';
|
||||||
|
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const fileExplorer = ref()
|
const fileExplorer = ref()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
path: Array<string>
|
path: Array<string>
|
||||||
})
|
})
|
||||||
|
const documents = computed(() => {
|
||||||
|
if (!props.path) return []
|
||||||
|
const loc = props.path.join('/')
|
||||||
|
// List the current location
|
||||||
|
if (!documentStore.search) return documentStore.document.filter(doc => doc.loc === loc)
|
||||||
|
// Find up to 100 newest documents that match the search
|
||||||
|
let docs = documentStore.recentDocuments
|
||||||
|
const search = documentStore.search
|
||||||
|
console.log('Searching for', search)
|
||||||
|
const needle = needleFormat(search)
|
||||||
|
docs = docs.filter(doc => localeIncludes(doc.haystack, needle)).slice(0, 100)
|
||||||
|
// Organize by folder, by relevance
|
||||||
|
const locsub = loc + '/'
|
||||||
|
docs.sort((a, b) => (
|
||||||
|
// @ts-ignore
|
||||||
|
(b.loc === loc) - (a.loc === loc) ||
|
||||||
|
// @ts-ignore
|
||||||
|
(b.loc.slice(0, locsub.length) === locsub) - (a.loc.slice(0, locsub.length) === locsub) ||
|
||||||
|
collator.compare(a.loc, b.loc) ||
|
||||||
|
// @ts-ignore
|
||||||
|
(a.type === 'file') - (b.type === 'file') ||
|
||||||
|
// @ts-ignore
|
||||||
|
b.name.includes(search) - a.name.includes(search) ||
|
||||||
|
collator.compare(a.name, b.name)
|
||||||
|
))
|
||||||
|
return docs
|
||||||
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
documentStore.fileExplorer = fileExplorer.value
|
documentStore.fileExplorer = fileExplorer.value
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user