From 70b42d5577bfe79091530d4732e6352fe7e6544f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 16 Apr 2024 11:34:26 +0200 Subject: [PATCH 1/8] Move FileInput from Bokeh to Panel --- panel/models/__init__.py | 6 +- panel/models/file_input.ts | 116 +++++++++++++++++++++++++++++++++++++ panel/models/index.ts | 1 + panel/models/widgets.py | 6 +- panel/widgets/input.py | 5 +- 5 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 panel/models/file_input.ts diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 550cbf4fc1..0bafe6e3e5 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -17,7 +17,7 @@ from .state import State # noqa from .trend import TrendIndicator # noqa from .widgets import ( # noqa - Audio, Button, CheckboxButtonGroup, CustomSelect, FileDownload, Player, - Progress, RadioButtonGroup, SingleSelect, TextAreaInput, TooltipIcon, - Video, VideoStream, + Audio, Button, CheckboxButtonGroup, CustomSelect, FileDownload, FileInput, + Player, Progress, RadioButtonGroup, SingleSelect, TextAreaInput, + TooltipIcon, Video, VideoStream, ) diff --git a/panel/models/file_input.ts b/panel/models/file_input.ts new file mode 100644 index 0000000000..9d7c008334 --- /dev/null +++ b/panel/models/file_input.ts @@ -0,0 +1,116 @@ +import {InputWidget, InputWidgetView} from "@bokehjs/models/widgets/input_widget" +import type {StyleSheetLike} from "@bokehjs/core/dom" +import {input} from "@bokehjs/core/dom" +import {isString} from "@bokehjs/core/util/types" +import * as p from "@bokehjs/core/properties" +import * as inputs from "@bokehjs/styles/widgets/inputs.css" +import buttons_css from "@bokehjs/styles/buttons.css" + +export class FileInputView extends InputWidgetView { + declare model: FileInput + declare input_el: HTMLInputElement + + override stylesheets(): StyleSheetLike[] { + return [...super.stylesheets(), buttons_css] + } + + protected _render_input(): HTMLElement { + const {multiple, disabled} = this.model + + const accept = (() => { + const {accept} = this.model + return isString(accept) ? accept : accept.join(",") + })() + + return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled}) + } + + override render(): void { + super.render() + + this.input_el.addEventListener("change", async () => { + const {files} = this.input_el + if (files != null) { + await this.load_files(files) + } + }) + } + + async load_files(files: FileList): Promise { + const values: string[] = [] + const filenames: string[] = [] + const mime_types: string[] = [] + + for (const file of files) { + const data_url = await this._read_file(file) + const [, mime_type="",, value=""] = data_url.split(/[:;,]/, 4) + + values.push(value) + filenames.push(file.name) + mime_types.push(mime_type) + } + + const [value, filename, mime_type] = (() =>{ + if (this.model.multiple) { + return [values, filenames, mime_types] + } else if (files.length != 0) { + return [values[0], filenames[0], mime_types[0]] + } else { + return ["", "", ""] + } + })() + + this.model.setv({value, filename, mime_type}) + } + + protected _read_file(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + const {result} = reader + if (result != null) { + resolve(result as string) + } else { + reject(reader.error ?? new Error(`unable to read '${file.name}'`)) + } + } + reader.readAsDataURL(file) + }) + } +} + +export namespace FileInput { + export type Attrs = p.AttrsOf + export type Props = InputWidget.Props & { + value: p.Property + mime_type: p.Property + filename: p.Property + accept: p.Property + multiple: p.Property + } +} + +export interface FileInput extends FileInput.Attrs {} + +export class FileInput extends InputWidget { + declare properties: FileInput.Props + declare __view_type__: FileInputView + + constructor(attrs?: Partial) { + super(attrs) + } + + static override __module__ = "panel.models.widgets" + + static { + this.prototype.default_view = FileInputView + + this.define(({Bool, Str, List, Or}) => ({ + value: [ Or(Str, List(Str)), p.unset, {readonly: true} ], + mime_type: [ Or(Str, List(Str)), p.unset, {readonly: true} ], + filename: [ Or(Str, List(Str)), p.unset, {readonly: true} ], + accept: [ Or(Str, List(Str)), "" ], + multiple: [ Bool, false ], + })) + } +} diff --git a/panel/models/index.ts b/panel/models/index.ts index 4ecad36dfb..960ec11ce6 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -16,6 +16,7 @@ export {DeckGLPlot} from "./deckgl" export {ECharts} from "./echarts" export {Feed} from "./feed" export {FileDownload} from "./file_download" +export {FileInput} from "./file_input" export {HTML} from "./html" export {IPyWidget} from "./ipywidget" export {JSON} from "./json" diff --git a/panel/models/widgets.py b/panel/models/widgets.py index df1a99e6b3..e7c5e78827 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -10,7 +10,8 @@ from bokeh.models.ui.icons import Icon from bokeh.models.widgets import ( Button as bkButton, CheckboxButtonGroup as bkCheckboxButtonGroup, - InputWidget, RadioButtonGroup as bkRadioButtonGroup, Select, + FileInput as bkFileInput, InputWidget, + RadioButtonGroup as bkRadioButtonGroup, Select, TextAreaInput as BkTextAreaInput, Widget, ) @@ -253,3 +254,6 @@ class RadioButtonGroup(bkRadioButtonGroup): Delay (in milliseconds) to display the tooltip after the cursor has hovered over the Button, default is 500ms. """) + +class FileInput(bkFileInput): + ... diff --git a/panel/widgets/input.py b/panel/widgets/input.py index 97c777a3bb..427cd6bf5b 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -21,7 +21,7 @@ from bokeh.models.widgets import ( Checkbox as _BkCheckbox, ColorPicker as _BkColorPicker, DatePicker as _BkDatePicker, DateRangePicker as _BkDateRangePicker, - Div as _BkDiv, FileInput as _BkFileInput, NumericInput as _BkNumericInput, + Div as _BkDiv, NumericInput as _BkNumericInput, PasswordInput as _BkPasswordInput, Spinner as _BkSpinner, Switch as _BkSwitch, TextInput as _BkTextInput, ) @@ -29,7 +29,8 @@ from ..config import config from ..layout import Column, Panel from ..models import ( - DatetimePicker as _bkDatetimePicker, TextAreaInput as _bkTextAreaInput, + DatetimePicker as _bkDatetimePicker, FileInput as _BkFileInput, + TextAreaInput as _bkTextAreaInput, ) from ..util import param_reprs, try_datetime64_to_datetime from .base import CompositeWidget, Widget From 50e4c756e19e632d4e5087b670e72110f3140381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 16 Apr 2024 11:51:50 +0200 Subject: [PATCH 2/8] POC of how to enable directory support for FileInput --- panel/models/file_input.ts | 15 +++++++++++---- panel/models/widgets.py | 4 +++- panel/widgets/input.py | 3 +++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/panel/models/file_input.ts b/panel/models/file_input.ts index 9d7c008334..04f9e576af 100644 --- a/panel/models/file_input.ts +++ b/panel/models/file_input.ts @@ -15,14 +15,14 @@ export class FileInputView extends InputWidgetView { } protected _render_input(): HTMLElement { - const {multiple, disabled} = this.model + const {multiple, disabled, directory} = this.model const accept = (() => { const {accept} = this.model return isString(accept) ? accept : accept.join(",") })() - return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled}) + return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled, webkitdirectory: directory}) } override render(): void { @@ -40,18 +40,23 @@ export class FileInputView extends InputWidgetView { const values: string[] = [] const filenames: string[] = [] const mime_types: string[] = [] + const {directory, multiple} = this.model for (const file of files) { const data_url = await this._read_file(file) const [, mime_type="",, value=""] = data_url.split(/[:;,]/, 4) values.push(value) - filenames.push(file.name) mime_types.push(mime_type) + if (directory) { + filenames.push(file.webkitRelativePath) + } else { + filenames.push(file.name) + } } const [value, filename, mime_type] = (() =>{ - if (this.model.multiple) { + if (directory || multiple) { return [values, filenames, mime_types] } else if (files.length != 0) { return [values[0], filenames[0], mime_types[0]] @@ -87,6 +92,7 @@ export namespace FileInput { filename: p.Property accept: p.Property multiple: p.Property + directory: p.Property } } @@ -111,6 +117,7 @@ export class FileInput extends InputWidget { filename: [ Or(Str, List(Str)), p.unset, {readonly: true} ], accept: [ Or(Str, List(Str)), "" ], multiple: [ Bool, false ], + directory: [ Bool, false ], })) } } diff --git a/panel/models/widgets.py b/panel/models/widgets.py index e7c5e78827..4ca172c02f 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -256,4 +256,6 @@ class RadioButtonGroup(bkRadioButtonGroup): """) class FileInput(bkFileInput): - ... + directory = Bool(False, help=""" + Whether to allow selection of directories. + """) diff --git a/panel/widgets/input.py b/panel/widgets/input.py index 427cd6bf5b..0525df914c 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -176,6 +176,9 @@ class FileInput(Widget): description = param.String(default=None, doc=""" An HTML string describing the function of this component.""") + directory = param.Boolean(default=False, doc=""" + Selecting directories instead of files. Will not work with accept""") + filename = param.ClassSelector( default=None, class_=(str, list), is_instance=True) From 2c6e09ba0c7ba0f74b5b3978044829d2fbf60011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 2 May 2024 15:37:43 +0200 Subject: [PATCH 3/8] Add clear input method --- panel/models/file_input.ts | 15 ++++++++++++++- panel/models/widgets.py | 4 ++++ panel/widgets/input.py | 12 +++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/panel/models/file_input.ts b/panel/models/file_input.ts index 04f9e576af..ce78c09607 100644 --- a/panel/models/file_input.ts +++ b/panel/models/file_input.ts @@ -25,6 +25,12 @@ export class FileInputView extends InputWidgetView { return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled, webkitdirectory: directory}) } + override connect_signals(): void { + super.connect_signals() + const {_clear_input} = this.model.properties + this.on_change(_clear_input, this.clear_input) + } + override render(): void { super.render() @@ -82,6 +88,11 @@ export class FileInputView extends InputWidgetView { reader.readAsDataURL(file) }) } + protected clear_input(): void { + this.input_el.value = "" + this.model.setv({value: "", filename: "", mime_type: ""}) + } + } export namespace FileInput { @@ -93,6 +104,7 @@ export namespace FileInput { accept: p.Property multiple: p.Property directory: p.Property + _clear_input: p.Property } } @@ -111,13 +123,14 @@ export class FileInput extends InputWidget { static { this.prototype.default_view = FileInputView - this.define(({Bool, Str, List, Or}) => ({ + this.define(({Bool, Str, List, Or, Int}) => ({ value: [ Or(Str, List(Str)), p.unset, {readonly: true} ], mime_type: [ Or(Str, List(Str)), p.unset, {readonly: true} ], filename: [ Or(Str, List(Str)), p.unset, {readonly: true} ], accept: [ Or(Str, List(Str)), "" ], multiple: [ Bool, false ], directory: [ Bool, false ], + _clear_input: [ Int, 0], })) } } diff --git a/panel/models/widgets.py b/panel/models/widgets.py index 4ca172c02f..f800b83f28 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -259,3 +259,7 @@ class FileInput(bkFileInput): directory = Bool(False, help=""" Whether to allow selection of directories. """) + + _clear_input = Int(0, help=""" + A private property to clear the file input. + """) diff --git a/panel/widgets/input.py b/panel/widgets/input.py index 0525df914c..77b0067382 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -189,6 +189,8 @@ class FileInput(Widget): value = param.Parameter(default=None) + _clear_input = param.Integer(default=0, constant=True) + _rename: ClassVar[Mapping[str, str | None]] = { 'filename': None, 'name': None } @@ -216,9 +218,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): @@ -255,6 +261,10 @@ def save(self, filename): else: fn.write(val) + def clear_input(self): + with param.edit_constant(self): + self._clear_input += 1 + class StaticText(Widget): """ From 3b93c79b70f1001407d4b1305dd49e7d7cfcd57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 2 May 2024 16:11:07 +0200 Subject: [PATCH 4/8] Add filtering of filetype from directory --- panel/models/file_input.ts | 26 +++++++++++++------------- panel/widgets/input.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/panel/models/file_input.ts b/panel/models/file_input.ts index ce78c09607..0a28db2994 100644 --- a/panel/models/file_input.ts +++ b/panel/models/file_input.ts @@ -1,7 +1,7 @@ import {InputWidget, InputWidgetView} from "@bokehjs/models/widgets/input_widget" import type {StyleSheetLike} from "@bokehjs/core/dom" import {input} from "@bokehjs/core/dom" -import {isString} from "@bokehjs/core/util/types" +import {isString, is_nullish} from "@bokehjs/core/util/types" import * as p from "@bokehjs/core/properties" import * as inputs from "@bokehjs/styles/widgets/inputs.css" import buttons_css from "@bokehjs/styles/buttons.css" @@ -15,14 +15,9 @@ export class FileInputView extends InputWidgetView { } protected _render_input(): HTMLElement { - const {multiple, disabled, directory} = this.model - - const accept = (() => { - const {accept} = this.model - return isString(accept) ? accept : accept.join(",") - })() - - return this.input_el = input({type: "file", class: inputs.input, multiple, accept, disabled, webkitdirectory: directory}) + const {multiple, disabled, directory, accept} = this.model + const accept_str = isString(accept) ? accept : accept.join(",") + return this.input_el = input({type: "file", class: inputs.input, multiple, accept: accept_str, disabled, webkitdirectory: directory}) } override connect_signals(): void { @@ -46,18 +41,23 @@ export class FileInputView extends InputWidgetView { const values: string[] = [] const filenames: string[] = [] const mime_types: string[] = [] - const {directory, multiple} = this.model + const {directory, multiple, accept} = this.model for (const file of files) { const data_url = await this._read_file(file) const [, mime_type="",, value=""] = data_url.split(/[:;,]/, 4) - values.push(value) - mime_types.push(mime_type) if (directory) { - filenames.push(file.webkitRelativePath) + const ext = file.name.split(".").pop() + if ((!is_nullish(accept) && isString(ext)) ? accept.includes(`.${ext}`) : true) { + filenames.push(file.webkitRelativePath) + values.push(value) + mime_types.push(mime_type) + } } else { filenames.push(file.name) + values.push(value) + mime_types.push(mime_type) } } diff --git a/panel/widgets/input.py b/panel/widgets/input.py index 77b0067382..01cd09c608 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -177,7 +177,7 @@ class FileInput(Widget): An HTML string describing the function of this component.""") directory = param.Boolean(default=False, doc=""" - Selecting directories instead of files. Will not work with accept""") + Selecting directories instead of files. Can report wrong numbers when used with accept""") filename = param.ClassSelector( default=None, class_=(str, list), is_instance=True) From 8ff0b567da47d1572a669403c183c0af305010fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 25 Jun 2024 13:03:37 +0200 Subject: [PATCH 5/8] clean up --- panel/models/file_input.ts | 136 ------------------------------------- panel/widgets/input.py | 17 +++-- 2 files changed, 13 insertions(+), 140 deletions(-) delete mode 100644 panel/models/file_input.ts diff --git a/panel/models/file_input.ts b/panel/models/file_input.ts deleted file mode 100644 index 0a28db2994..0000000000 --- a/panel/models/file_input.ts +++ /dev/null @@ -1,136 +0,0 @@ -import {InputWidget, InputWidgetView} from "@bokehjs/models/widgets/input_widget" -import type {StyleSheetLike} from "@bokehjs/core/dom" -import {input} from "@bokehjs/core/dom" -import {isString, is_nullish} from "@bokehjs/core/util/types" -import * as p from "@bokehjs/core/properties" -import * as inputs from "@bokehjs/styles/widgets/inputs.css" -import buttons_css from "@bokehjs/styles/buttons.css" - -export class FileInputView extends InputWidgetView { - declare model: FileInput - declare input_el: HTMLInputElement - - override stylesheets(): StyleSheetLike[] { - return [...super.stylesheets(), buttons_css] - } - - protected _render_input(): HTMLElement { - const {multiple, disabled, directory, accept} = this.model - const accept_str = isString(accept) ? accept : accept.join(",") - return this.input_el = input({type: "file", class: inputs.input, multiple, accept: accept_str, disabled, webkitdirectory: directory}) - } - - override connect_signals(): void { - super.connect_signals() - const {_clear_input} = this.model.properties - this.on_change(_clear_input, this.clear_input) - } - - override render(): void { - super.render() - - this.input_el.addEventListener("change", async () => { - const {files} = this.input_el - if (files != null) { - await this.load_files(files) - } - }) - } - - async load_files(files: FileList): Promise { - const values: string[] = [] - const filenames: string[] = [] - const mime_types: string[] = [] - const {directory, multiple, accept} = this.model - - for (const file of files) { - const data_url = await this._read_file(file) - const [, mime_type="",, value=""] = data_url.split(/[:;,]/, 4) - - if (directory) { - const ext = file.name.split(".").pop() - if ((!is_nullish(accept) && isString(ext)) ? accept.includes(`.${ext}`) : true) { - filenames.push(file.webkitRelativePath) - values.push(value) - mime_types.push(mime_type) - } - } else { - filenames.push(file.name) - values.push(value) - mime_types.push(mime_type) - } - } - - const [value, filename, mime_type] = (() =>{ - if (directory || multiple) { - return [values, filenames, mime_types] - } else if (files.length != 0) { - return [values[0], filenames[0], mime_types[0]] - } else { - return ["", "", ""] - } - })() - - this.model.setv({value, filename, mime_type}) - } - - protected _read_file(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = () => { - const {result} = reader - if (result != null) { - resolve(result as string) - } else { - reject(reader.error ?? new Error(`unable to read '${file.name}'`)) - } - } - reader.readAsDataURL(file) - }) - } - protected clear_input(): void { - this.input_el.value = "" - this.model.setv({value: "", filename: "", mime_type: ""}) - } - -} - -export namespace FileInput { - export type Attrs = p.AttrsOf - export type Props = InputWidget.Props & { - value: p.Property - mime_type: p.Property - filename: p.Property - accept: p.Property - multiple: p.Property - directory: p.Property - _clear_input: p.Property - } -} - -export interface FileInput extends FileInput.Attrs {} - -export class FileInput extends InputWidget { - declare properties: FileInput.Props - declare __view_type__: FileInputView - - constructor(attrs?: Partial) { - super(attrs) - } - - static override __module__ = "panel.models.widgets" - - static { - this.prototype.default_view = FileInputView - - this.define(({Bool, Str, List, Or, Int}) => ({ - value: [ Or(Str, List(Str)), p.unset, {readonly: true} ], - mime_type: [ Or(Str, List(Str)), p.unset, {readonly: true} ], - filename: [ Or(Str, List(Str)), p.unset, {readonly: true} ], - accept: [ Or(Str, List(Str)), "" ], - multiple: [ Bool, false ], - directory: [ Bool, false ], - _clear_input: [ Int, 0], - })) - } -} diff --git a/panel/widgets/input.py b/panel/widgets/input.py index 19451c0862..6c2efd4f07 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -20,7 +20,7 @@ from bokeh.models.widgets import ( Checkbox as _BkCheckbox, ColorPicker as _BkColorPicker, DatePicker as _BkDatePicker, DateRangePicker as _BkDateRangePicker, - Div as _BkDiv, NumericInput as _BkNumericInput, + Div as _BkDiv, FileInput as _BkFileInput, NumericInput as _BkNumericInput, PasswordInput as _BkPasswordInput, Spinner as _BkSpinner, Switch as _BkSwitch, ) @@ -204,7 +204,18 @@ class FileInput(Widget): rendered as a tooltip icon.""") directory = param.Boolean(default=False, doc=""" - Selecting directories instead of files. Can report wrong numbers when used with accept""") + 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=""" @@ -222,8 +233,6 @@ class FileInput(Widget): The uploaded file(s) stored as a single bytes object if multiple is False or a list of bytes otherwise.""") - _clear_input = param.Integer(default=0, constant=True) - _rename: ClassVar[Mapping[str, str | None]] = { 'filename': None, 'name': None } From a6df4054a706c4cb3105c850b3ccffc209587667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 25 Jun 2024 13:34:23 +0200 Subject: [PATCH 6/8] add clear event --- panel/widgets/input.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/panel/widgets/input.py b/panel/widgets/input.py index 6c2efd4f07..8e03c384c8 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -27,6 +27,8 @@ from pyviz_comms import JupyterComm from ..config import config +from ..io.notebook import push +from ..io.state import state from ..layout import Column, Panel from ..models import ( DatetimePicker as _bkDatetimePicker, TextAreaInput as _bkTextAreaInput, @@ -303,9 +305,20 @@ def save(self, filename): else: fn.write(val) - def clear_input(self): - with param.edit_constant(self): - self._clear_input += 1 + def clear(self): + """ + Clear the file(s) in the FileInput widget + """ + for ref, (model, _) in self._models.items(): + if ref not in state._views or ref in state._fake_roots: + continue + _viewable, root, doc, comm = state._views[ref] + if comm or state._unblocked(doc) or not doc.session_context: + model.clear() + if comm and 'embedded' not in root.tags: + push(doc, comm) + else: + doc.add_next_tick_callback(model.clear) class FileDropper(Widget): From def30050d50b349f03740ebd9889e8015fdbab69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 25 Jun 2024 13:42:27 +0200 Subject: [PATCH 7/8] Update reference notebook --- examples/reference/widgets/FileInput.ipynb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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()" ] }, { From 65d87611250fc509de7302f7d49b6f27fcbfdb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 25 Jun 2024 14:50:41 +0200 Subject: [PATCH 8/8] Move send_event into reactive base class to avoid duplication --- panel/custom.py | 15 +-------------- panel/reactive.py | 29 ++++++++++++++++++++++++++++- panel/widgets/input.py | 14 ++------------ 3 files changed, 31 insertions(+), 27 deletions(-) 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 8e03c384c8..bed0f658b0 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -24,11 +24,10 @@ 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 -from ..io.notebook import push -from ..io.state import state from ..layout import Column, Panel from ..models import ( DatetimePicker as _bkDatetimePicker, TextAreaInput as _bkTextAreaInput, @@ -309,16 +308,7 @@ def clear(self): """ Clear the file(s) in the FileInput widget """ - for ref, (model, _) in self._models.items(): - if ref not in state._views or ref in state._fake_roots: - continue - _viewable, root, doc, comm = state._views[ref] - if comm or state._unblocked(doc) or not doc.session_context: - model.clear() - if comm and 'embedded' not in root.tags: - push(doc, comm) - else: - doc.add_next_tick_callback(model.clear) + self._send_event(ClearInput) class FileDropper(Widget):