Mercurial > libervia-web
view libervia/web/pages/_browser/dialog.py @ 1635:332822ceae85
browser (chat): Add rich editor, forward and extra recipients:
A new "extra" menu is now available next to input field, allowing to toggle to rich
editor. Rich editors allows message styling using bold, italic, underline, (un)numbered
list and link. Other features will probably follow with time.
An extra menu item allows to add recipients, with `to`, `cc` or `bcc` flag like for
emails.
Messages can now be forwarded to any entity with a new item in the 3 dots menu.
rel 461
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 04 Jul 2025 17:47:37 +0200 |
parents | 698eaabfca0e |
children |
line wrap: on
line source
"""manage common dialogs""" from typing import Callable, Literal from browser import document, window, timer, console as log from template import Template log.warning = log.warn class CancelError(Exception): """Dialog is cancelled""" def __init__(self, reason: str = "", text: str = "") -> None: self.reason = reason self.text = text super().__init__(text) class Confirm: def __init__(self, message, ok_label="", cancel_label="", ok_color="success"): self._tpl = Template("dialogs/confirm.html") self.message = message self.ok_label = ok_label assert ok_color in ("success", "danger") self.ok_color = ok_color self.cancel_label = cancel_label self._notif_elt = None self.reset() def reset(self): """Reset values of callbacks and notif element""" self._ok_cb = None self._cancel_cb = None self._reject_cb = None if self._notif_elt is not None: self._notif_elt.remove() self._notif_elt = None def _default_cancel_cb(self, evt, notif_elt): notif_elt.remove() def cancel(self, reason: str = "", text: str = ""): """Cancel the dialog, without calling any callback will raise a CancelError """ if self._notif_elt is None: log.warning("calling cancel on an unshown dialog") else: self._notif_elt.remove() self._notif_elt = None if self._reject_cb is not None: self._reject_cb(CancelError(reason, text)) else: log.warning("no reject callback set") self.reset() def on_ok_click(self, evt): evt.preventDefault() evt.stopPropagation() assert self._ok_cb is not None self._ok_cb(evt, self._notif_elt) self.reset() def on_cancel_click(self, evt) -> None: evt.preventDefault() evt.stopPropagation() assert self._cancel_cb is not None self._cancel_cb(evt, self._notif_elt) self.reset() def show(self, ok_cb, cancel_cb=None, reject_cb=None): if cancel_cb is None: cancel_cb = self._default_cancel_cb self._ok_cb = ok_cb self._cancel_cb = cancel_cb self._reject_cb = reject_cb notif_elt = self._tpl.get_elt({ "message": self.message, "ok_label": self.ok_label, "ok_color": self.ok_color, "cancel_label": self.cancel_label, }) self._notif_elt = notif_elt document['notifs_area'] <= notif_elt timer.set_timeout(lambda: notif_elt.classList.add('state_appended'), 0) for ok_elt in notif_elt.select(".click_to_ok"): ok_elt.bind("click", self.on_ok_click) for ok_elt in notif_elt.select(".click_to_cancel"): ok_elt.bind("click", self.on_cancel_click) def _ashow_cb(self, evt, notif_elt, resolve_cb, confirmed): evt.stopPropagation() notif_elt.remove() resolve_cb(confirmed) async def ashow(self) -> bool: """Return True if the dialog is confirmed.""" return window.Promise.new( lambda resolve_cb, reject_cb: self.show( lambda evt, notif_elt: self._ashow_cb(evt, notif_elt, resolve_cb, True), lambda evt, notif_elt: self._ashow_cb(evt, notif_elt, resolve_cb, False), reject_cb ) ) class Notification: def __init__(self): self._tpl = Template("dialogs/notification.html") def close(self, notif_elt): notif_elt.classList.remove('state_appended') notif_elt.bind("transitionend", lambda __: notif_elt.remove()) def show( self, message: str, level: str = "info", delay: int = 5 ) -> None: # we log in console error messages, may be useful if level in ("warning", "error"): print(f"[{level}] {message}") notif_elt = self._tpl.get_elt({ "message": message, "level": level, }) document["notifs_area"] <= notif_elt timer.set_timeout(lambda: notif_elt.classList.add('state_appended'), 0) timer.set_timeout(lambda: self.close(notif_elt), delay * 1000) for elt in notif_elt.select('.click_to_close'): elt.bind('click', lambda __: self.close(notif_elt)) class RetryNotification: def __init__(self, retry_cb): self._tpl = Template("dialogs/retry-notification.html") self.retry_cb = retry_cb self.counter = 0 self.timer = None def retry(self, notif_elt): if self.timer is not None: timer.clear_interval(self.timer) self.timer = None notif_elt.classList.remove('state_appended') notif_elt.bind("transitionend", lambda __: notif_elt.remove()) self.retry_cb() def update_counter(self, notif_elt): counter = notif_elt.select_one(".retry_counter") counter.text = str(self.counter) self.counter -= 1 if self.counter < 0: self.retry(notif_elt) def show( self, message: str, level: str = "warning", delay: int = 5 ) -> None: # we log in console error messages, may be useful if level == "error": log.error(message) elif level == "warning": log.warning(message) self.counter = delay notif_elt = self._tpl.get_elt({ "message": message, "level": level, }) self.update_counter(notif_elt) document["notifs_area"] <= notif_elt timer.set_timeout(lambda: notif_elt.classList.add('state_appended'), 0) self.timer = timer.set_interval(self.update_counter, 1000, notif_elt) for elt in notif_elt.select('.click_to_retry'): elt.bind('click', lambda __: self.retry(notif_elt)) class Modal: def __init__( self, content_elt, is_card: bool = False, closable: bool = False, close_cb: Callable|None = None, position: Literal["center", "left", "right", "top", "bottom"] = "center" ) -> None: self.position = position self.is_card = is_card if is_card and position == 'center': if not content_elt.classList.contains("modal-card"): raise ValueError( 'Element must have a "modal-card" class when `is_card` is used for center modal' ) self.closable = closable self._close_cb = close_cb self._tpl = Template("dialogs/modal.html") self.content_elt = content_elt self._modal_elt = None self._closing = False self._is_panel = position != 'center' def _cleanup(self): if self._modal_elt is not None: self._modal_elt.remove() self._modal_elt = None self._closing = False def close(self): if self._modal_elt is None or self._closing: return self._closing = True self._modal_elt.classList.remove('is-active') if self._is_panel: self._modal_elt.classList.add('closing') def on_close_finished(): if self._close_cb: self._close_cb() self._cleanup() timer.set_timeout(on_close_finished, 300) def on_background_click(self, evt): if self.closable and not self._closing: evt.preventDefault() evt.stopPropagation() self.close() def show(self) -> None: if self._modal_elt: self._cleanup() self._modal_elt = self._tpl.get_elt({ "closable": self.closable, "position": self.position, "is_panel": self._is_panel }) if self.position == 'center': container_elt = self._modal_elt.select_one(".modal-content") container_elt <= self.content_elt else: container_elt = self._modal_elt.select_one(".modal-panel-container") container_elt <= self.content_elt container_elt.classList.add(f"is-{self.position}") document['notifs_area'] <= self._modal_elt if self.closable: bg = self._modal_elt.select_one(".modal-background, .modal-panel-background") if bg: bg.bind("click", self.on_background_click) # Add active class after a small delay to trigger animation timer.set_timeout(lambda: self._modal_elt.classList.add('is-active'), 10) notification = Notification()