@@ -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 `
...
` 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 = """
diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb
index fa87bc2f71..ca813d98bb 100644
--- a/examples/reference/chat/ChatFeed.ipynb
+++ b/examples/reference/chat/ChatFeed.ipynb
@@ -75,6 +75,13 @@
"___"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Basics"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -175,6 +182,13 @@
"message = chat_feed.send({\"object\": \"Overtaken!\", \"user\": \"Bot\"}, user=\"MegaBot\")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Callbacks"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -339,6 +353,13 @@
"chat_feed.send(\"This will fail...\")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Async Callbacks"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -550,6 +571,13 @@
"message = chat_feed.send(\"Hello bots!\")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Serialize"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -712,7 +740,16 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "If a returned object is not a generator (notably LangChain output), it's still possible to stream the output with the `stream` method."
+ "#### Stream"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If a returned object is not a generator (notably LangChain output), it's still possible to stream the output with the `stream` method.\n",
+ "\n",
+ "Note, if you're working with `generator`s, use `yield` in your callback instead."
]
},
{
@@ -774,6 +811,13 @@
" message = chat_feed.stream(n, message=message)"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Customization"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
diff --git a/examples/reference/chat/ChatInterface.ipynb b/examples/reference/chat/ChatInterface.ipynb
index f04fe70e4c..01f7698cd6 100644
--- a/examples/reference/chat/ChatInterface.ipynb
+++ b/examples/reference/chat/ChatInterface.ipynb
@@ -59,6 +59,13 @@
"___"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Basics"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -110,6 +117,13 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Input Widgets"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -367,6 +381,13 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Buttons"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
diff --git a/examples/reference/chat/ChatMessage.ipynb b/examples/reference/chat/ChatMessage.ipynb
index d739d98881..90eb68fa01 100644
--- a/examples/reference/chat/ChatMessage.ipynb
+++ b/examples/reference/chat/ChatMessage.ipynb
@@ -65,6 +65,13 @@
"___"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Basics"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -192,23 +199,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "That way, the `value`, `user`, and/or `avatar` can be dynamically updated either by setting the `value` like this..."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "chat_message.object = pn.pane.Markdown(\"## Cheers!\")"
+ "#### Updates"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Or updating multiple values at once with the `.param.update` method!"
+ "That way, the `value`, `user`, and/or `avatar` can be dynamically updated either by setting the `value` like this..."
]
},
{
@@ -217,14 +215,14 @@
"metadata": {},
"outputs": [],
"source": [
- "chat_message.param.update(user=\"Jolly Guy\", avatar=\"🎅\");"
+ "chat_message.object = pn.pane.Markdown(\"## Cheers!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "If you don't specify an `avatar` on construction, then an `avatar` will be looked up in the `default_avatars` dictionary."
+ "Or updating multiple values at once with the `.param.update` method!"
]
},
{
@@ -233,16 +231,20 @@
"metadata": {},
"outputs": [],
"source": [
- "ChatMessage.default_avatars"
+ "chat_message.param.update(user=\"Jolly Guy\", avatar=\"🎅\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "You can modify the `ChatMessage.default_avatars` in-place.\n",
+ "The easiest and most optimal way to stream output to the `ChatMessage` is through async generators.\n",
"\n",
- "Note, the keys are *only* alphanumeric sensitive, meaning spaces, special characters, and case sensitivity is disregarded, e.g. `\"Chat-GPT3.5\"`, `\"chatgpt 3.5\"` and `\"Chat GPT 3.5\"` all map to the same value."
+ "If you're unfamiliar with this term, don't fret!\n",
+ "\n",
+ "It's simply prefixing your function with `async` and replacing `return` with `yield`.\n",
+ "\n",
+ "This example will show you how to **replace** the `ChatMessage` `value`."
]
},
{
@@ -251,22 +253,19 @@
"metadata": {},
"outputs": [],
"source": [
- "ChatMessage.default_avatars[\"Wolfram\"] = \"🐺\"\n",
- "ChatMessage.default_avatars[\"#1 good-to-go guy\"] = \"👍\"\n",
+ "async def replace_response():\n",
+ " for value in range(0, 28):\n",
+ " await asyncio.sleep(0.2)\n",
+ " yield value\n",
"\n",
- "pn.Column(\n",
- " ChatMessage(\"Mathematics!\", user=\"Wolfram\"),\n",
- " ChatMessage(\"Good to go!\", user=\"#1 Good-to-Go Guy\"),\n",
- " ChatMessage(\"What's up?\", user=\"Other Guy\"),\n",
- " max_width=300,\n",
- ")"
+ "ChatMessage(replace_response)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "The `timestamp` can be formatted using `timestamp_format`. Additionally, a timestamp_tz can be provided to use any timezones supported by `zoneinfo`."
+ "This example will show you how to **append** to the `ChatMessage` `value`. "
]
},
{
@@ -275,24 +274,26 @@
"metadata": {},
"outputs": [],
"source": [
- "pn.chat.ChatMessage(timestamp_format=\"%b %d, %Y %I:%M %p\", timestamp_tz=\"US/Pacific\")"
+ "sentence = \"\"\"\n",
+ " The greatest glory in living lies not in never falling,\n",
+ " but in rising every time we fall.\n",
+ "\"\"\"\n",
+ "\n",
+ "async def append_response():\n",
+ " value = \"\"\n",
+ " for token in sentence.split():\n",
+ " value += f\" {token}\"\n",
+ " await asyncio.sleep(0.2)\n",
+ " yield value\n",
+ "\n",
+ "ChatMessage(append_response, user=\"Wise guy\", avatar=\"🤓\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "The `ChatMessage` can serialized into a string."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "widget = pn.widgets.FloatSlider(value=3, name=\"Number selected\")\n",
- "pn.chat.ChatMessage(widget).serialize()"
+ "#### Styling"
]
},
{
@@ -406,6 +407,13 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Reactions"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -468,13 +476,32 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The easiest and most optimal way to stream output to the `ChatMessage` is through async generators.\n",
- "\n",
- "If you're unfamiliar with this term, don't fret!\n",
- "\n",
- "It's simply prefixing your function with `async` and replacing `return` with `yield`.\n",
+ "#### Misc"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you don't specify an `avatar` on construction, then an `avatar` will be looked up in the `default_avatars` dictionary."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ChatMessage.default_avatars"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can modify the `ChatMessage.default_avatars` in-place.\n",
"\n",
- "This example will show you how to **replace** the `ChatMessage` `value`."
+ "Note, the keys are *only* alphanumeric sensitive, meaning spaces, special characters, and case sensitivity is disregarded, e.g. `\"Chat-GPT3.5\"`, `\"chatgpt 3.5\"` and `\"Chat GPT 3.5\"` all map to the same value."
]
},
{
@@ -483,19 +510,22 @@
"metadata": {},
"outputs": [],
"source": [
- "async def replace_response():\n",
- " for value in range(0, 28):\n",
- " await asyncio.sleep(0.2)\n",
- " yield value\n",
+ "ChatMessage.default_avatars[\"Wolfram\"] = \"🐺\"\n",
+ "ChatMessage.default_avatars[\"#1 good-to-go guy\"] = \"👍\"\n",
"\n",
- "ChatMessage(replace_response)"
+ "pn.Column(\n",
+ " ChatMessage(\"Mathematics!\", user=\"Wolfram\"),\n",
+ " ChatMessage(\"Good to go!\", user=\"#1 Good-to-Go Guy\"),\n",
+ " ChatMessage(\"What's up?\", user=\"Other Guy\"),\n",
+ " max_width=300,\n",
+ ")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "This example will show you how to **append** to the `ChatMessage` `value`. "
+ "The `timestamp` can be formatted using `timestamp_format`. Additionally, a timestamp_tz can be provided to use any timezones supported by `zoneinfo`."
]
},
{
@@ -504,19 +534,24 @@
"metadata": {},
"outputs": [],
"source": [
- "sentence = \"\"\"\n",
- " The greatest glory in living lies not in never falling,\n",
- " but in rising every time we fall.\n",
- "\"\"\"\n",
- "\n",
- "async def append_response():\n",
- " value = \"\"\n",
- " for token in sentence.split():\n",
- " value += f\" {token}\"\n",
- " await asyncio.sleep(0.2)\n",
- " yield value\n",
- "\n",
- "ChatMessage(append_response, user=\"Wise guy\", avatar=\"🤓\")"
+ "pn.chat.ChatMessage(timestamp_format=\"%b %d, %Y %I:%M %p\", timestamp_tz=\"US/Pacific\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `ChatMessage` can serialized into a string."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "widget = pn.widgets.FloatSlider(value=3, name=\"Number selected\")\n",
+ "pn.chat.ChatMessage(widget).serialize()"
]
},
{
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/dist/css/button.css b/panel/dist/css/button.css
index 5ae415e813..b86c067213 100644
--- a/panel/dist/css/button.css
+++ b/panel/dist/css/button.css
@@ -140,12 +140,20 @@
.bk-btn a {
align-items: center;
display: inline;
+ flex-grow: 1;
height: 100%;
justify-content: center;
padding: 6px;
- width: 100%;
}
:host(.bk-panel-models-widgets-FileDownload) .bk-btn {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
padding: 0px;
}
+
+:host(.bk-panel-models-widgets-FileDownload) .bk-btn .bk-TablerIcon {
+ margin-left: 0.5em;
+}
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/jupyter_executor.py b/panel/io/jupyter_executor.py
index e123d9b4db..7def6193c6 100644
--- a/panel/io/jupyter_executor.py
+++ b/panel/io/jupyter_executor.py
@@ -55,6 +55,8 @@ class PanelExecutor(WSHandler):
to send and receive messages to and from the frontend.
"""
+ _tasks = set()
+
def __init__(self, path, token, root_url):
self.path = path
self.token = token
@@ -98,7 +100,9 @@ def _set_state(self):
state.rel_path = self.root_url
def _receive_msg(self, msg) -> None:
- asyncio.ensure_future(self._receive_msg_async(msg))
+ task = asyncio.ensure_future(self._receive_msg_async(msg))
+ self._tasks.add(task)
+ task.add_done_callback(self._tasks.discard)
async def _receive_msg_async(self, msg) -> None:
try:
diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py
index 4c866f4e0d..876c8924ef 100644
--- a/panel/io/jupyter_server_extension.py
+++ b/panel/io/jupyter_server_extension.py
@@ -331,6 +331,8 @@ class PanelWSProxy(WSHandler, JupyterHandler):
receives Bokeh protocol messages via a Jupyter Comm.
"""
+ _tasks = set()
+
def __init__(self, tornado_app, *args, **kw) -> None:
# Note: tornado_app is stored as self.application
kw['application_context'] = None
@@ -398,7 +400,9 @@ async def open(self, path, *args, **kwargs) -> None:
await self.send_message(msg)
self._ping_job.start()
- asyncio.create_task(self._check_for_message())
+ task = asyncio.create_task(self._check_for_message())
+ self._tasks.add(task)
+ task.add_done_callback(self._tasks.discard)
async def _check_for_message(self):
while True:
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 = ""
diff --git a/panel/io/pyodide.py b/panel/io/pyodide.py
index a774764300..cd7eca8a57 100644
--- a/panel/io/pyodide.py
+++ b/panel/io/pyodide.py
@@ -114,10 +114,14 @@ def _read_json(*args, **kwargs):
return _read_json_original(*args, **kwargs)
pandas.read_json = _read_json
+_tasks = set()
+
def async_execute(func: Any):
event_loop = asyncio.get_running_loop()
if event_loop.is_running():
- asyncio.create_task(func())
+ task = asyncio.create_task(func())
+ _tasks.add(task)
+ task.add_done_callback(_tasks.discard)
else:
event_loop.run_until_complete(func())
return
@@ -410,7 +414,9 @@ def render_script(obj: Any, target: str) -> str:
render_item.roots._roots[root] = target
document.getElementById(target).classList.add('bk-root')
script = script_for_render_items(docs_json, [render_item])
- asyncio.create_task(_link_model(doc.roots[0].ref['id'], doc))
+ task = asyncio.create_task(_link_model(doc.roots[0].ref['id'], doc))
+ _tasks.add(task)
+ task.add_done_callback(_tasks.discard)
return wrap_in_script_tag(script)
def serve(*args, **kwargs):
diff --git a/panel/io/resources.py b/panel/io/resources.py
index 169ad3772a..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)
@@ -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/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()
+ }
}
}
diff --git a/panel/models/layout.ts b/panel/models/layout.ts
index 95d06128eb..0d9c5f1c5d 100644
--- a/panel/models/layout.ts
+++ b/panel/models/layout.ts
@@ -34,16 +34,16 @@ export class PanelMarkupView extends WidgetView {
for (const sts of this._applied_stylesheets) {
const style_el = (sts as any).el
if (style_el instanceof HTMLLinkElement) {
- this._initialized_stylesheets[style_el.href] = false
- style_el.addEventListener("load", () => {
- this._initialized_stylesheets[style_el.href] = true
- if (
- Object.values(this._initialized_stylesheets).every(Boolean)
- )
- this.style_redraw()
- })
+ this._initialized_stylesheets[style_el.href] = false
+ style_el.addEventListener('load', () => {
+ this._initialized_stylesheets[style_el.href] = true
+ if (Object.values(this._initialized_stylesheets).every(Boolean))
+ this.style_redraw()
+ })
}
}
+ if (Object.keys(this._initialized_stylesheets).length === 0)
+ this.style_redraw()
}
style_redraw(): void {
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/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/pane/image.py b/panel/pane/image.py
index 4a31f56e98..144b263b5d 100644
--- a/panel/pane/image.py
+++ b/panel/pane/image.py
@@ -23,6 +23,8 @@
if TYPE_CHECKING:
from bokeh.model import Model
+_tasks = set()
+
class FileBase(HTMLBasePane):
embed = param.Boolean(default=False, doc="""
@@ -110,7 +112,9 @@ def _data(self, obj: Any) -> bytes | None:
from pyodide.http import pyfetch
async def replace_content():
self.object = await (await pyfetch(obj)).bytes()
- asyncio.create_task(replace_content())
+ task = asyncio.create_task(replace_content())
+ _tasks.add(task)
+ task.add_done_callback(_tasks.discard)
else:
import requests
r = requests.request(url=obj, method='GET')
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/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 @@