Hello,
I’m currently trying to amend the FileInput widget with support for
multiple file selection.
This is prevented in the original (branch “master”) by a hardcoded
this.dialogEl.multiple = false
in bokeh/bokehjs/src/lib/models/widgets/file_input.ts.
I set-up a dedicated virtual machine with ubuntu 18.04, conda with python 3.8 according to instruction from Setting up a development environment — Bokeh 2.4.2 Documentation
The Problem:
generally speaking I modified
- bokeh/bokehjs/src/lib/models/widgets/file_input.ts
- bokeh/bokeh/models/widgets/inputs.py
to use Array/List for the properties value, mimetype, filename.
And wrote a small test case for bokeh server to check.
(The code is below)
And I can see in the browser-console that the JS-part is working as expected.
But there is no longer any callback triggered on the Python-part neither
FileInput.on_change() nor curdoc().on_change() is triggered.
I crosschecked the use of Array/List with other bokeh components (bokehjs/src/lib/models/widgets/checkbox_button_group.ts), but could not detect the error in my coding.
Can someone from the experts have a look, please?
Sources:
file_input.ts
import * as p from "core/properties"
import {Widget, WidgetView} from "models/widgets/widget"
export class FileInputView extends WidgetView {
model: FileInput
protected dialogEl: HTMLInputElement
connect_signals(): void {
super.connect_signals()
this.connect(this.model.change, () => this.render())
this.connect(this.model.properties.width.change, () => this.render())
}
render(): void {
if (this.dialogEl == null) {
this.dialogEl = document.createElement('input')
this.dialogEl.type = "file"
console.log("DEBUG: file_input.ts:")
this.dialogEl.multiple = false
this.dialogEl.onchange = (e) => this.load_file(e)
this.el.appendChild(this.dialogEl)
}
if (this.model.accept != null && this.model.accept != '')
this.dialogEl.accept = this.model.accept
if (this.model.multiple == 'multiple' || this.model.multiple == 'True'){
this.dialogEl.multiple = true
this.dialogEl.onchange = (e) => this.load_files(e)
}
this.dialogEl.style.width = `{this.model.width}px`
this.dialogEl.disabled = this.model.disabled
}
load_file(e: any): void {
const reader = new FileReader()
this.model.filename.push(e.target.files[0].name)
reader.onload = (e) => this.file(e)
reader.readAsDataURL(e.target.files[0])
}
load_files(e: any): void {
const files = e.target.files
var i : number
for (i=0; i< files.length ; i++){
//console.log(files[i].name);
this.readfile(files[i])
}
console.log(this.model.filename)
}
readfile(file: any): void {
const reader = new FileReader()
this.model.filename.push(file.name)
reader.onload = (e) => this.file(e)
reader.readAsDataURL(file)
}
file(e: any): void {
const file = e.target.result
const file_arr = file.split(",")
const content = file_arr[1]
const header = file_arr[0].split(":")[1].split(";")[0]
this.model.value.push(content)
this.model.mime_type.push(header)
//console.log(content)
}
}
export namespace FileInput {
export type Attrs = p.AttrsOf<Props>
export type Props = Widget.Props & {
value: p.Property<string[]>
mime_type: p.Property<string[]>
filename: p.Property<string[]>
accept: p.Property<string>
multiple: p.Property<string>
}
}
export interface FileInput extends FileInput.Attrs {}
export abstract class FileInput extends Widget {
properties: FileInput.Props
constructor(attrs?: Partial<FileInput.Attrs>) {
super(attrs)
}
static init_FileInput(): void {
this.prototype.default_view = FileInputView
this.define<FileInput.Props>({
value: [ p.Array, [] ],
mime_type: [ p.Array, [] ],
filename: [ p.Array, [] ],
accept: [ p.String, '' ],
multiple: [ p.String, '' ],
})
}
}
inputs.py (only FileInput API)
class FileInput(Widget):
''' Present a file-chooser dialog to users and return the contents of a
selected file.
'''
value = List(String( help="""
A base64-encoded string of the contents of the selected file.
"""), default=[])
mime_type = List(String( default="", readonly=True, help="""
The mime type of the selected file.
"""))
filename = List(String( help="""
The filename of the selected file.
.. note::
The full file path is not included since browsers will not provide
access to that information for security reasons.
"""),default=[])
accept = String(default="", help="""
Comma-separated list of standard HTML file input filters that restrict what
files the user can pick from. Values can be:
`<file extension>`:
Specific file extension(s) (e.g: .gif, .jpg, .png, .doc) are pickable
`audio/*`:
all sound files are pickable
`video/*`:
all video files are pickable
`image/*`:
all image files are pickable
`<media type>`:
A valid `IANA Media Type`_, with no parameters.
.. _IANA Media Type: https://www.iana.org/assignments/media-types/media-types.xhtml
""")
multiple = String(default="False", help="""
set to "multiple" or "True" if selection of more than one file at a time
should be supported.
""")
(Testcase) test1.py:
from bokeh.io import curdoc
from bokeh.models.widgets import FileInput
from bokeh.layouts import column
#output_file("file_input.html")
def showfiles(attr,old, new):
print("showfiles()")
print(new)
print(attr)
print(len(new))
def docchange(event):
print("document changed")
print(event)
#file_input = FileInput() # This is for original bokeh 1.4
file_input = FileInput(multiple='True') # this is for the modified FileInput widget
file_input.on_change('filename', showfiles)
curdoc().add_root(column(file_input))
curdoc().on_change(docchange)