Run WebAssembly tasks everywhere.
Drag & Drop WASM App
-.wasm files only
+Drop WASM anywhere
+or choose a .wasm file
diff --git a/main.js b/main.js index 992ba16..062b82a 100644 --- a/main.js +++ b/main.js @@ -78,6 +78,7 @@ const cpuValue = document.querySelector('#cpuValue'); const cpuProgress = document.querySelector('#cpuProgress'); const fileInput = document.querySelector('#wasmFile'); const dropZone = document.querySelector('#dropZone'); +const pageDropOverlay = document.querySelector('#pageDropOverlay'); const launchButton = document.querySelector('#launchButton'); const newTaskButton = document.querySelector('#newTaskButton'); const fuelRateSlider = document.querySelector('#fuelRateSlider'); @@ -1084,6 +1085,51 @@ function handleFiles(files) { Array.from(files).forEach((file) => addFile(file).catch((error) => appendLog(`${file.name}: ${error.message}`))); } +let pageFileDragDepth = 0; + +function dragContainsFiles(event) { + return Array.from(event.dataTransfer?.types || []).includes('Files'); +} + +function setPageDropActive(active) { + pageDropOverlay.classList.toggle('is-active', active); + pageDropOverlay.setAttribute('aria-hidden', String(!active)); +} + +function resetPageFileDrag() { + pageFileDragDepth = 0; + setPageDropActive(false); +} + +function handlePageDragEnter(event) { + if (!dragContainsFiles(event)) return; + event.preventDefault(); + pageFileDragDepth += 1; + setPageDropActive(true); +} + +function handlePageDragOver(event) { + if (!dragContainsFiles(event)) return; + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + setPageDropActive(true); +} + +function handlePageDragLeave(event) { + if (pageFileDragDepth === 0) return; + event.preventDefault(); + pageFileDragDepth -= 1; + if (pageFileDragDepth === 0) setPageDropActive(false); +} + +function handlePageDrop(event) { + if (!dragContainsFiles(event)) return; + event.preventDefault(); + const files = event.dataTransfer.files; + resetPageFileDrag(); + if (files.length > 0) handleFiles(files); +} + ['click', 'keydown'].forEach((type) => { dropZone.addEventListener(type, (event) => { if (event.type === 'click' || event.key === 'Enter' || event.key === ' ') openPicker(); @@ -1092,15 +1138,11 @@ function handleFiles(files) { launchButton.addEventListener('click', (event) => { event.stopPropagation(); openPicker(); }); newTaskButton.addEventListener('click', openPicker); fileInput.addEventListener('change', () => { handleFiles(fileInput.files); fileInput.value = ''; }); -['dragenter', 'dragover'].forEach((type) => dropZone.addEventListener(type, (event) => { - event.preventDefault(); - dropZone.classList.add('is-dragover'); -})); -['dragleave', 'drop'].forEach((type) => dropZone.addEventListener(type, (event) => { - event.preventDefault(); - dropZone.classList.remove('is-dragover'); -})); -dropZone.addEventListener('drop', (event) => handleFiles(event.dataTransfer.files)); +document.addEventListener('dragenter', handlePageDragEnter, true); +document.addEventListener('dragover', handlePageDragOver, true); +document.addEventListener('dragleave', handlePageDragLeave, true); +document.addEventListener('drop', handlePageDrop, true); +document.addEventListener('dragend', resetPageFileDrag, true); function refreshRuntimeState() { if (!runtime) return; diff --git a/page-drop.css b/page-drop.css new file mode 100644 index 0000000..32dca08 --- /dev/null +++ b/page-drop.css @@ -0,0 +1,72 @@ +.page-drop-overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: grid; + place-items: center; + padding: clamp(20px, 5vw, 72px); + background: rgba(10, 10, 10, 0.82); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 140ms ease, visibility 140ms ease; +} + +.page-drop-overlay::before { + content: ""; + position: absolute; + inset: clamp(20px, 5vw, 72px); + border: 2px dashed rgba(238, 238, 238, 0.72); + border-radius: clamp(18px, 2vw, 30px); + box-shadow: inset 0 0 80px rgba(255, 255, 255, 0.035); +} + +.page-drop-overlay.is-active { + opacity: 1; + visibility: visible; +} + +.page-drop-overlay-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + color: #f4f4f4; +} + +.page-drop-overlay-label { + margin-bottom: 22px; + color: #9e9e9e; + font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.page-drop-overlay-icon { + width: 76px; + height: 76px; + margin-bottom: 20px; + color: #eeeeee; + filter: drop-shadow(0 0 22px rgba(255, 255, 255, 0.14)); +} + +.page-drop-overlay-card strong { + margin-bottom: 10px; + font-size: clamp(34px, 5vw, 66px); + font-weight: 550; + letter-spacing: -0.045em; +} + +.page-drop-overlay-card > span:last-child { + color: #b8b8b8; + font-size: clamp(14px, 1.4vw, 18px); +} + +@media (prefers-reduced-motion: reduce) { + .page-drop-overlay { + transition: none; + } +}