Using scripts inside a HTML pane (dropzone.js)

I am trying to use dropzone.js (https://dropzonejs.com) in a panel app, but I cannot get the script to load either in the jupyter notebook or using pn.serve.

The dropzone element will show fine in a HTML pane and the css is loading, but the script is not working.
I have tried looking into the solution discussed at Adding local .js files to my app, but to no avail.

Is there any guide how to use external or local js in panel apps anywhere? Or does anyone know what I am doing wrong? (panel.holoviz.org/user_guide/Deploy_and_Export.html mentions it very briefly)

The html works fine if saved as foo.html and opened in firefox/chrome

My minimal example:

import panel as pn
pn.extension()
html = """
<body>
    <script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>
    <link rel="stylesheet" href="https://rawgit.com/enyo/dropzone/master/dist/dropzone.css">
    
    <p> Text </p>

    <!-- Change /upload-target to your upload address -->
    <form action="/upload" class="dropzone dz-clickable" style="width:500px;text-align: center;">
        <div class="dz-default dz-message">
            <span>text</span>
        </div>
    </form>


    <input type="file" multiple="multiple" class="dz-hidden-input" style="visibility: hidden; position: absolute; top: 0px; left: 0px; height: 0px; width: 0px;">
    
</body>
"""
pn_html = pn.pane.HTML(html)

server_i = pn.serve(pn_html, websocket_origin='*', port=9000, verbose=True, websocket_max_message_size = 200000000)
1 Like

Hi @fogo5.

Welcome to the community. A few questions to understand your question.

How would you expect the example you have provided to work? What functionality? How would it look (screenshot please).

Would you expect to be able to use the uploaded files in your Panel app? How?

Additional Context

There is already a Fileupload widget https://panel.holoviz.org/reference/widgets/FileInput.html. It works but could work better.

There is a discussion on how the existing FileInput can be styled and improved here What should a better FileInput look like?.

Additional comment

I think dropzone.js is the best fileupload out there and would like it to be a native Panel widget. The starting point for that is the implementation of the existing FileInput and knowledge on how to create Bokeh/ Panel extensions https://awesome-panel.readthedocs.io/en/latest/guides/awesome-panel-extensions-guide/bokeh-extensions.html

Hi Marc.

I completely agree. I actually found it via your comment in the FAST github issue (https://github.com/microsoft/fast/issues/3859).
I was looking to try your awesome-panel extension WebComponent framework to implement it. This is a step on the way.

This is what it looks like when served with pn.serve inside a HTML pane.
When clicking the frame an upload dialog should be initiated and drag an drop should also work. Neither works. The element is completely passive.

This is what it looks like when loaded as a foo.html file
This works. The drag and drop works. The upload dialog on click works and a neat animation while loading the file works. It is the exact same html body as the one in the HTML pane in the example above.

The actual upload feature needs to be implemented in some other code. This issue is just about getting the dropzone script to execute as expected in the panel context.

That said. Here is how I would do the server part.
Example serverside code to handle upload

from tornado import web

class UploadHandler(web.RequestHandler):
    def post(self):
        for form_data_name in self.request.files:
            for file in self.request.files[form_data_name]:
                save = open('uploads/' + file['filename'], 'wb')
                save.write(file['body'])
        self.finish("files are uploaded")
        
server_i._tornado.add_handlers(r'.*', [
                (r"/upload", UploadHandler)
])
1 Like

I have also tried including the entire dropzone.js script directly in the html pane and tried serving it via a static folder using static_dirs after upgrading to panel 0.10.0a24. Like: pn.serve(… , static_dirs={‘js’: ‘./js’}).

I don’t think the problem is that the script is not loading but rather that it is not executed in the panel context somehow. Using the firefox debugger I can see that the dropzone.js is present in both the pn.serve and static html-file case. But it is only executing properly for the static html-file.

I just tried using the html from the panel app in a seperate file

File: discourse_1255_dropzonejs.html

<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>
<link rel="stylesheet" href="https://rawgit.com/enyo/dropzone/master/dist/dropzone.css">

<p> Text </p>

<!-- Change /upload-target to your upload address -->
<form action="/upload" class="dropzone dz-clickable" style="width:500px;text-align: center;">
    <div class="dz-default dz-message">
        <span>text</span>
    </div>
</form>


<input type="file" multiple="multiple" class="dz-hidden-input" style="visibility: hidden; position: absolute; top: 0px; left: 0px; height: 0px; width: 0px;">

And if I run python -m http.server 8000 and navigate to http://localhost:8000/scripts/discourse_1255_dropzonejs.html it also looks like

UPDATED: I misunderstood the question

After having investigated the .css a bit I can see it’s actually the included css file that is the problem. If you want it to look like on the dropzone site you will need to include an additional css file

<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>
<link rel="stylesheet" href="https://www.dropzonejs.com/css/dropzone.css?v=1595510599">
<link rel="stylesheet" href="https://www.dropzonejs.com/css/style.css?v=1595510599">
</head><body>
<p> Text </p>

<!-- Change /upload-target to your upload address -->
<form action="/upload" class="dropzone needsclick dz-clickable" id="demo-upload">

    <div class="dz-message needsclick">
      <button type="button" class="dz-button">Drop files here or click to upload.</button><br>
      <span class="note needsclick">(This is just a demo dropzone. Selected files are <strong>not</strong> actually uploaded.)</span>
    </div>

  </form>
</body></html>

UPDATED: I misunderstood the question

This works

import panel as pn
# pn.config.js_files["dropzonejs"]="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"
# pn.config.css_files.append("https://rawgit.com/enyo/dropzone/master/dist/dropzone.css")
pn.extension()
html = """
    <script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>
    <link rel="stylesheet" href="https://www.dropzonejs.com/css/dropzone.css?v=1595510599">
    <link rel="stylesheet" href="https://www.dropzonejs.com/css/style.css?v=1595510599">
    <p> Text </p>

    <!-- Change /upload-target to your upload address -->
    <form action="/upload" class="dropzone needsclick dz-clickable" id="demo-upload">

    <div class="dz-message needsclick">
      <button type="button" class="dz-button">Drop files here or click to upload.</button><br>
      <span class="note needsclick">(This is just a demo dropzone. Selected files are <strong>not</strong> actually uploaded.)</span>
    </div>
"""
pn.pane.HTML(html).servable()

UPDATED: I misunderstood the question

But I guess I would be using

import panel as pn
pn.config.js_files["dropzonejs"]="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"
pn.config.css_files.append("https://rawgit.com/enyo/dropzone/master/dist/dropzone.css")
pn.config.css_files.append("https://www.dropzonejs.com/css/style.css?v=1595510599")
pn.extension()
html = """
    <p> Text </p>

    <!-- Change /upload-target to your upload address -->
    <form action="/upload" class="dropzone needsclick dz-clickable" id="demo-upload">

    <div class="dz-message needsclick">
      <button type="button" class="dz-button">Drop files here or click to upload.</button><br>
      <span class="note needsclick">(This is just a demo dropzone. Selected files are <strong>not</strong> actually uploaded.)</span>
    </div>
"""
pn.pane.HTML(html).servable()

Ahh. Now I understand the question/ problem.

Ok so the Panel version does not react when you drop stuff onto it.

If you take a look via CTRL+SHIFT+I you can see that the same events are not handled

I believe this is because at some point in time the dropzone.js adds event listeners to all dropzones. But because the Panel html is only available later not event listeners are added.

The solution is to either force dropzone.js to run later or to re-run later I believe. Currently I don’t know how that is done.

This works

import panel as pn
pn.config.js_files["dropzonejs"]="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"
pn.config.css_files.append("https://rawgit.com/enyo/dropzone/master/dist/dropzone.css")
pn.config.css_files.append("https://www.dropzonejs.com/css/style.css?v=1595510599")
pn.extension()
html = """
    <p> Text </p>

    <!-- Change /upload-target to your upload address -->
    <form action="/upload" class="dropzone" id="demo-upload">

    <div class="dz-message needsclick">
      <button type="button" class="dz-button">Drop files here or click to upload.</button><br>
      <span class="note needsclick">(This is just a demo dropzone. Selected files are <strong>not</strong> actually uploaded.)</span>
    </div>
    <script>var myDropzone = new Dropzone("form#demo-upload", { url: "/file/post"});</script>
"""
pn.pane.HTML(html).servable()

1 Like

Hi @fogo5

The next step after using the above would be to inspect the existing file input and see how it uploads the data. There might be an existing endpoint you can use instead of a new one?

Hi @Marc!

Thanks for all the replies! I am sorry that I did not make it more clear that the problem was about the panel version not reacting to drops/clicks. The timing of the event listeners make sense! (Super nice that you made the css work properly too)

Regarding the actual upload, I agree it could be nice to reuse or hook into the solution already in use.
Looking at the FileInput widget from bokeh, it seems to me like it reads the uploaded file as a bytestream into a javascript variable and syncs this directly to panel/bokeh via websocket. I could be wrong here. I know very little about java/typescript (yet!).
The websocket approach is fine for small files, but if you give the widget something bigger it throws an error, complaining that the websocket message is too big. This happens around 15 mb.


We can tell panel to tell tornado to allow for bigger websocket messages; pn.serve(FileInput_widget, websocket_max_message_size = 200000000). This helps, but firefox will then throw a ‘1006 websocket error’ for uploads at bigger than ~80mb and chrome starts throwing errors around 200-500mb (Uncaught TypeError: Cannot read property ‘split’ of undefined).

For testing I used:

import panel as pn
FileInput_widget = pn.widgets.FileInput()
server_i = pn.serve(FileInput_widget, 
                    websocket_origin='*', 
                    port=20000
                    websocket_max_message_size = 2000000000)
print(FileInput_widget.filename)
print(str(FileInput_widget.value)[:50])

I think that it would probably be best to implement the upload server as a separate instance. For example like the upload handler for tornado I posted above or one of the examples from dropzonejs website. That does make it a bit more cumbersome though.

1 Like

Thanks.

The question to me is how to to get the file Back into the Fileinput value and if it should? Maybe it should just get a Path to the file?

I understand. I think a path to the file is better. You might not want a 4gb upload hanging around in memory for no reason.

1 Like

And maybe as much meta data as possible like for example file size?

import panel as pn
pn.config.js_files["dropzonejs"]="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"
pn.config.css_files.append("https://rawgit.com/enyo/dropzone/master/dist/dropzone.css")
pn.config.css_files.append("https://www.dropzonejs.com/css/style.css?v=1595510599")
pn.extension()
html = """
    <p> Text </p>

    <!-- Change /upload-target to your upload address -->
    <form action="/upload" class="dropzone" id="demo-upload">

    <div class="dz-message needsclick">
      <button type="button" class="dz-button">Drop files here or click to upload.</button><br>
      <span class="note needsclick">(This is just a demo dropzone. Selected files are <strong>not</strong> actually uploaded.)</span>
    </div>
    <script>var myDropzone = new Dropzone("form#demo-upload", { url: "/file/post"});</script>
"""
pn.pane.HTML(html).servable()

This is really nice and close to what I want! I was wondering how can I read the contents after it finishes upload?

And whether this is related?
image

@ahuang11 - any luck linking the contents of your file to pythoon and/or linking it to the existing FileInput Widget?
I am also looking at some information here - a more concise example would help?