Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluto ux process file drop #707

Merged
merged 33 commits into from Dec 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
70522da
Add Support for file drag & drop
Nov 22, 2020
05ad525
Interoperability with Drop Ruler
Nov 22, 2020
8a48f6b
Save file and get path in the front-end
Nov 23, 2020
0fb00ea
Working POC
Nov 23, 2020
b5f5ea0
Add 'raw' before path string literals
Nov 24, 2020
7ca9d56
Move code templates to Julia
Nov 24, 2020
773eda8
Change Image sample to use Images
Nov 24, 2020
88820f9
Use TableIOInterface for Files -> Table conversions
Nov 25, 2020
5fb007c
Move drop handler out of Cell.js
Nov 25, 2020
e708577
Update the code for images & text to use let block
Nov 25, 2020
43e8fd1
Make file transfer !!!25%!!! FASTER dropping base64 usage
Nov 25, 2020
e2eb8fb
Use correct is_extension_supported
Nov 26, 2020
7f21590
Handle non-empty cells
Nov 27, 2020
21941bd
Rename hook, var for debounce ms
Nov 29, 2020
54a5f0f
Prevent error message while saving
Nov 30, 2020
7c64753
Make JavaScripty names more Plutonic
Nov 30, 2020
4da1b55
update the handling of Juia files
Nov 30, 2020
de9c46c
Move assets folder along with notebook
Dec 3, 2020
2c286b3
Don't run file event when moving cells
Dec 5, 2020
218f21f
Save files with the same name with incremental numbers
Dec 5, 2020
d2706cd
make warning message less aggressive
Dec 5, 2020
81d81be
🦆 Fix expression equality again
fonsp Dec 9, 2020
fdf8bcb
Revert "🦆 Fix expression equality again"
fonsp Dec 9, 2020
ad61515
Fix generated path code by lungben
Dec 11, 2020
dbd4f3b
Add statistics
Dec 11, 2020
e62adb5
Merge branch 'master' of github.com:fonsp/Pluto.jl into PlutoUX-Proce…
Dec 22, 2020
62aaee4
Benjamin's compat suggestion
Dec 22, 2020
15b3dfd
Merge branch 'master' of github.com:fonsp/Pluto.jl into PlutoUX-Proce…
Dec 30, 2020
e2527a3
Use the latest dralbase
Dec 30, 2020
20c0c8c
fix 1 bug, handle drops on body
Dec 30, 2020
4c5e9e7
fix typo
Dec 30, 2020
c0d5e0b
Fix frontend tests thanks)
Dec 30, 2020
7c69e92
don't change whitespace!
Dec 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Expand Up @@ -17,6 +17,7 @@ MsgPack = "99f44e22-a591-53d1-9472-aa23ef4bd671"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
TableIOInterface = "d1efa939-5518-4425-949f-ab857e148477"
fonsp marked this conversation as resolved.
Show resolved Hide resolved
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

Expand All @@ -26,6 +27,7 @@ HTTP = "0.8.18,0.9"
MsgPack = "1.1"
Tables = "1"
julia = "^1.0.4"
TableIOInterface = "0.1"

[extras]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Expand Down
1 change: 1 addition & 0 deletions frontend/common/Feedback.js
Expand Up @@ -5,6 +5,7 @@ export const create_counter_statistics = () => {
numEvals: 0, // integer
numRuns: 0, // integer
numBondSets: 0, // integer
numFileDrops: 0, // integer
}
}

Expand Down
11 changes: 10 additions & 1 deletion frontend/components/Cell.js
@@ -1,9 +1,10 @@
import { html, useState, useEffect, useLayoutEffect, useRef, useContext } from "../imports/Preact.js"
import { html, useState, useEffect, useMemo, useRef, useContext } from "../imports/Preact.js"

import { CellOutput } from "./CellOutput.js"
import { CellInput } from "./CellInput.js"
import { RunArea, useMillisSinceTruthy } from "./RunArea.js"
import { cl } from "../common/ClassTable.js"
import { useDropHandler } from "./useDropHandler.js"
import { PlutoContext } from "../common/PlutoContext.js"

