From ba4db567cfe78f02c25f63d532fec6acb4378693 Mon Sep 17 00:00:00 2001 From: Chandler Abraham Date: Thu, 29 May 2025 22:27:48 -0700 Subject: [PATCH] drag n drop --- packages/codemirror/codemirror.mjs | 2 + packages/codemirror/dragdrop.mjs | 142 +++++++++++++++++++++++++++++ website/src/repl/Repl.css | 7 ++ 3 files changed, 151 insertions(+) create mode 100644 packages/codemirror/dragdrop.mjs diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index e96b533ec..dda23acf0 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; +import { dragDropPlugin, dragDropStyles } from './dragdrop.mjs'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), @@ -78,6 +79,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo javascript(), sliderPlugin, widgetPlugin, + dragDropPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything syntaxHighlighting(defaultHighlightStyle), diff --git a/packages/codemirror/dragdrop.mjs b/packages/codemirror/dragdrop.mjs new file mode 100644 index 000000000..05dc3b45d --- /dev/null +++ b/packages/codemirror/dragdrop.mjs @@ -0,0 +1,142 @@ +import { ViewPlugin } from '@codemirror/view'; +import { logger } from '@strudel/core'; + +// Helper function to read file content +async function readFileContent(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => resolve(e.target.result); + reader.onerror = reject; + reader.readAsText(file); + }); +} + +// Helper function to check if file is a code file +function isCodeFile(file) { + const codeExtensions = [ + '.js', '.mjs', '.ts', '.tsx', '.jsx', + '.json', '.txt', '.md', '.tidal', '.strudel', + '.html', '.css', '.scss', '.yaml', '.yml', + '.xml', '.csv', '.log', '.ini', '.conf' + ]; + const fileName = file.name.toLowerCase(); + return codeExtensions.some(ext => fileName.endsWith(ext)) || file.type.startsWith('text/'); +} + +// Create drag and drop extension +export const dragDropPlugin = ViewPlugin.fromClass( + class { + constructor(view) { + this.view = view; + this.handleDrop = this.handleDrop.bind(this); + this.handleDragOver = this.handleDragOver.bind(this); + this.handleDragEnter = this.handleDragEnter.bind(this); + this.handleDragLeave = this.handleDragLeave.bind(this); + + // Add event listeners + view.dom.addEventListener('drop', this.handleDrop); + view.dom.addEventListener('dragover', this.handleDragOver); + view.dom.addEventListener('dragenter', this.handleDragEnter); + view.dom.addEventListener('dragleave', this.handleDragLeave); + } + + handleDragOver(e) { + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = 'copy'; + } + + handleDragEnter(e) { + e.preventDefault(); + e.stopPropagation(); + this.view.dom.classList.add('cm-drag-over'); + } + + handleDragLeave(e) { + e.preventDefault(); + e.stopPropagation(); + // Only remove the class if we're leaving the editor entirely + if (!this.view.dom.contains(e.relatedTarget)) { + this.view.dom.classList.remove('cm-drag-over'); + } + } + + async handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + this.view.dom.classList.remove('cm-drag-over'); + + const files = Array.from(e.dataTransfer.files); + + // Filter for code files only + const codeFiles = files.filter(isCodeFile); + + if (codeFiles.length === 0) { + logger('No code files were dropped. Please drop text-based files.', 'warning'); + return; + } + + try { + // Read all files + const fileContents = await Promise.all( + codeFiles.map(async (file) => { + const content = await readFileContent(file); + return `// File: ${file.name}\n${content}`; + }) + ); + + // Combine content + const newContent = fileContents.join('\n\n'); + + // Replace entire editor contents + this.view.dispatch({ + changes: { from: 0, to: this.view.state.doc.length, insert: newContent }, + selection: { anchor: newContent.length } + }); + + // Focus the editor + this.view.focus(); + + // Show success message + const fileNames = codeFiles.map(f => f.name).join(', '); + logger(`Successfully loaded ${codeFiles.length} file(s): ${fileNames}`, 'highlight'); + + // Dispatch custom event for external notification handling + this.view.dom.dispatchEvent(new CustomEvent('files-dropped', { + detail: { + success: true, + fileCount: codeFiles.length, + fileNames: fileNames + } + })); + } catch (error) { + console.error('Error reading dropped files:', error); + logger(`Error loading files: ${error.message}`, 'error'); + + // Dispatch error event + this.view.dom.dispatchEvent(new CustomEvent('files-dropped', { + detail: { + success: false, + error: error.message + } + })); + } + } + + destroy() { + this.view.dom.removeEventListener('drop', this.handleDrop); + this.view.dom.removeEventListener('dragover', this.handleDragOver); + this.view.dom.removeEventListener('dragenter', this.handleDragEnter); + this.view.dom.removeEventListener('dragleave', this.handleDragLeave); + } + } +); + +// CSS for drag over effect +export const dragDropStyles = ` + .cm-editor.cm-drag-over { + outline: 2px dashed #4CAF50; + outline-offset: -2px; + background-color: rgba(76, 175, 80, 0.05); + } +`; \ No newline at end of file diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 3e13ff5a2..78740551d 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -69,3 +69,10 @@ text-decoration: underline 0.18rem; text-underline-offset: 0.22rem; } + +/* Drag and drop styles */ +#code .cm-editor.cm-drag-over { + outline: 2px dashed #4CAF50; + outline-offset: -2px; + background-color: rgba(76, 175, 80, 0.05) !important; +}