Major changes:

- File selections working
- CSS more responsive, more consistent use of colors and variables
- Keyboard navigation
- Added context menu buttons and handler, the menu is still missing
- Added download and settings buttons (no functions yet)
- Various minor fixes everywhere
This commit is contained in:
Leo Vasanko
2023-11-04 14:10:18 +00:00
parent 997e0b8549
commit 8c6690ea98
79 changed files with 381 additions and 230 deletions

View File

@@ -92,10 +92,10 @@ const props = withDefaults(
.breadcrumb a:nth-child(even) {
background: var(--breadcrumb-background-even);
}
.breadcrumb a:nth-child(odd):hover {
.breadcrumb a:nth-child(odd):hover, .breadcrumb a:focus:nth-child(odd) {
background: var(--breadcrumb-hover-background-odd);
}
.breadcrumb a:nth-child(even):hover {
.breadcrumb a:nth-child(even):hover, .breadcrumb a:focus:nth-child(even) {
background: var(--breadcrumb-hover-background-even);
}
.breadcrumb a:hover {

View File

@@ -1,96 +1,104 @@
<template>
<main>
<table v-if="props.documents.length || editing">
<thead>
<tr>
<th class="selection">
<input
type="checkbox"
v-model="allSelected"
:indeterminate="selectionIndeterminate"
/>
</th>
<th
class="sortcolumn"
:class="{ sortactive: sort === 'name' }"
@click="toggleSort('name')"
>
Name
</th>
<th
class="sortcolumn modified right"
:class="{ sortactive: sort === 'modified' }"
@click="toggleSort('modified')"
>
Modified
</th>
<th
class="sortcolumn size right"
:class="{ sortactive: sort === 'size' }"
@click="toggleSort('size')"
>
Size
</th>
</tr>
</thead>
<tbody>
<tr v-if="editing?.key === 'new'" class="folder">
<td class="selection"></td>
<td class="name">
<FileRenameInput
:doc="editing"
:rename="mkdir"
<table v-if="props.documents.length || editing" @blur="cursor = null">
<thead>
<tr>
<th class="selection">
<input
type="checkbox"
tabindex="-1"
v-model="allSelected"
:indeterminate="selectionIndeterminate"
/>
</th>
<th
class="sortcolumn"
:class="{ sortactive: sort === 'name' }"
@click="toggleSort('name')"
>
Name
</th>
<th
class="sortcolumn modified right"
:class="{ sortactive: sort === 'modified' }"
@click="toggleSort('modified')"
>
Modified
</th>
<th
class="sortcolumn modified right"
:class="{ sortactive: sort === 'size' }"
@click="toggleSort('size')"
>
Size
</th>
<th class="menu"></th>
</tr>
</thead>
<tbody>
<tr v-if="editing?.key === 'new'" class="folder">
<td class="selection"></td>
<td class="name">
<FileRenameInput
:doc="editing"
:rename="mkdir"
:exit="
() => {
editing = null
}
"
/>
</td>
<td class="modified right">{{ editing.modified }}</td>
<td class="size right">{{ editing.sizedisp }}</td>
<td class="menu"></td>
</tr>
<tr
v-for="doc of sorted(props.documents as FolderDocument[])"
:key="doc.key"
:id="`file-${doc.key}`"
:class="{
file: doc.type === 'file',
folder: doc.type === 'folder',
cursor: cursor === doc
}"
@click="cursor = cursor === doc ? null : doc"
@contextmenu.prevent="contextMenu($event, doc)"
>
<td class="selection" @click.up.stop="cursor = cursor === doc ? doc : null">
<input
type="checkbox"
tabindex="-1"
:checked="documentStore.selected.has(doc.key)"
@change="($event.target as HTMLInputElement).checked ? documentStore.selected.add(doc.key) : documentStore.selected.delete(doc.key)"
/>
</td>
<td class="name">
<template v-if="editing === doc"
><FileRenameInput
:doc="doc"
:rename="rename"
:exit="
() => {
editing = null
}
"
/>
</td>
<td class="right">{{ editing.modified }}</td>
<td class="right">{{ editing.sizedisp }}</td>
</tr>
<tr
v-for="doc of sorted(props.documents as FolderDocument[])"
:key="doc.key"
:class="doc.type === 'folder' ? 'folder' : 'file'"
>
<td class="selection">
<input
type="checkbox"
:checked="doc.key in documentStore.selected"
@change="documentStore.selected.add(doc.key)"
/>
</td>
<td class="name">
<template v-if="editing === doc"
><FileRenameInput
:doc="doc"
:rename="rename"
:exit="
() => {
editing = null
}
"
/></template>
<template v-else>
<a :href="url_for(doc)">{{ doc.name }}</a>
<button @click="() => (editing = doc)">🖊</button>
</template>
</td>
<td class="right">{{ doc.modified }}</td>
<td class="right">{{ doc.sizedisp }}</td>
</tr>
</tbody>
</table>
<div v-else>
<p>No files</p>
</div>
</main>
/></template>
<template v-else>
<a :href="url_for(doc)" tabindex="-1" @contextmenu.stop @click.stop @focus.stop="cursor = doc">{{ doc.name }}</a>
<button @click="() => (editing = doc)">🖊</button>
</template>
</td>
<td class="modified right">{{ doc.modified }}</td>
<td class="size right">{{ doc.sizedisp }}</td>
<td class="menu"><button tabindex="-1" @click.stop="cursor = doc; contextMenu($event, doc)"></button></td>
</tr>
</tbody>
</table>
<div v-else class="empty-container">Nothing to see here</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watchEffect } from 'vue'
import { useDocumentStore } from '@/stores/documents'
import type { Document, FolderDocument } from '@/repositories/Document'
import FileRenameInput from './FileRenameInput.vue'
@@ -113,8 +121,9 @@ const linkBasePath = computed(() => {
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
const url_for = (doc: FolderDocument) =>
doc.type === 'folder'
? `#${linkBasePath.value}/${doc.name}`
? `#${linkBasePath.value}/${doc.name}/`
: `${filesBasePath.value}/${doc.name}`
const cursor = ref<FolderDocument | null>(null)
// File rename
const editing = ref<FolderDocument | null>(null)
const rename = (doc: FolderDocument, newName: string) => {
@@ -152,6 +161,46 @@ defineExpose({
modified: formatUnixDate(now)
}
},
toggleSelectAll() {
console.log("Select")
allSelected.value = !allSelected.value
},
isCursor() {
return cursor.value !== null && editing.value === null
},
cursorRename() {
editing.value = cursor.value
},
cursorSelect() {
console.log("select", documentStore.selected)
const doc = cursor.value
if (!doc) return
if (documentStore.selected.has(doc.key)) {
documentStore.selected.delete(doc.key)
} else {
documentStore.selected.add(doc.key)
}
},
cursorMove(d: number) {
// Move cursor up or down (keyboard navigation)
const documents = sorted(props.documents as FolderDocument[])
if (documents.length === 0) {
cursor.value = null
return
}
const mod = (a: number, b: number) => ((a % b) + b) % b
const index = cursor.value !== null ? documents.indexOf(cursor.value) : -1
cursor.value = documents[mod(index + d, documents.length + 1)] ?? null
const tr = document.getElementById(`file-${cursor.value.key}`) as HTMLTableRowElement | null
// @ts-ignore
if (tr) tr.scrollIntoView({ block: 'center' })
}
})
watchEffect(() => {
if (cursor.value) {
const a = document.querySelector(`#file-${cursor.value.key} .name a`) as HTMLAnchorElement | null
if (a) a.focus()
}
})
const mkdir = (doc: FolderDocument, name: string) => {
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
@@ -194,7 +243,7 @@ const selectionIndeterminate = computed({
get: () => {
return (
props.documents.length > 0 &&
props.documents.some((doc: Document) => doc.key in documentStore.selected) &&
props.documents.some((doc: Document) => documentStore.selected.has(doc.key)) &&
!allSelected.value
)
},
@@ -205,10 +254,11 @@ const allSelected = computed({
get: () => {
return (
props.documents.length > 0 &&
props.documents.every((doc: Document) => doc.key in documentStore.selected)
props.documents.every((doc: Document) => documentStore.selected.has(doc.key))
)
},
set: (value: boolean) => {
console.log("Setting allSelected", value)
for (const doc of props.documents) {
if (value) {
documentStore.selected.add(doc.key)
@@ -218,26 +268,39 @@ const allSelected = computed({
}
}
})
const contextMenu = (ev: Event, doc: Document) => {
console.log('Context menu', ev, doc)
}
</script>
<style>
<style scoped>
table {
width: 100%;
table-layout: fixed;
}
table input[type='checkbox'] {
width: 1em;
height: 1em;
width: 1rem;
height: 1rem;
}
table .selection {
width: 1rem;
}
table .modified {
width: 10em;
width: 10rem;
}
table .size {
width: 6em;
width: 5rem;
}
table .menu {
width: 1rem;
}
tbody td {
font-size: 1.2rem;
}
table th,
table td {
padding: 0.5em;
padding: 0 0.5rem;
font-weight: normal;
text-align: left;
white-space: nowrap;
@@ -246,28 +309,20 @@ table td {
}
.name {
white-space: nowrap;
text-overflow: initial;
overflow: initial;
}
.name button {
visibility: hidden;
padding-left: 1em;
padding-left: 1rem;
}
.name:hover button {
visibility: visible;
}
.name button {
cursor: pointer;
border: 0;
background: transparent;
}
thead tr {
border: 1px solid #ddd;
background: #ddd;
background: linear-gradient(to bottom, #eee, #fff 30%, #ddd);
color: #000;
}
tbody tr:hover {
background: #00f8;
tbody tr.cursor {
background: var(--accent-color);
}
.right {
text-align: right;
@@ -279,7 +334,7 @@ tbody tr:hover {
cursor: pointer;
}
.sortcolumn:hover::after {
color: #f80;
color: var(--accent-color);
}
.sortcolumn {
padding-right: 1.7em;
@@ -289,16 +344,12 @@ tbody tr:hover {
color: #888;
margin: 0 1em 0 0.5em;
position: absolute;
transition: all 0.2s linear;
transition: all var(--transition-time) linear;
}
.sortactive::after {
transform: rotate(90deg);
color: #000;
}
main {
padding: 5px;
height: 100%;
}
.more-action {
display: flex;
flex-direction: column;
@@ -325,4 +376,10 @@ main {
content: '📁 ';
font-size: 1.5em;
}
.empty-container {
padding-top: 3rem;
text-align: center;
font-size: 3rem;
color: var(--accent-color);
}
</style>

View File

@@ -43,5 +43,6 @@ input#FileRenameInput {
width: 90%;
outline: none;
background: transparent;
font: inherit;
}
</style>

View File

@@ -1,30 +1,27 @@
<script setup lang="ts">
import { useDocumentStore } from '@/stores/documents'
import { ref, nextTick } from 'vue'
import { ref, nextTick, watchEffect } from 'vue'
const documentStore = useDocumentStore()
const searchQuery = ref<string>('')
const showSearchInput = ref<boolean>(false)
const search = ref<HTMLInputElement | null>()
const toggleSearchInput = () => {
showSearchInput.value = !showSearchInput.value
if (!showSearchInput.value) {
searchQuery.value = ''
}
nextTick(() => {
const input = search.value
if (input) input.focus()
executeSearch()
})
}
const executeSearch = (ev: Event) => {
// FIXME: Make reactive instead of this update handler
const query = (ev.target as HTMLInputElement).value
console.log('Searching', query)
documentStore.setFilter(query)
console.log('Filtered')
const executeSearch = () => {
documentStore.setFilter(search.value?.value ?? '')
}
defineExpose({
toggleSearchInput
})
</script>
<template>
@@ -32,26 +29,28 @@ const executeSearch = (ev: Event) => {
<div class="buttons">
<UploadButton />
<SvgButton name="create-folder" @click="() => documentStore.fileExplorer.newFolder()"/>
<template v-if="true">
<template v-if="documentStore.selected.size > 0">
<div class="smallgap"></div>
<p>N selected files:</p>
<p class="select-text">{{ documentStore.selected.size }} selected </p>
<!-- Needs better icons for copy/move/remove -->
<SvgButton name="copy" />
<SvgButton name="download" />
<SvgButton name="copy" />
<SvgButton name="paste" />
<SvgButton name="trash" />
<button @click="documentStore.selected.clear()"></button>
</template>
<div class="spacer"></div>
<SvgButton name="find" @click="toggleSearchInput" />
<template v-if="showSearchInput">
<input
ref="search"
type="search"
v-model="searchQuery"
class="margin-input"
@keyup.esc="toggleSearchInput"
@input="executeSearch"
/>
</template>
<SvgButton name="find" @click="toggleSearchInput" />
<SvgButton name="cog" @click="console.log('TODO open settings')" />
</div>
</nav>
</template>
@@ -68,6 +67,9 @@ const executeSearch = (ev: Event) => {
.smallgap {
margin-left: 2em;
}
.select-text {
color: var(--accent-color);
}
.search-widget {
display: flex;
align-items: center;

View File

@@ -26,7 +26,7 @@
<h3 v-if="loginForm.error.length > 0" class="error-text">
{{ loginForm.error }}
</h3>
<input type="submit" class="button-login" />
<input id="submit" type="submit" class="button-login" />
</form>
</ModalDialog>
</template>

View File

@@ -24,14 +24,17 @@ button {
transition: all 0.2s ease;
padding: 0.5rem;
}
button:hover {
button:hover, button:focus {
color: #fff;
transform: scale(1.1);
}
svg {
fill: #ccc;
transform: fill 0.2s ease;
width: 1rem;
height: 1rem;
}
button:hover svg {
button:hover svg, button:focus svg {
fill: #fff;
}
</style>