From f1375f27d0fecc18ed0831d031bce877d8788ffa Mon Sep 17 00:00:00 2001 From: MickaelK Date: Fri, 14 Jun 2024 08:12:16 +1000 Subject: [PATCH] chore (rewrite): drag and drop to move things around --- public/assets/components/breadcrumb.js | 25 +++++++++------ public/assets/pages/filespage/ctrl_upload.js | 32 +++++++++---------- public/assets/pages/filespage/helper.js | 2 ++ .../pages/filespage/model_virtual_layer.js | 14 ++++---- public/assets/pages/filespage/thing.js | 28 ++++++++++------ 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/public/assets/components/breadcrumb.js b/public/assets/components/breadcrumb.js index e07a43f6..c68760d0 100644 --- a/public/assets/components/breadcrumb.js +++ b/public/assets/components/breadcrumb.js @@ -1,8 +1,14 @@ import { animate, slideYOut, slideYIn, opacityOut } from "../lib/animate.js"; import { loadCSS } from "../helpers/loader.js"; -import { mv } from "../pages/filespage/model_files.js"; -import { extractPath, isDir } from "../pages/filespage/helper.js"; +import { extractPath, isDir, isNativeFileUpload } from "../pages/filespage/helper.js"; +import { mv as mv$ } from "../pages/filespage/model_files.js"; +import { mv as mvVL, withVirtualLayer } from "../pages/filespage/model_virtual_layer.js"; + +const mv = (from, to) => withVirtualLayer( + mv$(from, to), + mvVL(from, to), +); class ComponentBreadcrumb extends window.HTMLDivElement { constructor() { @@ -158,13 +164,6 @@ class ComponentBreadcrumb extends window.HTMLDivElement { setupDragDropTarget() { this.querySelectorAll("a.label").forEach(($folder) => { const $path = $folder.closest(".component_path-element"); - $folder.ondragover = (e) => { - e.preventDefault(); - $path.classList.add("highlight"); - }; - $folder.ondragleave = () => { - $path.classList.remove("highlight"); - }; $folder.ondrop = async (e) => { $path.classList.remove("highlight"); const from = e.dataTransfer.getData("path"); @@ -175,6 +174,14 @@ class ComponentBreadcrumb extends window.HTMLDivElement { if (isDir(from)) to += "/"; await mv(from, to).toPromise(); }; + $folder.ondragover = (e) => { + if (isNativeFileUpload(e)) return; + e.preventDefault(); + $path.classList.add("highlight"); + }; + $folder.ondragleave = () => { + $path.classList.remove("highlight"); + }; }); } diff --git a/public/assets/pages/filespage/ctrl_upload.js b/public/assets/pages/filespage/ctrl_upload.js index c8fa1b7d..36790816 100644 --- a/public/assets/pages/filespage/ctrl_upload.js +++ b/public/assets/pages/filespage/ctrl_upload.js @@ -5,7 +5,7 @@ import { loadCSS } from "../../helpers/loader.js"; import { qs } from "../../lib/dom.js"; import { AjaxError } from "../../lib/error.js"; import assert from "../../lib/assert.js"; -import { currentPath } from "./helper.js"; +import { currentPath, isNativeFileUpload } from "./helper.js"; import { mkdir, save } from "./model_virtual_layer.js"; import t from "../../locales/index.js"; @@ -55,22 +55,17 @@ function componentUploadFAB(render, { workers$ }) { } function componentFilezone(render, { workers$ }) { - const $target = document.body.querySelector(`[data-bind="filemanager-children"]`); + const selector = `[data-bind="filemanager-children"]`; + const $target = document.body.querySelector(selector); + $target.ondragenter = (e) => { - e.preventDefault(); - e.stopPropagation(); + if (!isNativeFileUpload(e)) return; $target.classList.add("dropzone"); - e.dataTransfer.setData("type", "fileupload"); - }; - $target.ondragover = (e) => { - e.preventDefault(); - }; - $target.ondragleave = () => { - // console.log("DRAGLEAVE"); }; $target.ondrop = async (e) => { + if (!isNativeFileUpload(e)) return; + $target.classList.remove("dropzone"); e.preventDefault(); - e.stopPropagation(); const loadID = setTimeout(() => render(createElement("
LOADING
")), 2000); if (e.dataTransfer.items instanceof window.DataTransferItemList) { workers$.next(await processItems(e.dataTransfer.items)); @@ -79,10 +74,17 @@ function componentFilezone(render, { workers$ }) { } else { assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry); } - $target.classList.remove("dropzone"); clearTimeout(loadID); render(createFragment("")); }; + $target.ondragleave = (e) => { + if (!isNativeFileUpload(e)) return; + if (!(e.relatedTarget === null || // eg: drag outside the window + !e.relatedTarget.closest(selector) // eg: drag on the breadcrumb, ... + )) return; + $target.classList.remove("dropzone"); + }; + $target.ondragover = (e) => e.preventDefault(); } const MAX_WORKERS = 4; @@ -113,13 +115,12 @@ function componentUploadQueue(render, { workers$ }) { // feature1: close the queue onClick(qs($page, `img[alt="close"]`)).pipe( - rxjs.mergeMap(() => animate($page, { time: 200, keyframes: slideYOut(50) })), + // rxjs.mergeMap(() => animate($page, { time: 200, keyframes: slideYOut(50) })), rxjs.tap(() => $page.classList.add("hidden")), ).subscribe(); // feature2: setup the task queue in the dom workers$.subscribe(({ tasks }) => { - console.log("TASKS SETUP DOM", tasks); if (tasks.length === 0) return; $page.classList.remove("hidden"); const $fragment = document.createDocumentFragment(); @@ -216,7 +217,6 @@ function componentUploadQueue(render, { workers$ }) { }; const noFailureAllowed = (fn) => fn().catch(() => noFailureAllowed(fn)); workers$.subscribe(async ({ tasks: newTasks }) => { - console.log("TASKS PROCESS", newTasks); tasks = tasks.concat(newTasks); // add new tasks to the pool while(true) { const nworker = reservations.indexOf(false); diff --git a/public/assets/pages/filespage/helper.js b/public/assets/pages/filespage/helper.js index b162629c..347885fa 100644 --- a/public/assets/pages/filespage/helper.js +++ b/public/assets/pages/filespage/helper.js @@ -111,3 +111,5 @@ function _moveHiddenFilesDownward(fileA, fileB) { if (!aIsHidden && bIsHidden) return -1; return 0; } + +export const isNativeFileUpload = (e) => JSON.stringify(e.dataTransfer.types) === '["Files"]'; diff --git a/public/assets/pages/filespage/model_virtual_layer.js b/public/assets/pages/filespage/model_virtual_layer.js index 69219cc0..693b80fa 100644 --- a/public/assets/pages/filespage/model_virtual_layer.js +++ b/public/assets/pages/filespage/model_virtual_layer.js @@ -63,7 +63,7 @@ export function touch(path) { async afterSuccess() { removeLoading(virtualFiles$, basepath, filename); onDestroy(() => statePop(virtualFiles$, basepath, filename)); - await fscache().update(basepath, ({ files, ...rest }) => ({ + await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.concat([file]), ...rest, })); @@ -100,7 +100,7 @@ export function mkdir(path) { async afterSuccess() { removeLoading(virtualFiles$, basepath, dirname); onDestroy(() => statePop(virtualFiles$, basepath, dirname)); - await fscache().update(basepath, ({ files, ...rest }) => ({ + await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.concat([file]), ...rest, })); @@ -137,7 +137,7 @@ export function save(path, size) { async afterSuccess() { removeLoading(virtualFiles$, basepath, filename); onDestroy(() => statePop(virtualFiles$, basepath, filename)); - await fscache().update(basepath, ({ files, ...rest }) => ({ + await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.concat([file]), ...rest, })); @@ -190,7 +190,7 @@ export function rm(...paths) { }); onDestroy(() => statePop(mutationFiles$, basepath, basepath)); await Promise.all(paths.map((path) => fscache().remove(path, false))); - await fscache().update(basepath, ({ files, ...rest }) => ({ + await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.filter(({ name }) => { for (let i=0;i { + await fscache().update(fromBasepath, ({ files = [], ...rest }) => { return { files: files.map((file) => { if (file.name === fromName) { @@ -300,11 +300,11 @@ export function mv(fromPath, toPath) { }); onDestroy(() => statePop(mutationFiles$, fromBasepath, fromName)); statePop(virtualFiles$, toBasepath, toName); - await fscache().update(fromBasepath, ({ files, ...rest }) => ({ + await fscache().update(fromBasepath, ({ files = [], ...rest }) => ({ files: files.filter((file) => file.name === fromName ? false : true), ...rest, })) - await fscache().update(toBasepath, ({ files, ...rest }) => ({ + await fscache().update(toBasepath, ({ files = [], ...rest }) => ({ files: files.concat([{ name: fromName, time: new Date().getTime(), diff --git a/public/assets/pages/filespage/thing.js b/public/assets/pages/filespage/thing.js index 456091c7..0466351c 100644 --- a/public/assets/pages/filespage/thing.js +++ b/public/assets/pages/filespage/thing.js @@ -3,12 +3,19 @@ import { qs } from "../../lib/dom.js"; import { animate, opacityIn } from "../../lib/animate.js"; import assert from "../../lib/assert.js"; -import { extractPath, isDir } from "./helper.js"; -import { mv } from "./model_files.js"; +import { extractPath, isDir, isNativeFileUpload } from "./helper.js"; import { get as getConfig } from "./model_config.js"; import { files$ } from "./ctrl_filesystem.js"; import { addSelection, isSelected, clearSelection } from "./state_selection.js"; +import { mv as mv$ } from "./model_files.js"; +import { mv as mvVL, withVirtualLayer } from "./model_virtual_layer.js"; + +const mv = (from, to) => withVirtualLayer( + mv$(from, to), + mvVL(from, to), +); + const IMAGE = { FILE: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiPgogIDxwYXRoIHN0eWxlPSJjb2xvcjojMDAwMDAwO3RleHQtaW5kZW50OjA7dGV4dC10cmFuc2Zvcm06bm9uZTtmaWxsOiM4YzhjOGM7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjAuOTg0ODEwNDEiIGQ9Im0gMiwxMy4wODI0MTIgMC4wMTk0NjIsMS40OTIzNDcgYyA1ZS02LDAuMjIyMTQ1IDAuMjA1NTkwMiwwLjQyNDI2MiAwLjQzMTE1MDIsMC40MjQyNzIgTCAxMy41ODk2MTIsMTUgQyAxMy44MTUxNzMsMTQuOTk5OTk1IDEzLjk5OTk5LDE0Ljc5Nzg3NCAxNCwxNC41NzU3MjkgdiAtMS40OTMzMTcgYyAtNC4xNzE4NjkyLDAuNjYyMDIzIC03LjY1MTY5MjgsMC4zOTg2OTYgLTEyLDAgeiIgLz4KICA8cGF0aCBzdHlsZT0iY29sb3I6IzAwMDAwMDt0ZXh0LWluZGVudDowO3RleHQtdHJhbnNmb3JtOm5vbmU7ZGlzcGxheTppbmxpbmU7ZmlsbDojYWFhYWFhO3N0cm9rZS13aWR0aDowLjk4NDA4MTI3IiBkPSJNIDIuMzUwMSwxLjAwMTMzMTIgQyAyLjE1MjU5LDEuMDM4MzI0NyAxLjk5NjU5LDEuMjI3MjcyMyAyLjAwMDA5LDEuNDI0OTM1NiBWIDE0LjEzMzQ1NyBjIDVlLTYsMC4yMjE4MTYgMC4yMDUyMywwLjQyMzYzNCAwLjQzMDc5LDAuNDIzNjQ0IGwgMTEuMTM5LC0xLjAxZS00IGMgMC4yMjU1NiwtNmUtNiAwLjQzMDExLC0wLjIwMDc1OCAwLjQzMDEyLC0wLjQyMjU3NCBsIDYuN2UtNCwtOS44MjI2NDI2IGMgLTIuNDg0MDQ2LC0xLjM1NTAwNiAtMi40MzUyMzQsLTIuMDMxMjI1NCAtMy41MDAxLC0zLjMwOTcwNyAtMC4wNDMsLTAuMDE1ODgyIDAuMDQ2LDAuMDAxNzQgMCwwIEwgMi40MzA2NywxLjAwMTEwOCBDIDIuNDAzODMsMC45OTg1OSAyLjM3Njc0LDAuOTk4NTkgMi4zNDk5LDEuMDAxMTA4IFoiIC8+CiAgPHBhdGggc3R5bGU9ImRpc3BsYXk6aW5saW5lO2ZpbGw6IzhjOGM4YztmaWxsLW9wYWNpdHk6MTtzdHJva2U6IzllNzU3NTtzdHJva2Utd2lkdGg6MDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2Utb3BhY2l0eToxIiBkPSJtIDEwLjUwMDU3LDEuMDAyMDc2NCBjIDAsMy4yNzY4MDI4IC0wLjAwNTIsMy4xNzM5MTYxIDAuMzYyOTIxLDMuMjY5ODIwMiAwLjI4MDEwOSwwLjA3Mjk4NCAzLjEzNzE4LDAuMDM5ODg3IDMuMTM3MTgsMC4wMzk4ODcgLTEuMTIwMDY3LC0xLjA1NTY2OTIgLTIuMzMzNCwtMi4yMDY0NzEzIC0zLjUwMDEsLTMuMzA5NzA3NCB6IiAvPgo8L3N2Zz4K", FOLDER: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiPgogIDxnIHRyYW5zZm9ybT0ibWF0cml4KDAuODY2NjY0MzEsMCwwLDAuODY2NjcsLTE3Mi4wNDU3OCwtODY0LjMyNzU5KSIgc3R5bGU9ImZpbGw6Izc1YmJkOTtmaWxsLW9wYWNpdHk6MC45NDExNzY0NztmaWxsLXJ1bGU6ZXZlbm9kZCI+CiAgICA8cGF0aCBzdHlsZT0iZmlsbDojNzViYmQ5O2ZpbGwtb3BhY2l0eTowLjk0MTE3NjQ3O2ZpbGwtcnVsZTpldmVub2RkIiBkPSJtIDIwMC4yLDk5OS43MiBjIC0wLjI4OTEzLDAgLTAuNTMxMjUsMC4yNDIxIC0wLjUzMTI1LDAuNTMxMiB2IDEyLjc4NCBjIDAsMC4yOTg1IDAuMjMyNjQsMC41MzEyIDAuNTMxMjUsMC41MzEyIGggMTUuMDkxIGMgMC4yOTg2LDAgMC41MzEyNCwtMC4yMzI3IDAuNTMxMjQsLTAuNTMxMiBsIDRlLTQsLTEwLjQ3NCBjIDAsLTAuMjg4OSAtMC4yNDIxMSwtMC41MzM4IC0wLjUzMTI0LC0wLjUzMzggbCAtNy41NDU3LDVlLTQgLTIuMzA3NiwtMi4zMDc4MyB6IiAvPgogIDwvZz4KICA8ZyB0cmFuc2Zvcm09Im1hdHJpeCgwLjg2NjY3LDAsMCwwLjg2NjY3LC0xNzIuMDQ2OTIsLTg2NC43ODM0KSIgc3R5bGU9ImZpbGw6IzlhZDFlZDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZCI+CiAgICA8cGF0aCBzdHlsZT0iZmlsbDojOWFkMWVkO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkIiBkPSJtIDIwMC4yLDk5OS43MiBjIC0wLjI4OTEzLDAgLTAuNTMxMjUsMC4yNDIxIC0wLjUzMTI1LDAuNTMxMiB2IDEyLjc4NCBjIDAsMC4yOTg1IDAuMjMyNjQsMC41MzEyIDAuNTMxMjUsMC41MzEyIGggMTUuMDkxIGMgMC4yOTg2LDAgMC41MzEyNCwtMC4yMzI3IDAuNTMxMjQsLTAuNTMxMiBsIDRlLTQsLTEwLjQ3NCBjIDAsLTAuMjg4OSAtMC4yNDIxMSwtMC41MzM4IC0wLjUzMTI0LC0wLjUzMzggbCAtNy41NDU3LDVlLTQgLTIuMzA3NiwtMi4zMDc4MyB6IiAvPgogIDwvZz4KPC9zdmc+Cg==", @@ -153,14 +160,6 @@ export function createThing({ e.dataTransfer.setData("path", path); e.dataTransfer.setDragImage($thing, e.offsetX, -10); }; - $thing.ondragover = (e) => { - if ($thing.getAttribute("data-droptarget") !== "true") return; - e.preventDefault(); - $thing.classList.add("hover"); - }; - $thing.ondragleave = () => { - $thing.classList.remove("hover"); - }; $thing.ondrop = async (e) => { $thing.classList.remove("hover"); const from = e.dataTransfer.getData("path"); @@ -173,6 +172,15 @@ export function createThing({ } await mv(from, to).toPromise(); }; + $thing.ondragover = (e) => { + if(isNativeFileUpload(e)) return; + else if ($thing.getAttribute("data-droptarget") !== "true") return; + e.preventDefault(); + $thing.classList.add("hover"); + }; + $thing.ondragleave = () => { + $thing.classList.remove("hover"); + }; return $thing; }