Mercurial > libervia-web
view libervia/web/pages/_browser/dialog.py @ 1625:698eaabfca0e
browser (chat): side/panel and keyword handling:
- `dialog.Modal` can now be used with a `position` argument. The default `center` keeps
the old behaviour of modal in the middle of the screen, while using one of the
direction makes the modal appear from top/bottom/right/left with an animation. The
closing cross has been removed in favor of clicking/touching any part outside of the
modal.
- A method to create mockup clones of elements is now used to make placeholders. It is
used for the input panel in the main area when it is moved to a sub-panel modal, this
way there is not element disappearing.
- Clicking on a keyword now shows a sub-messages panel with all messages with this
keyword, in a similar way as for threads. Writing a messages there will add the keyword
automatically.
- Sub-messages panel will auto-scroll when messages are added.
rel 458
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 06 Jun 2025 11:08:05 +0200 |
parents | 3a60bf3762ef |
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): 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()