5 | This is a placeholder for your app's custom HTML. Edit it by changing the theme assets.
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | SPDX-License-Identifier: MIT
2 |
3 | Copyright (c) 2021 The Anvil Extras project team members listed at
4 | https://github.com/anvilistas/anvil-extras/graphs/contributors
5 |
6 | This software is published at https://github.com/anvilistas/anvil-extras
7 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # This repository is an Anvil app. Learn more at https://anvil.works/
3 | # To run the server-side code on your own machine, run:
4 | # pip install anvil-uplink
5 | # python -m anvil.run_app_via_uplink YourAppPackageName
6 |
7 | __path__ = [__path__[0] + "/server_code", __path__[0] + "/client_code"]
8 |
--------------------------------------------------------------------------------
/anvil.yaml:
--------------------------------------------------------------------------------
1 | services: []
2 | startup: {type: form, module: Demo}
3 | package_name: anvil_extras
4 | allow_embedding: false
5 | name: anvil_extras
6 | runtime_options: {version: 2, client_version: '3', server_version: python3-full}
7 | metadata: {}
8 | startup_form: Demo
9 | native_deps: {head_html: ''}
10 | db_schema: []
11 | renamed: true
12 |
--------------------------------------------------------------------------------
/client_code/routing/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | __version__ = "1.7.1"
9 |
10 | from ._routing import *
11 |
--------------------------------------------------------------------------------
/docs/guides/components/switch.rst:
--------------------------------------------------------------------------------
1 | Switch
2 | ======
3 | A material design switch. A subclass of CheckBox.
4 |
5 | Properties
6 | ----------
7 |
8 | :checked: boolean
9 |
10 | :checked_color: Color
11 |
12 | The background colour of the switch when it is checked
13 |
14 | Events
15 | ------
16 |
17 | :changed:
18 |
19 | Raised whenever the switch is clicked
20 |
--------------------------------------------------------------------------------
/docs/guides/components/message_pill.rst:
--------------------------------------------------------------------------------
1 | MessagePill
2 | ===========
3 | A rounded text label with background colour and icon in one of four levels.
4 |
5 | .. image:: /images/message_pill.png
6 |
7 | Properties
8 | ----------
9 |
10 | :level: string
11 |
12 | "info", "success", "warning" or "error"
13 |
14 | :message: string
15 |
16 | The text to be displayed
17 |
--------------------------------------------------------------------------------
/docs/guides/components/indeterminate_progress_bar.rst:
--------------------------------------------------------------------------------
1 | Indeterminate ProgressBar
2 | =========================
3 | A linear progress bar to indicate processing of unknown duration.
4 |
5 | Properties
6 | ----------
7 |
8 | :track_colour: Color
9 |
10 | The colour of the background track
11 |
12 | :indicator_colour: Color
13 |
14 | The colour of the progress indicator bar
15 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.7.1
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:glob:client_code/**/*.py]
7 |
8 | [bumpversion:glob:server_code/**/*.py]
9 |
10 | [isort]
11 | known_first_party = anvil_extras
12 | known_anvil = anvil
13 | sections = FUTURE, STDLIB, ANVIL, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
14 | line_length = 88
15 | include_trailing_comma = True
16 | multi_line_output = 3
17 |
--------------------------------------------------------------------------------
/docs/guides/components/determinate_progress_bar.rst:
--------------------------------------------------------------------------------
1 | Determinate ProgressBar
2 | =======================
3 | A linear progress bar displaying completion towards a known target.
4 |
5 | Properties
6 | ----------
7 |
8 | :track_colour: Color
9 |
10 | The colour of the background track
11 |
12 | :indicator_colour: Color
13 |
14 | The colour of the progress indicator bar
15 |
16 | :progress: Number
17 |
18 | Between 0 and 1 to indicate progress
19 |
--------------------------------------------------------------------------------
/js/designer_components/index.ts:
--------------------------------------------------------------------------------
1 | export { DesignerComponent } from "./DesignerComponent.ts";
2 | export { DesignerMultiSelectDropDown } from "./DesignerMultSelectDropDown.ts";
3 | export { DesignerQuill } from "./DesignerQuill.ts";
4 | export { DesignerSlider } from "./DesignerSlider.ts";
5 | export { DesignerSwitch } from "./DesignerSwitch.ts";
6 | export { DesignerTabs } from "./DesignerTabs.ts";
7 | export { DesignerChip, DesignerChipsInput } from "./DesignerChips.ts";
8 | export { DesignerPivot } from "./DesignerPivot.ts";
9 |
--------------------------------------------------------------------------------
/docs/guides/components/editable_card.rst:
--------------------------------------------------------------------------------
1 | EditableCard
2 | ============
3 | A card to display a value and allow it to be edited by clicking.
4 |
5 | Properties
6 | ----------
7 |
8 | :editable: Boolean
9 |
10 | Whether the card should allow its value to be edited
11 |
12 | :icon: Icon
13 |
14 | To display in the top right corner of the card
15 |
16 | :datatype: String
17 |
18 | "text", "number", "date", "time" or "yesno"
19 | Setting this property will affect which type of component is displayed to edit the value
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | # top-most EditorConfig file
6 | root = true
7 |
8 | [*]
9 | end_of_line = lf
10 | insert_final_newline = true
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | indent_style = space
14 |
15 | [*.{py, md, rst}]
16 | indent_size = 4
17 |
18 | [*.yml]
19 | indent_size = 2
20 |
21 | [*.ts]
22 | indent_size = 4
23 | max_line_length = 119
24 |
25 | [*.js]
26 | indent_size = 4
27 | max_line_length = 119
28 |
--------------------------------------------------------------------------------
/docs/guides/components/autocomplete.rst:
--------------------------------------------------------------------------------
1 | Autocomplete
2 | ============
3 | A material design TextBox with autocomplete. A subclass of TextBox - other properties, events and methods inherited from TextBox.
4 |
5 | Properties
6 | ----------
7 |
8 | :suggestions: list[str]
9 |
10 | A list of autocomplete suggestions
11 |
12 | :suggest_if_empty: bool
13 |
14 | If True then autocomplete will show all options when the textbox is empty
15 |
16 | Events
17 | ------
18 |
19 | :suggestion_clicked:
20 |
21 | When a suggestion is clicked. If a suggestion is selected with enter the ``pressed_enter`` event fires instead.
22 |
--------------------------------------------------------------------------------
/js/designer_components/build-script.ts:
--------------------------------------------------------------------------------
1 | import * as esbuild from "https://deno.land/x/esbuild@v0.11.10/mod.js";
2 | // import * as esbuild from "esbuild";
3 | // deno run -A build-script.ts
4 |
5 | let result = await esbuild.build({
6 | entryPoints: ["index.ts"],
7 | bundle: true,
8 | format: "esm",
9 | outfile: "bundle.min.js",
10 | minify: true,
11 | });
12 |
13 | console.log("result:", result);
14 |
15 | result = await esbuild.build({
16 | entryPoints: ["index.ts"],
17 | bundle: true,
18 | format: "esm",
19 | outfile: "bundle.js",
20 | });
21 |
22 | console.log("result:", result);
23 | esbuild.stop();
24 |
--------------------------------------------------------------------------------
/docs/guides/components/pivot.rst:
--------------------------------------------------------------------------------
1 | Pivot
2 | =====
3 | A pivot table component based on https://github.com/nicolaskruchten/pivottable
4 |
5 | Properties
6 | ----------
7 |
8 | :items: list of dicts
9 |
10 | The dataset to be pivoted
11 |
12 | :rows: list of strings
13 |
14 | attribute names to prepopulate in rows area
15 |
16 | :columns: list of strings
17 |
18 | attribute names to prepopulate in columns area
19 |
20 | :values: list of strings
21 |
22 | attribute names to prepopulate in vals area (gets passed to aggregator generating function)
23 |
24 | :aggregator: string
25 |
26 | aggregator to prepopulate in dropdown (e.g. "Count" or "Sum")
27 |
--------------------------------------------------------------------------------
/client_code/MessagePill/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: Label
6 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', visible: true,
7 | text: '', font_size: null, font: '', spacing_above: small, icon_align: left, spacing_below: small,
8 | italic: false, background: '', bold: false, underline: false, icon: ''}
9 | name: label
10 | layout_properties: {grid_position: 'XOVYVY,IBMHOM'}
11 | data_bindings: []
12 | is_package: true
13 | custom_component: true
14 | properties:
15 | - {name: level, type: string, default_value: info, default_binding_prop: true}
16 | - {name: message, type: string, default_value: ''}
17 |
--------------------------------------------------------------------------------
/theme/templates.yaml:
--------------------------------------------------------------------------------
1 | - name: Standard Page
2 | description: ''
3 | img: null
4 | form:
5 | class_name: Form
6 | is_package: true
7 | container:
8 | type: HtmlTemplate
9 | properties: {html: '@theme:standard-page.html'}
10 | components:
11 | - type: ColumnPanel
12 | properties: {}
13 | name: content_panel
14 | layout_properties: {slot: default}
15 | code: "from ._anvil_designer import $NAME$Template\nfrom anvil import *\n\nclass\
16 | \ $NAME$($NAME$Template):\n\n def __init__(self, **properties):\n # Set\
17 | \ Form properties and Data Bindings.\n self.init_components(**properties)\n\
18 | \n # Any code you write here will run when the form opens.\n \n"
19 |
--------------------------------------------------------------------------------
/docs/guides/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 | Anvil-Extras is intended to be used as a dependency for other Anvil applications:
4 |
5 | * From the gear icon at the top of your app's left hand sidebar, select 'Dependencies'
6 | * In the buttons to the right of 'Add a dependency', click the 'Third Party' button
7 | * Enter the id of the Anvil-Extras app: C6ZZPAPN4YYF5NVJ
8 | * Hit enter and ensure that the library appears in your list of dependencies
9 | * Select whether you wish to use the 'Development' or 'Published' version
10 |
11 | For the published version, the dependency will be automatically updated as new versions are released.
12 | On the development version, the update will occur whenever we merge new changes into the library's code base.
13 |
14 | Whilst we wouldn't intentionally merge broken code into the development version, you should
15 | consider it unstable and not suitable for production use.
16 |
--------------------------------------------------------------------------------
/client_code/ProgressBar/Indeterminate/form_template.yaml:
--------------------------------------------------------------------------------
1 | components:
2 | - type: ColumnPanel
3 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
4 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{}',
5 | spacing_below: small, background: ''}
6 | name: indicator_panel
7 | layout_properties: {grid_position: 'BSBUXX,ZCPRGS', full_width_row: true}
8 | components: []
9 | data_bindings: []
10 | container:
11 | type: ColumnPanel
12 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
13 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{}',
14 | spacing_below: small, background: ''}
15 | is_package: true
16 | custom_component: true
17 | properties:
18 | - {name: track_colour, type: color, default_value: '#b3d4fc', default_binding_prop: true}
19 | - {name: indicator_colour, type: color, default_value: '#1976D2'}
20 |
--------------------------------------------------------------------------------
/client_code/ProgressBar/Determinate/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
4 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{}',
5 | spacing_below: small, background: ''}
6 | components:
7 | - type: ColumnPanel
8 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
9 | wrap_on: mobile, col_spacing: none, spacing_above: small, col_widths: '{}', spacing_below: small,
10 | background: ''}
11 | name: indicator_panel
12 | layout_properties: {grid_position: 'BSBUXX,ZCPRGS', full_width_row: true}
13 | components: []
14 | data_bindings: []
15 | is_package: true
16 | custom_component: true
17 | properties:
18 | - {name: track_colour, type: color, default_value: '#b3d4fc', default_binding_prop: true}
19 | - {name: indicator_colour, type: color, default_value: '#1976D2'}
20 | - {name: progress, type: number, default_value: 0.25}
21 |
--------------------------------------------------------------------------------
/client_code/Pivot/form_template.yaml:
--------------------------------------------------------------------------------
1 | components: []
2 | is_package: true
3 | custom_component: true
4 | properties:
5 | - {name: rows, type: 'text[]', default_value: '', default_binding_prop: true}
6 | - {name: columns, type: 'text[]', default_value: '', default_binding_prop: false}
7 | - {name: values, type: 'text[]', default_value: '', default_binding_prop: false}
8 | - {name: items, type: object, default_binding_prop: false}
9 | - {name: aggregator, type: string, default_value: Count, default_binding_prop: false}
10 | container:
11 | type: HtmlTemplate
12 | event_bindings: {show: form_show}
13 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
14 | role: null, html: '
15 |
22 |
23 | '}
24 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerPivot.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 |
3 | export class DesignerPivot extends DesignerComponent {
4 | static links = ["https://cdnjs.cloudflare.com/ajax/libs/pivottable/2.23.0/pivot.min.css"];
5 | static scripts = [
6 | "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js",
7 | "https://cdnjs.cloudflare.com/ajax/libs/pivottable/2.23.0/pivot.min.js"
8 | ];
9 | static init() {
10 | super.init(".pivot-placeholder");
11 | }
12 | constructor(domNode, pyComponent, el) {
13 | super(domNode, pyComponent, el);
14 | this.pivot = $(domNode.querySelector(".anvil-extras-pivot"));
15 | }
16 | update({rows, columns: cols, values: vals, aggregator: aggregatorName}) {
17 | const keys = [...rows, ...cols, ...vals, ...['key A', 'key B', 'key C']];
18 | const item = Object.fromEntries(keys.map(key => [key, 1]));
19 | this.pivot.pivotUI([item], {rows, cols, vals, aggregatorName}, true);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client_code/routing/_alert.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | __version__ = "1.7.1"
9 |
10 | from anvil.js.window import jQuery as _S
11 |
12 | from . import _navigation
13 |
14 | alert_modal = _S("#alert-modal")
15 |
16 |
17 | def handle_alert_unload() -> bool:
18 | """
19 | if there is an active alert which is not dismissible then navigation is prevented
20 | return value indicates whether this function took control of the on_navigation
21 | """
22 | data = alert_modal.data("bs.modal")
23 | if data is None:
24 | return False
25 | elif not data.isShown:
26 | return False
27 | elif data.options and data.options.backdrop != "static":
28 | # bootstrap alerts have a backdrom of static when not dismissible
29 | alert_modal.modal("hide")
30 | return False
31 | _navigation.stopUnload()
32 | return True
33 |
--------------------------------------------------------------------------------
/client_code/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | from functools import cache
9 |
10 | __version__ = "1.7.1"
11 |
12 |
13 | def __dir__():
14 | return ["auto_refreshing", "wait_for_writeback", "timed", "BindingRefreshDict"]
15 |
16 |
17 | @cache
18 | def __getattr__(name):
19 | # todo use dynamic imports but __import__ is not yet supported in skult
20 | if name == "auto_refreshing":
21 | from ._auto_refreshing import auto_refreshing
22 |
23 | return auto_refreshing
24 | elif name == "timed":
25 | from ._timed import timed
26 |
27 | return timed
28 | elif name == "wait_for_writeback":
29 | from ._writeback_waiter import wait_for_writeback
30 |
31 | return wait_for_writeback
32 | elif name == "BindingRefreshDict":
33 | from ._auto_refreshing import BindingRefreshDict
34 |
35 | return BindingRefreshDict
36 | else:
37 | raise AttributeError(name)
38 |
--------------------------------------------------------------------------------
/docs/guides/components/page_break.rst:
--------------------------------------------------------------------------------
1 | PageBreak
2 | =========
3 | For use in forms which are rendered to PDF to indicate that a page break is required.
4 |
5 | The optional ``margin_top`` property changes the amount of white space at the top of the page.
6 | You can set the ``margin_top`` property to a positive/negative number to adjust the whitespace.
7 | Most of the time this is unnecessary. This won't have any effect on the designer, only the generated PDF.
8 |
9 | The optional ``border`` property defines the style of the component in the IDE.
10 | The value of the property affects how a ``PageBreak`` component looks in the browser during the execution.
11 | It has no effect in the generated PDF, where the component is never visible or in the IDE, where the component
12 | is always ``"1px solid grey"``.
13 |
14 | It is possible to change the default style for all the ``PageBreak``\ s in the app by adding the following code to ``theme.css``:
15 |
16 | .. code-block:: css
17 |
18 | .break-container {
19 | border: 2px dashed red !important;
20 | }
21 |
22 | Using this technique rather than the ``border`` property affects how the component looks both in the IDE and at runtime.
23 |
--------------------------------------------------------------------------------
/client_code/uuid.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | import anvil.js
9 |
10 | __version__ = "1.7.1"
11 |
12 | _js_uuid = anvil.js.import_from("https://jspm.dev/uuid@8.3.2")
13 | _v4, _parse, _validate = _js_uuid.v4, _js_uuid.parse, _js_uuid.validate
14 |
15 |
16 | class UUID(str):
17 | def __init__(self, val):
18 | if not _validate(val):
19 | raise ValueError("badly formed hexadecimal UUID string")
20 |
21 | def __repr__(self):
22 | return "UUID('" + self + "')"
23 |
24 | @property
25 | def bytes(self):
26 | return _parse(self)
27 |
28 |
29 | def uuid4():
30 | """returns a uuid"""
31 | return UUID(_v4())
32 |
33 |
34 | if __name__ == "__main__":
35 | x = uuid4()
36 | print(repr(x))
37 | print(x)
38 | print(x.bytes)
39 | try:
40 | UUID("foo")
41 | except ValueError as e:
42 | print(f"Succesfully raised - {e!r}")
43 | else:
44 | print("Value Error Not Raised")
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 The Anvil Extras project team members listed at
4 | https://github.com/anvilistas/anvil-extras/graphs/contributors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/tests/test_publisher.py:
--------------------------------------------------------------------------------
1 | from client_code import messaging
2 |
3 |
4 | def test_default_logging(capsys):
5 | publisher = messaging.Publisher()
6 | publisher.publish("test_channel", "test_message")
7 | captured = capsys.readouterr()
8 | assert (
9 | captured.out
10 | == "Published 'test_message' message on 'test_channel' channel to 0 subscriber(s)\n"
11 | )
12 |
13 |
14 | def test_no_logging_default(capsys):
15 | publisher = messaging.Publisher(with_logging=False)
16 | publisher.publish("test_channel", "test_message")
17 | captured = capsys.readouterr()
18 | assert captured.out == ""
19 |
20 |
21 | def test_default_logging_override(capsys):
22 | publisher = messaging.Publisher()
23 | publisher.publish("test_channel", "test_message", with_logging=False)
24 | captured = capsys.readouterr()
25 | assert captured.out == ""
26 |
27 |
28 | def test_no_logging_override(capsys):
29 | publisher = messaging.Publisher(with_logging=False)
30 | publisher.publish("test_channel", "test_message", with_logging=True)
31 | captured = capsys.readouterr()
32 | assert (
33 | captured.out
34 | == "Published 'test_message' message on 'test_channel' channel to 0 subscriber(s)\n"
35 | )
36 |
--------------------------------------------------------------------------------
/client_code/ProgressBar/Indeterminate/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | from anvil.js import get_dom_node
8 |
9 | from anvil_extras import ProgressBar
10 | from anvil_extras.utils._component_helpers import _get_dom_node_id, _html_injector
11 |
12 | from ._anvil_designer import IndeterminateTemplate
13 |
14 | __version__ = "1.7.1"
15 |
16 | _html_injector.css(ProgressBar.css)
17 |
18 |
19 | class Indeterminate(IndeterminateTemplate):
20 | def __init__(self, track_colour, indicator_colour, **properties):
21 | dom_node = get_dom_node(self)
22 | dom_node.style.setProperty("background-color", indicator_colour)
23 |
24 | indicator_id = _get_dom_node_id(self.indicator_panel)
25 | css = f"""
26 | #{indicator_id}:before {{
27 | background-color: {track_colour}
28 | }}
29 | """
30 | _html_injector.css(css)
31 | self.role = "progress-track"
32 | self.indicator_panel.role = "indeterminate-progress-indicator"
33 | self.indicator_panel.background = indicator_colour
34 | self.init_components(**properties)
35 |
--------------------------------------------------------------------------------
/client_code/ProgressBar/Determinate/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | from anvil.js import get_dom_node
8 |
9 | from anvil_extras import ProgressBar
10 | from anvil_extras.utils._component_helpers import _html_injector
11 |
12 | from ._anvil_designer import DeterminateTemplate
13 |
14 | __version__ = "1.7.1"
15 |
16 | _html_injector.css(ProgressBar.css)
17 |
18 |
19 | class Determinate(DeterminateTemplate):
20 | def __init__(self, track_colour, indicator_colour, **properties):
21 | self.indicator_dom_node = get_dom_node(self.indicator_panel)
22 | self.role = "progress-track"
23 | self.indicator_panel.role = "progress-indicator"
24 | self.background = track_colour
25 | self.indicator_panel.background = indicator_colour
26 | self.init_components(**properties)
27 |
28 | @property
29 | def progress(self):
30 | return self._progress
31 |
32 | @progress.setter
33 | def progress(self, value):
34 | self._progress = value
35 | self.indicator_dom_node.style.setProperty("width", f"{value:%}")
36 |
--------------------------------------------------------------------------------
/client_code/PageBreak/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: HtmlTemplate
3 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
4 | role: null, html: "\n
\n \n \n
\n\n\n"}
10 | components: []
11 | is_package: true
12 | custom_component: true
13 | properties:
14 | - {name: margin_top, type: number, default_value: 0, default_binding_prop: true, description: Use to adjust whitespace at the top of each page in the generated pdf. This is an optional property and defaults to 0. Can be positive or negative. Negative numbers will reduce whitespace in the generated pdf.}
15 | - {name: border, type: string, default_value: 1px solid gray, description: Use to set the style in the form. Useful when the PDF has many grey horizontal lines and the default border style would be confusing. Has no effect on the printed PDF.}
16 |
--------------------------------------------------------------------------------
/client_code/PageBreak/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | import anvil
8 | from anvil.js.window import jQuery as _S
9 |
10 | from ._anvil_designer import PageBreakTemplate
11 |
12 | __version__ = "1.7.1"
13 |
14 |
15 | class PageBreak(PageBreakTemplate):
16 | def __init__(self, margin_top=0, border="1px solid grey", **properties):
17 | dom_node = _S(anvil.js.get_dom_node(self))
18 | self.margin_node = dom_node.find(".margin-element")
19 | self.break_container = dom_node.find(".break-container")
20 |
21 | self.margin_top = margin_top
22 | self.border = border
23 |
24 | self.init_components(**properties)
25 |
26 | @property
27 | def margin_top(self):
28 | return self._margin_top
29 |
30 | @margin_top.setter
31 | def margin_top(self, value):
32 | self.margin_node.css("margin-top", value)
33 | self._margin_top = value
34 |
35 | @property
36 | def border(self):
37 | return self._border
38 |
39 | @border.setter
40 | def border(self, value):
41 | self.break_container.css("border", value)
42 | self._border = value
43 |
--------------------------------------------------------------------------------
/client_code/routing/_session_expired.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | __version__ = "1.7.1"
9 |
10 |
11 | from anvil.js.window import jQuery as _S
12 | from anvil.js.window import location
13 |
14 | modal = _S("#session-expired-modal")
15 | modal_button = _S("#session-expired-modal .modal-footer button")
16 | modal_close = _S("#session-expired-modal .modal-header button")
17 |
18 |
19 | def trigger_refresh(e):
20 | modal.off("click")
21 | modal_button.trigger("click")
22 |
23 |
24 | def reload_page(e):
25 | location.reload()
26 |
27 |
28 | def session_expired_handler(reload_hash, allow_cancel):
29 | if reload_hash:
30 | modal_button.removeClass("refresh").off("click").on("click", reload_page)
31 | else:
32 | modal_button.addClass("refresh").off("click")
33 |
34 | if not allow_cancel:
35 | modal_button.css("display", "none")
36 | modal_close.css("display", "none")
37 | modal.off("click", trigger_refresh).on("click", trigger_refresh)
38 | else:
39 | modal_close.removeAttr("style")
40 | modal_button.removeAttr("style")
41 | modal.off("click", trigger_refresh)
42 |
--------------------------------------------------------------------------------
/theme/assets/loading-spinner.js:
--------------------------------------------------------------------------------
1 | function _rgbToHsl(r, g, b) {
2 | (r /= 255), (g /= 255), (b /= 255);
3 | const max = Math.max(r, g, b),
4 | min = Math.min(r, g, b);
5 | let h,
6 | s,
7 | l = (max + min) / 2;
8 | if (max === min) {
9 | h = s = 0; // achromatic
10 | } else {
11 | const d = max - min;
12 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
13 | switch (max) {
14 | case r:
15 | h = (g - b) / d + (g < b ? 6 : 0);
16 | break;
17 | case g:
18 | h = (b - r) / d + 2;
19 | break;
20 | case b:
21 | h = (r - g) / d + 4;
22 | break;
23 | }
24 | h /= 6;
25 | }
26 | return [h * 360, s * 100, l * 100];
27 | }
28 |
29 | function _loadingSpinnerColor(rgb) {
30 | rgb = rgb.replace("#", "");
31 | const _rgb = (i) => parseInt(rgb.substr(i * 2, 2), 16);
32 | let [h, s, _] = _rgbToHsl(_rgb(0), _rgb(1), _rgb(2));
33 | h = Math.trunc(h - 206);
34 | s = 100 + s - 89;
35 | const L = document.createElement("style");
36 | L.textContent = `#loadingSpinner, .plotly-loading-spinner {filter: hue-rotate(${h}deg) saturate(${s}%);}`;
37 | document.head.appendChild(L);
38 | }
39 |
40 | _loadingSpinnerColor(document.currentScript.getAttribute("color"));
41 |
--------------------------------------------------------------------------------
/client_code/utils/_timed.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 |
9 | from functools import wraps
10 | from time import gmtime, strftime, time
11 |
12 | __version__ = "1.7.1"
13 |
14 |
15 | def _signature(func, args, kwargs):
16 | """Text representation of a function's signature"""
17 | arguments = [str(a) for a in args]
18 | arguments.extend([f"{key}={value}" for key, value in kwargs.items()])
19 | return f"{func.__name__}({','.join(arguments)})"
20 |
21 |
22 | def _timestamp(seconds):
23 | """Text representation of a unix timestamp"""
24 | return strftime("%Y-%m-%d %H:%M:%S", gmtime(seconds))
25 |
26 |
27 | def timed(func):
28 | """A decorator to time the execution of a function"""
29 |
30 | @wraps(func)
31 | def wrapper(*args, **kwargs):
32 | signature = _signature(func, args, kwargs)
33 | started_at = time()
34 | print(f"{_timestamp(started_at)} {signature} called")
35 | result = func(*args, **kwargs)
36 | finished_at = time()
37 | duration = f"{(finished_at - started_at):.2f}s"
38 | print(f"{_timestamp(finished_at)} {signature} completed({duration})")
39 | return result
40 |
41 | return wrapper
42 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.3.0
4 | hooks:
5 | - id: check-yaml
6 | - id: end-of-file-fixer
7 | - id: trailing-whitespace
8 | - repo: https://github.com/psf/black
9 | rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags
10 | hooks:
11 | - id: black
12 | args: # arguments to configure black
13 | - --line-length=88
14 | language_version: python3 # Should be a command that runs python3.6+
15 |
16 |
17 | - repo: https://github.com/pycqa/isort
18 | rev: 5.6.3
19 | hooks:
20 | - id: isort
21 | name: isort (python)
22 |
23 | # flake8
24 | - repo: https://gitlab.com/pycqa/flake8
25 | rev: 3.9.0
26 | hooks:
27 | - id: flake8
28 | args: # arguments to configure flake8
29 | # making isort line length compatible with black
30 | - "--max-line-length=88"
31 | # these are errors that will be ignored by flake8
32 | # check out their meaning here
33 | # https://flake8.pycqa.org/en/latest/user/error-codes.html
34 | - "--ignore=E203,E266,E501,W503,F403,F401,E402"
35 | - repo: https://github.com/Lucas-C/pre-commit-hooks
36 | rev: v1.1.10
37 | hooks:
38 | - id: insert-license
39 | files: "^.*py$"
40 | exclude: "^(docs|tests|__init__.py)"
41 | args:
42 | - --detect-license-in-X-top-lines=6
43 |
--------------------------------------------------------------------------------
/client_code/Autocomplete/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: TextBox
3 | properties: {}
4 | event_bindings: {show: _on_show, hide: _on_hide}
5 | is_package: true
6 | custom_component: true
7 | properties:
8 | - {name: suggestions, type: 'text[]', default_value: null}
9 | - {name: placeholder, type: string, default_value: ''}
10 | - name: text
11 | type: string
12 | default_value: ''
13 | allow_binding_writeback: true
14 | default_binding_prop: true
15 | binding_writeback_events: [suggestion_clicked]
16 | group: text
17 | important: true
18 | - {name: enabled, type: boolean, default_value: true, group: interaction, important: true}
19 | - {name: visible, type: boolean, default_value: true, group: appearance, important: true}
20 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
21 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
22 | - {name: suggest_if_empty, type: boolean, default_value: false}
23 | - {name: foreground, type: color, default_value: '', default_binding_prop: false,
24 | group: appearance, important: false, description: Only applies changes to the TextBox}
25 | - {name: background, type: color, default_value: '', default_binding_prop: false,
26 | description: Only applies changes to the TextBox, group: appearance, important: false}
27 | events:
28 | - {name: change}
29 | - {name: suggestion_clicked}
30 | - {name: pressed_enter, default_event: true}
31 | - {name: focus}
32 | - {name: lost_focus}
33 |
--------------------------------------------------------------------------------
/.github/workflows/config.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | check:
9 | name: Check code base
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | python-version: [3.6]
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python ${{ matrix.python-version }}
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: ${{ matrix.python-version }}
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | python -m pip install flake8 pytest black isort sphinx sphinx_rtd_theme bump2version
25 | - name: Lint with flake8
26 | run: |
27 | python -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
29 | python -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
30 | - name: Check format with black
31 | run: |
32 | python -m black --check .
33 | - name: Check imports with isort
34 | run: |
35 | python -m isort --check-only client_code
36 | python -m isort --check-only server_code
37 | - name: Run test suite
38 | run: |
39 | python -m pytest
40 | - name: Check docs build
41 | run: |
42 | sphinx-build docs build
43 | - name: Check version numbering
44 | run: |
45 | python -m bumpversion --dry-run patch
46 |
--------------------------------------------------------------------------------
/docs/guides/components/tabs.rst:
--------------------------------------------------------------------------------
1 | Tabs
2 | ============
3 | A simple way to implement tabs. Works well above another container abover or below. Set the container spacing property to none.
4 | It also understand the role material design role ``'card'``
5 |
6 | Properties
7 | ----------
8 |
9 | :tab_titles: list[str]
10 |
11 | The titles of each tab.
12 |
13 | :active_tab_index: int
14 |
15 | Which tab should be active.
16 |
17 | :foreground: color
18 | the color of the highlight and text. Defaults to ``"theme:Primary 500"``
19 |
20 | :background: color
21 | the background for all tabs. Defaults to ``"transparent"``
22 |
23 | :role:
24 | set the role to ``'card'`` or create your own role
25 |
26 | :align: str
27 |
28 | ``"left"``, ``"right"``, ``"center"`` or ``"full"``
29 |
30 | :bold: bool
31 |
32 | applied to all tabs
33 |
34 | :italic: bool
35 |
36 | applied to all tabs
37 |
38 | :font_size: int
39 |
40 | applied to all tabs
41 |
42 | :font: str
43 |
44 | applied to all tabs
45 |
46 | :visible: Boolean
47 |
48 | Is the component visible
49 |
50 | :spacing_above: String
51 |
52 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
53 |
54 | :spacing_below: String
55 |
56 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
57 |
58 |
59 | Events
60 | ----------
61 | :tab_click:
62 |
63 | When any tab is clicked. Inclues the paramters ``tab_index`` ``tab_title`` and ``tab_component`` as part of the ``event_args``
64 |
65 | :show:
66 |
67 | When the component is shown
68 |
69 | :hide:
70 |
71 | When the component is hidden
72 |
--------------------------------------------------------------------------------
/server_code/server_utils.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | import logging
8 | import sys
9 | from functools import wraps
10 | from time import time
11 |
12 | __version__ = "1.7.1"
13 |
14 |
15 | def get_logger():
16 | logging.basicConfig(level=logging.INFO)
17 | handler = logging.StreamHandler(sys.stdout)
18 | formatter = logging.Formatter(
19 | "%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
20 | )
21 | handler.setFormatter(formatter)
22 | logger = logging.getLogger(__name__)
23 | logger.addHandler(handler)
24 | return logger
25 |
26 |
27 | LOGGER = get_logger()
28 |
29 |
30 | def _signature(func, args, kwargs):
31 | arguments = [str(a) for a in args]
32 | arguments.extend([f"{key}={value}" for key, value in kwargs.items()])
33 | return f"{func.__name__}({','.join(arguments)})"
34 |
35 |
36 | def timed(func, logger=LOGGER, level=logging.INFO):
37 | @wraps(func)
38 | def wrapper(*args, **kwargs):
39 | signature = _signature(func, args, kwargs)
40 | logger.log(msg=f"{signature} called", level=level)
41 | started_at = time()
42 | result = func(*args, **kwargs)
43 | finished_at = time()
44 | duration = f"{(finished_at - started_at):.2f}s"
45 | logger.log(
46 | msg=f"{signature} completed({duration})",
47 | level=level,
48 | )
49 | return result
50 |
51 | return wrapper
52 |
--------------------------------------------------------------------------------
/client_code/routing/_logging.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | __version__ = "1.7.1"
9 |
10 |
11 | class Logger:
12 | def __init__(self, debug, msg="", save=False):
13 | self.debug = debug
14 | self.msg = msg
15 | self.save = save
16 | self._log = [(msg,)]
17 |
18 | def print(self, *args, **kwargs):
19 | if self.debug:
20 | print(self.msg, *args, **kwargs) if self.msg else print(*args, **kwargs)
21 | if self.save:
22 | self._log.append(args)
23 |
24 | def show_log(self):
25 | log_rows = [" ".join(str(arg) for arg in args) for args in self._log]
26 | return "\n".join(log_rows)
27 |
28 | @property
29 | def debug(self):
30 | return self._debug
31 |
32 | @debug.setter
33 | def debug(self, value):
34 | if not isinstance(value, bool):
35 | raise TypeError(f"debug should be a boolean, not {type(value).__name__}")
36 | self._debug = value
37 |
38 | @property
39 | def msg(self):
40 | return self._msg
41 |
42 | @msg.setter
43 | def msg(self, value):
44 | if not isinstance(value, str):
45 | raise TypeError(f"msg should be type str, not {type(value).__name__}")
46 | self._msg = value
47 |
48 |
49 | logger = Logger(debug=False, msg="#routing:")
50 | # set to false if you don't wish to debug. You can also - in your main form - do routing.logger.debug = False
51 |
--------------------------------------------------------------------------------
/client_code/ProgressBar/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | __version__ = "1.7.1"
8 |
9 | css = """ .anvil-role-progress-track, .anvil-role-progress-indicator {
10 | display: block;
11 | height: 3px;
12 | margin: 0;
13 | }
14 |
15 | .anvil-role-progress-track {
16 | width: 100%;
17 | }
18 |
19 | .anvil-role-progress-indicator {
20 | top: 0 !important;
21 | }
22 |
23 | .anvil-role-progress-track > .holder, .anvil-role-progress-indicator > .holder {
24 | display: block !important;
25 | }
26 |
27 | .anvil-role-indeterminate-progress-indicator, .anvil-role-indeterminate-progress-indicator:before {
28 | height: 3px;
29 | width: 100%;
30 | margin: 0;
31 | }
32 |
33 | .anvil-role-indeterminate-progress-indicator {
34 | display: -webkit-flex;
35 | display: flex;
36 | }
37 |
38 | .anvil-role-indeterminate-progress-indicator:before {
39 | content: '';
40 | -webkit-animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
41 | animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
42 | }
43 |
44 | @-webkit-keyframes running-progress {
45 | 0% { margin-left: 0px; margin-right: 100%; }
46 | 50% { margin-left: 25%; margin-right: 0%; }
47 | 100% { margin-left: 100%; margin-right: 0; }
48 | }
49 |
50 | @keyframes running-progress {
51 | 0% { margin-left: 0px; margin-right: 100%; }
52 | 50% { margin-left: 25%; margin-right: 0%; }
53 | 100% { margin-left: 100%; margin-right: 0; }
54 | }
55 | """
56 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerMultSelectDropDown.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 | export class DesignerMultiSelectDropDown extends DesignerComponent {
3 | static defaults = {
4 | placeholder: "None Selected",
5 | enabled: true,
6 | visible: true,
7 | spacing_above: "small",
8 | spacing_below: "small",
9 | };
10 | static links = ["https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.18/dist/css/bootstrap-select.min.css"];
11 | static scripts = ["https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.18/dist/js/bootstrap-select.min.js"];
12 | static postLoad() {
13 | // deno-lint-ignore
14 | $.fn.selectpicker.Constructor.BootstrapVersion = "3";
15 | }
16 | static init() {
17 | super.init(".select-picker");
18 | }
19 | picker: JQuery;
20 | constructor(domNode: HTMLElement, pyComponent: any, select: HTMLElement) {
21 | super(domNode, pyComponent, select);
22 | this.picker = $(select);
23 | this.picker.selectpicker();
24 | }
25 | update(this: any, props: any, propName: string) {
26 | if (propName && !(propName in this.constructor.defaults)) {
27 | return;
28 | }
29 | this.picker.attr("title", props["placeholder"] || null);
30 | this.picker.selectpicker({ title: props["placeholder"] });
31 | this.picker.attr("disabled", props["enabled"] ? null : "");
32 | this.updateSpacing(props);
33 | this.updateVisible(props);
34 | this.picker.selectpicker("refresh");
35 | this.picker.selectpicker("render");
36 | }
37 |
38 | get [Symbol.toStringTag]() {
39 | return "MultiSelectDropDown";
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client_code/utils/_auto_refreshing.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 |
9 | from functools import cache
10 |
11 | __version__ = "1.7.1"
12 |
13 | _dict_setitem = dict.__setitem__
14 |
15 |
16 | class BindingRefreshDict(dict):
17 | """A dict that calls refresh_data_bindings when its content changes"""
18 |
19 | def _refresh_data_bindings(self):
20 | forms = getattr(self, "_forms", [])
21 | for form in forms:
22 | form.refresh_data_bindings()
23 |
24 | def __setitem__(self, key, value):
25 | _dict_setitem(self, key, value)
26 | self._refresh_data_bindings()
27 |
28 | def update(self, *args, **kwargs):
29 | super().update(*args, **kwargs)
30 | self._refresh_data_bindings()
31 |
32 |
33 | def _item_override(cls):
34 | """Generate a property to override a form's 'item' attribute"""
35 | base_item = super(cls, cls).item
36 |
37 | def item_getter(self):
38 | return base_item.__get__(self, cls)
39 |
40 | def item_setter(self, item):
41 | item = (
42 | item if isinstance(item, BindingRefreshDict) else BindingRefreshDict(item)
43 | )
44 | # use a set here so that subforms using the same item can also trigger
45 | # refresh_data_bindings
46 | forms = item._forms = getattr(item, "_forms", set())
47 | forms.add(self)
48 | base_item.__set__(self, item)
49 |
50 | return property(item_getter, item_setter)
51 |
52 |
53 | @cache
54 | def auto_refreshing(cls):
55 | """A decorator for a form class to refresh data bindings automatically"""
56 | cls.item = _item_override(cls)
57 | return cls
58 |
--------------------------------------------------------------------------------
/client_code/MessagePill/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | from ..utils._component_helpers import _html_injector
8 | from ._anvil_designer import MessagePillTemplate
9 |
10 | __version__ = "1.7.1"
11 |
12 | css = """
13 | .anvil-role-message-pill {
14 | padding-left: 1em;
15 | border-radius: 2em;
16 | }
17 | """
18 | _html_injector.css(css)
19 |
20 |
21 | class MessagePill(MessagePillTemplate):
22 | backgrounds = dict(
23 | info="#bde5f8", success="#dff2bf", warning="#feefb3", error="#ffd2d2"
24 | )
25 | foregrounds = dict(
26 | info="#00529b", success="#4f8a10", warning="#9f6000", error="#d8000c"
27 | )
28 | icons = dict(
29 | info="fa:info-circle",
30 | success="fa:check",
31 | warning="fa:warning",
32 | error="fa:times-circle",
33 | )
34 |
35 | def __init__(self, **properties):
36 | self.label.role = "message-pill"
37 | self.init_components(**properties)
38 |
39 | @property
40 | def level(self):
41 | return self.item["level"]
42 |
43 | @level.setter
44 | def level(self, value):
45 | if value not in ("info", "success", "warning", "error"):
46 | raise ValueError(
47 | "level must be one of 'info', 'success', 'warning' or 'error'"
48 | )
49 | self.item["level"] = value
50 | self.label.background = self.backgrounds[value]
51 | self.label.foreground = self.foregrounds[value]
52 | self.label.icon = self.icons[value]
53 |
54 | @property
55 | def message(self):
56 | return self.item["message"]
57 |
58 | @message.setter
59 | def message(self, value):
60 | self.item["message"] = value
61 | self.label.text = value
62 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerQuill.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 |
3 | declare var Quill: any;
4 | export class DesignerQuill extends DesignerComponent {
5 | static defaults = {
6 | auto_expand: true,
7 | content: "",
8 | height: 150,
9 | placeholder: null,
10 | readonly: false,
11 | theme: "snow",
12 | toolbar: true,
13 | visible: true,
14 | };
15 | static links = ["//cdn.quilljs.com/1.3.6/quill.snow.css", "//cdn.quilljs.com/1.3.6/quill.bubble.css"];
16 | static scripts = ["//cdn.quilljs.com/1.3.6/quill.min.js"];
17 |
18 | static init() {
19 | super.init(".quill-editor");
20 | }
21 | editor: HTMLElement;
22 | constructor(domNode: HTMLElement, pyComponent: any, editor: HTMLElement) {
23 | super(domNode, pyComponent, editor);
24 | this.editor = editor;
25 | }
26 | update(props: any): void {
27 | if (this.editor.firstElementChild) {
28 | this.editor.removeChild(this.editor.firstElementChild); // remove the editor and the toolabar
29 | }
30 | if (this.domNode.firstElementChild !== this.editor) {
31 | this.domNode.removeChild(this.domNode.firstElementChild); // remove the toolbar
32 | }
33 | const q = new Quill(this.editor, {
34 | modules: { toolbar: props.toolbar || false },
35 | readOnly: props.readonly,
36 | theme: props.theme,
37 | placeholder: props.placeholder,
38 | });
39 | this.updateSpacing(props);
40 | this.updateVisible(props);
41 | let len = props.height;
42 | len = ("" + len).match(/[a-zA-Z%]/g) ? len : len + "px";
43 | this.editor.style.minHeight = len;
44 | this.editor.style.height = props.auto_expand ? len : "auto";
45 | q.setText(props.content || "");
46 | }
47 |
48 | get [Symbol.toStringTag]() {
49 | return "Quill";
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/.anvil_editor.yaml:
--------------------------------------------------------------------------------
1 | unique_ids:
2 | forms: {Switch: '1621859715130702811176885.8342', Slider: VQKM7U4OEOF6Y4PUA7EDRVABBMLBEHPP,
3 | MessagePill: 4HMQ4AYEDJT22I2YNFTDLCQCFDA4FZ3F, Chip: JJUXHRHU5OMAZAT6Q3QT4K22KCL67Q4O,
4 | MultiSelectDropDown: HVA3ELK4BMIHXM33RHLUC5MR76GSTQ4G, PageBreak: WZMHYFGHS2XAT4DWAZNBJ77DOTUR2AIA,
5 | Tabs: REIFUFOHW7GVIYTAAVUTDZAKATHFJ5OW, Quill: ENPP7ERNGECFLY24YCU7S5PRQQHJEQ7V,
6 | ChipsInput: 2IUXJPHSKH5CW7WV2N2PTL2O6WAEWBPV, Demo: 7OHCYYNDEID2BZ5AN5DDP7K4XXGJTRZB,
7 | Autocomplete: BCY2XYD72YQ6XE35WSGDYNNDBRCSMYNV, ProgressBar.Determinate: J7HWVD2WBMZY7OCSOPNLPLHFQ4XCJBR2,
8 | Pivot: XGSRYGA4QFP5H57CKNWXCVOT5H7DGTEV, ProgressBar.Indeterminate: KSULDKU4IPBMWPU7OQLLU4YN35XCYTSV}
9 | modules: {utils._component_helpers: '1621686339258612945000771.5087', utils._writeback_waiter: HPGIMFEAETCBPEDMGJSTIJXSMGGV4LMU,
10 | storage: YCJGWKTKWGKIXPMF7R6FT2N5QH3TBETA, augment: DXXSTVAMYSLYZJOQS23I7R67JNE3YDEQ,
11 | routing._navigation: MXGZMA7IRSF5XJMMR4XOL4O2J7KNYSI4, uuid: COAWBUSWZWBIJD75BBIBE7GAILKFICZF,
12 | routing: 763MIXLEPUMGF6VS2I7VHTDMXSNANWA3, utils._auto_refreshing: XM2GLNNTPYT25IMV2377R4PXTQCJE2SU,
13 | utils._timed: M2FMBJMUW4YW7ZZXZWEVMWV5PJQPEGTV, routing._alert: YOYXSYGZW45MSIHCYVJIZUTLLK5GI7WI,
14 | messaging: L6OUZBWTSWZU33JG4LAY36FYKSGCO7SS, popover: B6UIKKOXGZR7QK3ZO4P32GLP5AERNF3R,
15 | navigation: YSGUSWEZXBEZEJKRIX7UPAS2Y64Q6EZ6, session: I6CPANLB5O7M2VDR2CZKLBV7THBW2SJF,
16 | ProgressBar: 6RDMGEBZGTXV32KLZDKWPGBWXS3QKNRT, routing._logging: FAODPTGNRUOWMQZKLALPJUT73LTMUDRI,
17 | routing._routing: SDYG54CLQVFHGY7XHB2R44TZR6PXYUKY, utils: E7XISEGXUVZXV24JEVFT4SV64AJED3XJ,
18 | routing._session_expired: 7I5VIVGMNIHYLE3UOMQZULQDDF6XR7UX}
19 | server_modules: {authorisation: Z6AZ64JBAAHAVRYQJ63VZLOU4TESTAUN, server_utils: 4XEM7IGABFRXOMKOT7WB5BYR22LF4QH7}
20 | assets: {loading-spinner.js: GNN5BGLOS3BHSXWTHXXQUT5BPTJQEUXA, standard-page.html: JYT2VG6NOSSLRNZR5GDSLLXUFJ7AZF4J,
21 | theme.css: S5NWY72JOUFUHCDRRQX7DV3C5CKF4TGN}
22 |
--------------------------------------------------------------------------------
/server_code/authorisation.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | import functools
8 |
9 | import anvil.users
10 |
11 | __version__ = "1.7.1"
12 |
13 |
14 | def authentication_required(func):
15 | """A decorator to ensure only a valid user can call a server function"""
16 |
17 | @functools.wraps(func)
18 | def wrapper(*args, **kwargs):
19 | if anvil.users.get_user() is None:
20 | raise ValueError("Authentication required")
21 | else:
22 | return func(*args, **kwargs)
23 |
24 | return wrapper
25 |
26 |
27 | def authorisation_required(permissions):
28 | """A decorator to ensure a user has sufficient permissions to call a server function"""
29 |
30 | def decorator(func):
31 | @functools.wraps(func)
32 | def wrapper(*args, **kwargs):
33 | user = anvil.users.get_user()
34 | if user is None:
35 | raise ValueError("Authentication required")
36 | if isinstance(permissions, str):
37 | required_permissions = set([permissions])
38 | else:
39 | required_permissions = set(permissions)
40 | try:
41 | user_permissions = set(
42 | [
43 | permission["name"]
44 | for role in user["roles"]
45 | for permission in role["permissions"]
46 | ]
47 | )
48 | except TypeError:
49 | raise ValueError("Authorisation required")
50 |
51 | if not required_permissions.issubset(user_permissions):
52 | raise ValueError("Authorisation required")
53 | else:
54 | return func(*args, **kwargs)
55 |
56 | return wrapper
57 |
58 | return decorator
59 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | # sys.path.insert(0, os.path.abspath('.'))
16 |
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = "Anvil Extras"
21 | copyright = (
22 | "2021 The Anvil Extras project team members listed at "
23 | "https://github.com/anvilistas/anvil-extras/graphs/contributors"
24 | )
25 | author = "The Anvil Extras project team"
26 |
27 |
28 | # -- General configuration ---------------------------------------------------
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones
33 | extensions = [
34 | "sphinx_rtd_theme",
35 | ]
36 |
37 | # Add any paths that contain templates here, relative to this directory.
38 | templates_path = ["_templates"]
39 |
40 | # List of patterns, relative to source directory, that match files and
41 | # directories to ignore when looking for source files.
42 | # This pattern also affects html_static_path and html_extra_path.
43 | exclude_patterns = []
44 |
45 |
46 | # -- Options for HTML output -------------------------------------------------
47 |
48 | # The theme to use for HTML and HTML Help pages. See the documentation for
49 | # a list of builtin themes.
50 | #
51 | html_theme = "sphinx_rtd_theme"
52 |
53 | # Add any paths that contain custom static files (such as style sheets) here,
54 | # relative to this directory. They are copied after the builtin static files,
55 | # so a file named "default.css" will overwrite the builtin "default.css".
56 | # html_static_path = []
57 |
--------------------------------------------------------------------------------
/client_code/utils/_writeback_waiter.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | from functools import cache, wraps
9 |
10 | from anvil.js.window import Function, anvilFormTemplates
11 |
12 | __version__ = "1.7.1"
13 |
14 | _store_writebacks = Function(
15 | "form",
16 | """
17 | const self = PyDefUtils.unwrapOrRemapToPy(form);
18 | const bindings = self._anvil.dataBindings || [];
19 | const activeWritebacks = bindings.map(() => null);
20 |
21 | bindings.forEach(({pyComponent}, i) => {
22 | const bindingFunc = pyComponent._anvil.dataBindingWriteback;
23 | const wrapper = (component, name, val) => {
24 | const res = bindingFunc(component, name, val).finally(() => {
25 | activeWritebacks[i] = null;
26 | });
27 | activeWritebacks[i] = res;
28 | return res;
29 | };
30 | pyComponent._anvil.dataBindingWriteback = wrapper;
31 | });
32 |
33 | self._anvil.activeWritebacks = activeWritebacks;
34 | """,
35 | )
36 |
37 | _wait_for_writeback = Function(
38 | "form",
39 | """
40 | const self = PyDefUtils.unwrapOrRemapToPy(form);
41 | return new Promise((resolve) => {
42 | setTimeout(() => resolve(Promise.allSettled(self._anvil.activeWritebacks.slice(0))));
43 | }).then(() => Sk.builtin.none.none$);
44 | """,
45 | )
46 |
47 |
48 | def _init_wrapper(init):
49 | @wraps(init)
50 | def wrapper(self, *args, **kwargs):
51 | _store_writebacks(self)
52 | return init(self, *args, **kwargs)
53 |
54 | return wrapper
55 |
56 |
57 | @cache
58 | def _init_writeback(template):
59 | template.init_components = template.__init__ = _init_wrapper(template.__init__)
60 |
61 |
62 | for template in anvilFormTemplates:
63 | _init_writeback(template)
64 |
65 |
66 | def wait_for_writeback(fn):
67 | @wraps(fn)
68 | def wrapper(self, *args, **kwargs):
69 | _wait_for_writeback(self)
70 | return fn(self, *args, **kwargs)
71 |
72 | return wrapper
73 |
--------------------------------------------------------------------------------
/client_code/MultiSelectDropDown/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - {name: align, type: string, default_value: left, group: text, important: true}
3 | - {name: items, type: 'text[]', default_value: null, description: 'If set at runtime
4 | can use a list of tuples, (str, value) pairs. Or a list of dicts with keys: ''key'',
5 | ''value'', ''icon'', ''title'', ''enabled''', important: true}
6 | - {name: placeholder, type: string, default_value: None Selected, important: false}
7 | - {name: enable_filtering, type: boolean, default_value: null, group: interaction,
8 | important: true}
9 | - {name: multiple, type: boolean, default_value: true, group: interaction, important: true}
10 | - {name: enabled, type: boolean, default_value: true, group: interaction, important: true}
11 | - {name: visible, type: boolean, default_value: true, group: appearance, important: true}
12 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
13 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
14 | - name: selected
15 | type: object
16 | default_value: null
17 | allow_binding_writeback: true
18 | default_binding_prop: true
19 | binding_writeback_events: [change]
20 | important: true
21 | is_package: true
22 | events:
23 | - {name: change, default_event: true, description: when the selected values change}
24 | - {name: show, description: when the dropdown is shown}
25 | - {name: hide, description: when the dropdown is hidden}
26 | custom_component: true
27 | components: []
28 | container:
29 | type: HtmlTemplate
30 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
31 | role: null, html: "\n"}
37 | event_bindings: {hide: _form_hide}
38 |
--------------------------------------------------------------------------------
/docs/guides/components/multi_select_dropdown.rst:
--------------------------------------------------------------------------------
1 | MultiSelectDropdown
2 | ===================
3 | A multi select dropdown component with optional search bar
4 |
5 | Properties
6 | ----------
7 | :align: String
8 |
9 | ``"left"``, ``"right"``, ``"center"`` or ``"full"``
10 |
11 | :items: Iterable of Strings, Tuples or Dicts
12 |
13 | Strings and tuples as per Anvil's native dropdown component. More control can be added by setting the items to a list of dictionaries.
14 | e.g.
15 |
16 | .. code-block:: python
17 |
18 | self.multi_select_drop_down.items = [
19 | {"key": "1st": "value": 1, "subtext": "pick me"},
20 | {"key": "2nd": "value": 2, "enabled": False},
21 | "---",
22 | {"key": "item 3": "value": 3, "title": "3rd times a charm"},
23 | ]
24 |
25 | The ``"key"`` property is what is displayed in the dropdown.
26 | The ``value`` property is what is returned from the ``selected_values``.
27 | The remainder of the properties are optional.
28 | ``"enabled"`` determines if the option is enabled or not - defaults to ``True``.
29 | ``"title"`` determines what is displayed in the selected box - if not set it will use the value from ``"key"``.
30 | ``"subtext"`` adds subtext to the dropdown display.
31 |
32 | To create a divider include ``"---"`` at the appropriate index.
33 |
34 | :placeholder: String
35 |
36 | Placeholder when no items have been selected
37 |
38 | :enable_filtering: Boolean
39 |
40 | Allow searching of items by key
41 |
42 | :multiple: Boolean
43 |
44 | Can also be set to false to disable multiselect
45 |
46 | :enabled: Boolean
47 |
48 | Disable interactivity
49 |
50 | :visible: Boolean
51 |
52 | Is the component visible
53 |
54 | :spacing_above: String
55 |
56 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
57 |
58 | :spacing_below: String
59 |
60 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
61 |
62 | :selected: Object
63 |
64 | get or set the current selected values.
65 |
66 |
67 | Events
68 | ----------
69 | :change:
70 |
71 | When the selection changes
72 |
73 | :show:
74 |
75 | When the component is shown
76 |
77 | :hide:
78 |
79 | When the component is hidden
80 |
--------------------------------------------------------------------------------
/client_code/Chip/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - {name: text, type: string, default_value: chip, default_binding_prop: true}
3 | - {name: icon, type: icon, default_value: null}
4 | - {name: close_icon, type: boolean, default_value: true}
5 | - {name: background, type: color, default_value: null, group: appearance, important: false}
6 | - {name: foreground, type: color, default_value: null, group: appearance, important: false}
7 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
8 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
9 | - {name: visible, type: boolean, default_value: true, group: appearance, important: false}
10 | is_package: true
11 | events:
12 | - {name: close_click, default_event: true, description: when the close link is clicked}
13 | - {name: click, description: when the chip is clicked}
14 | - {name: show, description: when the chip is shown}
15 | - {name: hide, description: when the chip is hidden}
16 | custom_component: true
17 | components:
18 | - type: Label
19 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', visible: true,
20 | text: Chip, font_size: null, font: '', spacing_above: none, icon_align: left,
21 | spacing_below: none, italic: false, background: '', bold: false, underline: false,
22 | icon: 'fa:bolt'}
23 | name: chip_label
24 | layout_properties: {slot: default}
25 | - type: Link
26 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: 'rgba(0,0,0,0.6)',
27 | visible: true, text: ✕, font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
28 | spacing_above: none, icon_align: left, col_widths: '', spacing_below: none, italic: false,
29 | background: '', bold: false, underline: false, icon: ''}
30 | name: close_link
31 | layout_properties: {slot: default}
32 | container:
33 | type: HtmlTemplate
34 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
35 | role: null, html: '
36 |
37 |
44 |
45 | '}
46 |
--------------------------------------------------------------------------------
/docs/guides/components/chips.rst:
--------------------------------------------------------------------------------
1 | Chip
2 | =====
3 | A variation on a label that includes a close icon. Largely based on the Material design Chip component.
4 |
5 | Properties
6 | ----------
7 |
8 | :text: str
9 |
10 | Displayed text
11 |
12 | :icon: icon
13 |
14 | Can be a font awesome icon or a media object
15 |
16 | :close_icon: boolean
17 |
18 | Whether to include the close icon or not
19 |
20 | :foreground: color
21 | the color of the text and icons
22 |
23 | :background: color
24 | background color for the chip
25 |
26 | :spacing_above: str
27 |
28 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
29 |
30 | :spacing_below: str
31 |
32 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
33 |
34 | :visible: bool
35 |
36 | Is the component visible
37 |
38 |
39 |
40 | Events
41 | ------
42 | :close_click:
43 |
44 | When the close icon is clicked
45 |
46 | :click:
47 |
48 | When the chip is clicked
49 |
50 | :show:
51 |
52 | When the component is shown
53 |
54 | :hide:
55 |
56 | When the component is hidden
57 |
58 |
59 |
60 |
61 | ChipsInput
62 | ==========
63 | A component for adding tags/chips. Uses a Chip with no icon.
64 |
65 | Properties
66 | ----------
67 |
68 | :chips: tuple[str]
69 |
70 | the text of each chip displayed. Empty strings will be ignored, as will duplicates.
71 |
72 | :primary_placeholder: str
73 |
74 | The placeholder when no chips are displayed
75 |
76 | :secondary_placeholder: str
77 |
78 | The placeholder when at least one chip is displayed
79 |
80 | :spacing_above: str
81 |
82 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
83 |
84 | :spacing_below: str
85 |
86 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
87 |
88 | :visible: bool
89 |
90 | Is the component visible
91 |
92 |
93 | Events
94 | ------
95 | :chips_changed:
96 |
97 | When a chip is added or removed
98 |
99 | :chip_added:
100 |
101 | When a chip is added. Includes the chip text that was added as an event arg.
102 |
103 | :chip_removed:
104 |
105 | When a chip is removed. Includes the chip text that was removed as an event arg;
106 |
107 | :show:
108 |
109 | When the component is shown
110 |
111 | :hide:
112 |
113 | When the component is hidden
114 |
--------------------------------------------------------------------------------
/docs/guides/modules/authorisation.rst:
--------------------------------------------------------------------------------
1 | Authorisation
2 | =============
3 | A server module that provides user authentication and role based authorisation
4 | for server functions.
5 |
6 | Installation
7 | ------------
8 |
9 | You will need to setup the Users and Data Table services in your app:
10 |
11 | * Ensure that you have added the 'Users' service to your app
12 | * In the 'Data Tables' service, add:
13 | * a table named 'permissions' with a text column named 'name'
14 | * a table named 'roles' with a text column named 'name' and a 'link to table'column named 'permissions' that links to multiple rows of the permissions table
15 | * a new 'link to table' column in the Users table named 'roles' that links to multiple rows of the 'roles' table
16 |
17 | Usage
18 | -----
19 |
20 | Users and Permissions
21 | +++++++++++++++++++++
22 |
23 | * Add entries to the permissions table. (e.g. 'can_view_stuff', 'can_edit_sensitive_thing')
24 | * Add entries to the roles table (e.g. 'admin') with links to the relevant permissions
25 | * In the Users table, link users to the relevant roles
26 |
27 | Server Functions
28 | ++++++++++++++++
29 | The module includes two decorators which you can use on your server functions:
30 |
31 | `authentication_required`
32 |
33 | Checks that a user is logged in to your app before the function is called and raises
34 | an error if not. e.g.:
35 |
36 | .. code-block:: python
37 |
38 | import anvil.server
39 | from anvil_extras.authorisation import authentication_required
40 |
41 | @anvil.server.callable
42 | @authentication_required
43 | def sensitive_server_function():
44 | do_stuff()
45 |
46 | `authorisation_required`
47 |
48 | Checks that a user is logged in to your app and has sufficient permissions before the
49 | function is called and raises an error if not:
50 |
51 | .. code-block:: python
52 |
53 | import anvil.server
54 | from anvil_extras.authorisation import authorisation_required
55 |
56 | @anvil.server.callable
57 | @authorisation_required("can_edit_sensitive_thing")
58 | def sensitive_server_function():
59 | do_stuff()
60 |
61 | You can pass either a single string or a list of strings to the decorator. The function
62 | will only be called if the logged in user has ALL the permissions listed.
63 |
64 | Notes:
65 | * The order of the decorators matters. `anvil.server.callable` must come before either of the authorisation module decorators.
66 |
--------------------------------------------------------------------------------
/client_code/Switch/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - {name: checked, type: boolean, default_value: false, default_binding_prop: true,
3 | important: true, allow_binding_writeback: true}
4 | - {name: checked_color, type: color, default_value: null, group: appearance, important: false}
5 | - {name: enabled, type: boolean, default_value: true, group: interaction, important: true}
6 | - {name: foreground, type: color, default_value: null, group: appearance, important: false}
7 | - {name: background, type: color, default_value: null, group: appearance, important: false}
8 | - {name: font_size, type: number, default_value: 14, group: text, important: false}
9 | - {name: bold, type: boolean, default_value: null, group: text, important: false}
10 | - {name: italic, type: boolean, default_value: null, group: text, important: false}
11 | - {name: text_pre, type: string, default_value: '', group: text, important: true}
12 | - {name: text_post, type: string, default_value: '', group: text, important: true}
13 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
14 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
15 | - {name: tooltip, type: string, default_value: '', group: tooltip, important: false}
16 | - {name: visible, type: boolean, default_value: true, group: appearance, important: true}
17 | is_package: true
18 | events:
19 | - {name: change, default_event: true, description: when this switch is checked or unchecked}
20 | - {name: show, description: when this switch is shown}
21 | - {name: hide, description: when this switch is hidden}
22 | custom_component: true
23 | components:
24 | - type: CheckBox
25 | properties: {role: switch, align: left, tooltip: '', border: '', enabled: true,
26 | foreground: '', visible: true, text: '', font_size: null, font: '', spacing_above: small,
27 | spacing_below: small, italic: false, background: '', bold: false, checked: true,
28 | underline: false}
29 | name: check_box_1
30 | layout_properties: {slot: default}
31 | event_bindings: {change: check_box_1_change}
32 | container:
33 | type: HtmlTemplate
34 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
35 | role: switch, html: '
36 |
37 | '}
44 |
--------------------------------------------------------------------------------
/docs/guides/enterprise-installation.rst:
--------------------------------------------------------------------------------
1 | Installation on Anvil Enterprise
2 | ================================
3 |
4 | Enterprise installations of Anvil are entirely separate from the cloud version by design, so you won't be able to depend on the public version of anvil-extras directly.
5 | Instead, create an app on your Enterprise installation called Anvil Extras, then:
6 |
7 |
8 | Clone the Repository
9 | --------------------
10 | * In your browser, navigate to your blank Anvil Extras app within your Anvil IDE.
11 | * From the App Menu (with the gear icon), select 'Version History...' and click the 'Clone with Git' button.
12 | * Copy the displayed command to you clipboard.
13 | * In your terminal, navigate to a folder where you would like to create your local copy
14 | * Paste the command from your clipboard into your terminal and run it.
15 | * You should now have a new folder named 'Anvil_Extras'.
16 |
17 | Configure the Remote Repositories
18 | ---------------------------------
19 | Your local repository is now configured with a known remote repository pointing to your copy of the app at Anvil.
20 | That remote is currently named 'origin'. We will now rename it to something more meaningful and also add a second remote pointing to the repository on github.
21 |
22 | * In your terminal, navigate to your 'Anvil_Extras' folder.
23 | * Rename the 'origin' remote to 'anvil' with the command:
24 |
25 | .. code-block::
26 |
27 | git remote rename origin anvil
28 |
29 | * Add the github repository with the command:
30 |
31 | .. code-block::
32 |
33 | git remote add github git@github.com:anvilistas/anvil-extras.git
34 |
35 | Update your local app
36 | ---------------------
37 | To update your app, we will now fetch the latest version from github to your local copy and push it from there to Anvil.
38 |
39 | * In your terminal, fetch the lastest code from github using the commands:
40 |
41 | .. code-block::
42 |
43 | git fetch github
44 | git reset --hard github/main
45 |
46 | * Finally, push those changes to your copy of the app at Anvil:
47 |
48 | .. code-block::
49 |
50 | git push -f anvil
51 |
52 |
53 |
54 | Add anvil-extras as a dependency to your own app(s)
55 | ---------------------------------------------------
56 |
57 | * From the gear icon at the top of your app's left hand sidebar, select 'Dependencies'
58 | * From the 'Add a dependency' dropdown, select 'Anvil Extras'
59 |
60 | That's it! You should now see the extra components available in your app's toolbox on the right hand side and all the other features are available for you to import.
61 |
--------------------------------------------------------------------------------
/client_code/ChipsInput/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - name: chips
3 | type: text[]
4 | default_value: null
5 | allow_binding_writeback: true
6 | binding_writeback_events: [chips_changed]
7 | description: Set to a list of strings
8 | default_binding_prop: true
9 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
10 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
11 | - {name: visible, type: boolean, default_value: true, group: appearance, important: true}
12 | - {name: primary_placeholder, type: string, default_value: Enter a Tag, description: placeholder when there are no chips}
13 | - {name: secondary_placeholder, type: string, default_value: +Tag, description: placeholder when there are chips}
14 | is_package: true
15 | events:
16 | - {name: chips_changed, default_event: true, description: when a chip is added or removed}
17 | - name: chip_added
18 | description: when a chip is added
19 | parameters:
20 | - {name: chip, description: the chip that was added}
21 | - name: chip_removed
22 | description: when a chip is removed
23 | parameters:
24 | - {name: chip}
25 | - {name: show, description: when the component is shown}
26 | - {name: hide, description: when the component is hidden}
27 | custom_component: true
28 | components:
29 | - type: form:Chip
30 | properties: {text: chip, icon: null, close_icon: true, background: null, foreground: null}
31 | name: temp_chip
32 | layout_properties: {}
33 | - type: TextBox
34 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: +Tag,
35 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
36 | font: '', spacing_above: none, type: text, spacing_below: small, italic: false,
37 | background: '', bold: false, underline: false}
38 | name: chip_input
39 | layout_properties: {}
40 | event_bindings: {pressed_enter: _chip_input_pressed_enter, focus: _chip_input_focus,
41 | lost_focus: _chip_input_lost_focus}
42 | container:
43 | type: HtmlTemplate
44 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
45 | role: null, html: '
46 |
47 |
48 |
49 |
50 |
57 |
58 | '}
59 |
--------------------------------------------------------------------------------
/client_code/messaging.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | __version__ = "1.7.1"
8 |
9 |
10 | class Message:
11 | def __init__(self, title, content=None):
12 | self.title = title
13 | self.content = content
14 |
15 |
16 | class Subscriber:
17 | def __init__(self, subscriber, handler):
18 | self.subscriber = subscriber
19 | self.handler = handler
20 |
21 |
22 | class Publisher:
23 | def __init__(self, with_logging=True):
24 | self.with_logging = with_logging
25 | self.subscribers = {}
26 |
27 | def publish(self, channel, title, content=None, with_logging=None):
28 | if with_logging is None:
29 | with_logging = self.with_logging
30 | message = Message(title, content)
31 | subscribers = self.subscribers.get(channel, [])
32 | for subscriber in subscribers:
33 | subscriber.handler(message)
34 | if with_logging:
35 | print(
36 | f"Published '{message.title}' message on '{channel}' channel to "
37 | f"{len(subscribers)} subscriber(s)"
38 | )
39 |
40 | def subscribe(self, channel, subscriber, handler, with_logging=None):
41 | if with_logging is None:
42 | with_logging = self.with_logging
43 | if channel not in self.subscribers:
44 | self.subscribers[channel] = []
45 | self.subscribers[channel].append(Subscriber(subscriber, handler))
46 | if with_logging:
47 | print(f"Added subscriber to {channel} channel")
48 |
49 | def unsubscribe(self, channel, subscriber, with_logging=None):
50 | if with_logging is None:
51 | with_logging = self.with_logging
52 | if channel in self.subscribers:
53 | self.subscribers[channel] = [
54 | s for s in self.subscribers[channel] if s.subscriber != subscriber
55 | ]
56 | if with_logging:
57 | print(f"Removed subscriber from {channel} channel")
58 |
59 | def close_channel(self, channel, with_logging=None):
60 | if with_logging is None:
61 | with_logging = self.with_logging
62 | subscribers_count = len(self.subscribers[channel])
63 | del self.subscribers[channel]
64 | if with_logging:
65 | print(f"{channel} closed ({subscribers_count} subscribers)")
66 |
--------------------------------------------------------------------------------
/client_code/Tabs/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - {name: background, type: color, default_value: null, group: appearance, important: false}
3 | - {name: foreground, type: color, default_value: null, description: This should be a hex value or a theme color,
4 | group: appearance, important: false}
5 | - name: tab_titles
6 | type: text[]
7 | default_value: [Tab 1, Tab 2, Tab 3]
8 | description: Each line should be a new tab title
9 | important: true
10 | - {name: active_tab_index, type: number, default_value: 0, default_binding_prop: true,
11 | description: The current active tab, important: false}
12 | - {name: role, type: string, default_value: null, description: This component works well with the card role. Place a card below or above the tabs component,
13 | group: appearance, important: true}
14 | - {name: align, type: string, default_value: left, description: align tab text left center or right,
15 | group: text, important: false}
16 | - {name: visible, type: boolean, default_value: true, group: appearance, important: true}
17 | - {name: spacing_above, type: string, default_value: none, group: layout, important: false}
18 | - {name: spacing_below, type: string, default_value: none, group: layout, important: false}
19 | - {name: bold, type: boolean, default_value: null, group: text, important: false}
20 | - {name: font_size, type: string, default_value: null, group: text, important: false}
21 | - {name: italic, type: boolean, default_value: null, group: text, important: false}
22 | - {name: font, type: string, default_value: '', group: text, important: false}
23 | is_package: true
24 | events:
25 | - name: tab_click
26 | default_event: true
27 | description: when a tab is clicked
28 | parameters:
29 | - {name: tab_index}
30 | - {name: tab_title}
31 | - {name: show, description: when the tabs are shown}
32 | - {name: hide, description: when the tabs are hidden}
33 | custom_component: true
34 | components:
35 | - type: Link
36 | properties: {}
37 | name: link_1
38 | layout_properties: {slot: default}
39 | - type: Link
40 | properties: {}
41 | name: link_2
42 | layout_properties: {slot: default}
43 | container:
44 | type: HtmlTemplate
45 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
46 | role: null, html: "
\n \n \n
\n\n\n\n"}
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://anvil-extras.readthedocs.io/en/latest/)
2 | [](https://gitter.im/anvilistas/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
3 |
4 | # Anvil Extras
5 |
6 | A library of utilities and components for use in developing an [Anvil](https://anvil.works) application.
7 |
8 | See our **[Full documentation](https://anvil-extras.readthedocs.io/en/latest/)**
9 |
10 |
11 |
12 | ## Installation
13 |
14 | Add the library as a third party dependency to your app with the token `C6ZZPAPN4YYF5NVJ`.
15 | Full instructions can be found in the [Docs](https://anvil-extras.readthedocs.io/en/latest/guides/installation.html)
16 |
17 | ## Features
18 |
19 | #### Components
20 |
21 | | | | | |
22 | |-|:----:|-|:----:|
23 | | Message Pill | | Progress Bars | |
24 | | Auto Refreshing | | Tabs | | Switch | |
25 | | Multi Select Dropdown | | Quill Editor | |
26 | | Sliders | | Chips | |
27 | | Autocomplete | | Pivot | |
28 |
29 |
30 |
31 |
32 | #### Modules
33 |
34 | - [Publish/Subscribe Messaging](https://anvil-extras.readthedocs.io/en/latest/guides/modules/messaging.html)
35 | - [Dynamic Menu Construction](https://anvil-extras.readthedocs.io/en/latest/guides/modules/navigation.html)
36 | - [Role Based Authorisation](https://anvil-extras.readthedocs.io/en/latest/guides/modules/authorisation.html)
37 | - [Augmented Events](https://anvil-extras.readthedocs.io/en/latest/guides/modules/augmentation.html)
38 | - [Popovers ](https://anvil-extras.readthedocs.io/en/latest/guides/modules/popover.html)
39 | - [Client and server side logging/timing ](https://anvil_extras.readthedocs.io/en/latest/guides/modules/utils.html)
40 |
41 |
42 |
43 | ## Contributing
44 |
45 | All contributions are welcome!
46 |
47 | Please read our [Contribution guide](https://anvil-extras.readthedocs.io/en/latest/guides/contributing.html) before starting any work.
48 |
49 | ## Maintainers
50 |
51 | The maintainers of the project are:
52 |
53 | - [Owen Campbell](https://github.com/meatballs)
54 | - [Stu Cork](https://github.com/s-cork)
55 |
--------------------------------------------------------------------------------
/docs/guides/modules/augmentation.rst:
--------------------------------------------------------------------------------
1 | Augmentation
2 | ============
3 | A client module for adding custom jQuery events to any anvil component
4 |
5 | .. image:: https://anvil.works/img/forum/copy-app.png
6 | :height: 40px
7 | :target: https://anvil.works/build#clone:36T6RN2OO6KLBGV7=4LZ35S57ODPL7ORIUJ2AH6KY|223FMU5UYH5T2XSA=UYJICI36SETZB4DPFRHCKMVA
8 |
9 |
10 | Examples
11 | --------
12 |
13 | .. code-block:: python
14 |
15 | from anvil_extras import augment
16 | augment.set_event_handler(self.link, 'hover', self.link_hover)
17 | # equivalent to
18 | # augment.set_event_handler(self.link, 'mouseenter', self.link_hover)
19 | # augment.set_event_handler(self.link, 'mouseleave', self.link_hover)
20 | # or
21 | # augment.set_event_handler(self.link, 'mouseenter mouseleave', self.link_hover)
22 |
23 | def link_hover(self, **event_args):
24 | if 'enter' in event_args['event_type']:
25 | self.link.text = 'hover'
26 | else:
27 | self.link.text = 'hover_out'
28 |
29 | #================================================
30 | # augment.set_event_handler equivalent to
31 | augment.add_event(self.button, 'focus')
32 | self.button.set_event_handler('focus', self.button_focus)
33 |
34 | def button_focus(self, **event_args):
35 | self.button.text = 'Focus'
36 | self.button.role = 'secondary-color'
37 |
38 |
39 | need a trigger method?
40 | ----------------------
41 |
42 | .. code-block:: python
43 |
44 | def button_click(self, **event_args):
45 | self.textbox.trigger('select')
46 |
47 | Keydown example
48 | ---------------
49 |
50 | .. code-block:: python
51 |
52 | augment.set_event_handler(self.text_box, 'keydown', self.text_box_keydown)
53 |
54 | def text_box_keydown(self, **event_args):
55 | key_code = event_args.get('key_code')
56 | key = event_args.get('key')
57 | if key_code == 13:
58 | print(key, key_code)
59 |
60 |
61 | advanced feature
62 | ----------------
63 |
64 | you can prevent default behaviour of an event by returning a value in the event handler function - example use case*
65 |
66 |
67 | .. code-block:: python
68 |
69 | augment.set_event_handler(self.text_area, 'keydown', self.text_area_keydown)
70 |
71 | def text_area_keydown(self, **event_args):
72 | key = event_args.get('key')
73 | if key.lower() == 'enter':
74 | # prevent the standard enter new line behaviour
75 | # prevent default
76 | return True
77 |
78 |
79 | DataGrid pagination_click
80 | ----------------
81 |
82 | Importing the augment module gives DataGrid's a ``pagination_click`` event
83 |
84 | .. code-block:: python
85 |
86 | self.data_grid.set_event_handler('pagination_click', self.pagination_click)
87 |
88 | def pagination_click(self, **event_args):
89 | button = event_args["button"] # 'first', 'last', 'previous', 'next'
90 | print(button, "was clicked")
91 |
--------------------------------------------------------------------------------
/client_code/Pivot/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | import anvil
8 | import anvil.js
9 |
10 | from ..utils import _component_helpers as helpers
11 | from ._anvil_designer import PivotTemplate
12 |
13 | __version__ = "1.7.1"
14 |
15 | pivottable_version = "2.23.0"
16 | jqueryui_version = "1.11.4"
17 |
18 | prefix = "https://cdnjs.cloudflare.com/ajax/libs"
19 | dependencies = [
20 | f"{prefix}/pivottable/{pivottable_version}/pivot.min.css",
21 | f"{prefix}/pivottable/{pivottable_version}/pivot.min.js",
22 | ]
23 | jqueryui = f"{prefix}/jqueryui/{jqueryui_version}/jquery-ui.min.js"
24 |
25 | _jquery = anvil.js.window.jQuery
26 |
27 | helpers._html_injector.css(
28 | """.anvil-container-overflow, .anvil-panel-col {
29 | overflow: visible;
30 | }
31 | .anvil-extras-pivot-container {
32 | display: grid
33 | }
34 | .pivot-placeholder {
35 | overflow: auto
36 | }
37 | """
38 | )
39 |
40 | if "ui" not in _jquery.keys():
41 | helpers._html_injector.cdn(jqueryui)
42 |
43 | try:
44 | assert tuple(int(x) for x in _jquery.ui.version.split(".")) >= (1, 9, 0)
45 | except AssertionError:
46 | raise AssertionError("Incompatible jqueryui version already installed")
47 |
48 | try:
49 | assert "pivotUtilities" in _jquery.keys()
50 | except AssertionError:
51 | for dependency in dependencies:
52 | helpers._html_injector.cdn(dependency)
53 | assert "pivotUtilities" in _jquery.keys()
54 |
55 |
56 | class Pivot(PivotTemplate):
57 | option_names = {
58 | "rows": "rows",
59 | "columns": "cols",
60 | "values": "vals",
61 | "aggregator": "aggregatorName",
62 | }
63 |
64 | def __init__(self, **properties):
65 | self.pivot_initiated = False
66 | self.pivot_options = {
67 | option: properties[option] for option in self.option_names
68 | }
69 | dom_node = anvil.js.get_dom_node(self)
70 | self.pivot_node = dom_node.querySelector(".anvil-extras-pivot")
71 | dom_node.querySelector("script").remove()
72 | dom_node.classList.add("anvil-extras-pivot-container")
73 | self.init_components(**properties)
74 |
75 | def _init_pivot(self):
76 | card_node = self.pivot_node.closest(".anvil-role-card")
77 | if card_node:
78 | card_node.style.overflow = "visible"
79 | options = {
80 | value: self.pivot_options[key] for key, value in self.option_names.items()
81 | }
82 | _jquery(self.pivot_node).pivotUI(self.items, options)
83 |
84 | def form_show(self, **event_args):
85 | if not self.pivot_initiated:
86 | self._init_pivot()
87 | self.pivot_initiated = True
88 |
--------------------------------------------------------------------------------
/client_code/Quill/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - {name: enabled, type: boolean, default_value: true, default_binding_prop: true,
3 | group: interaction, important: true}
4 | - {name: visible, type: boolean, default_value: true, default_binding_prop: false,
5 | group: appearance, important: true}
6 | - {name: height, type: string, default_value: '150', default_binding_prop: false,
7 | description: If auto_expand is set to True this will be the min_height. otherwise it'll be the fixed height,
8 | group: height, important: false}
9 | - {name: auto_expand, type: boolean, default_value: true, default_binding_prop: false,
10 | description: If set to True the height becomes the starting height, important: false}
11 | - {name: readonly, type: boolean, default_value: false, default_binding_prop: false,
12 | description: Similar to enabled but cannot be updated, group: interaction, important: false}
13 | - {name: toolbar, type: boolean, default_value: true, default_binding_prop: false,
14 | description: This can be set at runtime for different tooolbar configurations, important: false}
15 | - {name: modules, type: object, default_value: null, default_binding_prop: false,
16 | description: This can be set at runtime and must include all modules. The toolbar can be set separately,
17 | important: false}
18 | - {name: theme, type: string, default_value: snow, default_binding_prop: false, description: snow or bubble - check quill for the difference - cannot be updated once set,
19 | group: appearance, important: false}
20 | - {name: placeholder, type: string, default_value: '', default_binding_prop: false,
21 | description: The text to display when there is not content, important: false}
22 | - {name: content, type: string, default_binding_prop: false, description: A json-able object you can store as simple object in a data table cell. Use get_contents to interact with the quill delta object directly.,
23 | default_value: null, important: false}
24 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
25 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
26 | is_package: true
27 | events:
28 | - {name: text_change, description: when the quill text changes, default: true}
29 | - {name: selection_change, description: when the quill text selection changes, default: false}
30 | - {name: show, description: when the component is shown, default: false}
31 | - {name: hide, description: when the component is no longer on the screen, default: false}
32 | custom_component: true
33 | components: []
34 | container:
35 | type: HtmlTemplate
36 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
37 | role: null, html: '
38 |
39 |
46 |
47 | '}
48 |
--------------------------------------------------------------------------------
/docs/guides/components/quill.rst:
--------------------------------------------------------------------------------
1 | Quill Editor
2 | ============
3 | A wrapper around the Quill editor.
4 |
5 | Properties
6 | ----------
7 |
8 | :auto_expand: Boolean
9 |
10 | When set to ``True`` the Editor will expand with the text. If ``False`` the height is the starting height.
11 |
12 | :content: Object
13 |
14 | This returns a list of dicts. The content of any Quill editor is represented as a Delta object. A Delta object is a wrapper around a JSON object that describes the state of the Quill editor. This property exposes the undelrying JSON which can then be stored in a data table simple object cell.
15 |
16 | When you do ``self.quill.content = some_object``, this will call the underlying ``setContents()`` method.
17 |
18 | You can also set the ``content`` property to a string. This will call the underlying ``setText()`` method.
19 |
20 | Retrieving the ``content`` property will always return the underlying JSON object that represents the contents of the Quill editor. It is equivalent to ``self.quill.getContents().ops``.
21 |
22 | :enabled: Boolean
23 |
24 | Disable interactivity
25 |
26 | :height: String
27 |
28 | With auto_expand this becomes the starting height. Without auto_expand this becomes the fixed height.
29 |
30 | :modules: Object
31 |
32 | Additional modules can be set at runtime. See Quill docs for examples. If a toolbar option is defined in modules this will override the toolbar property.
33 |
34 | :placeholder: String
35 |
36 | Placeholder when there is no text
37 |
38 | :readonly: Boolean
39 |
40 | Check the Quill docs.
41 |
42 | :spacing_above: String
43 |
44 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
45 |
46 | :spacing_below: String
47 |
48 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
49 |
50 | :theme: String
51 |
52 | Quill supports ``"snow"`` or ``"bubble"`` theme.
53 |
54 | :toolbar: Boolean or Object
55 |
56 | Check the Quill docs. If you want to use an Object you can set this at runtime. See quill docs for examples.
57 |
58 | :visible: Boolean
59 |
60 | Is the component visible
61 |
62 |
63 | Methods
64 | ----------
65 | All the methods from the Quill docs should work. You can use camel case or snake case. For example ``self.quill.get_text()`` or ``self.quill.getText()``. These will not come up in the autocomplete.
66 |
67 | Methods from the Quill docs call the underlying javascript Quill editor and the arguments/return values will be as described in the Quill documentation.
68 |
69 | There are two Anvil specific methods:
70 |
71 | :get_html:
72 |
73 | Returns a string representing the html of the contents of the Quill editor. Useful for presenting the text in a RichText component under the ``"restricted_html"`` format.
74 |
75 | :set_html:
76 |
77 | Set the contents of the Quill editor to html. The html will be sanitized in the same way that a RichText component sanitizes the html. See Anvil's documentation on the RichText component.
78 |
79 |
80 |
81 |
82 | Events
83 | ----------
84 | :text_change:
85 |
86 | When the text changes
87 |
88 | :selection_change:
89 |
90 | When the selection changes
91 |
92 | :show:
93 |
94 | When the component is shown
95 |
96 | :hide:
97 |
98 | When the component is hidden
99 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerTabs.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 |
3 | export class DesignerTabs extends DesignerComponent {
4 | static defaults = { tab_titles: ["Tab 1", "Tab 2"], active_tab_index: 0, visible: true, align: "left" };
5 |
6 | static css = `.anvil-extras-tabs.anvil-role-card{border-bottom-left-radius:0;border-bottom-right-radius:0;margin-bottom:-1px}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:var(--background, inherit);margin:0 auto;white-space:nowrap;padding:0;display:flex;z-index:1}.tabs .tab{flex-grow:1;display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(var(--color),0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color 0.28s ease, background-color 0.28s ease;transition:color 0.28s ease, background-color 0.28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgb(var(--color), 0.2);outline:none}.tabs .tab a.active,.tabs .tab a:hover{background-color:transparent;color:rgb(var(--color))}.tabs .indicator{position:absolute;bottom:0;height:3px;background-color:rgb(var(--color), 0.4);will-change:left, right}`;
7 |
8 | static init() {
9 | super.init(".anvil-container .tabs", "anvil-extras-designer");
10 | }
11 |
12 | tabs: HTMLDivElement;
13 |
14 | // deno-lint-ignore
15 | constructor(domNode: HTMLElement, pyComponent: any, el: HTMLDivElement) {
16 | super(domNode, pyComponent, el);
17 | this.tabs = el;
18 | this.domNode.style.paddingTop = "0";
19 | this.domNode.style.paddingBottom = "0";
20 | }
21 |
22 | update(props: any) {
23 | this.clearElement(this.tabs);
24 |
25 | let active: any = { offsetLeft: 0, offsetWidth: 0 };
26 | props.tab_titles ||= [];
27 | props.tab_titles.forEach((title: string, i: number) => {
28 | const li = document.createElement("li");
29 | li.className = "tab";
30 | const a = document.createElement("a");
31 | a.style.cssText = `
32 | font-weight:${props.bold ? "bold" : ""};
33 | text-align: ${props.align};
34 | font-size: ${props.font_size ? props.font_size + "px" : ""};
35 | font-family:${props.font || ""};
36 | font-style: ${props.italic ? "italic" : ""};
37 | `;
38 | a.textContent = title;
39 | li.appendChild(a);
40 | this.tabs.appendChild(li);
41 | if (i === props.active_tab_index) {
42 | a.className = "active";
43 | active = li;
44 | }
45 | });
46 | const indicator = document.createElement("li");
47 | this.tabs.appendChild(indicator);
48 | indicator.className = "indicator";
49 | indicator.style.left = active.offsetLeft + "px";
50 | indicator.style.right = this.tabs.offsetWidth - active.offsetLeft - active.offsetWidth + "px";
51 |
52 | const fg = this.getColorRGB(props.foreground, true);
53 | this.tabs.style.setProperty("--color", fg);
54 |
55 | const bg = this.getColor(props.background);
56 | this.tabs.style.setProperty("--background", bg);
57 |
58 | this.updateSpacing(props);
59 | this.updateRole(props);
60 | this.updateVisible(props);
61 | }
62 | get [Symbol.toStringTag]() {
63 | return "Tabs";
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client_code/Chip/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | from anvil import HtmlPanel as _HtmlPanel
9 | from anvil.js import get_dom_node as _get_dom_node
10 |
11 | from ..utils._component_helpers import _html_injector, _spacing_property
12 | from ._anvil_designer import ChipTemplate
13 |
14 | __version__ = "1.7.1"
15 |
16 | _html_injector.css(
17 | """.anvil-extras-chip{
18 | height: 32px;
19 | font-size: 14px;
20 | font-weight: 500;
21 | color: rgba(0,0,0,0.6);
22 | line-height: 32px;
23 | padding: 0 12px;
24 | border-radius: 16px;
25 | background-color: #e4e4e4;
26 | display: flex;
27 | gap: 14px;
28 | align-items:center;
29 | width: fit-content;
30 | padding-left: 12px;
31 | padding-right: 12px;
32 | position: relative;
33 | }
34 |
35 | .anvil-extras-chip i.anvil-component-icon.left {
36 | font-size: 1.5rem;
37 | }
38 | .anvil-extras-chip a {
39 | user-select: none;
40 | }
41 | .anvil-extras-chip a .link-text {
42 | padding: 0 !important;
43 | }
44 | .anvil-extras-chip span {
45 | padding: 0 !important;
46 | }
47 | """
48 | )
49 |
50 | _defaults = {
51 | "icon": "",
52 | "text": "",
53 | "foreground": "rgba(0,0,0,0.6)",
54 | "background": "",
55 | "close_icon": True,
56 | "spacing_above": "small",
57 | "spacing_below": "small",
58 | "visible": True,
59 | }
60 |
61 |
62 | class Chip(ChipTemplate):
63 | def __init__(self, **properties):
64 | dom_node = self._dom_node = _get_dom_node(self)
65 | dom_node.querySelector("script").remove()
66 | dom_node.querySelector(".chip-placeholder").remove()
67 | dom_node.addEventListener("click", lambda e: self.raise_event("click"))
68 | dom_node.classList.add("anvil-extras-chip")
69 | dom_node.tabIndex = 0
70 |
71 | self.close_link.set_event_handler(
72 | "click", lambda **e: self.raise_event("close_click")
73 | )
74 | properties = _defaults | properties
75 | self.init_components(**properties)
76 | # Any code you write here will run when the form opens.
77 |
78 | @property
79 | def text(self):
80 | return self.chip_label.text
81 |
82 | @text.setter
83 | def text(self, value):
84 | self.chip_label.text = value
85 |
86 | @property
87 | def icon(self):
88 | return self.chip_label.icon
89 |
90 | @icon.setter
91 | def icon(self, value):
92 | self.chip_label.icon = value
93 |
94 | @property
95 | def foreground(self):
96 | return self.chip_label.foreground
97 |
98 | @foreground.setter
99 | def foreground(self, value):
100 | self.chip_label.foreground = value or "rgba(0,0,0,0.6)"
101 | self.close_link.foreground = value or "rgba(0,0,0,0.6)"
102 |
103 | @property
104 | def close_icon(self):
105 | return self.close_link.visible
106 |
107 | @close_icon.setter
108 | def close_icon(self, value):
109 | self.close_link.visible = value
110 |
111 | background = _HtmlPanel.background
112 | visible = _HtmlPanel.visible
113 | spacing_above = _spacing_property("above")
114 | spacing_below = _spacing_property("below")
115 |
--------------------------------------------------------------------------------
/docs/guides/modules/messaging.rst:
--------------------------------------------------------------------------------
1 | Messaging
2 | =========
3 |
4 | Introduction
5 | ------------
6 | This library provides a mechanism for forms (and other components) within an Anvil app
7 | to communicate in a 'fire and forget' manner.
8 |
9 | It's an alternative to raising and handling events - instead you 'publish' messages to
10 | a channel and, from anywhere else, you subscribe to that channel and process those
11 | messages as required.
12 |
13 |
14 | Usage
15 | -----
16 |
17 | Create the Publisher
18 | ++++++++++++++++++++
19 | You will need to create an instance of the Publisher class somewhere in your application
20 | that is loaded at startup.
21 |
22 | For example, you might create a client module at the top level of your app called 'common'
23 | with the following content:
24 |
25 | .. code-block:: python
26 |
27 | from anvil_extras.messaging import Publisher
28 |
29 | publisher = Publisher()
30 |
31 | and then import that module in your app's startup module/form.
32 |
33 | Publish Messages
34 | ++++++++++++++++
35 | From anywhere in your app, you can import the publisher and publish messages to a channel.
36 | e.g. Let's create a simple form that publishes a 'hello world' message when it's initiated:
37 |
38 |
39 | .. code-block:: python
40 |
41 | from ._anvil_designer import MyPublishingFormTemplate
42 | from .common import publisher
43 |
44 |
45 | class MyPublishingForm(MyPublishingFormTemplate):
46 |
47 | def __init__(self, **properties):
48 | publisher.publish(channel="general", title="Hello world")
49 | self.init_components(**properties)
50 |
51 | The publish method also has an optional 'content' parameter which can be passed any object.
52 |
53 | Subscribe to a Channel
54 | ++++++++++++++++++++++
55 | Also, from anywhere in your app, you can subscribe to a channel on the publisher by
56 | providing a handler function to process the incoming messages.
57 |
58 | The handler will be passed a Message object, which has the title and content of the
59 | message as attributes.
60 |
61 | e.g. On a separate form, let's subscribe to the 'general' channel and print any 'Hello
62 | world' messages:
63 |
64 |
65 | .. code-block:: python
66 |
67 | from ._anvil_designer import MySubscribingFormTemplate
68 | from .common import publisher
69 |
70 |
71 | class MySubscribingForm(MySubscribingFormTemplate):
72 |
73 | def __init__(self, **properties):
74 | publisher.subscribe(
75 | channel="general", subscriber=self, handler=self.general_messages_handler
76 | )
77 | self.init_components(**properties)
78 |
79 | def general_messages_handler(self, message):
80 | if message.title == "Hello world":
81 | print(message.title)
82 |
83 | You can unsubscribe from a channel using the publisher's `unsubscribe` method.
84 |
85 | You can also remove an entire channel using the publisher's `close_channel` method.
86 |
87 | Be sure to do one of these if you remove instances
88 | of a form as the publisher will hold references to those instances and the handlers will
89 | continue to be called.
90 |
91 | Logging
92 | +++++++
93 | By default, the publisher will log each message it receieves to your app's logs (and
94 | the output pane if you're in the IDE).
95 |
96 | You can change this default behaviour when you first create your publisher instance:
97 |
98 |
99 |
100 | .. code-block:: python
101 |
102 | from anvil_extras.messaging import Publisher
103 | publisher = Publisher(with_logging=False)
104 | )
105 |
106 | The `publish`, `subscribe`, `unsubscribe` and `close_channel` methods each take an
107 | optional `with_logging` parameter which can be used to override the default behaviour.
108 |
--------------------------------------------------------------------------------
/client_code/utils/_component_helpers.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | import random
8 |
9 | import anvil.js
10 | from anvil import app as _app
11 | from anvil.js import get_dom_node as _get_dom_node
12 | from anvil.js.window import Promise as _Promise
13 | from anvil.js.window import document as _document
14 |
15 | __version__ = "1.7.1"
16 |
17 | _characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
18 |
19 |
20 | class HTMLInjector:
21 | _injected_css = set()
22 |
23 | def css(self, css):
24 | """inject some custom css"""
25 | hashed = hash(css)
26 | if hashed in self._injected_css:
27 | return
28 | sheet = self._create_tag("style")
29 | sheet.innerHTML = css
30 | self._inject(sheet, head=False)
31 | self._injected_css.add(hashed)
32 |
33 | def cdn(self, cdn_url, **attrs):
34 | """inject a js/css cdn file"""
35 | if cdn_url.endswith("js"):
36 | tag = self._create_tag("script", src=cdn_url, **attrs)
37 | elif cdn_url.endswith("css"):
38 | tag = self._create_tag("link", href=cdn_url, rel="stylesheet", **attrs)
39 | else:
40 | raise ValueError("Unknown CDN type expected css or js file")
41 | self._inject(tag)
42 | self._wait_for_load(tag)
43 |
44 | def script(self, js):
45 | """inject some javascript code inside a script tag"""
46 | s = self._create_tag("script")
47 | s.textContent = js
48 | self._inject(s)
49 |
50 | def _create_tag(self, tag_name, **attrs):
51 | tag = _document.createElement(tag_name)
52 | for attr, value in attrs.items():
53 | tag.setAttribute(attr, value)
54 | return tag
55 |
56 | def _inject(self, tag, head=True):
57 | if head:
58 | _document.head.appendChild(tag)
59 | else:
60 | _document.body.appendChild(tag)
61 |
62 | def _wait_for_load(self, tag):
63 | if not tag.get("src"):
64 | return
65 |
66 | def do_wait(res, rej):
67 | tag.onload = res
68 | tag.onerror = rej
69 |
70 | p = _Promise(do_wait)
71 | anvil.js.await_promise(p)
72 |
73 |
74 | _html_injector = HTMLInjector()
75 |
76 |
77 | def _get_dom_node_id(component):
78 | node = _get_dom_node(component)
79 | if not node.id:
80 | node.id = "".join([random.choice(_characters) for _ in range(8)])
81 | return node.id
82 |
83 |
84 | def _spacing_property(a_b):
85 | def getter(self):
86 | return getattr(self, "_spacing_" + a_b)
87 |
88 | def setter(self, value):
89 | self._dom_node.classList.remove(
90 | f"anvil-spacing-{a_b}-{getattr(self, '_spacing_' + a_b, '')}"
91 | )
92 | self._dom_node.classList.add(f"anvil-spacing-{a_b}-{value}")
93 | setattr(self, "_spacing_" + a_b, value)
94 |
95 | return property(getter, setter, None, a_b)
96 |
97 |
98 | _primary = _app.theme_colors.get("Primary 500", "#2196F3")
99 |
100 |
101 | def _get_color(value):
102 | if not value:
103 | return _primary
104 | elif value.startswith("theme:"):
105 | return _app.theme_colors.get(value.replace("theme:", ""), _primary)
106 | else:
107 | return value
108 |
109 |
110 | def _get_rgb(value):
111 | value = _get_color(value)
112 | if value.startswith("#"):
113 | value = value[1:]
114 | value = ",".join(str(int(value[i : i + 2], 16)) for i in (0, 2, 4))
115 | elif value.startswith("rgb") and value.endswith(")"):
116 | value = value[value.find("("), -1]
117 | else:
118 | raise ValueError(
119 | f"expected a hex value, theme color or rgb value, not, {value}"
120 | )
121 | return value
122 |
--------------------------------------------------------------------------------
/docs/guides/modules/utils.rst:
--------------------------------------------------------------------------------
1 | Utils
2 | =====
3 | Client and server side utility functions.
4 |
5 | Timing
6 | ------
7 | There are client and server side decorators which you can use to show that a function has been called and the length of time it took to execute.
8 |
9 | Client Code
10 | ^^^^^^^^^^^
11 | Import the ``timed`` decorator and apply it to a function:
12 |
13 | .. code-block:: python
14 |
15 | from anvil_extras.utils import timed
16 |
17 | @timed
18 | def target_function(args, **kwargs):
19 | print("hello world")
20 |
21 | When the decorated function is called, you will see messages in the IDE console showing the arguments that were passed to it and the execution time.
22 |
23 | Server Code
24 | ^^^^^^^^^^^
25 | Import the ``timed`` decorator and apply it to a function:
26 |
27 | .. code-block:: python
28 |
29 | import anvil.server
30 | from anvil_extras.server_utils import timed
31 |
32 |
33 | @anvil.server.callable
34 | @timed
35 | def target_function(args, **kwargs):
36 | print("hello world")
37 |
38 | On the server side, the decorator takes a ``logging.Logger`` instance as one of its optional arguments. The default instance will log to stdout, so that messages will appear in your app's logs and in the IDE console. You can, however, create your own logger and pass that instead if you need more sophisticated behaviour:
39 |
40 | .. code-block:: python
41 |
42 | import logging
43 | import anvil.server
44 | from anvil_extras.server_utils import timed
45 |
46 | my_logger = logging.getLogger(__name__)
47 |
48 |
49 | @timed(logger=my_logger)
50 | def target_function(args, **kwargs):
51 | ...
52 |
53 | The decorator also takes an optional ``level`` argument which must be one of the standard levels from the logging module. When no argument is passed, the default level is ``logging.INFO``.
54 |
55 | Auto-Refresh
56 | ------------
57 | Whenever you set a form's ``item`` attribute, the form's ``refresh_data_bindings`` method is called automatically.
58 |
59 | The ``utils`` module includes a decorator you can add to a form's class so that ``refresh_data_bindings`` is called whenever ``item`` changes at all.
60 |
61 | To use it, import the decorator and apply it to the class for a form:
62 |
63 | .. code-block:: python
64 |
65 | from anvil_extras.utils import auto_refreshing
66 | from ._anvil_designer import MyFormTemplate
67 |
68 |
69 | @auto_refreshing
70 | class MyForm(MyFormTemplate):
71 | def __init__(self, **properties):
72 | self.init_components(**properties)
73 |
74 | Now, the form has an ``item`` property which behaves like a dictionary. Whenever a value of that dictionary changes, the form's ``refresh_data_bindings`` method will be called.
75 |
76 | Note: The ``item`` property will no longer reference the same object. Rather, in the following example, it is as though auto-refresh adds the ``item = dict(item)`` line:
77 |
78 | .. code-block:: python
79 |
80 | other_item = {"x": 1}
81 | item = other_item
82 |
83 | item = dict(item)
84 | item["x"] = 2
85 |
86 | As in the above code, with auto-refresh, ``item`` is changed but ``other_item`` is not.
87 |
88 |
89 | Wait for writeback
90 | ------------------
91 | Using ``wait_for_writeback`` as a decorator prevents a function executing before any queued writebacks have completed.
92 |
93 | This is particularly useful if you have a form with text fields. Race condidtions can occur between a text field writing back to an item and a click event that uses the item.
94 |
95 | To use ``wait_for_writeback``, import the decorator and apply it to a function, usually an event_handler:
96 |
97 | .. code-block:: python
98 |
99 | from anvil_extras.utils import wait_for_writeback
100 |
101 | class MyForm(MyFormTemplate):
102 | ...
103 |
104 | @wait_for_writeback
105 | def button_1_click(self, **event_args):
106 | anvil.server.call("save_item", self.item)
107 |
108 |
109 | The click event will now only be called after all active writebacks have finished executing.
110 |
--------------------------------------------------------------------------------
/docs/guides/contributing.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 | All contributions to this project are welcome via pull request (PR) on the `Github repository `_
4 |
5 | Issues
6 | ------
7 | Please open an `Issue `_ and describe the contribution you'd like to make before submitting any code. This prevents duplication of effort and makes reviewing the eventual PR much easier for the maintainers.
8 |
9 | Commits
10 | -------
11 | Please try to use commit messages that give a meaningful history for anyone using git's log features. Try to use messages that complete the sentence, "This commit will..." There is some excellent guidance on the subject from `Chris Beams `_
12 |
13 | Please ensure that your commits do not include changes to either `anvil.yaml` or `.anvil_editor.yaml`.
14 |
15 | Components
16 | ----------
17 | All the components in the library are intended to work from the anvil toolbox as soon as the dependency has been added to an application, without any further setup. This means that they cannot use any of the features within the library's theme.
18 |
19 | If you are thinking of submitting a new component, please ensure that it is entirely standalone and does not require any css or javascript from within a theme element or native library.
20 |
21 | If your component has custom properties or events, it must be able to cope with multiple instances of itself on the same form. There are examples of how to do this using a unique id in several of the existing components.
22 |
23 | Whilst canvas based components will be considered, the preference is for solutions using standard Anvil components, custom HTML forms and css.
24 |
25 | Python Code
26 | -----------
27 | Please try, as far as possible, to follow `PEP8 `_.
28 |
29 | Use the `Black formatter `_ to format all code and the `isort utility `_ to sort import statements.
30 |
31 | Add the licence text and copyright statement to the top of your code.
32 |
33 | Ensure that there is a line with the current version number towards the top of your code.
34 |
35 | This can be automated by using `pre-commit `_.
36 | To use ``pre-commit``, first install ``pre-commit`` with pip and then run ``pre-commit install`` inside your local ``anvil-extras`` repository.
37 | All commits thereafter will be adjusted according to the above ``anvil-extras`` python requirements.
38 |
39 |
40 | Documentation
41 | -------------
42 | Please include documentation for your contribution as part of your PR. Our documents are written in `reStructuredText `_ and hosted at `Read The Docs `_
43 |
44 | Our docs are built using `Sphinx `_ which you can install locally and use to view your work before submission. To build a local copy of the docs in a 'build' directory:
45 |
46 | .. code-block::
47 |
48 | sphinx-build docs build
49 |
50 | You can then open 'index.html' from within the build directory using your favourite browser.
51 |
52 | Testing
53 | -------
54 | The project uses the `Pytest `_ library and its test suite can be run with:
55 |
56 | .. code-block::
57 |
58 | python -m pytest
59 |
60 | We appreciate the difficulty of writing unit tests for Anvil applications but, if you are submitting pure Python code with no dependency on any of the Anvil framework, we'll expect to see some additions to the test suite for that code.
61 |
62 | Merging
63 | -------
64 | We require both maintainers to have reviewed and accepted a PR before it is merged.
65 |
66 | If you would like feedback on your contribution before it's ready to merge, please create a draft PR and request a review.
67 |
68 | Copyright
69 | ---------
70 | By submitting a PR, you agree that your work may be distributed under the terms of the project's `licence `_ and that you will become one of the project's `joint copyright holders `_.
71 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerSwitch.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 | //deno-lint-ignore
3 | declare var Sk: any;
4 | export class DesignerSwitch extends DesignerComponent {
5 | static css = `
6 | .switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
7 | .switch label{cursor:pointer}
8 | .switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:rgba(var(--color), .5)}
9 | .switch label input[type=checkbox]:checked+.lever:after,.switch label input[type=checkbox]:checked+.lever:before{left:18px}
10 | .switch label input[type=checkbox]:checked+.lever:after{background-color:rgb(var(--color))}
11 | .switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}
12 | .switch label .lever:after,.switch label .lever:before{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background 0.3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;transition:left 0.3s ease, background 0.3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;transition:left 0.3s ease, background 0.3s ease, box-shadow 0.1s ease, transform 0.1s ease;transition:left 0.3s ease, background 0.3s ease, box-shadow 0.1s ease, transform 0.1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease}
13 | .switch label .lever:before{background-color:rgb(var(--color), 0.15)}
14 | .switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0 rgba(0,0,0,0.14),0px 1px 5px 0 rgba(0,0,0,0.12);box-shadow:0 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0 rgba(0,0,0,0.14),0px 1px 5px 0 rgba(0,0,0,0.12)}
15 | input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgb(var(--color), 0.15)}
16 | input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}
17 | .switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}
18 | .switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}`;
19 |
20 | static init() {
21 | super.init(".anvil-extras-switch");
22 | }
23 |
24 | cb: any;
25 | cbNode: HTMLInputElement;
26 | textNodePre: Text;
27 | textNodePost: Text;
28 | constructor(domNode: HTMLElement, pyComponent: any, el: HTMLElement) {
29 | super(domNode, pyComponent, el);
30 | const cb = pyComponent._anvil.components[0].component;
31 | const cbNode = cb._anvil.domNode;
32 | cbNode.classList.add("switch");
33 | cbNode.style.setProperty("--color", this.getColorRGB("#2196F3"));
34 | const span = cbNode.querySelector("span");
35 | span.classList.add("lever");
36 | span.removeAttribute("style");
37 | const label = cbNode.querySelector("label");
38 | const textNodePre = document.createTextNode("");
39 | const textNodePost = document.createTextNode("");
40 | label.prepend(textNodePre);
41 | label.append(textNodePost);
42 | label.style.padding = "7px 0";
43 |
44 | this.cb = cb;
45 | this.cbNode = cbNode;
46 | this.textNodePre = textNodePre;
47 | this.textNodePost = textNodePost;
48 | }
49 |
50 | setProp(propName: string, v: any, props: any) {
51 | try {
52 | this.cb._anvil.setProp(propName, Sk.ffi.toPy(v));
53 | } catch (e) {
54 | if (propName === "checked_color") {
55 | this.cbNode.style.setProperty("--color", this.getColorRGB(v, true));
56 | } else if (propName === "text_pre") {
57 | this.textNodePre.textContent = v;
58 | } else {
59 | this.textNodePost.textContent = v;
60 | }
61 | }
62 | }
63 |
64 | get [Symbol.toStringTag]() {
65 | return "Switch";
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/docs/guides/modules/popover.rst:
--------------------------------------------------------------------------------
1 | Popovers
2 | ========
3 | A client module that allows bootstrap popovers in anvil
4 |
5 | Live Example: `popover-example.anvil.app `_
6 |
7 | Example Clone Link:
8 |
9 | .. image:: https://anvil.works/img/forum/copy-app.png
10 | :height: 40px
11 | :target: https://anvil.works/build#clone:YRRNNZJZV5IJM6NX=ACDZQ3LRIADCMMGFANOJZG5N
12 |
13 |
14 |
15 | Introduction
16 | ------------
17 | Popovers are already included with anvil since anvil `ships with bootstrap `_.
18 |
19 | This module provides a python wrapper around `bootstrap popovers `_.
20 | When the ``popover`` module is imported, all anvil components get two additional methods - ``pop`` and ``popover``.
21 |
22 |
23 | Usage
24 | -----
25 |
26 | .. code-block:: python
27 |
28 | from anvil_extras import popover
29 | # importing the module adds the popover method to Button and Link
30 |
31 | self.button = Button()
32 | self.button.popover(content='example text', title='Popover', placement="top")
33 |
34 |
35 | .. code-block:: python
36 |
37 | from anvil_extras import popover
38 |
39 | self.button_1.popover(Form2(), trigger="manual")
40 | # content can be an anvil component
41 |
42 | def button_1_click(self, **event_args):
43 | if self.button_1.pop("is_visible"):
44 | self.button_1.pop("hide")
45 | else:
46 | self.button_1.pop("show")
47 | # equivalent to self.button_1.pop("toggle")
48 |
49 | API
50 | ---
51 |
52 | .. method:: popover(self, content, title='', placement='right', trigger='click', animation=True, delay={"show": 100, "hide": 100}, max_width=None, auto_dismiss=True)
53 |
54 | popover is a method that can be used with any anvil component. Commonly used on ``Button`` and ``Link`` components.
55 |
56 | .. describe:: self
57 |
58 | the component used. No need to worry about this argument when using popover as a method e.g. ``self.button_1.popover(content='example text')``
59 |
60 | .. describe:: content
61 |
62 | content can be a string or an anvil component. If an anvil component is used - that component will have a new attribute ``popper`` added.
63 | This allows the the content form to close itself using ``self.popper.pop('hide')``.
64 |
65 | .. describe:: title
66 |
67 | optional string.
68 |
69 | .. describe:: placement
70 |
71 | One of ``'right'``, ``'left'``, ``'top'``, ``'bottom'``. If using ``left`` or ``right`` it may be best to place the component in a ``FlowPanel``.
72 |
73 | .. describe:: trigger
74 |
75 | One of ``'manual'``, ``'focus'``, ``'hover'``, ``'click'``, (can be a combination of two e.g. ``'hover focus'``). ``'stickyhover'`` is also available.
76 |
77 | .. describe:: animation
78 |
79 | ``True`` or ``False``
80 |
81 | .. describe:: delay
82 |
83 | A dictionary with the keys ``'show'`` and ``'hide'``. The values for ``'show'`` and ``'hide'`` are in milliseconds.
84 |
85 | .. describe:: max_width
86 |
87 | bootstrap default is 276px you might want this wider
88 |
89 | .. describe:: auto_dismiss
90 |
91 | When clicking outside a popover the popover will be closed. Setting this flag to ``False`` overrides that behaviour.
92 | Note that popovers will always be dismissed when the page is scrolled. This prevents popovers from appearing in weird places on the page.
93 |
94 |
95 | .. method:: pop(self, behaviour)
96 |
97 | pop is a method that can be used with any component that has a ``popover``
98 |
99 | .. describe:: self
100 |
101 | the component used. No need to worry about this argument when using ``self.button_1.pop('show')``
102 |
103 | .. describe:: behaviour
104 |
105 | ``'show'``, ``'hide'``, ``'toggle'``, ``'destroy'``. Also includes ``'shown'`` and ``'is_visible'``, which return a ``boolean``.
106 | ``'update'`` will update the popover's position. This is useful when a popover's height changes dynamically.
107 |
108 |
109 |
110 | .. function:: dismiss_on_outside_click(dismiss=True)
111 |
112 | by default if you click outside of a popover the popover will close. This behaviour can be overridden globally by calling this function. It can also be set per popover using the ``auto_dismiss`` argument.
113 | Note that popovers will always be dismissed when the page is scrolled. This prevents popovers from appearing in weird places on the page.
114 |
115 | .. function:: set_default_max_width(width)
116 |
117 | update the default max width - this is 276px by default - useful for wider components.
118 |
--------------------------------------------------------------------------------
/js/designer_components/README.md:
--------------------------------------------------------------------------------
1 | # Let's create a Designer Component
2 |
3 |
4 | ## Intro
5 |
6 | So you want to make a Designer Component that updates dynamically in the Desginer?
7 | First - this relies on Javascript.
8 | It also plays around with Skulpt.
9 | It's not officially supported.
10 | It may need updating in the future since it relies on some hackery and implementation details that may change in the future.
11 |
12 | BUT it is isolated to the Desiner so if does stop working - the actual python won't be affected.
13 |
14 | ### Notes
15 | Your custom component must be an HTMLPanel.
16 | We essentially create a Javascript version of our CustomComponent.
17 | Anvil never calls the Python class in the Designer so that's why we need to inject our own Javascript version.
18 | In the Python code we remove the script tags in the `__init__` method so that the Javascript version is never injected.
19 |
20 |
21 | ## Where to start?
22 |
23 | Write the python version first!
24 |
25 | When you want to create a Designer version look at the comments in `DesignerComponent.ts`.
26 | Anything that is marked as `private` should NOT be overriden.
27 |
28 | - Does your Python class have injected `css`?
29 | - *override the `static css` property*
30 | - Does your Python class have injected `link` tags?
31 | - *override the `static links` property*
32 | - Does your Python class have an injected `script` tag?
33 | - *override the `static script` property*
34 |
35 |
36 | ### `static init()`
37 |
38 | You must override the `init()` method and then call `super.init()`
39 |
40 | The custom component html must have a domNode with an identifiable selector.
41 | e.g.
42 |
43 | ```html
44 |
45 | ```
46 |
47 | The first argument to `super.init()` should be the selector to identify your custom component in the dom.
48 | ```typescript
49 | static init() {
50 | super.init(".quill-editor");
51 | }
52 | ```
53 |
54 | An optional second argument can be used. Which is a className. This className will be added to the HTMLPanel dom node.
55 | In the Designer world its purpose is to be a flag to prevent instantiating the same domNode multiple times.
56 | But it can be used to add classes to the HTMLPanel you would otherwise have added in your python init method.
57 |
58 | ```typescript
59 | static init() {
60 | super.init(".tabs", "anvil-extras-designer");
61 | }
62 | ```
63 |
64 |
65 | ### `constructor(domNode, pyComponent, el)`
66 |
67 | A constructor function in javascript is a bit like Python's `__init__` method and `this` acts like `self`.
68 | After the `init` method is called we call the constructor.
69 | There is no need to override the `constructor()` method - but you may want to if you want to add attributes to your `this` argument.
70 | The arguments to the `constructor()` will be:
71 | - the HTMLPanel's domNode for your custom component.
72 | - the python Component as a Skulpt object.
73 | (A Skulpt object is what your client side Python object looks like in Javacsript)
74 | - the domNode of the element found from the querySelector used in the `init()` method
75 |
76 |
77 | ### `setProp(propName, propVal, props)`
78 |
79 | You will need to override `setProp` or `update` (but not both). It's probably better to override `setProp`.
80 | (`update` was used in the initial versions and `setProp` was later added as a better implementation).
81 | In `setProp` you get the `propName` and the `propVal` as well as all the current `props`.
82 | These are the raw Javascript values.
83 | You can either update the dom directly or work with the Skulpt Python objects.
84 | There are some helper methods for common properties like `visible` and working with `color`.
85 |
86 | The DesignerChips component is an example that uses `setProp` and works with both Skulpt objects and the dom to update itself when a property changes.
87 |
88 |
89 | ## Bundling the javascript
90 | - Install deno
91 | - `cd js/designer_components`
92 | - `deno run -A build-script.ts`
93 | - This will override the bundled files.
94 |
95 |
96 |
97 | ## Hacking
98 |
99 | In each designer component you'll see code like
100 |
101 | ```html
102 |
109 | ```
110 |
111 | You'll need equivalent code in your custom component's html.
112 |
113 | When hacking, there's no need to worry about the deno link. Instead:
114 | - bundle the Javascript (see above instructions)
115 | - Copy the bundled file into your theme assets.
116 | - Replace the deno url with a relative theme url: `_/theme/bundle.js`
117 |
118 | Whenever you change the local files re-bundle and repeat.
119 |
--------------------------------------------------------------------------------
/client_code/routing/_navigation.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | __version__ = "1.7.1"
9 |
10 | from time import sleep
11 |
12 | from anvil import get_open_form
13 | from anvil.js.window import document, history, location, window
14 |
15 | from ._logging import logger
16 |
17 | # re-initialise the state object which was overridden on load or this is a new session
18 | state = history.state or {"url": location.hash, "pos": 0}
19 | history.replaceState(state, "", state["url"])
20 |
21 | # undo and pos are used for unload behavior
22 | current = {"undo": 0, "pos": state["pos"]}
23 |
24 |
25 | # when the window back or forward button is pressed onPopState is triggered
26 | # a popstate is also triggered from a change in the URL in this case the window.history.state will be None
27 | def onPopState(e):
28 | state = e.state
29 | if state: # then we're loading from back forward navigation
30 | current["undo"] = current["pos"] - state["pos"]
31 | current["pos"] = state["pos"]
32 | else:
33 | current["undo"] = -1
34 | current["pos"] += 1
35 | state = {"url": location.hash, "pos": current["pos"]}
36 |
37 | history.replaceState(state, "", state["url"])
38 | # we always favour the state['url'] over location.hash
39 | # since we allow (replace_current_url=True, set_in_history=False)
40 |
41 | main_form = get_open_form()
42 | on_navigation = getattr(main_form, "on_navigation", None)
43 | if on_navigation is not None:
44 | on_navigation()
45 | else:
46 | logger.print(
47 | "the open form is not using '@main_router' or has no 'on_navigation' method"
48 | )
49 |
50 |
51 | window.onpopstate = onPopState
52 |
53 |
54 | def pushState(url):
55 | # set_in_history = True, replace_current_url=False
56 | current["pos"] += 1
57 | current["undo"] = -1
58 | state = {"url": url, "pos": current["pos"]}
59 | history.pushState(state, "", url)
60 |
61 |
62 | def replaceState(url):
63 | # set_in_history=True, replace_current_url=True
64 | current["undo"] = 0
65 | state = {"url": url, "pos": history.state["pos"]}
66 | history.replaceState(state, "", url)
67 |
68 |
69 | def replaceUrlNotState(url):
70 | # set_in_history=False, replace_current_url=True
71 | current["undo"] = 0
72 | history.replaceState(history.state, "", url)
73 |
74 |
75 | #### some helpers #####
76 | defaultTitle = document.title
77 |
78 |
79 | def setTitle(title):
80 | document.title = defaultTitle if title is None else title
81 |
82 |
83 | def reloadPage():
84 | location.reload()
85 |
86 |
87 | def goBack():
88 | window.history.back()
89 |
90 |
91 | def goTo(x):
92 | window.history.go(x)
93 |
94 |
95 | def getUrlHash():
96 | return location.hash[1:] # without the hash
97 |
98 |
99 | ############ App unload behaviour ############
100 |
101 | # app unload behavior
102 | def onBeforeUnload(e):
103 | e.preventDefault() # cancel the event
104 | e.returnValue = "" # chrome requires a returnValue to be set
105 |
106 |
107 | def stopUnload():
108 | window.onpopstate = None
109 | history.go(current["undo"])
110 | sleep(0.1) # allow go to fire
111 | window.onpopstate = onPopState
112 | current["pos"] = history.state["pos"]
113 |
114 |
115 | def setAppUnloadBehaviour(warning):
116 | window.onbeforeunload = onBeforeUnload if warning else None
117 |
118 |
119 | def setUnloadPopStateBehaviour(flag):
120 | # at this point we're waiting for the unload function to complete
121 | # so we bind to the unloadPopStateTemp which prevents any forward back navigation
122 | window.onpopstate = unloadPopStateTemp if flag else onPopState
123 |
124 |
125 | # Form Unload Behaviour - here we prevent the user from navigating away from the current form
126 | # while we wait for the unload function to complete
127 | def unloadPopStateTemp(e):
128 | e.preventDefault()
129 | state = e.state
130 | if state:
131 | temp_undo = current["pos"] - state["pos"]
132 | window.onpopstate = None # unbind onpopstate
133 | history.go(temp_undo) # reverse the navigation
134 | sleep(0.1) # allow go to fire before rebinding onpopstate
135 | window.onpopstate = unloadPopStateTemp
136 |
137 | else:
138 | # the user is determined to navigate away and has changed the url manually so let them!
139 | # Not letting them will break the app...
140 | current["pos"] += 1
141 | state = {"url": location.hash, "pos": current["pos"]}
142 | history.replaceState(state, "")
143 | window.onbeforeunload = None
144 | location.reload()
145 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerChips.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 |
3 | declare var Sk: any;
4 |
5 | export class DesignerChip extends DesignerComponent {
6 | static css = `.anvil-extras-chip{height:32px;font-size:14px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;cursor:pointer;display:flex;gap:14px;align-items:center;width:fit-content;padding-left:12px;padding-right:12px}.anvil-extras-chip i.anvil-component-icon.left{font-size:1.5rem}.anvil-extras-chip a{user-select:none}.anvil-extras-chip a .link-text{}.anvil-extras-chip span{padding:0 !important}`;
7 | static init() {
8 | super.init(".chip-placeholder", "anvil-extras-chip");
9 | }
10 | label: any;
11 | link: any;
12 | linkNode: HTMLElement;
13 | constructor(container: HTMLElement, pyComponent: any, temp: HTMLElement) {
14 | super(container, pyComponent, temp);
15 | temp.remove();
16 | this.label = pyComponent._anvil.components[0].component;
17 | this.link = pyComponent._anvil.components[1].component;
18 | this.linkNode = this.link._anvil.domNode;
19 | this.linkNode.classList.remove("anvil-container");
20 | }
21 | setProp(propName: string, propVal: any, props: any) {
22 | switch (propName) {
23 | case "visible":
24 | this.updateVisible(props);
25 | break;
26 | case "spacing_above":
27 | case "spacing_below":
28 | this.updateSpacing(props);
29 | break;
30 | case "text":
31 | case "icon":
32 | this.label._anvil.setPropJS(propName, propVal);
33 | break;
34 | case "foreground":
35 | this.link._anvil.setPropJS(propName, propVal || "rgba(0,0,0,0.6)");
36 | this.label._anvil.setPropJS(propName, propVal || "rgba(0,0,0,0.6)");
37 | break;
38 | case "close_icon":
39 | this.linkNode.style.display = propVal ? "block" : "none";
40 | break;
41 | case "background":
42 | this.pyComponent._anvil.setPropJS(propName, propVal);
43 | break;
44 | }
45 | }
46 |
47 | get [Symbol.toStringTag]() {
48 | return "Chip";
49 | }
50 | }
51 |
52 | export class DesignerChipsInput extends DesignerComponent {
53 | static css = `.anvil-extras-chips-input input{box-shadow:none !important;border:none !important;padding:7px 0 !important;margin-bottom:0 !important;flex:1;min-width:50px}.anvil-extras-chips-input{display:flex;flex-wrap:wrap;gap:8px;border-bottom:1px solid;align-items:center;padding-bottom:4px}`;
54 | static init() {
55 | DesignerChip.init();
56 | super.init(".chips-input-placeholder", "anvil-extras-chips-input");
57 | }
58 | chipNode: HTMLElement;
59 | input: any;
60 | inputNode: HTMLElement;
61 | chips: HTMLElement[];
62 | constructor(container: HTMLElement, pyComponent: any, temp: HTMLElement) {
63 | super(container, pyComponent, temp);
64 | temp.remove();
65 | const tempChip = pyComponent._anvil.components[0].component;
66 | this.chipNode = tempChip._anvil.domNode;
67 | this.input = pyComponent._anvil.components[1].component;
68 | this.inputNode = this.input._anvil.domNode;
69 | this.chips = [];
70 | Sk.misceval.callsimArray(tempChip.tp$getattr(new Sk.builtin.str("remove_from_parent")));
71 | }
72 | setProp(propName: string, propVal: any, props: any) {
73 | switch (propName) {
74 | case "chips":
75 | debugger;
76 | propVal = Array.from(new Set((propVal || []).filter((t) => t)));
77 | this.chips.forEach((chip) => this.domNode.removeChild(chip));
78 | this.chips = [];
79 | propVal.forEach((text, i) => {
80 | const newNode = this.chipNode.cloneNode(true);
81 | newNode.querySelector("span").textContent = text;
82 | this.chips.push(newNode);
83 | this.domNode.insertBefore(newNode, this.inputNode);
84 | });
85 | this.chips.length
86 | ? this.input._anvil.setPropJS("placeholder", props.secondary_placeholder)
87 | : this.input._anvil.setPropJS("placeholder", props.primary_placeholder);
88 | break;
89 | case "spacing_above":
90 | case "spacing_below":
91 | this.updateSpacing(props);
92 | break;
93 | case "visible":
94 | this.updateVisible(props);
95 | break;
96 | case "primary_placeholder":
97 | !this.chips.length && this.input._anvil.setPropJS("placeholder", propVal);
98 | break;
99 | case "secondary_placeholder":
100 | this.chips.length && this.input._anvil.setPropJS("placeholder", propVal);
101 | break;
102 | }
103 | }
104 |
105 | get [Symbol.toStringTag]() {
106 | return "ChipsInput";
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/client_code/navigation.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | from anvil import Label, Link, get_open_form, set_url_hash
8 |
9 | __version__ = "1.7.1"
10 |
11 | # A dict mapping a form's name to a further dict with the form's class and title
12 | _forms = {}
13 |
14 | # A list of all navigation links
15 | _links = []
16 |
17 | # A dict mapping an event name to list of dicts.
18 | # Each of those dicts has keys "link" and "visible" with values of a link instance and boolean respectively.
19 | # The boolean is used to set the 'visible' property of the link when the event is raised.
20 | _visibility = {}
21 |
22 | _title_label = Label()
23 |
24 |
25 | class register:
26 | """A decorator to register a form in the _forms dict."""
27 |
28 | def __init__(self, name, title=None):
29 | self.name = name
30 | self.title = title
31 |
32 | def __call__(self, cls):
33 | _forms[self.name] = {"class": cls, "title": self.title}
34 | return cls
35 |
36 |
37 | def get_form(name, *args, **kwargs):
38 | """Create an instance of a registered form."""
39 | try:
40 | return _forms[name]["class"](*args, **kwargs)
41 | except KeyError:
42 | raise KeyError(f"No form registered under name: {name}")
43 |
44 |
45 | def open_form(form_name, full_width=False):
46 | """Use classic routing to open a registered form"""
47 | form = get_form(form_name)
48 | _title_label.text = _forms[form_name]["title"]
49 | get_open_form().content_panel.clear()
50 | get_open_form().content_panel.add_component(form, full_width_row=full_width)
51 |
52 |
53 | def go_to(target):
54 | """Emulate clicking a menu link"""
55 | for link in _links:
56 | if link.tag.target == target:
57 | break
58 | else: # no break
59 | raise ValueError(f"No menu link matching target: {target}")
60 | link.raise_event("click")
61 |
62 |
63 | def _default_link_click(**event_args):
64 | """A handler for navigation link click events
65 | * Clears the role of all links registered in this module
66 | * Set the calling link's role to 'selected'
67 | * Calls the relevant action for classic or hash routing
68 | """
69 | for link in _links:
70 | link.role = ""
71 | link = event_args["sender"]
72 | link.role = "selected"
73 | actions = {"classic": open_form, "hash": set_url_hash}
74 | kwargs = {}
75 | if link.tag.routing == "classic":
76 | kwargs["full_width"] = link.tag.full_width
77 | actions[link.tag.routing](link.tag.target, **kwargs)
78 |
79 |
80 | def _visibility_event_handler(**event_args):
81 | """A handler for all registered visibility events"""
82 | items = _visibility[event_args["event_name"]]
83 | for item in items:
84 | item["link"].visible = item["visible"]
85 |
86 |
87 | def _register_visibility(container, link, visibility):
88 | """Register the visibility events and set the handler on the container"""
89 | for event, visible in visibility.items():
90 | if event not in _visibility:
91 | _visibility[event] = []
92 | container.set_event_handler(event, _visibility_event_handler)
93 | _visibility[event].append({"link": link, "visible": bool(visible)})
94 |
95 |
96 | def build_menu(container, items, with_title=True):
97 | """Add links to the container and set their initial visibility"""
98 | _title_label.remove_from_parent()
99 | if with_title:
100 | container.parent.add_component(_title_label, slot="title")
101 | for item in items:
102 | link = navigation_link(**item)
103 | _links.append(link)
104 | visibility = link.tag.visibility
105 | if visibility is None:
106 | link.visible = True
107 | else:
108 | link.visible = False
109 | _register_visibility(container, link, visibility)
110 | container.add_component(link)
111 |
112 |
113 | def navigation_link(
114 | routing="classic",
115 | full_width=False,
116 | target=None,
117 | on_click=None,
118 | visibility=None,
119 | **kwargs,
120 | ):
121 | """Create a link instance
122 |
123 | Parameters
124 | ----------
125 | routing
126 | Either 'classic' or 'hash'
127 | full_width
128 | Whether the link target should open as full width
129 | target
130 | Either the name of a registered form for classic routing or
131 | a url_hash for hash routing
132 | on_click
133 | event handler to call when clicked
134 | visibility
135 | a dict mapping the names of events to either True or False
136 | kwargs
137 | will be passed the Link constructor
138 | """
139 | if routing not in ("classic", "hash"):
140 | raise ValueError(
141 | "A navigation link's routing must either be 'classic' or 'hash'"
142 | )
143 | link = Link(**kwargs)
144 | link.tag.routing = routing
145 | link.tag.full_width = full_width
146 | link.tag.target = target
147 | link.tag.visibility = visibility
148 | if on_click is None:
149 | link.set_event_handler("click", _default_link_click)
150 | else:
151 | link.set_event_handler("click", on_click)
152 | return link
153 |
--------------------------------------------------------------------------------
/docs/guides/components/slider.rst:
--------------------------------------------------------------------------------
1 | Slider
2 | ======
3 | Slider component based on the Javascript library noUiSlider.
4 |
5 | Properties
6 | ----------
7 |
8 | :start: number | list[number]
9 |
10 | The initial values of the slider. This property determines the number of handles. It is a required property.
11 | In the designer use comma separated values which will be parsed as JSON.
12 |
13 | :connect: "upper" | "lower" | bool | list[bool]
14 |
15 | The connect option can be used to control the bar color between the handles or the edges of the slider.
16 | When using one handle, set the value to either ``'lower'`` or ``'upper'`` (equivalently ``[True, False]`` or ``[False, True]``).
17 | For sliders with 2 or more handles, pass a list of True, False values. One value per gap. A single value of ``True`` will result in
18 | a coloured bar between all handles.
19 |
20 |
21 | :min: number
22 |
23 | Lower bound. This is a required property
24 |
25 | :max: number
26 |
27 | Upper bound. This is a required property
28 |
29 | :range: object
30 |
31 | An object with ``'min'``, ``'max'`` as keys. For additional options see noUiSlider documentation. This does not need to be set and will be inferred from the ``min``, ``max`` values.
32 |
33 | :step: number
34 |
35 | By default, the slider slides fluently. In order to make the handles jump between intervals, the step option can be used.
36 |
37 | :format:
38 |
39 | Provide a format for the values. This can either be a string to call with .format or a format spec.
40 | e.g. ``"{:.2f}"`` or just ``".2f"``. See python''s format string syntax for more options.
41 |
42 | For a mapping of values to descriptions, e.g. ``{1: 'strongly disagree', 2: 'agree', ...}`` use a custom formatter.
43 | This is a dictionary object with ``'to'`` and ``'from'`` as keys and can be set at runtime.
44 | The ``'to'`` function takes a float or int and returns a str. The ``'from'`` takes a str and returns a float or int. See the anvil-extras Demo for an example.
45 |
46 |
47 | :value: number
48 |
49 | returns the value of the first handle. This can only be set after initialization or with a databinding.
50 |
51 | :values: list[numbers]
52 |
53 | returns a list of numerical values. One value for each handle. This can only be set after initialization or with a databinding.
54 |
55 | :formatted_value: str
56 |
57 | returns the value of the first handle as a formatted string, based on the format property
58 |
59 | :formatted_values: list[str]
60 |
61 | returns the a list of values as formatted strings, based on the format property
62 |
63 | :padding: number | list[number, number]
64 |
65 | Padding limits how close to the slider edges handles can be. Either a single number for both edges.
66 | Or a list of two numbers, one for each edge.
67 |
68 | :margin: number
69 |
70 | When using two handles, the minimum distance between the handles can be set using the margin option. The
71 | margin value is relative to the value set in ``range``.
72 |
73 |
74 | :limit: number
75 |
76 | The limit option is the opposite of the margin option, limiting the maximum distance between two handles
77 |
78 |
79 | :animate: bool
80 |
81 | Set the animate option to False to prevent the slider from animating to a new value with when setting values in code.
82 |
83 |
84 | :behaviour: str
85 |
86 | This option accepts a ``"-"`` separated list of ``"drag"``, ``"tap"``, ``"fixed"``, ``"snap"``, ``"unconstrained"`` or ``"none"``
87 |
88 | :tooltips: bool
89 |
90 | Adds tooltips to the sliders. Uses the same formatting as the format property.
91 |
92 |
93 | :pips: bool
94 |
95 | Sets whether the slider has pips (ticks).
96 |
97 | :pips_mode: str
98 |
99 | One of ``'range'``, ``'steps'``, ``'positions'``, ``'count'``, ``'values'``
100 |
101 | :pips_values: list[number]
102 |
103 | a list of values. Interpreted differently depending on the mode
104 |
105 | :pips_density: int
106 |
107 | Controls how many pips are placed. With the default value of 1, there is one pip per percent.
108 | For a value of 2, a pip is placed for every 2 percent. A value of zero will place
109 | more than one pip per percentage. A value of -1 will remove all intermediate pips.
110 |
111 | :pips_stepped: bool
112 |
113 | the stepped option can be set to true to match the pips to the slider steps
114 |
115 | :color: str
116 |
117 | The color of the bars. Can be set to theme colors like ``'theme:Primary 500'`` or hex values ``'#2196F3'``.
118 |
119 | :enabled: bool
120 |
121 | Disable interactivity
122 |
123 | :visible: bool
124 |
125 | Is the component visible
126 |
127 | :spacing_above: str
128 |
129 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
130 |
131 | :spacing_below: str
132 |
133 | One of ``"none"``, ``"small"``, ``"medium"``, ``"large"``
134 |
135 |
136 |
137 | Methods
138 | -------
139 |
140 | :reset:
141 | Resets the slider to its initial position i.e. it's ``start`` property
142 |
143 |
144 | Events
145 | ------
146 |
147 | :slide:
148 |
149 | Raised whenever the slider is sliding. The handle is provided as an argument to determine which handle is sliding.
150 |
151 | :change:
152 |
153 | Raised whenever the slider has finished sliding. The handle is provided as an argument to determine which handle is sliding.
154 | Change is the writeback event.
155 |
156 |
157 | :show:
158 |
159 | Raised when the component is shown.
160 |
161 |
162 | :hide:
163 |
164 | Raised when the component is hidden.
165 |
--------------------------------------------------------------------------------
/client_code/Switch/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | from anvil import CheckBox, app
9 | from anvil.js import get_dom_node as _get_dom_node
10 | from anvil.js.window import document as _document
11 |
12 | from ..utils._component_helpers import _get_rgb, _html_injector
13 |
14 | __version__ = "1.7.1"
15 |
16 | primary = app.theme_colors.get("Primary 500", "#2196F3")
17 |
18 | css = """
19 | .switch,
20 | .switch * {
21 | -webkit-tap-highlight-color: transparent;
22 | -webkit-user-select: none;
23 | -moz-user-select: none;
24 | -ms-user-select: none;
25 | user-select: none;
26 | }
27 |
28 | .switch label {
29 | cursor: pointer;
30 | }
31 |
32 | .switch label input[type=checkbox] {
33 | opacity: 0;
34 | width: 0;
35 | height: 0;
36 | }
37 | .switch label input[type=checkbox]:checked+.lever {
38 | background-color: rgba(var(--color), .5);
39 | }
40 | .switch label input[type=checkbox]:checked+.lever:after,
41 | .switch label input[type=checkbox]:checked+.lever:before {
42 | left: 18px;
43 | }
44 | .switch label input[type=checkbox]:checked+.lever:after {
45 | background-color: rgb(var(--color));
46 | }
47 |
48 | .switch label .lever {
49 | content: "";
50 | display: inline-block;
51 | position: relative;
52 | width: 36px;
53 | height: 14px;
54 | background-color: rgba(0,0,0,0.38);
55 | border-radius: 15px;
56 | margin-right: 10px;
57 | -webkit-transition: background 0.3s ease;
58 | transition: background 0.3s ease;
59 | vertical-align: middle;
60 | margin: 0 16px;
61 | }
62 | .switch label .lever:after,
63 | .switch label .lever:before {
64 | content: "";
65 | position: absolute;
66 | display: inline-block;
67 | width: 20px;
68 | height: 20px;
69 | border-radius: 50%;
70 | left: 0;
71 | top: -3px;
72 | -webkit-transition: left 0.3s ease, background 0.3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
73 | transition: left 0.3s ease, background 0.3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
74 | transition: left 0.3s ease, background 0.3s ease, box-shadow 0.1s ease, transform 0.1s ease;
75 | transition: left 0.3s ease, background 0.3s ease, box-shadow 0.1s ease, transform 0.1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
76 | }
77 | .switch label .lever:before {
78 | background-color: rgb(var(--color), 0.15);
79 | }
80 | .switch label .lever:after {
81 | background-color: #F1F1F1;
82 | -webkit-box-shadow: 0 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0 rgba(0,0,0,0.14),0px 1px 5px 0 rgba(0,0,0,0.12);
83 | box-shadow: 0 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0 rgba(0,0,0,0.14),0px 1px 5px 0 rgba(0,0,0,0.12);
84 | }
85 | input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,
86 | input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before {
87 | -webkit-transform: scale(2.4);
88 | transform: scale(2.4);
89 | background-color: rgb(var(--color), 0.15);
90 | }
91 | input[type=checkbox]:not(:disabled) ~ .lever:active:before,
92 | input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before {
93 | -webkit-transform: scale(2.4);
94 | transform: scale(2.4);
95 | background-color: rgba(0,0,0,0.08);
96 | }
97 |
98 | .switch input[type=checkbox][disabled]+.lever {
99 | cursor: default;
100 | background-color: rgba(0,0,0,0.12);
101 | }
102 | .switch label input[type=checkbox][disabled]+.lever:after,
103 | .switch label input[type=checkbox][disabled]:checked+.lever:after {
104 | background-color: #949494;
105 | }
106 |
107 | """
108 | _html_injector.css(css)
109 |
110 |
111 | class Switch(CheckBox):
112 | def __init__(self, checked_color=primary, text_pre="", text_post="", **properties):
113 | dom_node = self._dom_node = _get_dom_node(self)
114 | dom_node.querySelector(".checkbox").classList.add("switch")
115 |
116 | span = dom_node.querySelector("span")
117 | span.classList.add("lever")
118 | span.removeAttribute("style")
119 |
120 | input = dom_node.querySelector("input")
121 | input.removeAttribute("style")
122 | input.style.marginTop = 0
123 |
124 | label = dom_node.querySelector("label")
125 | label.style.padding = "7px 0"
126 |
127 | self._textnode_pre = _document.createTextNode(text_pre)
128 | self._textnode_post = _document.createTextNode(text_post)
129 | label.prepend(self._textnode_pre)
130 | label.append(self._textnode_post)
131 |
132 | self.checked_color = checked_color or primary
133 |
134 | @property
135 | def checked_color(self):
136 | return self._checked_color
137 |
138 | @checked_color.setter
139 | def checked_color(self, value):
140 | self._checked_color = value
141 | self._dom_node.style.setProperty("--color", _get_rgb(value))
142 |
143 | @property
144 | def text_pre(self):
145 | return self._textnode_pre.textContent
146 |
147 | @text_pre.setter
148 | def text_pre(self, value):
149 | self._textnode_pre.textContent = value
150 |
151 | @property
152 | def text_post(self):
153 | return self._textnode_post.textContent
154 |
155 | @text_post.setter
156 | def text_post(self, value):
157 | self._textnode_post.textContent = value
158 |
159 | text = text_post # override the CheckBox property
160 |
--------------------------------------------------------------------------------
/js/designer_components/DesignerSlider.ts:
--------------------------------------------------------------------------------
1 | import { DesignerComponent } from "./DesignerComponent.ts";
2 |
3 | //deno-lint-ignore
4 | declare var noUiSlider: any;
5 |
6 | //deno-lint-ignore
7 | declare var Sk: any;
8 | export interface Slider extends HTMLElement {
9 | noUiSlider: any;
10 | }
11 |
12 | interface Formatter {
13 | to: (value: number) => string | number;
14 | from: (value: string) => number | false;
15 | }
16 |
17 | export class DesignerSlider extends DesignerComponent {
18 | static defaults = {
19 | start: [20, 80],
20 | connect: true,
21 | min: 0,
22 | max: 100,
23 | visible: true,
24 | enabled: true,
25 | };
26 | static links = ["https://cdn.jsdelivr.net/npm/nouislider@15.4.0/dist/nouislider.css"];
27 | static scripts = ["https://cdn.jsdelivr.net/npm/nouislider@15.4.0/dist/nouislider.js"];
28 | static css = `.anvil-container-overflow,.anvil-panel-col{overflow:visible}.anvil-slider-container{padding:10px 0;min-height:50px}
29 | .anvil-slider-container.has-pips{padding-bottom:40px}.noUi-connect{background:var(--primary)}
30 | .noUi-horizontal .noUi-handle{width:34px;height:34px;right:-17px;top:-10px;border-radius:50%}.noUi-handle::after,.noUi-handle::before{content:none}`;
31 | static init() {
32 | super.init(".anvil-slider", "anvil-slider-container");
33 | }
34 |
35 | slider: Slider;
36 | constructor(domNode: HTMLElement, pyComponent: any, slider: Slider) {
37 | super(domNode, pyComponent, slider);
38 | this.slider = slider;
39 | }
40 |
41 | parse(val: any, forceList = false) {
42 | if (typeof val !== "string") {
43 | return val;
44 | }
45 | val = val.toLowerCase().trim();
46 | if (!val) return forceList ? [] : null;
47 | try {
48 | return JSON.parse((forceList || val.includes(",")) && val[0] !== "[" ? `[${val}]` : val);
49 | } catch {
50 | return forceList ? [] : val;
51 | }
52 | }
53 | getFormatter(formatspec: string): Formatter {
54 | const first = formatspec.indexOf("{");
55 | const last = formatspec.indexOf("}");
56 | const prefix = first === -1 ? "" : formatspec.slice(0, first);
57 | const suffix = last === -1 ? "" : formatspec.slice(last + 1);
58 | const type = formatspec[last - 1] === "%" ? "%" : null;
59 |
60 | const pyformatspec = Sk.ffi.toPy(formatspec) as any;
61 | const format = pyformatspec.tp$getattr(Sk.ffi.toPy("format"));
62 |
63 | const doTo = (f: number): any => {
64 | const pyNum = Sk.ffi.toPy(f);
65 | return first === -1 ? Sk.builtin.format(pyNum, pyformatspec) : format.tp$call([pyNum]);
66 | };
67 |
68 | try {
69 | doTo(1.1);
70 | } catch (e: any) {
71 | throw new Error(e.toString());
72 | }
73 |
74 | return {
75 | to: (f: number): string | number => {
76 | try {
77 | return doTo(f);
78 | } catch {
79 | return f;
80 | }
81 | },
82 | from: (s: string): number => {
83 | if (s.startsWith(prefix)) {
84 | s = s.slice(prefix.length);
85 | }
86 | if (s.endsWith(suffix)) {
87 | s = s.slice(0, s.length - suffix.length);
88 | }
89 | const hasPercent = type === "%" && s.endsWith("%");
90 | s = s.trim().replace(/[,_]/g, "");
91 | let f = parseFloat(s);
92 | if (hasPercent) {
93 | f = f / 100;
94 | }
95 | return f;
96 | },
97 | };
98 | }
99 | update(props: any) {
100 | try {
101 | for (const prop of ["start", "connect", "margin", "padding", "limit", "pips_values"]) {
102 | props[prop] = this.parse(props[prop], prop === "pips_values");
103 | }
104 | props.range = { min: props.min, max: props.max };
105 | props.format = this.getFormatter(props.format || ".2f");
106 |
107 | if (props.pips) {
108 | props.pips = {
109 | format: props["format"],
110 | mode: props["pips_mode"],
111 | values: props["pips_values"],
112 | density: props["pips_density"],
113 | stepped: props["pips_stepped"],
114 | };
115 | }
116 |
117 | this.domNode.classList.toggle("has-pips", !!props.pips);
118 | this.slider.noUiSlider?.destroy();
119 | if (this.slider.firstElementChild) {
120 | this.slider.removeChild(this.slider.firstElementChild);
121 | }
122 | this.domNode.style.setProperty("--primary", this.getColor(props.color, true));
123 | this.updateSpacing(props);
124 | this.updateVisible(props);
125 | props.enabled ? this.slider.removeAttribute("disabled") : this.slider.setAttribute("disabled", "");
126 | noUiSlider.create(this.slider, props);
127 | } catch (e) {
128 | this.slider.noUiSlider?.destroy();
129 | if (this.slider.firstElementChild) {
130 | this.slider.removeChild(this.slider.firstElementChild);
131 | }
132 | const invalidComponent = $(`
133 |
134 |
${e.message.replaceAll("noUiSlider", "Slider")}
`);
135 | this.slider.appendChild(invalidComponent[0]);
136 | }
137 | }
138 |
139 | get [Symbol.toStringTag]() {
140 | return "Slider";
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/client_code/Demo/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 | import anvil.http
8 |
9 | from ..utils import auto_refreshing
10 | from ._anvil_designer import DemoTemplate
11 |
12 | __version__ = "1.7.1"
13 | dataset_url = "https://pivottable.js.org/examples/mps.json"
14 |
15 |
16 | #### AUTO REFRESING - the item property updates components
17 | @auto_refreshing
18 | class Demo(DemoTemplate):
19 | def __init__(self, **properties):
20 | self.init_custom_slider_formatter()
21 |
22 | self.progress = 0
23 | self.default_item = dict(
24 | tally=100,
25 | counter=0,
26 | values=self.slider.start,
27 | agree=self.slider_agree.value,
28 | chips=["a", "b", "c"],
29 | text="",
30 | )
31 | self.item = self.default_item
32 | self.pivot.items = anvil.http.request(dataset_url, json=True)
33 | self.init_components(**properties)
34 |
35 | def timer_1_tick(self, **event_args):
36 | if self.progress <= 1:
37 | self.progress_bar.progress = self.progress
38 | self.progress += 0.01
39 | else:
40 | self.timer_1.interval = 0
41 |
42 | def minus_button_click(self, **event_args):
43 | self.item["tally"] -= 1
44 | self.item["counter"] += 1
45 |
46 | def plus_button_click(self, **event_args):
47 | self.item["tally"] += 1
48 | self.item["counter"] += 1
49 |
50 | def reset_button_click(self, **event_args):
51 | self.item = self.default_item
52 |
53 | ###### MULTI SELECT ######
54 | def multi_select_drop_down_1_change(self, **event_args):
55 | """This method is called when the selected values change"""
56 | print(self.multi_select_drop_down_1.selected)
57 |
58 | ###### QUILL ######
59 | def quill_text_change(self, **event_args):
60 | """This method is called when the quill text changes"""
61 | print(self.quill.get_text())
62 |
63 | ###### SLIDER ######
64 | def set_text_boxes(self):
65 | self.text_box_left.text, self.text_box_right.text = self.slider.formatted_values
66 |
67 | def slider_button_reset_click(self, **event_args):
68 | """This method is called when the button is clicked"""
69 | self.slider.reset()
70 |
71 | def slider_change(self, handle, **event_args):
72 | """This method is called when the slider has finished sliding"""
73 | print(
74 | f"change\nhandle={handle} | value={self.slider.values[handle]} | formatted={self.slider.formatted_values[handle]}"
75 | )
76 |
77 | def slider_slide(self, handle, **event_args):
78 | """This method is called when the slider is sliding or dragging"""
79 | self.set_text_boxes()
80 | print(
81 | f"slide\nhandle={handle} | value={self.slider.values[handle]} | formatted={self.slider.formatted_values[handle]}"
82 | )
83 |
84 | def slider_textbox_enter(self, **event_args):
85 | """This method is called when the user presses Enter in this text box"""
86 | self.slider.values = self.text_box_left.text, self.text_box_right.text
87 | self.set_text_boxes()
88 |
89 | ###### SLIDER WITH CUSTOM FORMATTER
90 | def init_custom_slider_formatter(self):
91 | num_to_desc = {
92 | -5: "strongly disagree",
93 | -2.5: "disagree",
94 | 0: "neutral",
95 | 2.5: "agree",
96 | 5: "strongly agree",
97 | }
98 |
99 | desc_to_num = {v: k for k, v in num_to_desc.items()}
100 |
101 | self.slider_agree.format = {
102 | # to should return a str
103 | "to": lambda num: num_to_desc.get(num, str(num)),
104 | # from should return a number - if it fails then an attempt will be made to convert the str to float
105 | "from": lambda desc: desc_to_num[desc],
106 | }
107 |
108 | ### it's also possible to provide a custom formatter to tooltips - only to is required
109 | self.slider_agree.tooltips = {"to": lambda num: format(num, ".0f")}
110 |
111 | def slider_agree_change(self, handle, **event_args):
112 | """This method is called when the slider has finished sliding"""
113 | print("slider changed - value:", self.slider_agree.value)
114 |
115 | def slider_down_click(self, **event_args):
116 | """This method is called when the button is clicked"""
117 | self.slider_agree.value -= 1
118 | self.update_item_agree()
119 |
120 | def slider_reset_click(self, **event_args):
121 | """This method is called when the button is clicked"""
122 | self.slider_agree.reset()
123 | self.update_item_agree()
124 |
125 | def slider_up_click(self, **event_args):
126 | """This method is called when the button is clicked"""
127 | self.slider_agree.value += 1
128 | self.update_item_agree()
129 |
130 | def update_item_agree(self):
131 | """This method is called when the slider values are updated from code"""
132 | self.item["agree"] = self.slider_agree.value
133 | print("slider set - value:", self.slider_agree.value)
134 |
135 | def tabs_1_tab_click(self, tab_index, tab_title, **event_args):
136 | """This method is called when a tab is clicked"""
137 | self.tabs_label.text = f"{tab_title} is visible"
138 |
139 | def chips_1_chips_changed(self, **event_args):
140 | """This method is called when a chip is added or removed"""
141 | print(self.item["chips"])
142 |
143 | def autocomplete_event(self, event_name, **event_args):
144 | print(event_name, self.item["text"])
145 |
--------------------------------------------------------------------------------
/docs/guides/modules/navigation.rst:
--------------------------------------------------------------------------------
1 | Navigation
2 | ==========
3 | A client module for that provides dynamic menu construction.
4 |
5 | Introduction
6 | ------------
7 | This module builds a menu of link objects based on a simple dictionary definition.
8 |
9 | Rather than manually adding links and their associated click event handlers, the module does that for you!
10 |
11 | Usage
12 | -----
13 |
14 | Forms
15 | +++++
16 |
17 | In order for a form to act as a target of a menu link, it has to register a name with the navigation module using a decorator
18 | on its class definition. e.g. Assuming the module is installed as a dependency named 'Extras':
19 |
20 | .. code-block:: python
21 |
22 | from ._anvil_designer import HomeTemplate
23 | from anvil import *
24 | from anvil_extras import navigation
25 |
26 |
27 | @navigation.register(name="home")
28 | class Home(HomeTemplate):
29 | def __init__(self, **properties):
30 | self.init_components(**properties)
31 |
32 | Menu
33 | ++++
34 | * In the Main form for your app, add a content panel to the menu on the left hand side and call it 'menu_panel'
35 |
36 | * Add a menu definition dict to the code for your Main form and pass the panel and the dict to the menu builder. e.g.
37 |
38 | .. code-block:: python
39 |
40 | from ._anvil_designer import MainTemplate
41 | from anvil import *
42 | from anvil_extras import navigation
43 | from HashRouting import routing
44 |
45 | menu = [
46 | {"text": "Home", "target": "home"},
47 | {"text": "About", "target": "about"},
48 | ]
49 |
50 |
51 | class Main(MainTemplate):
52 |
53 | def __init__(self, **properties):
54 | self.advanced_mode = False
55 | navigation.build_menu(self.menu_panel, menu)
56 | self.init_components(**properties)
57 |
58 | will add 'Home' and 'About' links to the menu which will open registered forms named 'home' and 'about' respectively.
59 |
60 | Each item in the dict needs the 'text' and 'target' keys as a minimum. It may also include 'full_width', 'routing' and 'visibility' keys:
61 |
62 | * 'full_width' can be True or False to indicate whether the target form should be opened with 'full_width_row' or not.
63 | * 'routing' can be either 'classic' or 'hash' to indicate whether clicking the link should use Anvil's `add_component` function or hash routing to open the target form. Classic routing is the default if the key is not present in the menu dict.
64 | * 'visibility' can be a dict mapping an anvil event to either True or False to indicate whether the link should be made visible when that event is raised.
65 |
66 | All other keys in the menu dict are passed to the Link constructor.
67 |
68 | For example, to add icons to each of the examples above, a 'Contact' item that uses hash routing and a 'Settings' item that should only be visible when advanced mode is enabled:
69 |
70 | .. code-block:: python
71 |
72 | from ._anvil_designer import MainTemplate
73 | from anvil import *
74 | from anvil_extras import navigation
75 | from HashRouting import routing
76 |
77 | menu = [
78 | {"text": "Home", "target": "home", "icon": "fa:home"},
79 | {"text": "About", "routing": "hash", "target": "about", "icon": "fa:info"},
80 | {"text": "Contact", "routing": "hash", "target": "contact", "icon": "fa:envelope"},
81 | {
82 | "text": "Settings",
83 | "target": "settings",
84 | "icon": "fa:gear",
85 | "visibility": {
86 | "x-advanced-mode-enabled": True,
87 | "x-advanced-mode-disabled": False
88 | }
89 | }
90 | ]
91 |
92 |
93 | @routing.main_router
94 | class Main(MainTemplate):
95 |
96 | def __init__(self, **properties):
97 | self.advanced_mode = False
98 | navigation.build_menu(self.menu_panel, menu)
99 | self.init_components(**properties)
100 |
101 | def form_show(self, **event_args):
102 | self.set_advanced_mode(False)
103 |
104 | Note - since this example includes hash routing, it also requires a decorator from the `Hash Routing App `_ on the Main class.
105 |
106 | Startup
107 | +++++++
108 | In order for the registration to occur, the form classes need to be loaded before the menu is constructed. This can be achieved by using a startup module and importing each of the forms in the code for that module.
109 |
110 | e.g. Create a module called 'startup', set it as the startup module and import your Home form before opening the Main form:
111 |
112 | .. code-block:: python
113 |
114 | from anvil import open_form
115 | from .Main import Main
116 | from . import Home
117 |
118 | open_form(Main())
119 |
120 | Page Titles
121 | +++++++++++
122 | By default, the menu builder will also add a Label to the title slot of your Main form. If you register a form with a title as well as a name, the module will update that label as you navigate around your app. e.g. to add a title to the home page example:
123 |
124 | .. code-block:: python
125 |
126 | from ._anvil_designer import HomeTemplate
127 | from anvil import *
128 | from anvil_extras import navigation
129 |
130 |
131 | @navigation.register(name="home", title="Home")
132 | class Home(HomeTemplate):
133 | def __init__(self, **properties):
134 | self.init_components(**properties)
135 |
136 | If you want to disable this feature, set the `with_title` argument to `False` when you call `build_menu` in your Main form. e.g.
137 |
138 | .. code-block:: python
139 |
140 | class Main(MainTemplate):
141 |
142 | def __init__(self, **properties):
143 | self.advanced_mode = False
144 | navigation.build_menu(self.menu_column_panel, menu, with_title=False)
145 | self.init_components(**properties)
146 |
147 | Navigate with Code
148 | ++++++++++++++++++
149 | You can emulate clicking a menu link using the go_to function, which takes a 'target' key as its only parameter, e.g.
150 |
151 | .. code-block:: python
152 |
153 | navigation.go_to("contact")
154 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v1.7.0 06-Oct-2021
2 |
3 | ## New Features
4 | * Pivot - Dynamic pivot table component
5 | https://github.com/anvilistas/anvil-extras/pull/165
6 |
7 | ## Bug Fixes
8 | * MultiSelectDropdown - Hides menu when component is removed from the page
9 | https://github.com/anvilistas/anvil-extras/pull/149
10 | * Popover - content's show and hide events will be triggered when the popover shows and hides
11 | https://github.com/anvilistas/anvil-extras/pull/150
12 | * Autocomplete - Add missing TextBox properties to design view
13 | https://github.com/anvilistas/anvil-extras/pull/160
14 |
15 | # v1.6.0 17-Sep-2021
16 |
17 | ## New Features
18 | * Quill - dynamically add custom modules
19 | https://github.com/anvilistas/anvil-extras/pull/117
20 | * routing - adjusts the behaviour of anvil.alert to ensure dismissible alerts are closed on navigation. And navigation prevented for non-dismissible alerts.
21 | https://github.com/anvilistas/anvil-extras/pull/132
22 | * `storage.indexed_db` - Now supports the browser's `IndexedDB` with a dictionary like api
23 | https://github.com/anvilistas/anvil-extras/pull/135
24 | * storage - additional store objects can be created inside the browsers `localStorage` or `IndexedDB`. e.g. `todo_store = indexed_db.get_store('todos')`
25 | Each store object behaves like a dictionary object.
26 | https://github.com/anvilistas/anvil-extras/pull/135
27 | * PageBreak - `border` property added and documentation updated.
28 | https://github.com/anvilistas/anvil-extras/pull/139
29 |
30 | ## Bug Fixes
31 | * Autocomplete - can now be used inside an alert
32 | https://github.com/anvilistas/anvil-extras/pull/114
33 | * Popover - fix stickyhover
34 | https://github.com/anvilistas/anvil-extras/pull/121
35 | * storage - update and clear were missing from the documented api
36 | https://github.com/anvilistas/anvil-extras/pull/125
37 | * PageBreak - fix margin_top property and make it optional
38 | https://github.com/anvilistas/anvil-extras/pull/137
39 | * PageBreak and Multi-select - fix illegal HTML
40 | https://github.com/anvilistas/anvil-extras/pull/139
41 | * Popover - remove the requirement for delays in show/hide/destroy transitions
42 | https://github.com/anvilistas/anvil-extras/pull/146
43 |
44 | ## Deprecated
45 | * storage.session_storage was deprecated. Use local_storage instead
46 | https://github.com/anvilistas/anvil-extras/pull/135
47 |
48 | ## Updates
49 | * Slider Component - bump javascript dependency and refactor. No changes to the component's public API.
50 | https://github.com/anvilistas/anvil-extras/pull/112
51 | * Autocomple - duplicate suggestions are ignored and a warning is printed
52 | https://github.com/anvilistas/anvil-extras/pull/116
53 | * Popover - documentation added and clone link updated. The example now imports `anvil_extras`
54 | https://github.com/anvilistas/anvil-extras/pull/121
55 |
56 | # v1.5.2 23-Aug-2021
57 |
58 | ## New Features
59 | * `augment` - `add_event_handler()` method added. `original_event` passed as an `event_arg`.
60 | https://github.com/anvilistas/anvil-extras/pull/109
61 |
62 | ## Bug Fixes
63 | * Add missing support for binding writeback on the Switch component
64 | https://github.com/anvilistas/anvil-extras/pull/111
65 |
66 | # v1.5.1 05-Jul-2021
67 |
68 | ## Bug Fixes
69 | * Autocompleter suggestions on mobile
70 | https://github.com/anvilistas/anvil-extras/issues/103
71 |
72 | # v1.5.0 29-Jun-2021
73 |
74 | ## New Features
75 | * `local_storage` - wrapper around the browser localStorage object
76 | https://github.com/anvilistas/anvil-extras/pull/93
77 |
78 | ## Changes
79 | * Quill editor supports a toolbar and theme set at runtime.
80 | https://github.com/anvilistas/anvil-extras/pull/80
81 | * Add navigation.go_to function, improved navigation error messages
82 | https://github.com/anvilistas/anvil-extras/pull/99
83 |
84 | ## Bug Fixes
85 | * Autocompleter focus method doesn't trigger autocomplete suggestions
86 | https://github.com/anvilistas/anvil-extras/issues/94
87 | * Improve error reporting when passing an invalid content object to a popover
88 | https://github.com/anvilistas/anvil-extras/issues/90
89 | * Fixed the publisher.unsubscribe method in the Messaging module, making it functional
90 | https://github.com/anvilistas/anvil-extras/pull/92
91 | * Fix indeterminate progress bar not always displaying
92 | https://github.com/anvilistas/anvil-extras/issues/95
93 |
94 | # v1.4 07-June-2021
95 |
96 | ## New Features
97 | * Tabs Component
98 | https://github.com/anvilistas/anvil-extras/pull/64
99 | * uuid4 in the browser
100 | https://github.com/anvilistas/anvil-extras/pull/67
101 | * Chip Component and ChipsInput Component
102 | https://github.com/anvilistas/anvil-extras/pull/68
103 | * AutoComplete Component
104 | https://github.com/anvilistas/anvil-extras/pull/70
105 |
106 | ## Changes
107 | * Improved dynamic designer support for Switch, MultiSelectDropDown, Tabs, Quill and Slider
108 | https://github.com/anvilistas/anvil-extras/pull/66
109 |
110 | # v1.3.1 31-May-2021
111 | * Improved slider formatting
112 | https://github.com/anvilistas/anvil-extras/pull/61
113 |
114 | # v1.3.0 31-May-2021
115 |
116 | ## New Features
117 | * Update styling of switch component
118 | https://github.com/anvilistas/anvil-extras/pull/56
119 | * Include pagination_click event in augment module
120 | https://github.com/anvilistas/anvil-extras/pull/55
121 | * Slider Component
122 | https://github.com/anvilistas/anvil-extras/pull/60
123 |
124 | ## Changes
125 | * Refactor of progress bars
126 | https://github.com/anvilistas/anvil-extras/pull/59
127 |
128 | # v1.2.0 25-May-2021
129 |
130 | ## New Features
131 | * component.trigger('writeback')
132 | https://github.com/anvilistas/anvil-extras/pull/47
133 | * MultiSelectDropDown component
134 | https://github.com/anvilistas/anvil-extras/pull/44
135 | * @wait_for_writeback decorator
136 | https://github.com/anvilistas/anvil-extras/pull/50
137 | * Quill component
138 | https://github.com/anvilistas/anvil-extras/pull/52
139 | * Switch component
140 | https://github.com/anvilistas/anvil-extras/pull/31
141 |
142 | # v1.1.0 27-Mar-2021
143 |
144 | ## New Features
145 | * Auto Refreshing Item
146 | https://github.com/anvilistas/anvil-extras/pull/39
147 |
148 | # v1.0.0 11-Mar-2021
149 |
150 | * Initial release
151 |
--------------------------------------------------------------------------------
/client_code/augment.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | from functools import cache as _cache
9 | from functools import partial as _partial
10 |
11 | import anvil as _anvil
12 | from anvil import Component as _Component
13 | from anvil import DataGrid as _DataGrid
14 | from anvil import js as _js
15 | from anvil.js.window import Function as _Function
16 | from anvil.js.window import jQuery as _S
17 |
18 | __version__ = "1.7.1"
19 |
20 | __all__ = ["add_event", "add_event_handler", "set_event_handler", "trigger"]
21 |
22 | _Callable = type(lambda: None)
23 |
24 |
25 | # use cache so we don't add the same event to the component multiple times
26 | # we only need to add the event once and use anvil architecture to raise the event
27 | @_cache
28 | def add_event(component: _Component, event: str) -> None:
29 | """component: (instantiated) anvil component
30 | event: str - any jquery event string
31 | """
32 | if not isinstance(event, str):
33 | raise TypeError("event must be type str and not " + type(event))
34 | _add_event(component, event)
35 |
36 | def handler(e):
37 | event_args = {"event_type": e.type, "original_event": e}
38 | if event.startswith("key"):
39 | event_args |= {
40 | "key": e.key,
41 | "key_code": e.keyCode,
42 | "shift_key": e.shiftKey,
43 | "alt_key": e.altKey,
44 | "meta_key": e.metaKey,
45 | "ctrl_key": e.ctrlKey,
46 | }
47 | if component.raise_event(event, **event_args):
48 | e.preventDefault()
49 |
50 | js_event_name = "mouseenter mouseleave" if event == "hover" else event
51 | _get_jquery_for_component(component).on(js_event_name, handler)
52 |
53 |
54 | def set_event_handler(component: _Component, event: str, func: _Callable) -> None:
55 | """uses anvil's set_event_handler for any jquery event"""
56 | add_event(component, event)
57 | component.set_event_handler(event, func)
58 |
59 |
60 | def add_event_handler(component: _Component, event: str, func: _Callable) -> None:
61 | """uses anvil's add_event_handler for any jquery event"""
62 | add_event(component, event)
63 | component.add_event_handler(event, func)
64 |
65 |
66 | _trigger_writeback = _Function(
67 | "self",
68 | """
69 | self = PyDefUtils.unwrapOrRemapToPy(self);
70 | const mapPropToWriteback = (p) => () => PyDefUtils.suspensionFromPromise(self._anvil.dataBindingWriteback(self, p.name));
71 | const customPropsToWriteBack = (self._anvil.customComponentProperties || []).filter(p => p.allow_binding_writeback).map(mapPropToWriteback);
72 | const builtinPropsToWriteBack = self._anvil.propTypes.filter(p => p.allowBindingWriteback).map(mapPropToWriteback);
73 | return Sk.misceval.chain(Sk.builtin.none.none$, ...customPropsToWriteBack, ...builtinPropsToWriteBack);
74 | """,
75 | )
76 |
77 |
78 | def trigger(self: _Component, event: str):
79 | """trigger an event on a component, self is an anvil component, event is a component, event is a str or a dictionary
80 | if event is a dictionary it should include an event key e.g. {'event': 'keypress', 'which': 13}
81 | """
82 | if event == "writeback":
83 | return _trigger_writeback(self)
84 | if isinstance(event, dict):
85 | event = _S.Event(event["event"], event)
86 | event = "mouseenter mouseleave" if event == "hover" else event
87 | _get_jquery_for_component(self).trigger(event)
88 |
89 |
90 | _Component.trigger = trigger
91 |
92 |
93 | def _get_jquery_for_component(component):
94 | if isinstance(component, _anvil.Button):
95 | return _S(_js.get_dom_node(component).firstElementChild)
96 | elif isinstance(component, _anvil.FileLoader):
97 | return _S(_js.get_dom_node(component)).find("form")
98 | else:
99 | return _S(_js.get_dom_node(component))
100 |
101 |
102 | _add_event = _Function(
103 | "self",
104 | "event",
105 | """
106 | self = PyDefUtils.unwrapOrRemapToPy(self);
107 | self._anvil.eventTypes[event] = self._anvil.eventTypes[event] || {name: event};
108 | """,
109 | )
110 |
111 |
112 | old_data_grid_event_handler = _DataGrid.set_event_handler
113 |
114 |
115 | def datagrid_set_event_handler(self, event, handler):
116 | if event == "pagination_click":
117 | _set_pagination_handlers(self, handler)
118 | else:
119 | old_data_grid_event_handler.set_event_handler(self, event, handler)
120 |
121 |
122 | _DataGrid.set_event_handler = datagrid_set_event_handler
123 |
124 |
125 | def _prevent_disabled(js_event):
126 | if js_event.currentTarget.classList.contains("disabled"):
127 | js_event.stopPropagation()
128 |
129 |
130 | def _wrap_js_event(handler):
131 | def wrapper(e):
132 | handler()
133 |
134 | return wrapper
135 |
136 |
137 | def _set_pagination_handlers(data_grid, handler):
138 | grid_dom = _js.get_dom_node(data_grid)
139 | for name in ["first", "last", "previous", "next"]:
140 | btn = grid_dom.querySelector(f".{name}-page")
141 | # use True so that we capture this event before the anvil click event
142 | btn.addEventListener("click", _prevent_disabled, True)
143 | btn.addEventListener(
144 | "click",
145 | _wrap_js_event(
146 | _partial(
147 | handler,
148 | sender=data_grid,
149 | button=name,
150 | event_name="pagination_click",
151 | )
152 | ),
153 | )
154 | # note we don't tidy this up - we should probably call removeEventListener
155 | # but this will be called from code and is unlikely that the user will call this function twice
156 |
157 |
158 | if __name__ == "__main__":
159 | _ = _anvil.ColumnPanel()
160 | _.set_event_handler(
161 | "show",
162 | lambda **e: _anvil.Notification(
163 | "oops AnvilAugment is a dependency", timeout=None
164 | ).show(),
165 | )
166 | _anvil.open_form(_)
167 |
168 | _ = None
169 |
--------------------------------------------------------------------------------
/client_code/Slider/form_template.yaml:
--------------------------------------------------------------------------------
1 | properties:
2 | - {name: start, type: string, default_value: '20', description: The start option sets the number of handles and corresponding start positions. Use a single value or comma separated values. One for each slider. Other properties with lists must usually match the number of sliders,
3 | important: true}
4 | - {name: connect, type: string, default_value: 'True, False', description: 'The connect
5 | option can be used to control the bar between the handles or the edges of the
6 | slider. When using one handle, set the value to either ''upper'' or ''lower''. For
7 | sliders with 2 or more handles, pass a list of True, False values. One for each
8 | gap.', important: true}
9 | - {name: margin, type: string, default_value: null, description: 'When using two handles,
10 | the minimum distance between the handles can be set using the margin option. The
11 | margin value is relative to the value set in ''range''.', group: multi handle,
12 | important: false}
13 | - {name: limit, type: string, default_value: null, description: 'The limit option
14 | is the opposite of the margin option, limiting the maximum distance between two
15 | handles', group: multi handle, important: false}
16 | - {name: padding, type: string, default_value: '', description: Padding limits how close to the slider edges handles can be.,
17 | group: multi handle, important: false}
18 | - {name: step, type: number, default_value: null, description: 'By default, the slider
19 | slides fluently. In order to make the handles jump between intervals, the step
20 | option can be used.', important: false}
21 | - {name: tooltips, type: boolean, default_value: null, description: Adds tooltips to the sliders. Uses the same formatting as the format property,
22 | group: appearance, important: false}
23 | - {name: animate, type: boolean, default_value: true, description: Set the animate option to False to prevent the slider from animating to a new value with when setting values in code,
24 | group: interaction, important: false}
25 | - {name: min, type: number, default_value: 0, description: lower bound, important: true}
26 | - {name: max, type: number, default_value: 100, description: upper bound, important: true}
27 | - {name: range, type: object, default_value: null, description: 'An object with ''min'',
28 | ''max'' as keys. For additional options see noUiSlider for examples', group: range,
29 | important: false}
30 | - {name: behaviour, type: string, default_value: tap, description: 'This option accepts
31 | a "-" separated list of "drag", "tap", "fixed", "snap", "unconstrained" or "none"',
32 | group: interaction, important: false}
33 | - {name: color, type: color, default_value: null, group: appearance, important: false}
34 | - {name: visible, type: boolean, default_value: true, group: appearance, important: true}
35 | - {name: enabled, type: boolean, default_value: true, group: interaction, important: true}
36 | - {name: spacing_above, type: string, default_value: small, group: layout, important: false}
37 | - {name: spacing_below, type: string, default_value: small, group: layout, important: false}
38 | - {name: pips, type: boolean, default_value: false, description: Sets whether the slider has pips (ticks),
39 | group: pips, important: false}
40 | - {name: pips_mode, type: string, default_value: '', description: '''range'', ''steps'',
41 | ''positions'', ''count'', ''values''', group: pips, important: false}
42 | - {name: pips_density, type: number, default_value: null, description: 'Controls how
43 | many pips are placed. With the default value of 1, there is one pip per percent.
44 | For a value of 2, a pip is placed for every 2 percent. A value of zero will place
45 | more than one pip per percentage. A value of -1 will remove all intermediate pips.',
46 | group: pips, important: false}
47 | - {name: pips_values, type: string, default_value: '', description: a list of values. Interpreted differently depending on the mode,
48 | group: pips, important: false}
49 | - {name: pips_stepped, type: boolean, default_value: true, description: the stepped option can be set to true to match the pips to the slider steps,
50 | group: pips, important: false}
51 | - {name: format, type: string, default_value: .2f, description: 'Provide a format
52 | for the values. This can either be a string to call with .format or a format spec.
53 | e.g. "{:.2f}" or just ".2f". See python''s format string syntax for more options.',
54 | group: appearance, important: false}
55 | - binding_writeback_events: [x-writeback, change]
56 | default_value: null
57 | group: values
58 | important: false
59 | name: value
60 | default_binding_prop: true
61 | type: object
62 | allow_binding_writeback: true
63 | description: returns the value of the first handle. This can only be set after initialization or with a databinding.
64 | - name: values
65 | type: object
66 | default_value: null
67 | description: returns a list of numerical values. This can only be set after initialization or with a databinding
68 | allow_binding_writeback: true
69 | binding_writeback_events: [x-writeback, change]
70 | group: values
71 | important: false
72 | - name: formatted_value
73 | type: object
74 | default_value: null
75 | description: returns the value of the first handle as a formatted string, based on the format property
76 | allow_binding_writeback: true
77 | binding_writeback_events: [x-writeback, change]
78 | group: values
79 | important: false
80 | - name: formatted_values
81 | type: object
82 | default_value: null
83 | description: returns the a list of values as formatted strings, based on the format property
84 | allow_binding_writeback: true
85 | binding_writeback_events: [x-writeback, change]
86 | group: values
87 | important: false
88 | is_package: true
89 | events:
90 | - name: change
91 | default_event: true
92 | description: when the slider has finished sliding
93 | parameters:
94 | - {name: handle, description: an integer representing which handle caused the event}
95 | - name: slide
96 | parameters:
97 | - {name: handle, description: an integer representing which handle caused the event}
98 | description: when the slider is sliding or dragging
99 | - {name: show, description: when the slider is shown}
100 | - {name: hide, description: when the slider is hidden}
101 | custom_component: true
102 | components: []
103 | container:
104 | type: HtmlTemplate
105 | properties: {tooltip: '', background: '', foreground: '', border: '', visible: true,
106 | role: null, html: '
107 |
108 | '}
115 |
--------------------------------------------------------------------------------
/docs/guides/modules/storage.rst:
--------------------------------------------------------------------------------
1 | Storage
2 | =======
3 |
4 | Introduction
5 | ------------
6 | Browsers have various mechanisms to store data. ``localStorage`` and ``IndexedDB`` are two such mechanisms. These are particularly useful for storing data offline.
7 |
8 | The anvil_extras :mod:`storage` module provides wrappers around both these storage mechanisms in a convenient dictionary like API.
9 |
10 | In order to store data you'll need a store object. You can import the default store objects :attr:`local_storage` or :attr:`indexed_db`.
11 | Alternatively create your own store object using the classmethod ``create_store(store_name)``.
12 |
13 | *NB: when working in the IDE the app is running in an IFrame and the storage objects may not be available. This can be fixed by changing your browser settings.
14 | Turning the shields down in Brave or making sure not to block third party cookies in Chrome should fix this.*
15 |
16 |
17 | Which to chose?
18 | +++++++++++++++
19 | | If you have small amounts of data which can be converted to JSON - use :attr:`local_storage`.
20 | | If you have more data which can be converted to JSON (also ``bytes``) - use :attr:`indexed_db`.
21 |
22 | *NB: Datetime objects cannot be stored. You'll need to serialize and deserialize these - consider converting datetimes to timestamps (and dates to ordinals).*
23 |
24 |
25 | Usage Examples
26 | --------------
27 |
28 | Store user preference
29 | +++++++++++++++++++++
30 |
31 | .. code-block:: python
32 |
33 | from anvil_extras.storage import local_storage
34 |
35 | class UserPreferences(UserPreferencesTemplate):
36 | def __init__(self, **properties):
37 | self.init_components(**properties)
38 |
39 | def dark_mode_checkbox_change(self, **event_args):
40 | local_storage['dark_mode'] = self.dark_mode_checkbox.checked
41 |
42 |
43 | Change the theme at startup
44 | +++++++++++++++++++++++++++
45 |
46 | .. code-block:: python
47 |
48 | ## inside a startup module
49 | from anvil_extras.storage import local_storage
50 |
51 | if local_storage.get('dark_mode') is not None:
52 | # set the app theme to dark
53 | ...
54 |
55 |
56 | Create an offline todo app
57 | ++++++++++++++++++++++++++
58 |
59 | .. code-block:: python
60 |
61 | from anvil_extras.storage import indexed_db
62 | from anvil_extras.uuid import uuid4
63 |
64 | todo_store = indexed_db.create_store('todos')
65 | # create_store() is a classmethod that takes a store_name
66 | # it will create another store object inside the browsers IndexedDB
67 | # or return the store object if it already exists
68 | # the todo_store acts as dictionary like object
69 |
70 | class TodoPage(TodoPageTemplate):
71 | def __init__(self, **properties):
72 | self.init_components(**properties)
73 | self.todo_panel.items = list(todo_store.values())
74 |
75 | def save_todo_btn_click(self, **event_args):
76 | if not self.todo_input.text:
77 | return
78 | id = str(uuid4())
79 | todo = {"id": id, "todo": self.todo_input.text, "completed": False}
80 | todo_store[id] = todo
81 | self.todo_panel.items = self.todo_panel.items + [todo]
82 | self.todo_input.text = ""
83 |
84 |
85 |
86 | API
87 | ---
88 |
89 | .. class:: StorageWrapper()
90 | IndexedDBWrapper()
91 | LocalStorageWrapper()
92 |
93 | both :attr:`indexed_db` and :attr:`local_storage` are instances of the dictionary like classes :class:`IndexedDBWrapper` and :class:`LocalStorageWrapper` respectively.
94 |
95 | .. classmethod:: create_store(name)
96 |
97 | Create a store object. e.g. ``todo_store = indexed_db.create_store('todos')``. This will create a new store inside the browser's ``IndexedDB`` and return an :class:`IndexedDBWrapper` instance.
98 | The :attr:`indexed_db` object is equivalent to ``indexed_db.create_store('default')``. To explore this further, open up devtools and find ``IndexedDB`` in the Application tab.
99 | Since :attr:`create_store` is a classmethod you can also do ``todo_store = IndexedDBWrapper.create_store('todos')``.
100 |
101 | .. describe:: is_available()
102 |
103 | Check if the storage object is supported. Returns a ``boolean``.
104 |
105 |
106 | .. describe:: list(store)
107 |
108 | Return a list of all the keys used in the *store*.
109 |
110 | .. describe:: len(store)
111 |
112 | Return the number of items in *store*.
113 |
114 | .. describe:: store[key]
115 |
116 | Return the value of *store* with key *key*. Raises a :exc:`KeyError` if *key* is
117 | not in *store*.
118 |
119 | .. describe:: store[key] = value
120 |
121 | Set ``store[key]`` to *value*. If the value is not a JSONable data type it may be stored incorrectly. e.g. a ``datetime`` object.
122 | If storing ``bytes`` objects it is best to use the :attr:`indexed_db` store.
123 |
124 | .. describe:: del store[key]
125 |
126 | Remove ``store[key]`` from *store*.
127 |
128 | .. describe:: key in store
129 |
130 | Return ``True`` if *store* has a key *key*, else ``False``.
131 |
132 | .. describe:: iter(store)
133 |
134 | Return an iterator over the keys of the *store*. This is a shortcut
135 | for ``iter(store.keys())``.
136 |
137 | .. method:: clear()
138 |
139 | Remove all items from the *store*.
140 |
141 | .. method:: get(key[, default])
142 |
143 | Return the value for *key* if *key* is in *store*, else *default*.
144 | If *default* is not given, it defaults to ``None``, so that this method
145 | never raises a :exc:`KeyError`.
146 |
147 | .. method:: items()
148 |
149 | Return an iterator of the *store*'s ``(key, value)`` pairs.
150 |
151 | .. method:: keys()
152 |
153 | Return an iterator of the *store*'s keys.
154 |
155 | .. method:: pop(key[, default])
156 |
157 | If *key* is in *store*, remove it and return its value, else return
158 | *default*. If *default* is not given, it defaults to ``None``, so that this method
159 | never raises a :exc:`KeyError`.
160 |
161 | .. method:: store(key, value)
162 |
163 | Equivalent to ``store[key] = value``.
164 |
165 | .. method:: update([other])
166 |
167 | Update the *store* with the key/value pairs from *other*, overwriting
168 | existing keys. Return ``None``.
169 |
170 | :meth:`update` accepts either a dictionary object or an iterable of
171 | key/value pairs (as tuples or other iterables of length two). If keyword
172 | arguments are specified, *store* is then updated with those
173 | key/value pairs: ``store.update(red=1, blue=2)``.
174 |
175 | .. method:: values()
176 |
177 | Return an iterator of the *store*'s values.
178 |
--------------------------------------------------------------------------------
/client_code/ChipsInput/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: MIT
2 | #
3 | # Copyright (c) 2021 The Anvil Extras project team members listed at
4 | # https://github.com/anvilistas/anvil-extras/graphs/contributors
5 | #
6 | # This software is published at https://github.com/anvilistas/anvil-extras
7 |
8 | from anvil import HtmlPanel as _HtmlPanel
9 | from anvil.js import get_dom_node as _get_dom_node
10 |
11 | from ..Chip import Chip
12 | from ..utils._component_helpers import _get_color, _html_injector, _spacing_property
13 | from ._anvil_designer import ChipsInputTemplate
14 |
15 | __version__ = "1.7.1"
16 |
17 | _primary = _get_color(None)
18 |
19 | _html_injector.css(
20 | """
21 | .anvil-extras-chips-input input {
22 | box-shadow: none !important;
23 | border: none !important;
24 | padding: 7px 0 !important;
25 | margin-bottom: 0 !important;
26 | flex: 1;
27 | min-width: 50px;
28 | }
29 | .anvil-extras-chips-input{
30 | display: flex;
31 | flex-wrap: wrap;
32 | gap: 8px;
33 | border-bottom: 1px solid;
34 | align-items: center;
35 | padding-bottom: 4px;
36 | }
37 |
38 | """
39 | )
40 |
41 | _defaults = {
42 | "primary_placeholder": "",
43 | "secondary_placeholder": "",
44 | "chips": [],
45 | "visible": True,
46 | "spacing_above": "small",
47 | "spacing_below": "small",
48 | }
49 |
50 |
51 | class ChipsInput(ChipsInputTemplate):
52 | def __init__(self, **properties):
53 | self._chips = []
54 | self._deleting = False
55 | self._placeholder = self._placeholder_0 = self._placeholder_1 = ""
56 |
57 | input_node = _get_dom_node(self.chip_input)
58 | input_node.addEventListener("keydown", self._chip_input_key_down)
59 |
60 | dom_node = self._dom_node = _get_dom_node(self)
61 | dom_node.classList.add("anvil-extras-chips-input")
62 | dom_node.querySelector(".chips-input-placeholder").remove()
63 | dom_node.querySelector("script").remove()
64 | self.temp_chip.remove_from_parent()
65 |
66 | properties = _defaults | properties
67 | self.init_components(**properties)
68 |
69 | @property
70 | def primary_placeholder(self):
71 | return self._placeholder_0
72 |
73 | @primary_placeholder.setter
74 | def primary_placeholder(self, value):
75 | self._placeholder_0 = value
76 | if not len(self._chips):
77 | self.chip_input.placeholder = value
78 | self._placeholder = value
79 |
80 | @property
81 | def secondary_placeholder(self):
82 | return self._placeholder_1
83 |
84 | @secondary_placeholder.setter
85 | def secondary_placeholder(self, value):
86 | self._placeholder_1 = value
87 | if len(self._chips):
88 | self.chip_input.placeholder = value
89 | self._placeholder = value
90 |
91 | @property
92 | def chips(self):
93 | # make sure chips is immutable
94 | return tuple(self._chips)
95 |
96 | @chips.setter
97 | def chips(self, value):
98 | value = value or []
99 | if list(value) == self._chips:
100 | return
101 | self._chips = []
102 | self.clear(slot="chips")
103 |
104 | seen = set()
105 | for chip_text in value:
106 | if chip_text in seen or not chip_text:
107 | continue
108 | chip = Chip(text=chip_text, spacing_above="none", spacing_below="none")
109 | self.add_component(chip, slot="chips")
110 | chip.set_event_handler("close_click", self._chip_close_click)
111 | self._chips.append(chip_text)
112 | seen.add(chip_text)
113 |
114 | self._reset_placeholder()
115 |
116 | visible = _HtmlPanel.visible
117 | spacing_above = _spacing_property("above")
118 | spacing_below = _spacing_property("below")
119 |
120 | ###### PRIVATE METHODS AND PROPS ######
121 |
122 | @property
123 | def _last_chip(self):
124 | """throws an error if we have no chips, when used must be wrapped in try/except"""
125 | components = self.get_components()
126 | components.remove(self.chip_input)
127 | return components[-1]
128 |
129 | def _reset_placeholder(self):
130 | new_placeholder = self._placeholder_1 if self._chips else self._placeholder_0
131 | if new_placeholder != self._placeholder:
132 | self.chip_input.placeholder = self._placeholder = new_placeholder
133 |
134 | def _reset_deleting(self, val):
135 | try:
136 | self._deleting = val
137 | self._set_focus(self._last_chip, val)
138 | except IndexError:
139 | pass
140 |
141 | def _chip_input_pressed_enter(self, **event_args):
142 | """This method is called when the user presses Enter in this text box"""
143 | chip_text = self.chip_input.text
144 | if chip_text and chip_text not in self._chips:
145 | chip = Chip(text=chip_text, spacing_above="none", spacing_below="none")
146 | self.add_component(chip, slot="chips")
147 | chip.set_event_handler("close_click", self._chip_close_click)
148 | self._chips.append(chip_text)
149 | self.chip_input.text = ""
150 | self._reset_placeholder()
151 |
152 | self.raise_event("chips_changed")
153 | self.raise_event("chip_added", chip=chip_text)
154 |
155 | def _chip_input_key_down(self, js_event):
156 | """This method is called when on the user key down in this text box"""
157 | try:
158 | if not self.chip_input.text and js_event.key == "Backspace":
159 | if not self._deleting:
160 | self._reset_deleting(True)
161 | return
162 | _last_chip = self._last_chip
163 | self._chips.pop()
164 | chip_text = _last_chip.text
165 | _last_chip.remove_from_parent()
166 | self._reset_placeholder()
167 |
168 | self.raise_event("chips_changed")
169 | self.raise_event("chip_removed", chip=chip_text)
170 |
171 | self._set_focus(self._last_chip, True)
172 |
173 | elif self._deleting:
174 | self._reset_deleting(False)
175 | if js_event.key == "Tab":
176 | js_event.preventDefault()
177 | except IndexError:
178 | pass
179 |
180 | def _chip_input_focus(self, **event_args):
181 | """This method is called when the TextBox gets focus"""
182 | self._dom_node.style.borderBottom = f"1px solid {_primary}"
183 |
184 | def _chip_input_lost_focus(self, **event_args):
185 | """This method is called when the TextBox loses focus"""
186 | self._dom_node.style.borderBottom = "1px solid"
187 | self._reset_deleting(False)
188 |
189 | def _chip_close_click(self, sender, **event_args):
190 | chips = self._chips
191 | chip_text = sender.text
192 | chips.remove(chip_text)
193 | sender.remove_from_parent()
194 | self.raise_event("chips_changed")
195 | self.raise_event("chip_removed", chip=chip_text)
196 |
197 | @staticmethod
198 | def _set_focus(chip, val):
199 | chip.background = _primary if val else ""
200 | chip.chip_label.foreground = "#fff" if val else ""
201 | chip.close_link.foreground = "#fff" if val else ""
202 |
--------------------------------------------------------------------------------