Skip to content

Commit d60310a

Browse files
committed
WIP: FileUpload Widget
Resolves: issues#1542
1 parent e27c7a8 commit d60310a

File tree

6 files changed

+260
-61
lines changed

6 files changed

+260
-61
lines changed

ipywidgets/widgets/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@
2525
from .widget_layout import Layout
2626
from .widget_media import Image, Video, Audio
2727
from .widget_style import Style
28+
from .widget_upload import FileUpload
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
from unittest import TestCase
5+
6+
from traitlets import TraitError
7+
8+
from ipywidgets import FileUpload
9+
10+
11+
class TestFileUpload(TestCase):
12+
13+
def test_construction(self):
14+
FileUpload()
15+
# Defaults
16+
assert uploader.accept == ''
17+
assert uploader.multiple == False
18+
19+
20+
def test_construction_accept(self):
21+
uploader = FileUpload(accept='.txt)
22+
assert uploader.accept == '.txt'
23+
24+
def test_construction_multiple(self):
25+
uploader = FileUpload(accept='.txt)
26+
assert uploader.multiple == True

ipywidgets/widgets/widget_upload.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
from .widget_core import CoreWidget
5+
from .domwidget import DOMWidget
6+
from .widget import register
7+
from traitlets import Unicode, Bool, List, observe
8+
import base64
9+
import copy
10+
from datetime import datetime
11+
12+
@register()
13+
class FileUpload(DOMWidget, CoreWidget):
14+
"""Upload files from a browser to python
15+
16+
Parameters
17+
----------
18+
values : List
19+
List of file objects
20+
accept : string
21+
Limit accepted file types. See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept
22+
multiple : bool
23+
Allow for multiple files to be uploaded
24+
"""
25+
_view_name = Unicode('UploadView').tag(sync=True)
26+
_model_name = Unicode('UploadModel').tag(sync=True)
27+
28+
disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
29+
values = List().tag(sync=True)
30+
_values_base64 = List(help='File content, base64 encoded.').tag(trait=Unicode()).tag(sync=True)
31+
accept = Unicode(help='Type of files the input accepts. None for all. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept').tag(sync=True)
32+
multiple = Bool(help='If true, allow for multiple input files. Else only accept one').tag(sync=True)
33+
34+
35+
def __init__(self, multiple=False, accept='', **kwargs):
36+
self.accept = accept
37+
self.multiple = multiple
38+
super(FileUpload, self).__init__(**kwargs)
39+
40+
41+
@observe('_values_base64')
42+
def _on_files_changed(self, *args):
43+
"""
44+
Parse data from JS and put into python friendly form
45+
"""
46+
values = []
47+
for v in self._values_base64:
48+
f = copy.copy(v)
49+
if 'contents' in f:
50+
f['contents'] = base64.b64decode(f['contents'].split(',', 1)[1])
51+
if 'lastModified' in f:
52+
f['lastModified'] = datetime.fromtimestamp(f['lastModified']/1000.0)
53+
values.append(f)
54+
self.values = values

packages/controls/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './widget_selection';
1919
export * from './widget_selectioncontainer';
2020
export * from './widget_string';
2121
export * from './widget_description';
22+
export * from './widget_upload';
2223

2324
export
2425
const version = (require('../package.json') as any).version;
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import {
5+
DOMWidgetView
6+
} from '@jupyter-widgets/base';
7+
8+
import {
9+
CoreDOMWidgetModel
10+
} from './widget_core';
11+
12+
import * as _ from 'underscore';
13+
14+
export
15+
class UploadModel extends CoreDOMWidgetModel {
16+
defaults() {
17+
return _.extend(super.defaults(), {
18+
_model_name: 'UploadModel',
19+
_view_name: 'UploadView',
20+
values: [],
21+
_values_base64: [],
22+
accept: '',
23+
multiple: false
24+
});
25+
}
26+
}
27+
28+
export
29+
class UploadView extends DOMWidgetView {
30+
render() {
31+
/**
32+
* Called when view is rendered.
33+
*/
34+
super.render();
35+
this.pWidget.addClass('jupyter-widgets');
36+
this.el.classList.add('widget-inline-hbox');
37+
this.pWidget.addClass('widget-upload');
38+
39+
this.upload_container = document.createElement('input');
40+
this.upload_container.setAttribute('type', 'file');
41+
this.handleUploadChanged = this.handleUploadChanged.bind(this);
42+
this.upload_container.onchange = this.handleUploadChanged;
43+
44+
this.el.appendChild(this.upload_container);
45+
46+
47+
this.listenTo(this.model, 'change:values', this._update_values);
48+
this._update_values();
49+
this.update(); // Set defaults.
50+
}
51+
52+
_update_values() {
53+
// Only allow for value to clear. This is a rule enforced by browsers
54+
const values = this.model.get('values');
55+
if (values.length === 0) {
56+
this.upload_container.value = '';
57+
}
58+
}
59+
60+
update(options?) {
61+
/**
62+
* Update the contents of this view
63+
*
64+
* Called when the model is changed. The model may have been
65+
* changed by another view or by a state update from the back-end.
66+
*/
67+
if (options === undefined || options.updated_view !== this) {
68+
this.upload_container.disabled = this.model.get('disabled');
69+
this.upload_container.setAttribute('accept', this.model.get('accept'));
70+
if (this.model.get('multiple')) {
71+
this.upload_container.setAttribute('multiple', 'true');
72+
} else {
73+
this.upload_container.removeAttribute('multiple');
74+
}
75+
}
76+
return super.update(options);
77+
}
78+
79+
handleUploadChanged(event) {
80+
const that = this;
81+
const {files} = event.target;
82+
if (!files || files.length === 0) {
83+
that.model.set('_values_base64', []);
84+
that.touch();
85+
return;
86+
}
87+
88+
const fileContentsPromises = [];
89+
for (let file of files) { // files it NOT an array
90+
fileContentsPromises.push(new Promise((resolve, reject) => {
91+
const fileReader = new FileReader();
92+
fileReader.onload = function fileReaderOnload() {
93+
resolve({
94+
name: file.name,
95+
contents: fileReader.result,
96+
type: file.type,
97+
lastModified: file.lastModified
98+
});
99+
};
100+
fileReader.readAsDataURL(file);
101+
}));
102+
}
103+
Promise.all(fileContentsPromises)
104+
.then((contents) => {
105+
that.model.set('_values_base64', contents);
106+
that.touch();
107+
})
108+
.catch((err) => {
109+
console.error('FileUploadView Error loading files: %o', err);
110+
// FIXME: Set state?
111+
});
112+
// FIXME: Add an icon that shows it loading?
113+
}
114+
115+
upload_container: HTMLInputElement;
116+
model: UploadModel;
117+
}

0 commit comments

Comments
 (0)