type to focus
Massimo Melina committed
Sep 24, 2024 at 15:51 UTC
a6dada5ec62bcc5940942558c23c3481c46b21d2
3 files changed
+63
-6
README.md
+3
-2
@@ -148,12 +148,13 @@ Some actions you can take for improved security:
148
149
- Appending `#LOGIN` to address will bring up the login dialog
150
- Appending ?lang=CODE to address will force a specific language
151
-- right/ctrl/command click on toggle-all checkbox will invert each checkbox state
151
+- Right/ctrl/command click on toggle-all checkbox will invert each checkbox state
152
- Appending `?login=USER:PASSWORD` will automatically log in the browser
153
- Appending `?overwrite` on uploads, will override the dont_overwrite_uploading configuration, provided you also have delete permission
154
- Appending `?search=PATTERN` will trigger search at start
155
- Right-click on "check for updates" will let you input a URL of a version to install
156
-- shift+click on a file will show & play
156
+- Shift+click on a file will show & play
157
+- Type the name of a file/folder to focus it, and ctrl+backspace to go to parent folder
158
159
## Contribute
160
frontend/src/BrowseFiles.ts
+46
-4
@@ -1,10 +1,10 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Link } from 'react-router-dom'
3
+import { Link, useNavigate } from 'react-router-dom'
4
import { createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState,
5
useId} from 'react'
6
-import { useMediaQuery, useWindowSize } from 'usehooks-ts'
7
-import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, getHFS } from './misc'
6
+import { useEventListener, useMediaQuery, useWindowSize } from 'usehooks-ts'
7
+import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, isCtrlKey } from './misc'
8
import { Checkbox, CustomCode, iconBtn, Spinner } from './components'
9
import { Head } from './Head'
10
import { DirEntry, state, useSnapState } from './state'
@@ -63,6 +63,8 @@ function FilesList() {
63
const theList = filteredList || list
64
const total = theList.length
65
const nPages = Math.ceil(total / pageSize)
66
+ const pageEnd = offset + pageSize * (1+extraPages)
67
+ const thisPage = theList.slice(offset, pageEnd)
68
69
useEffect(() => setPage(0), [theList[0]]) // reset page if the list changes
70
// reset scrolling if the page changes
@@ -89,6 +91,44 @@ function FilesList() {
91
calcScrolledPages()
92
}), [page, extraPages, nPages])
93
94
+ // type to focus
95
+ const [focus, setFocus] = useState('')
96
+ useEffect(() => setFocus(''), [theList]) // reset
97
+ const navigate = useNavigate()
98
+ const timeout = useRef()
99
+ useEventListener('keydown', ev => {
100
+ if (isCtrlKey(ev as any) === 'Backspace' && location.pathname > '/')
101
+ return navigate(location.pathname + '..')
102
+ const { key } = ev
103
+ if (ev.metaKey || ev.ctrlKey || ev.altKey) return
104
+ setFocus(was => {
105
+ const will = key === 'Backspace' ? was.slice(0, -1)
106
+ : key === 'Escape' || key === 'Tab' ? ''
107
+ : key.length === 1 ? was + key.toLocaleLowerCase()
108
+ : was
109
+ if (will !== was) {
110
+ clearTimeout(timeout.current)
111
+ timeout.current = setTimeout(() => setFocus(''), 5_000) as any
112
+ }
113
+ return will
114
+ })
115
+ })
116
+ const focusIndex = useMemo(() => {
117
+ if (!focus) return -1
118
+ const match = (x: typeof theList[0]) => x.name.toLocaleLowerCase().startsWith(focus)
119
+ const inThisPage = thisPage.findIndex(match) // first attempt within this page
120
+ if (inThisPage >= 0)
121
+ return inThisPage + offset
122
+ const i = theList.findIndex(match) // search again on whole list
123
+ if (i >= 0)
124
+ setPage(Math.floor(i / pageSize))
125
+ return i
126
+ }, [focus])
127
+ useEffect(() => { // wait for possible page-change before focusing
128
+ if (focusIndex >= 0)
129
+ (document.querySelector(`a[href="${theList[focusIndex]?.uri}"]`) as HTMLElement)?.focus()
130
+ }, [focusIndex])
131
+
132
const ref = useRef<HTMLElement>()
133
134
const [goBottom, setGoBottom] = useState(false)
@@ -98,6 +138,7 @@ function FilesList() {
138
window.scrollTo(0, document.body.scrollHeight)
139
}, [goBottom])
140
const changePage = useCallback((i: number, pleaseGoBottom?: boolean) => {
141
+ setFocus('')
142
if (pleaseGoBottom)
143
setGoBottom(true)
144
if (i < page || i > page + extraPages)
@@ -114,9 +155,10 @@ function FilesList() {
155
: filteredList && !filteredList.length && t('filter_none', "No match for this filter")
156
157
return h(Fragment, {},
158
+ focus && h('div', { id: 'focus-typing', className: focusIndex < 0 ? 'focus-typing-mismatch' : '' }, focus, hIcon('info', { style: { cursor: 'default' }, title: `ESC: ${t`Cancel`}` })),
159
h('ul', { ref, className: 'dir' },
160
msgInstead ? h('p', {}, msgInstead)
119
- : theList.slice(offset, offset + pageSize * (1+extraPages)).map((entry, idx) =>
161
+ : thisPage.map((entry, idx) =>
162
h(Entry, {
163
key: entry.key || entry.n,
164
midnight,
frontend/src/index.scss
+14
@@ -544,6 +544,20 @@ form label+input { margin-top: .2em; }
544
li:first-of-type { margin-top: .5em }
545
}
546
547
+#focus-typing {
548
+ position: fixed;
549
+ top: 0;
550
+ right: 0;
551
+ background: var(--bg);
552
+ z-index: 3;
553
+ padding: .1em .5em;
554
+ border: 1px solid currentColor;
555
+ margin: -1px;
556
+}
557
+.focus-typing-mismatch {
558
+ color: var(--error);
559
+}
560
+
561
.tiles-mode {
562
max-width: none;
563
@media (min-width: 42em) {