/**
Expand Down Expand Up @@ -36,6 +37,7 @@ export const Cell = ({

// cm_forced_focus is null, except when a line needs to be highlighted because it is part of a stack trace
const [cm_forced_focus, set_cm_forced_focus] = useState(null)
const { saving_file, drag_active, handler } = useDropHandler()
const localTimeRunning = 10e5 * useMillisSinceTruthy(running)
useEffect(() => {
const focusListener = (e) => {
Expand Down Expand Up @@ -65,13 +67,19 @@ export const Cell = ({

return html`
<pluto-cell
onDragOver=${handler}
onDrop=${handler}
onDragEnter=${handler}
onDragLeave=${handler}
class=${cl({
queued: queued,
running: running,
errored: errored,
selected: selected,
code_differs: class_code_differs,
code_folded: class_code_folded,
drop_target: drag_active,
saving_file: saving_file,
})}
id=${cell_id}
>
Expand Down Expand Up @@ -110,6 +118,7 @@ export const Cell = ({
focus_after_creation=${focus_after_creation}
cm_forced_focus=${cm_forced_focus}
set_cm_forced_focus=${set_cm_forced_focus}
on_drag_drop_events=${handler}
on_submit=${() => {
pluto_actions.change_remote_cell(cell_id)
}}
Expand Down
20 changes: 20 additions & 0 deletions frontend/components/CellInput.js
Expand Up @@ -50,6 +50,7 @@ export const CellInput = ({
on_change,
on_update_doc_query,
on_focus_neighbor,
on_drag_drop_events,
cell_id,
notebook_id,
}) => {
Expand Down Expand Up @@ -354,6 +355,25 @@ export const CellInput = ({
}
return true
}

cm.on("dragover", (cm_, e) => {
on_drag_drop_events(e)
return true
})
cm.on("drop", (cm_, e) => {
on_drag_drop_events(e)
e.preventDefault()
return true
})
cm.on("dragenter", (cm_, e) => {
on_drag_drop_events(e)
return true
})
cm.on("dragleave", (cm_, e) => {
on_drag_drop_events(e)
return true
})

cm.on("cursorActivity", () => {
setTimeout(() => {
if (!cm.hasFocus()) return
Expand Down
7 changes: 7 additions & 0 deletions frontend/components/DropRuler.js
Expand Up @@ -50,17 +50,20 @@ export class DropRuler extends Component {
}
})
document.addEventListener("dragenter", (e) => {
if (e.dataTransfer.types[0] !== "text/pluto-cell") return
if (!this.state.drag_target) this.precompute_cell_edges()
this.lastenter = e.target
this.setState({ drag_target: true })
})
document.addEventListener("dragleave", (e) => {
if (e.dataTransfer.types[0] !== "text/pluto-cell") return
if (e.target === this.lastenter) {
this.setState({ drag_target: false })
}
})
document.addEventListener("dragover", (e) => {
// Called continuously during drag
if (e.dataTransfer.types[0] !== "text/pluto-cell") return
this.mouse_position = e

this.setState({
Expand All @@ -78,6 +81,10 @@ export class DropRuler extends Component {
})
document.addEventListener("drop", (e) => {
// Guaranteed to fire before the 'dragend' event
// Ignore files
if (e.dataTransfer.types[0] !== "text/pluto-cell") {
return
}
this.setState({
drag_target: false,
})
Expand Down
51 changes: 41 additions & 10 deletions frontend/components/Editor.js
Expand Up @@ -19,6 +19,7 @@ import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js"
import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js"
import { handle_log } from "../common/Logging.js"
import { PlutoContext, PlutoBondsContext } from "../common/PlutoContext.js"
import { useDropHandler } from "./useDropHandler.js"

const default_path = "..."
const DEBUG_DIFFING = false
Expand Down Expand Up @@ -60,6 +61,23 @@ function deserialize_cells(serialized_cells) {
return segments.map((s) => s.trim()).filter((s) => s !== "")
}

const Main = ({ children }) => {
const { handler } = useDropHandler()
useEffect(() => {
document.body.addEventListener("drop", handler)
document.body.addEventListener("dragover", handler)
document.body.addEventListener("dragenter", handler)
document.body.addEventListener("dragleave", handler)
return () => {
document.body.removeEventListener("drop", handler)
document.body.removeEventListener("dragover", handler)
document.body.removeEventListener("dragenter", handler)
document.body.removeEventListener("dragleave", handler)
}
})
return html`<main>${children}</main>`
}

/**
* @typedef CellInputData
* @type {{
Expand Down Expand Up @@ -141,14 +159,15 @@ export class Editor extends Component {
send: (...args) => this.client.send(...args),
update_notebook: (...args) => this.update_notebook(...args),
set_doc_query: (query) => this.setState({ desired_doc_query: query }),
set_local_cell: (cell_id, new_val) => {
this.setState(
set_local_cell: (cell_id, new_val, callback) => {
return this.setState(
immer((state) => {
state.cell_inputs_local[cell_id] = {
code: new_val,
}
state.selected_cells = []
})
}),
callback
)
},
focus_on_neighbor: (cell_id, delta, line = delta === -1 ? Infinity : -1, ch) => {
Expand Down Expand Up @@ -286,24 +305,24 @@ export class Editor extends Component {
notebook.cell_order = [...before, ...cell_ids, ...after]
})
},
add_remote_cell_at: async (index) => {
add_remote_cell_at: async (index, code = "") => {
let id = uuidv4()
this.setState({ last_created_cell: id })
await update_notebook((notebook) => {
notebook.cell_inputs[id] = {
cell_id: id,
code: "",
code,
code_folded: false,
}
notebook.cell_order = [...notebook.cell_order.slice(0, index), id, ...notebook.cell_order.slice(index, Infinity)]
})
await this.client.send("run_multiple_cells", { cells: [id] }, { notebook_id: this.state.notebook.notebook_id })
return id
},
add_remote_cell: async (cell_id, before_or_after) => {
add_remote_cell: async (cell_id, before_or_after, code) => {
const index = this.state.notebook.cell_order.indexOf(cell_id)
const delta = before_or_after == "before" ? 0 : 1

await this.actions.add_remote_cell_at(index + delta)
return await this.actions.add_remote_cell_at(index + delta, code)
},
confirm_delete_multiple: async (verb, cell_ids) => {
if (cell_ids.length <= 1 || confirm(`${verb} ${cell_ids.length} cells?`)) {
Expand Down Expand Up @@ -397,6 +416,18 @@ export class Editor extends Component {
false
)
},
write_file: (cell_id, { file, name, type }) => {
this.counter_statistics.numFileDrops++
return this.client.send(
"write_file",
{ file, name, type, path: this.state.notebook.path },
{
notebook_id: this.state.notebook.notebook_id,
cell_id: cell_id,
},
true
)
},
}

// these are update message that are _not_ a response to a `send(*, *, {create_promise: true})`
Expand Down Expand Up @@ -777,7 +808,7 @@ export class Editor extends Component {
</button>
</nav>
</header>
<main>
<${Main}>
<preamble>
<button
onClick=${() => {
Expand Down Expand Up @@ -819,7 +850,7 @@ export class Editor extends Component {
}
}}
/>
</main>
</${Main}>
<${LiveDocs}
desired_doc_query=${this.state.desired_doc_query}
on_update_doc_query=${this.actions.set_doc_query}
Expand Down
1 change: 1 addition & 0 deletions frontend/components/Notebook.js
Expand Up @@ -2,6 +2,7 @@ import { PlutoContext } from "../common/PlutoContext.js"
import { html, useContext, useEffect, useMemo, useState } from "../imports/Preact.js"

import { Cell } from "./Cell.js"
import { useDropHandler } from "./useDropHandler.js"

let CellMemo = ({
cell_input,
Expand Down
90 changes: 90 additions & 0 deletions frontend/components/useDropHandler.js
@@ -0,0 +1,90 @@
import { PlutoContext } from "../common/PlutoContext.js"
import { useState, useMemo, useContext } from "../imports/Preact.js"

const MAGIC_TIMEOUT = 500
const DEBOUNCE_MAGIC_MS = 250

const prepareFile = (file) =>
new Promise((resolve, reject) => {
const { name, type } = file
const fr = new FileReader()
fr.onerror = () => reject("Failed to read file!")
fr.onloadstart = () => {}
fr.onprogress = ({ loaded, total }) => {}
fr.onload = () => {}
fr.onloadend = ({ target: { result } }) => resolve({ file: result, name, type })
fr.readAsArrayBuffer(file)
})

export const useDropHandler = () => {
let pluto_actions = useContext(PlutoContext)
const [saving_file, set_saving_file] = useState(false)
const [drag_active, set_drag_active_fast] = useState(false)
const set_drag_active = useMemo(() => _.debounce(set_drag_active_fast, DEBOUNCE_MAGIC_MS), [set_drag_active_fast])

const handler = useMemo(() => {
const uploadAndCreateCodeTemplate = async (file, drop_cell_id) => {
if (!(file instanceof File)) return " # File can't be read"
set_saving_file(true)
const {
message: { success, code },
} = await prepareFile(file).then(
(preparedObj) => {
return pluto_actions.write_file(drop_cell_id, preparedObj)
},
() => alert("Pluto can't save this file 😥")
)
set_saving_file(false)
set_drag_active_fast(false)
if (!success) {
alert("Pluto can't save this file 😥")
return "# File save failed"
}
if (code) return code
alert("Pluto doesn't know what to do with this file 😥. Feel that's wrong? Open an issue!")
return ""
}
return (ev) => {
ev.stopPropagation()
// dataTransfer is in Protected Mode here. see type, let Pluto DropRuler handle it.
if (ev.dataTransfer.types[0] === "text/pluto-cell") return
switch (ev.type) {
case "cmdrop":
case "drop":
ev.preventDefault() // don't file open
const cell_element = ev.path.find((el) => el.tagName === "PLUTO-CELL")
const drop_cell_id = cell_element?.id || document.querySelector("pluto-cell:last-child")?.id
const drop_cell_value = cell_element?.querySelector(".CodeMirror")?.CodeMirror?.getValue()
const is_empty = drop_cell_value?.length === 0 && !cell_element?.classList?.contains("code_folded")
set_drag_active(false)
if (!ev.dataTransfer.files.length) {
return
}
uploadAndCreateCodeTemplate(ev.dataTransfer.files[0], drop_cell_id).then((code) => {
if (code) {
if (!is_empty) {
pluto_actions.add_remote_cell(drop_cell_id, "after", code)
} else {
pluto_actions.set_local_cell(drop_cell_id, code, () => pluto_actions.set_and_run_multiple([drop_cell_id]))
}
}
})
break
case "dragover":
ev.preventDefault()
ev.dataTransfer.dropEffect = "copy"
set_drag_active(true)
setTimeout(() => set_drag_active(false), MAGIC_TIMEOUT)
break
case "dragenter":
set_drag_active_fast(true)
break
case "dragleave":
set_drag_active(false)
break
default:
}
}
}, [set_drag_active, set_drag_active_fast, set_saving_file, pluto_actions])
return { saving_file, drag_active, handler }
}
31 changes: 31 additions & 0 deletions frontend/editor.css
Expand Up @@ -990,6 +990,37 @@ pluto-cell.running.errored > pluto-trafficlight::after {
background-size: 4px var(--patternHeight); /* 16 * sqrt(2) */
}

pluto-cell.drop_target {
position: relative;
}

pluto-cell.drop_target pluto-input::after {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
color: black;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
background-color: rgba(255, 255, 255, 0.75);
content: "+ Drop file here";
}

pluto-cell.drop_target.saving_file pluto-input::after {
content: "The 💾 File you dropped is being saved... ";
color: white;
background-color: rgba(3, 92, 151, 0.75);
}

pluto-cell.drop_target.drop_invalid pluto-input::after {
background-color: #ece5e5bf;
color: rgba(0, 0, 0, 0.85);
content: "Try dropping your file in an empty cell!";
}

/* Define --patternHeight for this keyframes animation to work! */
@keyframes scrollbackground {
0% {
Expand Down