diff --git a/.travis.yml b/.travis.yml index 27d4462678..3f3704587c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,6 +52,7 @@ jobs: stage: test env: DESC="dev test_all" before_install: + - python --version # install doit/pyctdev and use to install miniconda... - pip install pyctdev && doit miniconda_install && pip uninstall -y doit pyctdev - export PATH="$HOME/miniconda/bin:$PATH" && hash -r diff --git a/examples/reference/indicators/BooleanStatus.ipynb b/examples/reference/indicators/BooleanStatus.ipynb new file mode 100644 index 0000000000..010be0e7fa --- /dev/null +++ b/examples/reference/indicators/BooleanStatus.ipynb @@ -0,0 +1,103 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "\n", + "from panel.widgets.indicators import BooleanStatus\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``BooleanStatus`` is a boolean indicator providing a visual representation of a boolean status as filled or non-filled circle. If the `value` is set to `True` the indicator will be filled while setting it to `False` will cause it to be non-filled.\n", + "\n", + "#### Parameters:\n", + "\n", + "For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n", + "\n", + "* **``color``** (str): The color of the bar, one of 'primary', 'secondary', 'success', 'info', 'warn', 'danger', 'light', 'dark'\n", + "* **``value``** (int or None): Whether the status indicator is filled or not.\n", + "\n", + "___" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `BooleanStatus` widget can be instantiated as either `False` or `True`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "false_status = BooleanStatus(value=False)\n", + "true_status = BooleanStatus(value=True)\n", + "\n", + "pn.Row(false_status, true_status)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `BooleanStatus` indicator also supports a range of colors:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = pn.GridBox('', 'False', 'True', ncols=3)\n", + "\n", + "for color in BooleanStatus.param.color.objects:\n", + " false = BooleanStatus(width=50, height=50, value=False, color=color)\n", + " true = BooleanStatus(width=50, height=50, value=True, color=color)\n", + " grid.extend((color, false, true))\n", + "\n", + "grid" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/reference/indicators/LoadingSpinner.ipynb b/examples/reference/indicators/LoadingSpinner.ipynb new file mode 100644 index 0000000000..ab876a6bff --- /dev/null +++ b/examples/reference/indicators/LoadingSpinner.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "from panel.widgets.indicators import LoadingSpinner\n", + "\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``LoadingSpinner`` is a boolean indicator providing a visual representation of the loading status. If the `value` is set to `True` the spinner will rotate while setting it to `False` will disable the rotating segment.\n", + "\n", + "#### Parameters:\n", + "\n", + "For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n", + "\n", + "* **``bgcolor``** (str): The color of spinner background segment, either 'light' or 'dark'\n", + "* **``color``** (str): The color of the spinning segment, one of 'primary', 'secondary', 'success', 'info', 'warn', 'danger', 'light', 'dark'\n", + "* **``value``** (boolean): Whether the indicator is spinning or not.\n", + "\n", + "___" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `LoadingSpinner` can be instantiated in a spinning or idle state:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "idle = LoadingSpinner(value=False, width=100, height=100)\n", + "loading = LoadingSpinner(value=True, width=100, height=100)\n", + "\n", + "pn.Row(idle, loading)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `LoadingSpinner` indicator also supports a range of spinner colors and backgrounds:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = pn.GridBox('', 'light', 'dark', ncols=3)\n", + "\n", + "for color in LoadingSpinner.param.color.objects:\n", + " dark = LoadingSpinner(width=50, height=50, value=True, color=color, bgcolor='dark')\n", + " light = LoadingSpinner(width=50, height=50, value=True, color=color, bgcolor='light')\n", + " grid.extend((color, light, dark))\n", + "\n", + "grid" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/panel/_styles/widgets.css b/panel/_styles/widgets.css index 39e5a88ed7..cc311e68b3 100644 --- a/panel/_styles/widgets.css +++ b/panel/_styles/widgets.css @@ -139,3 +139,150 @@ progress:not([value])::before { from {background-position: 0%} to {background-position: 100%} } + +.bk.loader::after { + content: ""; + border-radius: 50%; + -webkit-mask-image: radial-gradient(transparent 50%, rgba(0, 0, 0, 1) 54%); + width: 100%; + height: 100%; + left: 0; + top: 0; + position: absolute; +} + +.bk-root .bk.loader.dark::after { + background: #0f0f0f; +} + +.bk-root .bk.loader.light::after { + background: #f0f0f0; +} + +.bk-root .bk.loader.spin::after { + animation: spin 2s linear infinite; +} + +.bk-root div.bk.loader.spin.primary-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #007bff 50%); +} + +.bk-root div.bk.loader.spin.secondary-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #6c757d 50%); +} + +.bk-root div.bk.loader.spin.success-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #28a745 50%); +} + +.bk-root div.bk.loader.spin.danger-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #dc3545 50%); +} + +.bk-root div.bk.loader.spin.warning-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #ffc107 50%); +} + +.bk-root div.bk.loader.spin.info-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #17a2b8 50%); +} + +.bk-root div.bk.loader.spin.light-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #f8f9fa 50%); +} + +.bk-root div.bk.loader.dark-light::after { + background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #343a40 50%); +} + +.bk-root div.bk.loader.spin.primary-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #007bff 50%); +} + +.bk-root div.bk.loader.spin.secondary-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #6c757d 50%); +} + +.bk-root div.bk.loader.spin.success-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #28a745 50%); +} + +.bk-root div.bk.loader.spin.danger-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #dc3545 50%) +} + +.bk-root div.bk.loader.spin.warning-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #ffc107 50%); +} + +.bk-root div.bk.loader.spin.info-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #17a2b8 50%); +} + +.bk-root div.bk.loader.spin.light-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #f8f9fa 50%); +} + +.bk-root div.bk.loader.spin.dark-dark::after { + background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #343a40 50%); +} + +/* Safari */ +@-webkit-keyframes spin { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.dot div { + height: 100%; + width: 100%; + border: 1px solid #000 !important; + background-color: #fff; + border-radius: 50%; + display: inline-block; +} + +.dot-filled div { + height: 100%; + width: 100%; + border: 1px solid #000 !important; + border-radius: 50%; + display: inline-block; +} + +.dot-filled.primary div { + background-color: #007bff; +} + +.dot-filled.secondary div { + background-color: #6c757d; +} + +.dot-filled.success div { + background-color: #28a745; +} + +.dot-filled.danger div { + background-color: #dc3545; +} + +.dot-filled.warning div { + background-color: #ffc107; +} + +.dot-filled.info div { + background-color: #17a2b8; +} + +.dot-filled.dark div { + background-color: #343a40; +} + +.dot-filled.light div { + background-color: #f8f9fa; +} \ No newline at end of file diff --git a/panel/io/state.py b/panel/io/state.py index 0f0e0a724f..cdcd454989 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -117,7 +117,7 @@ def sync_busy(self, indicator): """ self._indicators.append(indicator) - @param.depends('busy') + @param.depends('busy', watch=True) def _update_busy(self): for indicator in self._indicators: indicator.value = self.busy diff --git a/panel/template/base.py b/panel/template/base.py index 462a8b2d53..0e9298aacc 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -27,18 +27,20 @@ from ..pane import panel as _panel, HTML, Str, HoloViews from ..viewable import ServableMixin, Viewable from ..widgets import Button +from ..widgets.indicators import BooleanIndicator, LoadingSpinner from .theme import DefaultTheme, Theme _server_info = ( 'Running server: ' - 'https://localhost:{port}') + 'https://localhost:{port}' +) class BaseTemplate(param.Parameterized, ServableMixin): # Dictionary of property overrides by bokeh Model type _modifiers = {} - + __abstract = True def __init__(self, template=None, items=None, nb_template=None, **params): @@ -84,11 +86,18 @@ def __repr__(self): return template.format( cls=cls, objs=('%s' % spacer).join(objs), spacer=spacer) - def _apply_modifiers(self, viewable, mref): + @classmethod + def _apply_hooks(cls, viewable, root): + ref = root.ref['id'] + for o in viewable.select(): + cls._apply_modifiers(o, ref) + + @classmethod + def _apply_modifiers(cls, viewable, mref): if mref not in viewable._models: return model, _ = viewable._models[mref] - modifiers = self._modifiers.get(type(viewable), {}) + modifiers = cls._modifiers.get(type(viewable), {}) child_modifiers = modifiers.get('children', {}) if child_modifiers: for child in viewable: @@ -97,11 +106,15 @@ def _apply_modifiers(self, viewable, mref): if getattr(child, k) == child.param[k].default } child.param.set_param(**child_params) + child_props = child._process_param_change(child_params) + child._models[mref][0].update(**child_props) params = { k: v for k, v in modifiers.items() if k != 'children' and getattr(viewable, k) == viewable.param[k].default } viewable.param.set_param(**params) + props = viewable._process_param_change(params) + model.update(**props) def _apply_root(self, name, viewable, tags): pass @@ -114,6 +127,8 @@ def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=Tr preprocess_root = col.get_root(doc, comm) ref = preprocess_root.ref['id'] for name, (obj, tags) in self._render_items.items(): + if self._apply_hooks not in obj._hooks: + obj._hooks.append(self._apply_hooks) model = obj.get_root(doc, comm) mref = model.ref['id'] doc.on_session_destroyed(obj._server_destroy) @@ -129,8 +144,6 @@ def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=Tr model.name = name model.tags = tags self._apply_root(name, model, tags) - for o in obj.select(): - self._apply_modifiers(o, mref) add_to_doc(model, doc, hold=bool(comm)) state._fake_roots.append(ref) @@ -303,6 +316,10 @@ class BasicTemplate(BaseTemplate): feel without having to write any Jinja2 template themselves. """ + busy_indicator = param.ClassSelector(default=LoadingSpinner(width=20, height=20), + class_=BooleanIndicator, constant=True, doc=""" + Visual indicator of application busy state.""") + header = param.ClassSelector(class_=ListLike, constant=True, doc=""" A list-like container which populates the header bar.""") @@ -312,6 +329,9 @@ class BasicTemplate(BaseTemplate): sidebar = param.ClassSelector(class_=ListLike, constant=True, doc=""" A list-like container which populates the sidebar.""") + modal = param.ClassSelector(class_=ListLike, constant=True, doc=""" + A list-like container which populates the modal""") + title = param.String(doc="A title to show in the header.") header_background = param.String(doc="Optional header background color override") @@ -337,11 +357,23 @@ def __init__(self, **params): params['main'] = ListLike() if 'sidebar' not in params: params['sidebar'] = ListLike() + if 'modal' not in params: + params['modal'] = ListLike() super(BasicTemplate, self).__init__(template=template, **params) + if self.busy_indicator: + state.sync_busy(self.busy_indicator) + self._js_area = HTML(margin=0, width=0, height=0) + self._render_items['js_area'] = (self._js_area, []) self._update_vars() + self._update_busy() self.main.param.watch(self._update_render_items, ['objects']) + self.modal.param.watch(self._update_render_items, ['objects']) self.sidebar.param.watch(self._update_render_items, ['objects']) self.header.param.watch(self._update_render_items, ['objects']) + self.main.param.trigger('objects') + self.sidebar.param.trigger('objects') + self.header.param.trigger('objects') + self.modal.param.trigger('objects') self.param.watch(self._update_vars, ['title', 'header_background', 'header_color']) @@ -369,17 +401,30 @@ def _update_vars(self, *args): self._render_variables['header_background'] = self.header_background self._render_variables['header_color'] = self.header_color + def _update_busy(self): + if self.busy_indicator: + self._render_items['busy_indicator'] = (self.busy_indicator, []) + elif 'busy_indicator' in self._render_items: + del self._render_items['busy_indicator'] + self._render_variables['busy'] = self.busy_indicator is not None + def _update_render_items(self, event): + if event.obj is self and event.name == 'busy_indicator': + return self._update_busy() if event.obj is self.main: tag = 'main' elif event.obj is self.sidebar: tag = 'nav' elif event.obj is self.header: tag = 'header' + elif event.obj is self.modal: + tag = 'modal' + for obj in event.old: ref = str(id(obj)) if obj not in event.new and ref in self._render_items: del self._render_items[ref] + labels = {} for obj in event.new: ref = str(id(obj)) @@ -391,6 +436,27 @@ def _update_render_items(self, event): self._render_variables['header'] = any('header' in ts for ts in tags) self._render_variables['root_labels'] = labels + def open_modal(self): + """ + Opens the modal area + """ + self._js_area.object = """ + + """ + + def close_modal(self): + """ + Closes the modal area + """ + self._js_area.object = """ + + """ class Template(BaseTemplate): diff --git a/panel/template/bootstrap/bootstrap.css b/panel/template/bootstrap/bootstrap.css index f0437e8894..a65fb8bfc6 100644 --- a/panel/template/bootstrap/bootstrap.css +++ b/panel/template/bootstrap/bootstrap.css @@ -36,3 +36,42 @@ a.navbar-brand { p.bk.card-button { display: none; } + +.pn-modal { + overflow-y: scroll; + width: 100%; + display: none; + position: absolute; + top: 0; + left: 0; +} + +.pn-modal-content { + background-color: #fefefe; + margin: auto; + margin-top: 25px; + margin-bottom: 25px; + padding: 15px 20px 20px 20px; + border: 1px solid #888; + width: 80% !important; +} + +.pn-modal-close { + position: absolute; + right: 25px; + z-index: 100; +} + +.pn-modal-close:hover, +.pn-modal-close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.pn-busy-container { + align-items: center; + justify-content: center; + display: flex; + margin-left: auto; +} diff --git a/panel/template/bootstrap/bootstrap.html b/panel/template/bootstrap/bootstrap.html index 6648e30135..5a0a4f5d2e 100644 --- a/panel/template/bootstrap/bootstrap.html +++ b/panel/template/bootstrap/bootstrap.html @@ -5,7 +5,6 @@ - {% endblock %} @@ -25,6 +24,11 @@ {% endif %} {% endfor %} {% endfor %} + {% if busy %} +
+ {{ embed(roots.busy_indicator) | indent(6) }} +
+ {% endif %}
@@ -50,6 +54,18 @@ {% endif %} {% endfor %} {% endfor %} +
+
+ × + {% for doc in docs %} + {% for root in doc.roots %} + {% if "modal" in root.tags %} + {{ embed(root) | indent(6) }} + {% endif %} + {% endfor %} + {% endfor %} +
+
@@ -63,5 +79,22 @@ setTimeout(function () { clearInterval(interval) }, 210) }); }); + + var modal = document.getElementById("pn-Modal"); + var span = document.getElementById("pn-closeModal"); + + span.onclick = function() { + modal.style.display = "none"; + } + + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } + } + +{{ embed(roots.js_area) }} + {% endblock %} + diff --git a/panel/template/golden/golden.css b/panel/template/golden/golden.css index df54c72dcb..0947059d5b 100644 --- a/panel/template/golden/golden.css +++ b/panel/template/golden/golden.css @@ -65,4 +65,44 @@ p.bk.golden-card-button { .bk-canvas { padding-right: 2px !important; -} \ No newline at end of file +} + + +.pn-modal { + overflow-y: scroll; + width: 100%; + position: absolute; + display: none; + top: 0; + left: 0; +} + +.pn-modal-content { + background-color: #fefefe; + margin: auto; + margin-top: 25px; + margin-bottom: 25px; + padding: 15px 20px 20px 20px; + border: 1px solid #888; + width: 80% !important; +} + +.pn-modal-close { + position: absolute; + right: 25px; + z-index: 100; +} + +.pn-modal-close:hover, +.pn-modal-close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.pn-busy-container { + align-items: center; + justify-content: center; + display: flex; + margin-left: auto; +} diff --git a/panel/template/golden/golden.html b/panel/template/golden/golden.html index e01f76c533..75cf4f0a34 100644 --- a/panel/template/golden/golden.html +++ b/panel/template/golden/golden.html @@ -1,35 +1,52 @@ -{% extends base %} +{% extends base %} - + {% block preamble %} - - - + + + {% endblock %} - -{% block contents %} - + +{% block contents %} + -
-
-
+
+
+
+
+ × + {% for doc in docs %} + {% for root in doc.roots %} + {% if "modal" in root.tags %} + {{ embed(root) | indent(6) }} + {% endif %} + {% endfor %} + {% endfor %} +
+
+
- +{{ embed(roots.js_area) }} + {% endblock %} diff --git a/panel/template/material/material.css b/panel/template/material/material.css index 1ff0524d03..41ecfc18b7 100644 --- a/panel/template/material/material.css +++ b/panel/template/material/material.css @@ -22,6 +22,12 @@ body { z-index: 7; } +.pn-busy-container { + align-items: center; + justify-content: center; + display: flex; +} + button.mdc-button.mdc-card-button { color: transparent; height: 50px; @@ -39,3 +45,35 @@ div.bk.mdc-card { font-size: 1.5em; font-weight: bold; } + +.pn-modal { + overflow-y: scroll; + width: 100%; + display: none; + position: absolute; + top: 0; + left: 0; +} + +.pn-modal-content { + background-color: #fefefe; + margin: auto; + margin-top: 25px; + margin-bottom: 25px; + padding: 15px 20px 20px 20px; + border: 1px solid #888; + width: 80% !important; +} + +.pn-modal-close { + position: absolute; + right: 25px; + z-index: 100; +} + +.pn-modal-close:hover, +.pn-modal-close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} diff --git a/panel/template/material/material.html b/panel/template/material/material.html index b4c9d93a50..b3a774ed51 100644 --- a/panel/template/material/material.html +++ b/panel/template/material/material.html @@ -25,6 +25,11 @@ {% endfor %} {% endfor %} + {% if busy %} +
+ {{ embed(roots.busy_indicator) | indent(6) }} +
+ {% endif %} @@ -54,12 +59,23 @@ {% endfor %} {% endfor %} + +
+
+ × + {% for doc in docs %} + {% for root in doc.roots %} + {% if "modal" in root.tags %} + {{ embed(root) | indent(4) }} + {% endif %} + {% endfor %} + {% endfor %} +
+
+{{ embed(roots.js_area) }} {% endblock %} diff --git a/panel/template/vanilla/vanilla.css b/panel/template/vanilla/vanilla.css index c51cd6f82c..967c85a2b0 100644 --- a/panel/template/vanilla/vanilla.css +++ b/panel/template/vanilla/vanilla.css @@ -1,116 +1,157 @@ body { - height: 100vh; - margin: 0px; - } - - #container { - padding:0px; - width: 100vw; - } - - #header{ - padding: 10px; - } - - .title{ - color: #fff; - padding-left: 10px; - text-decoration: none; - text-decoration-line: none; - text-decoration-style: initial; - text-decoration-color: initial; - font-weight: 400; - font-size: 28px; - } - - .bk-canvas { - padding-right: 2px !important; - } - - #content { - height: 100vh; - margin: 0px; - width: 100vw; - display: inline-flex; - } - - #sidebar { - transition: all 0.2s cubic-bezier(0.945, 0.020, 0.270, 0.665); - transform-origin: center left; /* Set the transformed position of sidebar to center left side. */ - width: 15vw; - margin-top: 60px; - } - - #sidebar.active { - margin-left: -16.7%; - } - - #main { - overflow-y: scroll; - width: 100vw; - margin-left: 15vw; - } - - #sidebarCollapse { - background: none; - border: none; - } - - a.navbar-brand { - padding-left: 10px; - } - - p.bk.card-button { - display: none; - } - - body { - font-family: "Lato", sans-serif; - } - - .sidenav { - height: 100%; - width: 0; - position: absolute; - z-index: 1; - top: 0; - left: 0; - background-color: #eeeeee; - overflow-x: hidden; - transition: 0.5s; - padding-top: 15px; - } - - .bk.card-title { - position: absolute !important; - } - - .sidenav a { - padding: 8px 8px 8px 32px; - text-decoration: none; - font-size: 25px; - color: #818181; - display: block; - transition: 0.3s; - } - - .sidenav a:hover { - color: #f1f1f1; - } - - .sidenav .closebtn { - position: absolute; - top: 0; - right: 25px; - font-size: 36px; - margin-left: 50px; - } - - @media screen and (max-height: 450px) { - .sidenav {padding-top: 15px;} - .sidenav a {font-size: 18px;} - } - - .nav.flex-column { - padding-inline-start: 0px; - } \ No newline at end of file + height: 100vh; + margin: 0px; + font-family: "Lato", sans-serif; +} + +#container { + padding:0px; + width: 100vw; +} + +#header{ + display: flex; + align-items: center; + padding: 10px; +} + +.title { + color: #fff; + padding-left: 10px; + text-decoration: none; + text-decoration-line: none; + text-decoration-style: initial; + text-decoration-color: initial; + font-weight: 400; + font-size: 28px; +} + +.pn-bar { + width: 20px; + height: 2px; + background-color: white; + margin: 4px 0; +} + +.bk-canvas { + padding-right: 2px !important; +} + +#content { + height: 100%; + margin: 0px; + width: 100vw; + display: inline-flex; +} + +#sidebar { + transition: all 0.2s cubic-bezier(0.945, 0.020, 0.270, 0.665); + transform-origin: center left; /* Set the transformed position of sidebar to center left side. */ + width: 16.7vw; +} + +#sidebar.active { + margin-left: -16.7%; +} + +#main { + transition: all 0.2s cubic-bezier(0.945, 0.020, 0.270, 0.665); + overflow-y: scroll; + width: 100vw; + margin-left: 16.7vw; + overflow-y: scroll; +} + +#sidebarCollapse { + background: none; + border: none; +} + +a.navbar-brand { + padding-left: 10px; +} + +p.bk.card-button { + display: none; +} + +.sidenav { + height: 100%; + overflow-x: hidden; + padding-top: 15px; + position: absolute; + overflow-y: scroll; +} + +.bk.card-title { + position: absolute !important; +} + +.sidenav a { + padding: 8px 8px 8px 32px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; + transition: 0.3s; +} + +.sidenav a:hover { + color: #f1f1f1; +} + +.sidenav .closebtn { + position: absolute; + top: 0; + right: 25px; + font-size: 36px; + margin-left: 50px; +} + +@media screen and (max-height: 450px) { + .sidenav {padding-top: 15px;} + .sidenav a {font-size: 18px;} +} + +.nav.flex-column { + padding-inline-start: 0px; +} + +.pn-modal { + overflow-y: scroll; + width: 100%; + position: absolute; + display: none; + top: 0; + left: 0; +} + +.pn-modal-content { + background-color: #fefefe; + margin: auto; + margin-top: 25px; + margin-bottom: 25px; + padding: 15px 20px 20px 20px; + border: 1px solid #888; + width: 80% !important; +} + +.pn-modal-close { + position: absolute; + right: 25px; + z-index: 100; +} + +.pn-modal-close:hover, +.pn-modal-close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.pn-busy-container { + align-items: center; + justify-content: center; + display: flex; + margin-left: auto; +} diff --git a/panel/template/vanilla/vanilla.html b/panel/template/vanilla/vanilla.html index 592c68a64e..a2471b7948 100644 --- a/panel/template/vanilla/vanilla.html +++ b/panel/template/vanilla/vanilla.html @@ -5,7 +5,11 @@
-
+
{% if nav %} {% endif %} -
+
{% for doc in docs %} {% for root in doc.roots %} {% if "main" in root.tags %} @@ -40,23 +51,55 @@ {% endif %} {% endfor %} {% endfor %} + +
+
+ × + {% for doc in docs %} + {% for root in doc.roots %} + {% if "modal" in root.tags %} + {{ embed(root) | indent(10) }} + {% endif %} + {% endfor %} + {% endfor %} +
+
+ + +{{ embed(roots.js_area) }} + {% endblock %} diff --git a/panel/tests/test_template.py b/panel/tests/test_template.py index b91a6aaa72..9c09472cc5 100644 --- a/panel/tests/test_template.py +++ b/panel/tests/test_template.py @@ -9,10 +9,12 @@ hv = None import param +import pytest from panel.layout import Row -from panel.pane import HoloViews +from panel.pane import HoloViews, Markdown from panel.template import Template +from panel.template.base import BasicTemplate from panel.widgets import FloatSlider from .util import hv_available @@ -75,3 +77,48 @@ def test_template_session_destroy(document, comm): assert len(row._models) == 0 assert len(row[0]._models) == 0 assert len(row[1]._models) == 0 + + +@pytest.mark.parametrize('template', list(param.concrete_descendents(BasicTemplate).values())) +def test_basic_template(template, document, comm): + tmplt = template(title='BasicTemplate', header_background='blue', header_color='red') + + tvars = tmplt._render_variables + + assert tvars['app_title'] == 'BasicTemplate' + assert tvars['header_background'] == 'blue' + assert tvars['header_color'] == 'red' + assert tvars['nav'] == False + assert tvars['busy'] == True + assert tvars['header'] == False + + titems = tmplt._render_items + + assert titems['busy_indicator'] == (tmplt.busy_indicator, []) + + markdown = Markdown('# Some title') + tmplt.main.append(markdown) + + assert titems[str(id(markdown))] == (markdown, ['main']) + + slider = FloatSlider() + tmplt.sidebar.append(slider) + + assert titems[str(id(slider))] == (slider, ['nav']) + assert tvars['nav'] == True + + tmplt.sidebar[:] = [] + assert tvars['nav'] == False + assert str(id(slider)) not in titems + + subtitle = Markdown('## Some subtitle') + tmplt.header.append(subtitle) + + assert titems[str(id(subtitle))] == (subtitle, ['header']) + assert tvars['header'] == True + + tmplt.header[:] = [] + assert str(id(subtitle)) not in titems + assert tvars['header'] == False + + diff --git a/panel/viewable.py b/panel/viewable.py index 5fedf04f47..cb632fd07f 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -470,6 +470,11 @@ class Viewable(Renderable, Layoutable, ServableMixin): _preprocessing_hooks = [] + def __init__(self, **params): + hooks = params.pop('hooks', []) + super().__init__(**params) + self._hooks = hooks + def __repr__(self, depth=0): return '{cls}({params})'.format(cls=type(self).__name__, params=', '.join(param_reprs(self))) diff --git a/panel/widgets/indicators.py b/panel/widgets/indicators.py new file mode 100644 index 0000000000..4b5cb8db37 --- /dev/null +++ b/panel/widgets/indicators.py @@ -0,0 +1,83 @@ +import param + +from ..models import HTML +from .base import Widget + + +class Indicator(Widget): + """ + Indicator is a baseclass for widgets which indicate some state. + """ + + sizing_mode = param.ObjectSelector(default='fixed', objects=[ + 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', + 'scale_width', 'scale_height', 'scale_both', None]) + + __abstract = True + + +class BooleanIndicator(Indicator): + + value = param.Boolean(default=False, doc=""" + Whether the indicator is active or not.""") + + __abstract = True + + +class BooleanStatus(BooleanIndicator): + + color = param.ObjectSelector(default='dark', objects=[ + 'primary', 'secondary', 'success', 'info', 'danger', 'warning', + 'light', 'dark']) + + height = param.Integer(default=20, doc=""" + height of the circle.""") + + width = param.Integer(default=20, doc=""" + Width of the circle.""") + + value = param.Boolean(default=False, doc=""" + Whether the indicator is active or not.""") + + _widget_type = HTML + + _rename = {'color': None} + + def _process_param_change(self, msg): + msg = super()._process_param_change(msg) + value = msg.pop('value', None) + if value is None: + return msg + msg['css_classes'] = ['dot-filled', self.color] if value else ['dot'] + return msg + + +class LoadingSpinner(BooleanIndicator): + + bgcolor = param.ObjectSelector(default='light', objects=['dark', 'light']) + + color = param.ObjectSelector(default='dark', objects=[ + 'primary', 'secondary', 'success', 'info', 'danger', 'warning', + 'light', 'dark']) + + height = param.Integer(default=125, doc=""" + height of the circle.""") + + width = param.Integer(default=125, doc=""" + Width of the circle.""") + + value = param.Boolean(default=False, doc=""" + Whether the indicator is active or not.""") + + _widget_type = HTML + + _rename = {'color': None, 'bgcolor': None} + + def _process_param_change(self, msg): + msg = super()._process_param_change(msg) + value = msg.pop('value', None) + if value is None: + return msg + color_cls = f'{self.color}-{self.bgcolor}' + msg['css_classes'] = ['loader', 'spin', color_cls] if value else ['loader', self.bgcolor] + return msg diff --git a/tox.ini b/tox.ini index 495083a883..9c10f01477 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ [tox] # python version test group extra envs extra commands -envlist = {py27,py36,py37,py38}-{flakes,unit,unit_deploy,examples,all_recommended,deprecations}-{default}-{dev,pkg} +envlist = {py36,py37,py38,py39}-{flakes,unit,unit_deploy,examples,all_recommended,deprecations}-{default}-{dev,pkg} build = wheel [_flakes]