From 5afbe2b0c17c3abefed79e9b176823defa3aea79 Mon Sep 17 00:00:00 2001 From: Brad Date: Mon, 19 Feb 2024 07:26:57 +1100 Subject: [PATCH 01/11] Bump perspective version to 2.8.0 (#5722) --- examples/reference/panes/Perspective.ipynb | 2 +- panel/_templates/js_resources.html | 2 +- panel/compiler.py | 12 +++++-- panel/io/resources.py | 4 +++ panel/models/perspective.py | 41 +++++++++++++--------- panel/models/perspective.ts | 12 +++---- panel/pane/perspective.py | 19 ++++++---- panel/tests/ui/pane/test_perspective.py | 3 +- 8 files changed, 60 insertions(+), 35 deletions(-) diff --git a/examples/reference/panes/Perspective.ipynb b/examples/reference/panes/Perspective.ipynb index 6b87f493db..0b3e09889e 100644 --- a/examples/reference/panes/Perspective.ipynb +++ b/examples/reference/panes/Perspective.ipynb @@ -41,7 +41,7 @@ "* **``selectable``** (bool, default=True): Whether rows are selectable\n", "* **``sort``** (list): List of sorting specs, e.g. `[[\"x\", \"desc\"]]`\n", "* **``split_by``** (list): A list of columns to pivot by. e.g. `[\"x\", \"y\"]`\n", - "* **``theme``** (str): The theme of the viewer, available options include `'material'`, `'material-dark'`, `'monokai'`, `'solarized'`, `'solarized-dark'` and `'vaporwave'`\n", + "* **``theme``** (str): The theme of the viewer, available options include `'pro'`, `'pro-dark'`, `'monokai'`, `'solarized'`, `'solarized-dark'` and `'vaporwave'`\n", "* **``toggle_config``** (bool): Whether to show the config menu. Default is True.\n", "\n", "##### Callbacks\n", diff --git a/panel/_templates/js_resources.html b/panel/_templates/js_resources.html index 2a616d144f..efc5680f22 100644 --- a/panel/_templates/js_resources.html +++ b/panel/_templates/js_resources.html @@ -25,7 +25,7 @@ {%- for name, file in js_module_exports.items() %} {%- endfor %} diff --git a/panel/compiler.py b/panel/compiler.py index 0627047042..fb1096d745 100644 --- a/panel/compiler.py +++ b/panel/compiler.py @@ -66,7 +66,7 @@ def write_bundled_files(name, files, explicit_dir=None, ext=None): filename = str(filename) if ext and not str(filename).endswith(ext): filename += f'.{ext}' - if filename.endswith('.ttf'): + if filename.endswith(('.ttf', '.wasm')): with open(filename, 'wb') as f: f.write(response.content) else: @@ -250,11 +250,17 @@ def bundle_models(verbose=False, external=True): continue if verbose: print(f'Collecting {name} resources') - prev_jsfiles = getattr(model, '__javascript_raw__', None) + prev_jsfiles = ( + getattr(model, '__javascript_raw__', []) + + getattr(model, '__javascript_modules_raw__', []) + ) or None prev_jsbundle = getattr(model, '__tarball__', None) prev_cls = model for cls in model.__mro__[1:]: - jsfiles = getattr(cls, '__javascript_raw__', None) + jsfiles = ( + getattr(cls, '__javascript_raw__', []) + + getattr(cls, '__javascript_modules_raw__', []) + ) or None if ((jsfiles is None and prev_jsfiles is not None) or (jsfiles is not None and jsfiles != prev_jsfiles)): if prev_jsbundle: diff --git a/panel/io/resources.py b/panel/io/resources.py index 169ad3772a..b7d42137c0 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -751,6 +751,10 @@ def js_modules(self): from ..reactive import ReactiveHTML modules = list(config.js_modules.values()) + for model in Model.model_class_reverse_map.values(): + if hasattr(model, '__javascript_modules__'): + modules.extend(model.__javascript_modules__) + self.extra_resources(modules, '__javascript_modules__') if config.design: design_resources = config.design().resolve_resources( diff --git a/panel/models/perspective.py b/panel/models/perspective.py index 58574326d9..b513772bef 100644 --- a/panel/models/perspective.py +++ b/panel/models/perspective.py @@ -1,5 +1,5 @@ from bokeh.core.properties import ( - Any, Bool, Dict, Either, Enum, Instance, List, Null, Nullable, String, + Any, Bool, Dict, Either, Enum, Instance, List, Null, String, ) from bokeh.events import ModelEvent from bokeh.models import ColumnDataSource @@ -10,10 +10,11 @@ from .layout import HTMLBox PERSPECTIVE_THEMES = [ - 'material', 'material-dark', 'monokai', 'solarized', 'solarized-dark', 'vaporwave' + 'monokai', 'solarized', 'solarized-dark', 'vaporwave', 'dracula', + 'pro', 'pro-dark', 'gruvbox', 'gruvbox-dark', ] -PERSPECTIVE_VERSION = '1.9.3' +PERSPECTIVE_VERSION = '2.8.0' THEME_PATH = f"@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/css/" THEME_URL = f"{config.npm_cdn}/{THEME_PATH}" @@ -51,7 +52,7 @@ class Perspective(HTMLBox): columns = Either(List(Either(String, Null)), Null()) - expressions = Nullable(List(String)) + expressions = Either(Dict(String, Any), Null()) editable = Bool(default=True) @@ -73,26 +74,34 @@ class Perspective(HTMLBox): toggle_config = Bool(True) - theme = Enum(*PERSPECTIVE_THEMES, default="material") + theme = Enum(*PERSPECTIVE_THEMES, default="pro") - # pylint: disable=line-too-long - __javascript__ = [ - f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/umd/perspective.js", - f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer.js", - f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-datagrid.js", - f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-d3fc.js", + __javascript_module_exports__ = ['perspective'] + + __javascript_modules_raw__ = [ + f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective.js", + f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective.worker.js", + f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective.cpp.wasm", + f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer.js", + f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/cdn/perspective_bg.wasm", + f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-datagrid.js", + f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-d3fc.js", ] + @classproperty + def __javascript_modules__(cls): + return [js for js in bundled_files(cls, 'javascript_modules') if 'wasm' not in js and 'worker' not in js] + __js_skip__ = { - "perspective": __javascript__, + "perspective": __javascript_modules__, } __js_require__ = { "paths": { - "perspective": f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/umd/perspective", - "perspective-viewer": f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer", - "perspective-viewer-datagrid": f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-datagrid", - "perspective-viewer-d3fc": f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-d3fc", + "perspective": f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective", + "perspective-viewer": f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer", + "perspective-viewer-datagrid": f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-datagrid", + "perspective-viewer-d3fc": f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-d3fc", }, "exports": { "perspective": "perspective", diff --git a/panel/models/perspective.ts b/panel/models/perspective.ts index f04fed58af..8d1d09e16d 100644 --- a/panel/models/perspective.ts +++ b/panel/models/perspective.ts @@ -6,8 +6,8 @@ import {HTMLBox, HTMLBoxView, set_size} from "./layout" import type {Attrs} from "@bokehjs/core/types" const THEMES: any = { - 'material-dark': 'Material Dark', - 'material': 'Material Light', + 'pro-dark': 'Pro Dark', + 'pro': 'Pro Light', 'vaporwave': 'Vaporwave', 'solarized': 'Solarized', 'solarized-dark': 'Solarized Dark', @@ -200,7 +200,7 @@ export class PerspectiveView extends HTMLBoxView { const props: any = {} for (let option in config) { let value = config[option] - if (value === undefined || (option == 'plugin' && value === "debug") || option === 'settings') + if (value === undefined || (option == 'plugin' && value === "debug") || this.model.properties.hasOwnProperty(option) === undefined) continue if (option === 'filter') option = 'filters' @@ -256,7 +256,7 @@ export namespace Perspective { aggregates: p.Property split_by: p.Property columns: p.Property - expressions: p.Property + expressions: p.Property editable: p.Property filters: p.Property group_by: p.Property @@ -288,7 +288,7 @@ export class Perspective extends HTMLBox { this.define(({Any, Array, Boolean, Ref, Nullable, String}) => ({ aggregates: [ Any, {} ], columns: [ Array(Nullable(String)), [] ], - expressions: [ Nullable(Array(String)), null ], + expressions: [ Any, {} ], split_by: [ Nullable(Array(String)), null ], editable: [ Boolean, true ], filters: [ Nullable(Array(Any)), null ], @@ -300,7 +300,7 @@ export class Perspective extends HTMLBox { toggle_config: [ Boolean, true ], sort: [ Nullable(Array(Array(String))), null ], source: [ Ref(ColumnDataSource), ], - theme: [ String, 'material' ] + theme: [ String, 'pro' ] })) } } diff --git a/panel/pane/perspective.py b/panel/pane/perspective.py index 2f870bd672..45babb0aea 100644 --- a/panel/pane/perspective.py +++ b/panel/pane/perspective.py @@ -29,10 +29,11 @@ from ..model.perspective import PerspectiveClickEvent -DEFAULT_THEME = "material" +DEFAULT_THEME = "pro" THEMES = [ - 'material', 'material-dark', 'monokai', 'solarized', 'solarized-dark', 'vaporwave' + 'material', 'material-dark', 'monokai', 'solarized', 'solarized-dark', + 'vaporwave', 'pro', 'pro-dark' ] class Plugin(Enum): @@ -264,7 +265,7 @@ class Perspective(ModelPane, ReactiveData): :Example: - >>> Perspective(df, plugin='hypergrid', theme='material-dark') + >>> Perspective(df, plugin='hypergrid', theme='pro-dark') """ aggregates = param.Dict(default=None, nested_refs=True, doc=""" @@ -276,7 +277,7 @@ class Perspective(ModelPane, ReactiveData): editable = param.Boolean(default=True, allow_None=True, doc=""" Whether items are editable.""") - expressions = param.List(default=None, nested_refs=True, doc=""" + expressions = param.ClassSelector(class_=(dict, list), default=None, nested_refs=True, doc=""" A list of expressions computing new columns from existing columns. For example [""x"+"index""]""") @@ -310,8 +311,8 @@ class Perspective(ModelPane, ReactiveData): toggle_config = param.Boolean(default=True, doc=""" Whether to show the config menu.""") - theme = param.ObjectSelector(default='material', objects=THEMES, doc=""" - The style of the PerspectiveViewer. For example material-dark""") + theme = param.ObjectSelector(default='pro', objects=THEMES, doc=""" + The style of the PerspectiveViewer. For example pro-dark""") priority: ClassVar[float | bool | None] = None @@ -373,6 +374,8 @@ def _filter_properties(self, properties): def _get_properties(self, doc, source=None): props = super()._get_properties(doc) + if 'theme' in props and 'material' in props['theme']: + props['theme'] = props['theme'].replace('material', 'pro') del props['object'] if props.get('toggle_config'): props['height'] = self.height or 300 @@ -431,6 +434,8 @@ def _process_param_change(self, params): params['stylesheets'] = [ ImportedStyleSheet(url=ss) for ss in css ] + params.get('stylesheets', self.stylesheets) + if 'theme' in params and 'material' in params['theme']: + params['theme'] = params['theme'].replace('material', 'pro') props = super()._process_param_change(params) for p in ('columns', 'group_by', 'split_by'): if props.get(p): @@ -441,6 +446,8 @@ def _process_param_change(self, params): props['filters'] = [[str(col), *args] for col, *args in props['filters']] if props.get('aggregates'): props['aggregates'] = {str(col): agg for col, agg in props['aggregates'].items()} + if isinstance(props.get('expressions'), list): + props['expressions'] = {f'expression_{i}': exp for i, exp in enumerate(props['expressions'])} return props def _as_digit(self, col): diff --git a/panel/tests/ui/pane/test_perspective.py b/panel/tests/ui/pane/test_perspective.py index 1b61828a20..8e3265cc32 100644 --- a/panel/tests/ui/pane/test_perspective.py +++ b/panel/tests/ui/pane/test_perspective.py @@ -34,9 +34,8 @@ def test_perspective_click_event(page): perspective.on_click(lambda e: events.append(e)) serve_component(page, perspective) - page.wait_for_timeout(1000) - page.locator('tr').nth(3).click() + page.locator('.pnx-perspective-viewer').locator('tr').nth(4).locator('td').nth(3).click(force=True) wait_until(lambda: len(events) == 1, page) From c798697df0a7b29a4dc06cc2b6410ab6a1880a59 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 18 Feb 2024 21:31:43 +0100 Subject: [PATCH 02/11] Ensure that ripple matches notification size (#6360) --- panel/dist/css/notifications.css | 3 +++ panel/io/notifications.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 panel/dist/css/notifications.css diff --git a/panel/dist/css/notifications.css b/panel/dist/css/notifications.css new file mode 100644 index 0000000000..c87f539a0a --- /dev/null +++ b/panel/dist/css/notifications.css @@ -0,0 +1,3 @@ +.notyf__ripple { + height: calc(100% * 2.75); +} diff --git a/panel/io/notifications.py b/panel/io/notifications.py index fc5ae53c43..11771f30c2 100644 --- a/panel/io/notifications.py +++ b/panel/io/notifications.py @@ -10,7 +10,7 @@ from ..reactive import ReactiveHTML from ..util import classproperty from .datamodel import _DATA_MODELS, construct_data_model -from .resources import CSS_URLS, bundled_files +from .resources import CSS_URLS, bundled_files, get_dist_path from .state import state if TYPE_CHECKING: @@ -176,7 +176,9 @@ def __js_skip__(cls): @classproperty def __css__(cls): - return bundled_files(cls, 'css') + return bundled_files(cls, 'css') + [ + f"{get_dist_path()}css/notifications.css" + ] _template = "" From 11a15047abbbe8422409f9edf2b8062efe90e010 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 18 Feb 2024 22:19:26 +0100 Subject: [PATCH 03/11] Do not resolve server paths twice --- panel/io/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/io/resources.py b/panel/io/resources.py index b7d42137c0..21c709487a 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -604,7 +604,7 @@ def extra_resources(self, resources, resource_type): if not (getattr(model, resource_type, None) and model._loaded()): continue for resource in getattr(model, resource_type, []): - if not isurl(resource) and not resource.startswith('static/extensions'): + if not isurl(resource) and not resource.lstrip('./').startswith('static/extensions'): resource = component_resource_path(model, resource_type, resource) if resource not in resources: resources.append(resource) From 508b23e56693673b58b2e68841a693ace9951fb3 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sun, 18 Feb 2024 23:00:35 +0100 Subject: [PATCH 04/11] fix reactive html issues (#6358) --- .../reactive_html/reactive_html_layout.md | 43 +++++++++++-------- .../reactive_html/reactive_html_styling.md | 4 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_layout.md b/doc/how_to/custom_components/reactive_html/reactive_html_layout.md index 7ca624f029..a5f518378f 100644 --- a/doc/how_to/custom_components/reactive_html/reactive_html_layout.md +++ b/doc/how_to/custom_components/reactive_html/reactive_html_layout.md @@ -12,11 +12,8 @@ import param pn.extension() - class LayoutSingleObject(pn.reactive.ReactiveHTML): - object = param.Parameter() - - _ignored_refs = ("object",) + object = param.Parameter(allow_refs=False) _template = """
@@ -26,7 +23,6 @@ class LayoutSingleObject(pn.reactive.ReactiveHTML):
""" - dial = pn.widgets.Dial( name="°C", value=37, @@ -42,7 +38,9 @@ LayoutSingleObject( ).servable() ``` -Please notice +:::{note} + +Please note - We define the HTML layout in the `_template` attribute. - We can refer to the parameter `object` in the `_template` via the *template parameter* `${object}`. @@ -53,6 +51,8 @@ parameter can be called anything. For example `value`, `dial` or `temperature`. - We add the `border` in the `styles` parameter so that we can better see how the `_template` layes out inside the `ReactiveHTML` component. This can be very useful for development. +::: + ## Layout multiple parameters ```{pyodide} @@ -62,10 +62,8 @@ import param pn.extension() class LayoutMultipleValues(pn.reactive.ReactiveHTML): - object1 = param.Parameter() - object2 = param.Parameter() - - _ignored_refs = ("object1", "object2") + object1 = param.Parameter(allow_refs=False) + object2 = param.Parameter(allow_refs=False) _template = """
@@ -121,7 +119,6 @@ class LayoutLiteralValues(pn.reactive.ReactiveHTML): object1 = param.Parameter() object2 = param.Parameter() - _ignored_refs = ("object1", "object2") _child_config = {"object1": "literal", "object2": "literal"} _template = """ @@ -182,17 +179,21 @@ LayoutOfList(objects=[ The component will trigger a rerendering if you update the `List` value. -Please note that you must +:::{note} + +You must - wrap the `{% for object in objects %}` loop in an HTML element with an `id`. Here it is wrapped with `
...
`. - close all HTML tags! `
` is valid HTML, but not valid with `ReactiveHTML`. You must close it as `
`. -Please note you can optionally +You can optionally - get the index of the `{% for object in objects %}` loop via `{{ loop.index0 }}`. +::: + ## Create a list like layout If you want to create a *list like* layout similar to `Column` and `Row`, you can @@ -226,13 +227,15 @@ layout = ListLikeLayout( layout.servable() ``` -Please note that you must +You can now use `[...]` indexing and the `.append`, `.insert`, `pop`, ... methods that you would +expect. + +:::{note} -- list `NamedListLike, ReactiveHTML` in exactly that order when you define the class! The other +You must list `NamedListLike, ReactiveHTML` in exactly that order when you define the class! The other way around `ReactiveHTML, NamedListLike` will not work. -You can now use `[...]` indexing and the `.append`, `.insert`, `pop`, ... methods that you would -expect. +:::: ## Layout a dictionary @@ -266,9 +269,13 @@ LayoutOfDict(object={ ).servable() ``` +:::{note} + Please note - We can insert the `key` as a literal value only using `{{ key }}`. Inserting it as a template -variable ${key} will not work. +variable `${key}` will not work. - We must not give the HTML element containing `{{ key }}` an `id`. If we do, an exception will be raised. + +::: diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_styling.md b/doc/how_to/custom_components/reactive_html/reactive_html_styling.md index 98b47935f7..9fb8339c55 100644 --- a/doc/how_to/custom_components/reactive_html/reactive_html_styling.md +++ b/doc/how_to/custom_components/reactive_html/reactive_html_styling.md @@ -16,9 +16,7 @@ from panel.reactive import ReactiveHTML pn.extension() class SensorLayout(ReactiveHTML): - object = param.Parameter() - - _ignored_refs = ("object",) + object = param.Parameter(allow_refs=False) _template = """
From 0d026d651aa811e56b70bb1f45b00a4859931441 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 18 Feb 2024 23:15:41 +0100 Subject: [PATCH 05/11] Fully re-render Ace on render calls (#6361) --- panel/models/ace.ts | 60 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/panel/models/ace.ts b/panel/models/ace.ts index 792e5aecbe..d35d6a362b 100644 --- a/panel/models/ace.ts +++ b/panel/models/ace.ts @@ -24,18 +24,6 @@ export class AcePlotView extends HTMLBoxView { protected _modelist: ModeList protected _container: HTMLDivElement - initialize(): void { - super.initialize() - this._container = div({ - id: ID(), - style: { - width: "100%", - height: "100%", - zIndex: 0, - } - }) - } - connect_signals(): void { super.connect_signals() this.connect(this.model.properties.code.change, () => this._update_code_from_model()) @@ -51,24 +39,32 @@ export class AcePlotView extends HTMLBoxView { render(): void { super.render() - if (!(this._container === this.shadow_el.childNodes[0])) - this.shadow_el.append(this._container) - this._container.textContent = this.model.code - this._editor = ace.edit(this._container) - this._editor.renderer.attachToShadowRoot() - this._langTools = ace.require('ace/ext/language_tools') - this._modelist = ace.require("ace/ext/modelist") - this._editor.setOptions({ - enableBasicAutocompletion: true, - enableSnippets: true, - fontFamily: "monospace", //hack for cursor position - }); - this._update_theme() - this._update_filename() - this._update_language() - this._editor.setReadOnly(this.model.readonly) - this._editor.setShowPrintMargin(this.model.print_margin); - this._editor.on('change', () => this._update_code_from_editor()) + + this._container = div({ + id: ID(), + style: { + width: "100%", + height: "100%", + zIndex: 0, + } + }) + this.shadow_el.append(this._container) + this._container.textContent = this.model.code + this._editor = ace.edit(this._container) + this._editor.renderer.attachToShadowRoot() + this._langTools = ace.require('ace/ext/language_tools') + this._modelist = ace.require("ace/ext/modelist") + this._editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + fontFamily: "monospace", //hack for cursor position + }); + this._update_theme() + this._update_filename() + this._update_language() + this._editor.setReadOnly(this.model.readonly) + this._editor.setShowPrintMargin(this.model.print_margin); + this._editor.on('change', () => this._update_code_from_editor()) } _update_code_from_model(): void { @@ -109,7 +105,9 @@ export class AcePlotView extends HTMLBoxView { after_layout(): void{ super.after_layout() - this._editor.resize() + if (this._editor !== undefined) { + this._editor.resize() + } } } From e5955d394547dadf6a4bd05271773d1cf487763b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 19 Feb 2024 15:16:52 +0100 Subject: [PATCH 06/11] Fix modal overflow and resizing issues (#6355) --- panel/models/tabulator.ts | 34 ++++++++++++++++++- panel/template/base.py | 5 ++- panel/template/bootstrap/bootstrap.css | 3 +- panel/template/fast/fast.css | 5 +++ .../fast/grid/fast_grid_template.html | 2 +- .../fast/list/fast_list_template.html | 2 +- panel/template/vanilla/vanilla.css | 3 +- 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 83c9e78a2f..8d84a793f3 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -302,8 +302,10 @@ export class DataTabulatorView extends HTMLBoxView { _selection_updating: boolean = false _initializing: boolean _lastVerticalScrollbarTopPosition: number = 0; + _lastHorizontalScrollbarLeftPosition: number = 0; _applied_styles: boolean = false _building: boolean = false + _restore_scroll: boolean = false connect_signals(): void { super.connect_signals() @@ -378,7 +380,7 @@ export class DataTabulatorView extends HTMLBoxView { this.connect(this.model.source.patching, () => { const inds = this.model.source.selected.indices this.updateOrAddData(); - this.tabulator.rowManager.element.scrollTop = this._lastVerticalScrollbarTopPosition; + this.record_scroll() // Restore indices since updating data may have reset checkbox column this.model.source.selected.indices = inds; }) @@ -427,6 +429,7 @@ export class DataTabulatorView extends HTMLBoxView { this.renderChildren() this.setStyles() } + this._restore_scroll = true } after_layout(): void { @@ -436,6 +439,11 @@ export class DataTabulatorView extends HTMLBoxView { this._initializing = false } + after_resize(): void { + super.after_resize() + this.redraw() + } + setCSSClasses(el: HTMLDivElement): void { el.className = "pnx-tabulator tabulator" for (const cls of this.model.theme_classes) @@ -492,14 +500,19 @@ export class DataTabulatorView extends HTMLBoxView { return cell.getColumn().getField() + ": " + cell.getValue(); }) this.tabulator.on("scrollVertical", debounce(() => { + this.record_scroll() this.setStyles() }, 50, false)) + this.tabulator.on("scrollHorizontal", debounce(() => { + this.record_scroll() + }, 50, false)) // Sync state with model this.tabulator.on("rowSelectionChanged", (data: any, rows: any, selected: any, deselected: any) => this.rowSelectionChanged(data, rows, selected, deselected)) this.tabulator.on("rowClick", (e: any, row: any) => this.rowClicked(e, row)) this.tabulator.on("cellEdited", (cell: any) => this.cellEdited(cell)) this.tabulator.on("dataFiltering", (filters: any) => { + this.record_scroll() this.model.filters = filters }) this.tabulator.on("dataFiltered", (_: any, rows: any[]) => { @@ -925,6 +938,10 @@ export class DataTabulatorView extends HTMLBoxView { postUpdate(): void { this.setSelection() this.setStyles() + if (this._restore_scroll) { + this.restore_scroll() + this._restore_scroll = false + } } updateOrAddData(): void { @@ -1058,7 +1075,22 @@ export class DataTabulatorView extends HTMLBoxView { this._selection_updating = false } + restore_scroll(): void { + const opts = { + top: this._lastVerticalScrollbarTopPosition, + left: this._lastHorizontalScrollbarLeftPosition, + behavior: "instant" + } + setTimeout(() => this.tabulator.rowManager.element.scrollTo(opts), 0) + } + // Update model + + record_scroll() { + this._lastVerticalScrollbarTopPosition = this.tabulator.rowManager.element.scrollTop + this._lastHorizontalScrollbarLeftPosition = this.tabulator.rowManager.element.scrollLeft + } + rowClicked(e: any, row: any) { if ( this._selection_updating || diff --git a/panel/template/base.py b/panel/template/base.py index f58842caae..cda235bae8 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -555,7 +555,10 @@ class TemplateActions(ReactiveHTML): _template: ClassVar[str] = "" _scripts: ClassVar[Dict[str, List[str] | str]] = { - 'open_modal': ["document.getElementById('pn-Modal').style.display = 'block'"], + 'open_modal': [""" + document.getElementById('pn-Modal').style.display = 'block' + window.dispatchEvent(new Event('resize')); + """], 'close_modal': ["document.getElementById('pn-Modal').style.display = 'none'"], } diff --git a/panel/template/bootstrap/bootstrap.css b/panel/template/bootstrap/bootstrap.css index 6f05eb9115..7646a02f4b 100644 --- a/panel/template/bootstrap/bootstrap.css +++ b/panel/template/bootstrap/bootstrap.css @@ -162,6 +162,7 @@ img.app-logo { .pn-modal-content { display: block; min-height: 42px; + overflow-x: auto; } .pn-modal-close { @@ -182,8 +183,8 @@ img.app-logo { .pn-busy-container { align-items: center; - justify-content: center; display: flex; + justify-content: center; margin-left: auto; } diff --git a/panel/template/fast/fast.css b/panel/template/fast/fast.css index 9210315638..e75c38df64 100644 --- a/panel/template/fast/fast.css +++ b/panel/template/fast/fast.css @@ -366,3 +366,8 @@ fast-card { --background-color: var(--neutral-layer-floating); z-index: 10; } + +.pn-modal-content { + display: block; + overflow-x: auto; +} diff --git a/panel/template/fast/grid/fast_grid_template.html b/panel/template/fast/grid/fast_grid_template.html index 702e8e2f4d..a4765cdaef 100644 --- a/panel/template/fast/grid/fast_grid_template.html +++ b/panel/template/fast/grid/fast_grid_template.html @@ -183,7 +183,7 @@