├── theme ├── assets │ ├── theme.css │ ├── standard-page.html │ └── loading-spinner.js ├── parameters.yaml └── templates.yaml ├── requirements.txt ├── images ├── chips.gif ├── pivot.gif ├── quill.gif ├── tabs.gif ├── sliders.gif ├── switch.gif ├── auto_refresh.gif ├── autocomplete.gif ├── extras_demo.gif ├── message_pill.png ├── multi_select.gif └── progress_bars.gif ├── docs ├── guides │ ├── modules │ │ ├── index.rst │ │ ├── authorisation.rst │ │ ├── augmentation.rst │ │ ├── messaging.rst │ │ ├── utils.rst │ │ ├── popover.rst │ │ ├── navigation.rst │ │ └── storage.rst │ ├── components │ │ ├── index.rst │ │ ├── switch.rst │ │ ├── message_pill.rst │ │ ├── indeterminate_progress_bar.rst │ │ ├── determinate_progress_bar.rst │ │ ├── editable_card.rst │ │ ├── autocomplete.rst │ │ ├── pivot.rst │ │ ├── page_break.rst │ │ ├── tabs.rst │ │ ├── multi_select_dropdown.rst │ │ ├── chips.rst │ │ ├── quill.rst │ │ └── slider.rst │ ├── index.rst │ ├── installation.rst │ ├── enterprise-installation.rst │ └── contributing.rst ├── images │ └── message_pill.png ├── index.rst └── conf.py ├── .gitignore ├── LICENSE.txt ├── __init__.py ├── anvil.yaml ├── client_code ├── routing │ ├── __init__.py │ ├── _alert.py │ ├── _session_expired.py │ ├── _logging.py │ └── _navigation.py ├── MessagePill │ ├── form_template.yaml │ └── __init__.py ├── ProgressBar │ ├── Indeterminate │ │ ├── form_template.yaml │ │ └── __init__.py │ ├── Determinate │ │ ├── form_template.yaml │ │ └── __init__.py │ └── __init__.py ├── Pivot │ ├── form_template.yaml │ └── __init__.py ├── utils │ ├── __init__.py │ ├── _timed.py │ ├── _auto_refreshing.py │ ├── _writeback_waiter.py │ └── _component_helpers.py ├── uuid.py ├── PageBreak │ ├── form_template.yaml │ └── __init__.py ├── Autocomplete │ └── form_template.yaml ├── MultiSelectDropDown │ └── form_template.yaml ├── Chip │ ├── form_template.yaml │ └── __init__.py ├── Switch │ ├── form_template.yaml │ └── __init__.py ├── ChipsInput │ ├── form_template.yaml │ └── __init__.py ├── messaging.py ├── Tabs │ └── form_template.yaml ├── Quill │ └── form_template.yaml ├── navigation.py ├── Demo │ └── __init__.py ├── augment.py └── Slider │ └── form_template.yaml ├── setup.cfg ├── js └── designer_components │ ├── index.ts │ ├── build-script.ts │ ├── DesignerPivot.ts │ ├── DesignerMultSelectDropDown.ts │ ├── DesignerQuill.ts │ ├── DesignerTabs.ts │ ├── DesignerSwitch.ts │ ├── README.md │ ├── DesignerChips.ts │ └── DesignerSlider.ts ├── .editorconfig ├── LICENSE ├── tests └── test_publisher.py ├── .pre-commit-config.yaml ├── .github └── workflows │ └── config.yml ├── server_code ├── server_utils.py └── authorisation.py ├── .anvil_editor.yaml ├── README.md └── CHANGELOG.md /theme/assets/theme.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /theme/parameters.yaml: -------------------------------------------------------------------------------- 1 | roles: [] 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bump2version 2 | pytest 3 | -------------------------------------------------------------------------------- /images/chips.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/chips.gif -------------------------------------------------------------------------------- /images/pivot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/pivot.gif -------------------------------------------------------------------------------- /images/quill.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/quill.gif -------------------------------------------------------------------------------- /images/tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/tabs.gif -------------------------------------------------------------------------------- /images/sliders.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/sliders.gif -------------------------------------------------------------------------------- /images/switch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/switch.gif -------------------------------------------------------------------------------- /docs/guides/modules/index.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | * 8 | -------------------------------------------------------------------------------- /images/auto_refresh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/auto_refresh.gif -------------------------------------------------------------------------------- /images/autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/autocomplete.gif -------------------------------------------------------------------------------- /images/extras_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/extras_demo.gif -------------------------------------------------------------------------------- /images/message_pill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/message_pill.png -------------------------------------------------------------------------------- /images/multi_select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/multi_select.gif -------------------------------------------------------------------------------- /images/progress_bars.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/images/progress_bars.gif -------------------------------------------------------------------------------- /docs/guides/components/index.rst: -------------------------------------------------------------------------------- 1 | Components 2 | ========== 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | * 8 | -------------------------------------------------------------------------------- /docs/images/message_pill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/anvil-extras/main/docs/images/message_pill.png -------------------------------------------------------------------------------- /docs/guides/index.rst: -------------------------------------------------------------------------------- 1 | Guides 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | installation 7 | enterprise-installation 8 | contributing 9 | components/index 10 | modules/index 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | 4 | guides/index 5 | 6 | Indices and tables 7 | ================== 8 | 9 | * :ref:`genindex` 10 | * :ref:`modindex` 11 | * :ref:`search` 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | __pycache__ 4 | .anvil-data 5 | .vscode 6 | /venv/ 7 | /build/* 8 | */node_modules/ 9 | /node_modules/ 10 | .yarn 11 | yarn.lock 12 | yarn-error.log 13 | .yarnrc.yml 14 | -------------------------------------------------------------------------------- /theme/assets/standard-page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
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"} 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation Status](https://readthedocs.org/projects/anvil-extras/badge/)](https://anvil-extras.readthedocs.io/en/latest/) 2 | [![Gitter](https://badges.gitter.im/anvilistas/community.svg)](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 | message pill | Progress Bars | progress_bars | 24 | | Auto Refreshing | auto refreshing | Tabs | tabs | Switch | switch | 25 | | Multi Select Dropdown | multi select | Quill Editor | quill | 26 | | Sliders | sliders | Chips | chips | 27 | | Autocomplete | auto complete | Pivot | 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 | --------------------------------------------------------------------------------