diff 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 332822ceae85
line wrap: on
line diff
--- a/libervia/web/pages/_browser/dialog.py	Wed May 21 15:58:56 2025 +0200
+++ b/libervia/web/pages/_browser/dialog.py	Fri Jun 06 11:08:05 2025 +0200
@@ -1,5 +1,6 @@
 """manage common dialogs"""
 
+from typing import Callable, Literal
 from browser import document, window, timer, console as log
 from template import Template
 
@@ -183,63 +184,86 @@
 
 class Modal:
 
-    def __init__(self, content_elt, is_card=False, closable=False, close_cb=None):
-        """Init a Modal instance.
-
-        @param content_elt: Content of the modal.
-        @param is_card: If True, a Modal card will be used. The ``content_elt`` must be a
-            <div> with the "modal-card" class.
-        @param closable: if True, add a close cross at the top right of the 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:
+        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'
+                    '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.reset()
+        self._closing = False
+        self._is_panel = position != 'center'
 
-    def reset(self):
-        """Reset values of callbacks and notif element"""
+    def _cleanup(self):
         if self._modal_elt is not None:
             self._modal_elt.remove()
             self._modal_elt = None
-
-    def _default_cancel_cb(self, evt, notif_elt):
-        notif_elt.remove()
+        self._closing = False
 
     def close(self):
-        """Close the dialog."""
-        if self._modal_elt is None:
-            log.warning("Calling close on an unshown dialog.")
-        self.reset()
+        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_click(self, evt) -> None:
-        evt.preventDefault()
-        evt.stopPropagation()
-        if self._close_cb is not None:
-            self._close_cb()
-        self.close()
+        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:
-        modal_elt = self._tpl.get_elt({
+        if self._modal_elt:
+            self._cleanup()
+
+        self._modal_elt = self._tpl.get_elt({
             "closable": self.closable,
+            "position": self.position,
+            "is_panel": self._is_panel
         })
-        self._modal_elt = modal_elt
-        if self.is_card:
-            container_elt = modal_elt
+
+        if self.position == 'center':
+            container_elt = self._modal_elt.select_one(".modal-content")
+            container_elt <= self.content_elt
         else:
-            container_elt = modal_elt.select_one(".modal-content")
-        container_elt <= self.content_elt
+            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
 
-        document['notifs_area'] <= modal_elt
-        for ok_elt in modal_elt.select(".modal-close"):
-            ok_elt.bind("click", self.on_close_click)
+        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()