Skip to content

Commit

Permalink
Merge branch 'main' into leaflet
Browse files Browse the repository at this point in the history
  • Loading branch information
falkoschindler committed Dec 15, 2023
2 parents bf0f7ca + cda492f commit 0485665
Show file tree
Hide file tree
Showing 26 changed files with 430 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8",
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
}
}
}
6 changes: 4 additions & 2 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from typing_extensions import Self

from . import context, core, events, json, outbox, storage
from . import context, core, events, helpers, json, outbox, storage
from .awaitable_response import AwaitableResponse, NullResponse
from .dependencies import Component, Library, register_library, register_resource, register_vue_component
from .elements.mixins.visibility import Visibility
Expand Down Expand Up @@ -397,7 +397,7 @@ def on(self,
if handler:
listener = EventListener(
element_id=self.id,
type=type,
type=helpers.kebab_to_camel_case(type),
args=[args] if args and isinstance(args[0], str) else args, # type: ignore
handler=handler,
throttle=throttle,
Expand All @@ -417,6 +417,8 @@ def _handle_event(self, msg: Dict) -> None:

def update(self) -> None:
"""Update the element on the client side."""
if self.is_deleted:
return
outbox.enqueue_update(self)

def run_method(self, name: str, *args: Any, timeout: float = 1, check_interval: float = 0.01) -> AwaitableResponse:
Expand Down
1 change: 1 addition & 0 deletions nicegui/elements/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ def __init__(self,
:param on_change: callback to be invoked when the value changes
"""
super().__init__(tag='q-editor', value=value, on_value_change=on_change)
self._classes.append('nicegui-editor')
if placeholder is not None:
self._props['placeholder'] = placeholder
6 changes: 3 additions & 3 deletions nicegui/elements/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default {
`,
props: {
id: String,
autocomplete: Array,
_autocomplete: Array,
value: String,
},
data() {
Expand All @@ -41,14 +41,14 @@ export default {
computed: {
shadowText() {
if (!this.inputValue) return "";
const matchingOption = this.autocomplete.find((option) =>
const matchingOption = this._autocomplete.find((option) =>
option.toLowerCase().startsWith(this.inputValue.toLowerCase())
);
return matchingOption ? matchingOption.slice(this.inputValue.length) : "";
},
withDatalist() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
return isMobile && this.autocomplete && this.autocomplete.length > 0;
return isMobile && this._autocomplete && this._autocomplete.length > 0;
},
},
methods: {
Expand Down
4 changes: 2 additions & 2 deletions nicegui/elements/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ def toggle_type(_):
self.props(f'type={"text" if is_hidden else "password"}')
icon = Icon('visibility_off').classes('cursor-pointer').on('click', toggle_type)

self._props['autocomplete'] = autocomplete or []
self._props['_autocomplete'] = autocomplete or []

def set_autocomplete(self, autocomplete: Optional[List[str]]) -> None:
"""Set the autocomplete list."""
self._props['autocomplete'] = autocomplete
self._props['_autocomplete'] = autocomplete
self.update()

def _handle_value_change(self, value: Any) -> None:
Expand Down
22 changes: 1 addition & 21 deletions nicegui/elements/markdown.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import re
from functools import lru_cache
from typing import List

Expand Down Expand Up @@ -41,26 +40,7 @@ def _handle_content_change(self, content: str) -> None:
@lru_cache(maxsize=int(os.environ.get('MARKDOWN_CONTENT_CACHE_SIZE', '1000')))
def prepare_content(content: str, extras: str) -> str:
"""Render Markdown content to HTML."""
html = markdown2.markdown(remove_indentation(content), extras=extras.split())
return apply_tailwind(html) # we need explicit Markdown styling because tailwind CSS removes all default styles


def apply_tailwind(html: str) -> str:
"""Apply tailwind CSS classes to the HTML."""
rep = {
'<h1': '<h1 class="text-5xl mb-4 mt-6"',
'<h2': '<h2 class="text-4xl mb-3 mt-5"',
'<h3': '<h3 class="text-3xl mb-2 mt-4"',
'<h4': '<h4 class="text-2xl mb-1 mt-3"',
'<h5': '<h5 class="text-1xl mb-0.5 mt-2"',
'<a': '<a class="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"',
'<ul': '<ul class="list-disc ml-6"',
'<p>': '<p class="mb-2">',
r'<div\ class="codehilite">': '<div class="codehilite mb-2 p-2">',
'<code': '<code style="background-color: transparent"',
}
pattern = re.compile('|'.join(rep.keys()))
return pattern.sub(lambda m: rep[re.escape(m.group(0))], html)
return markdown2.markdown(remove_indentation(content), extras=extras.split())


def remove_indentation(text: str) -> str:
Expand Down
11 changes: 11 additions & 0 deletions nicegui/elements/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
mounted() {
this.notification = Quasar.Notify.create(this.options);
},
updated() {
this.notification(this.options);
},
props: {
options: Object,
},
};
163 changes: 163 additions & 0 deletions nicegui/elements/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from typing import Any, Literal, Optional, Union

from .. import context
from ..element import Element
from .timer import Timer

NotificationPosition = Literal[
'top-left',
'top-right',
'bottom-left',
'bottom-right',
'top',
'bottom',
'left',
'right',
'center',
]

NotificationType = Optional[Literal[
'positive',
'negative',
'warning',
'info',
'ongoing',
]]


class Notification(Element, component='notification.js'):

def __init__(self,
message: Any = '', *,
position: NotificationPosition = 'bottom',
close_button: Union[bool, str] = False,
type: NotificationType = None, # pylint: disable=redefined-builtin
color: Optional[str] = None,
multi_line: bool = False,
icon: Optional[str] = None,
spinner: bool = False,
timeout: Optional[float] = 5.0,
**kwargs: Any,
) -> None:
"""Notification element
Displays a notification on the screen.
In contrast to `ui.notify`, this element allows to update the notification message and other properties once the notification is displayed.
:param message: content of the notification
:param position: position on the screen ("top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right" or "center", default: "bottom")
:param close_button: optional label of a button to dismiss the notification (default: `False`)
:param type: optional type ("positive", "negative", "warning", "info" or "ongoing")
:param color: optional color name
:param multi_line: enable multi-line notifications
:param timeout: optional timeout in seconds after which the notification is dismissed (default: 5.0)
Note: You can pass additional keyword arguments according to `Quasar's Notify API <https://quasar.dev/quasar-plugins/notify#notify-api>`_.
"""
with context.get_client().layout:
super().__init__()
self._props['options'] = {
'message': str(message),
'position': position,
'type': type,
'color': color,
'multiLine': multi_line,
'icon': icon,
'spinner': spinner,
'closeBtn': close_button,
'timeout': (timeout or 0) * 1000,
'group': False,
'attrs': {'data-id': f'nicegui-dialog-{self.id}'},
}
self._props['options'].update(kwargs)
with self:
def delete():
self.clear()
self.delete()

async def try_delete():
query = f'''!!document.querySelector("[data-id='nicegui-dialog-{self.id}']")'''
if not await self.client.run_javascript(query):
delete()

Timer(1.0, try_delete)

@property
def message(self) -> str:
"""Message text."""
return self._props['options']['message']

@message.setter
def message(self, value: Any) -> None:
self._props['options']['message'] = str(value)
self.update()

@property
def position(self) -> NotificationPosition:
"""Position on the screen."""
return self._props['options']['position']

@position.setter
def position(self, value: NotificationPosition) -> None:
self._props['options']['position'] = value
self.update()

@property
def type(self) -> NotificationType:
"""Type of the notification."""
return self._props['options']['type']

@type.setter
def type(self, value: NotificationType) -> None:
self._props['options']['type'] = value
self.update()

@property
def color(self) -> Optional[str]:
"""Color of the notification."""
return self._props['options']['color']

@color.setter
def color(self, value: Optional[str]) -> None:
self._props['options']['color'] = value
self.update()

@property
def multi_line(self) -> bool:
"""Whether the notification is multi-line."""
return self._props['options']['multiLine']

@multi_line.setter
def multi_line(self, value: bool) -> None:
self._props['options']['multiLine'] = value
self.update()

@property
def icon(self) -> Optional[str]:
"""Icon of the notification."""
return self._props['options']['icon']

@icon.setter
def icon(self, value: Optional[str]) -> None:
self._props['options']['icon'] = value
self.update()

@property
def spinner(self) -> bool:
"""Whether the notification is a spinner."""
return self._props['options']['spinner']

@spinner.setter
def spinner(self, value: bool) -> None:
self._props['options']['spinner'] = value
self.update()

@property
def close_button(self) -> Union[bool, str]:
"""Whether the notification has a close button."""
return self._props['options']['closeBtn']

@close_button.setter
def close_button(self, value: Union[bool, str]) -> None:
self._props['options']['closeBtn'] = value
self.update()
3 changes: 2 additions & 1 deletion nicegui/elements/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default {
},
methods: {
filterFn(val, update, abort) {
update(() => (this.filteredOptions = this.findFilteredOptions()));
update(() => (this.filteredOptions = val ? this.findFilteredOptions() : this.initialOptions));
},
findFilteredOptions() {
const needle = this.$el.querySelector("input[type=search]")?.value.toLocaleLowerCase();
Expand All @@ -30,6 +30,7 @@ export default {
},
},
updated() {
if (!this.$attrs.multiple) return;
const newFilteredOptions = this.findFilteredOptions();
if (newFilteredOptions.length !== this.filteredOptions.length) {
this.filteredOptions = newFilteredOptions;
Expand Down
21 changes: 20 additions & 1 deletion nicegui/elements/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .. import optional_features
from ..element import Element
from ..events import GenericEventArguments, TableSelectionEventArguments, handle_event
from ..events import GenericEventArguments, TableSelectionEventArguments, ValueChangeEventArguments, handle_event
from .mixins.filter_element import FilterElement

try:
Expand All @@ -24,6 +24,7 @@ def __init__(self,
selection: Optional[Literal['single', 'multiple']] = None,
pagination: Optional[Union[int, dict]] = None,
on_select: Optional[Callable[..., Any]] = None,
on_pagination_change: Optional[Callable[..., Any]] = None,
) -> None:
"""Table
Expand All @@ -36,6 +37,7 @@ def __init__(self,
:param selection: selection type ("single" or "multiple"; default: `None`)
:param pagination: a dictionary correlating to a pagination object or number of rows per page (`None` hides the pagination, 0 means "infinite"; default: `None`).
:param on_select: callback which is invoked when the selection changes
:param on_pagination_change: callback which is invoked when the pagination changes
If selection is 'single' or 'multiple', then a `selected` property is accessible containing the selected rows.
"""
Expand Down Expand Up @@ -63,6 +65,13 @@ def handle_selection(e: GenericEventArguments) -> None:
handle_event(on_select, arguments)
self.on('selection', handle_selection, ['added', 'rows', 'keys'])

def handle_pagination_change(e: GenericEventArguments) -> None:
self.pagination = e.args
self.update()
arguments = ValueChangeEventArguments(sender=self, client=self.client, value=self.pagination)
handle_event(on_pagination_change, arguments)
self.on('update:pagination', handle_pagination_change)

@staticmethod
def from_pandas(df: pd.DataFrame,
row_key: str = 'id',
Expand Down Expand Up @@ -146,6 +155,16 @@ def selected(self, value: List[Dict]) -> None:
self._props['selected'][:] = value
self.update()

@property
def pagination(self) -> dict:
"""Pagination object."""
return self._props['pagination']

@pagination.setter
def pagination(self, value: dict) -> None:
self._props['pagination'] = value
self.update()

@property
def is_fullscreen(self) -> bool:
"""Whether the table is in fullscreen mode."""
Expand Down
5 changes: 5 additions & 0 deletions nicegui/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,8 @@ def in_thread(host: str, port: int) -> None:
thread = threading.Thread(target=in_thread, args=(host, port), daemon=True)
thread.start()
return thread, cancel


def kebab_to_camel_case(string: str) -> str:
"""Convert a kebab-case string to camelCase."""
return ''.join(word.capitalize() if i else word for i, word in enumerate(string.split('-')))
Empty file added nicegui/py.typed
Empty file.
Loading

0 comments on commit 0485665

Please sign in to comment.