clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

pick or drop a folder instead of toggling the file input

Dami committed c656e44
commit
c656e4458f032c5baeeed9bdd279bd2ee726ce43
parent
c49b9072ae7ffef6d956cecd1ead029556b304a9
author
Dami <dami.etoile@gmail.com>
4 files changed, +63 -14
Mdocs/files-and-private-repositories.md2+-
Minternal/webui/file-workspace.js73+++++++++++++++++++++++++-----
Minternal/webui/style.css1+
Mscripts/file-workspace.test.mjs1-
M docs/files-and-private-repositories.md +1 -1
M internal/webui/file-workspace.js +61 -12
@@ -151,13 +151,23 @@
151151 if (this.bound) return;152152 this.bound = true;153153 this.innerHTML =154- '<form><label>File or folder <input type="file" name="file" multiple required></label><label><input type="checkbox" name="folder"> folder</label><label><input type="checkbox" name="encrypt"> encrypt</label><button>Upload</button></form><p data-controls hidden><button type="button" data-cancel disabled>Cancel</button> <button type="button" data-retry disabled>Retry</button></p><label data-share hidden>Share link <input data-share-url readonly></label><output role="status"></output>';154+ '<form><label>Files <input type="file" name="file" multiple></label><input type="file" name="folder" webkitdirectory multiple hidden><p data-drop><button type="button" data-pick-folder>Choose folder</button> or drop files or a folder here</p><label><input type="checkbox" name="encrypt"> encrypt</label><button>Upload</button></form><p data-controls hidden><button type="button" data-cancel disabled>Cancel</button> <button type="button" data-retry disabled>Retry</button></p><label data-share hidden>Share link <input data-share-url readonly></label><output role="status"></output>';155155 this.out = this.querySelector("output");156156 const form = this.querySelector("form");157- const input = form.elements.namedItem("file");158- form.elements.namedItem("folder").addEventListener("change", event => {159- if (event.currentTarget.checked) input.setAttribute("webkitdirectory", "");160- else input.removeAttribute("webkitdirectory");157+ const files = form.elements.namedItem("file");158+ const folder = form.elements.namedItem("folder");159+ files.addEventListener("change", () => this.choose([...files.files]));160+ folder.addEventListener("change", () => this.choose([...folder.files]));161+ this.querySelector("[data-pick-folder]").addEventListener("click", () => folder.click());162+ this.addEventListener("dragover", event => {163+ event.preventDefault();164+ this.dataset.over = "";165+ });166+ this.addEventListener("dragleave", () => delete this.dataset.over);167+ this.addEventListener("drop", event => {168+ event.preventDefault();169+ delete this.dataset.over;170+ this.dropped(event.dataTransfer).catch(error => this.say(error.message, true));161171 });162172 form.addEventListener("submit", event => {163173 event.preventDefault();
@@ -166,6 +176,49 @@
166176 this.querySelector("[data-cancel]").addEventListener("click", () => this.controller?.abort());167177 this.querySelector("[data-retry]").addEventListener("click", () => this.run().catch(() => {}));168178 }179+ // choose records a selection. A folder is recognized from the relative180+ // paths that folder pickers and folder drops attach to their files.181+ choose(files) {182+ const folder = files.some(file => (file.webkitRelativePath || "").includes("/"));183+ this.chosen = {files, folder};184+ if (files.length)185+ this.say(186+ folder187+ ? files.length + " files in a folder selected."188+ : files.length + (files.length === 1 ? " file selected." : " files selected.")189+ );190+ }191+ async dropped(transfer) {192+ const entries = [...(transfer.items || [])].map(item => item.webkitGetAsEntry?.()).filter(Boolean);193+ if (!entries.length) return this.choose([...transfer.files]);194+ const files = [];195+ const walk = (entry, path) =>196+ new Promise((resolve, reject) => {197+ if (files.length > MAX_FILES) return reject(Error("Choose a folder with at most 10,000 files."));198+ if (entry.isFile) {199+ entry.file(file => {200+ if (path) Object.defineProperty(file, "webkitRelativePath", {value: path + file.name});201+ files.push(file);202+ resolve();203+ }, reject);204+ return;205+ }206+ const reader = entry.createReader();207+ const next = () =>208+ reader.readEntries(async batch => {209+ if (!batch.length) return resolve();210+ try {211+ for (const child of batch) await walk(child, path + entry.name + "/");212+ } catch (error) {213+ return reject(error);214+ }215+ next();216+ }, reject);217+ next();218+ });219+ for (const entry of entries) await walk(entry, "");220+ this.choose(files);221+ }169222 disconnectedCallback() {170223 this.controller?.abort();171224 }
@@ -183,12 +236,8 @@
183236 async run(form) {184237 if (this.busy) return;185238 if (form) {186- const files = [...form.elements.namedItem("file").files];187- this.selection = {188- files,189- folder: Boolean(form.elements.namedItem("folder").checked),190- encrypt: Boolean(form.elements.namedItem("encrypt").checked)191- };239+ if (!this.chosen) this.choose([...form.elements.namedItem("file").files]);240+ this.selection = {...this.chosen, encrypt: Boolean(form.elements.namedItem("encrypt").checked)};192241 this.pending = null;193242 }194243 if (!this.selection?.files.length) throw Error("Choose a file or folder first.");
@@ -221,7 +270,7 @@
221270 } finally {222271 this.busy = false;223272 this.controls(false, completed);224- if (completed) this.selection = null;273+ if (completed) this.selection = this.chosen = null;225274 }226275 }227276 async storePlain(files) {
M internal/webui/style.css +1 -0
M scripts/file-workspace.test.mjs +0 -1