diff --git a/examples/reference/widgets/FileInput.ipynb b/examples/reference/widgets/FileInput.ipynb index d3093a9564..43c360cb10 100644 --- a/examples/reference/widgets/FileInput.ipynb +++ b/examples/reference/widgets/FileInput.ipynb @@ -26,6 +26,7 @@ "##### Core\n", "\n", "* **``accept``** (str): A list of file input filters that restrict what files the user can pick from\n", + "* **``directory``** (str): If directories is upload instead of files\n", "* **``filename``** (str/list): The filename(s) of the uploaded file(s)\n", "* **``mime_type``** (str/list): The mime type(s) of the uploaded file(s)\n", "* **``multiple``** (boolean): Whether to allow uploading multiple files\n", @@ -113,7 +114,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To allow uploading multiple files we can also set `multiple=True`:" + "To allow uploading multiple files we can also set `multiple=True` or if you want to upload a whole directory `directory=True`:" ] }, { @@ -131,7 +132,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When uploading one or more files the `filename`, `mime_type` and `value` parameters will now be lists. " + "When uploading one or more files the `filename`, `mime_type` and `value` parameters will now be lists. \n", + "\n", + "You can also clear the file input with the `.clear()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "file_input.clear()" ] }, { diff --git a/panel/custom.py b/panel/custom.py index 78c30f5913..08c761ec63 100644 --- a/panel/custom.py +++ b/panel/custom.py @@ -7,7 +7,6 @@ import textwrap from collections import defaultdict -from functools import partial from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, Literal, Mapping, Optional, ) @@ -18,7 +17,6 @@ from .config import config from .io.datamodel import construct_data_model -from .io.notebook import push from .io.state import state from .models import ( AnyWidgetComponent as _BkAnyWidgetComponent, @@ -406,15 +404,4 @@ def send(self, msg: dict): --------- msg: dict """ - for ref, (model, _) in self._models.items(): - if ref not in state._views or ref in state._fake_roots: - continue - event = ESMEvent(model=model, data=msg) - viewable, root, doc, comm = state._views[ref] - if comm or state._unblocked(doc) or not doc.session_context: - doc.callbacks.send_event(event) - if comm and 'embedded' not in root.tags: - push(doc, comm) - else: - cb = partial(doc.callbacks.send_event, event) - doc.add_next_tick_callback(cb) + self._send_event(ESMEvent, data=msg) diff --git a/panel/reactive.py b/panel/reactive.py index c462533cf4..361bc470d9 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -59,7 +59,7 @@ from bokeh.document import Document from bokeh.events import Event - from bokeh.model import Model + from bokeh.model import Model, ModelEvent from bokeh.models.sources import DataDict, Patches from pyviz_comms import Comm @@ -874,6 +874,33 @@ def jslink( return Link(self, target, properties=links, code=code, args=args, bidirectional=bidirectional) + def _send_event(self, Event: ModelEvent, **event_kwargs): + """ + Send an event to the frontend + + Arguments + ---------- + Event: Bokeh.Event + The event to send to the frontend + event_kwargs: dict + Additional keyword arguments to pass to the event + This will create the following event: + Event(model=model, **event_kwargs) + + """ + for ref, (model, _) in self._models.items(): + if ref not in state._views or ref in state._fake_roots: + continue + event = Event(model=model, **event_kwargs) + _viewable, root, doc, comm = state._views[ref] + if comm or state._unblocked(doc) or not doc.session_context: + doc.callbacks.send_event(event) + if comm and 'embedded' not in root.tags: + push(doc, comm) + else: + cb = partial(doc.callbacks.send_event, event) + doc.add_next_tick_callback(cb) + TData = Union['pd.DataFrame', 'DataDict'] diff --git a/panel/widgets/input.py b/panel/widgets/input.py index ad9e9d55b8..bed0f658b0 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -24,6 +24,7 @@ PasswordInput as _BkPasswordInput, Spinner as _BkSpinner, Switch as _BkSwitch, ) +from bokeh.models.widgets.inputs import ClearInput from pyviz_comms import JupyterComm from ..config import config @@ -203,6 +204,20 @@ class FileInput(Widget): An HTML string describing the function of this component rendered as a tooltip icon.""") + directory = param.Boolean(default=False, doc=""" + Whether to allow selection of directories instead of files. + The filename will be relative paths to the uploaded directory. + + .. note:: + When a directory is uploaded it will give add a confirmation pop up. + The confirmation pop up cannot be disabled, as this is a security feature + in the browser. + + .. note:: + The `accept` parameter only works with file extension. + When using `accept` with `directory`, the number of files + reported will be the total amount of files, not the filtered.""") + filename = param.ClassSelector( default=None, class_=(str, list), is_instance=True, doc=""" Name of the uploaded file(s).""") @@ -246,9 +261,13 @@ def _process_property_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg: if isinstance(msg['value'], str): - msg['value'] = b64decode(msg['value']) + msg['value'] = b64decode(msg['value']) if msg['value'] else None else: msg['value'] = [b64decode(content) for content in msg['value']] + if 'filename' in msg and len(msg['filename']) == 0: + msg['filename'] = None + if 'mime_type' in msg and len(msg['mime_type']) == 0: + msg['mime_type'] = None return msg def save(self, filename): @@ -285,6 +304,12 @@ def save(self, filename): else: fn.write(val) + def clear(self): + """ + Clear the file(s) in the FileInput widget + """ + self._send_event(ClearInput) + class FileDropper(Widget): """