Przeglądaj źródła

Add Clipboard component for handling global on_paste event (#3513)

* Add Clipboard component for handling global on_paste event

* py3.8 compat

* py3.8 compat (p2)
Masen Furer 11 miesięcy temu
rodzic
commit
956bc0a397

+ 59 - 0
reflex/.templates/web/utils/helpers/paste.js

@@ -0,0 +1,59 @@
+import { useEffect } from "react";
+
+const handle_paste_data = (clipboardData) =>
+  new Promise((resolve, reject) => {
+    const pasted_data = [];
+    const n_items = clipboardData.items.length;
+    const extract_data = (item) => {
+      const type = item.type;
+      if (item.kind === "string") {
+        item.getAsString((data) => {
+          pasted_data.push([type, data]);
+          if (pasted_data.length === n_items) {
+            resolve(pasted_data);
+          }
+        });
+      } else if (item.kind === "file") {
+        const file = item.getAsFile();
+        const reader = new FileReader();
+        reader.onload = (e) => {
+          pasted_data.push([type, e.target.result]);
+          if (pasted_data.length === n_items) {
+            resolve(pasted_data);
+          }
+        };
+        if (type.indexOf("text/") === 0) {
+          reader.readAsText(file);
+        } else {
+          reader.readAsDataURL(file);
+        }
+      }
+    };
+    for (const item of clipboardData.items) {
+      extract_data(item);
+    }
+  });
+
+export default function usePasteHandler(target_ids, event_actions, on_paste) {
+  return useEffect(() => {
+    const handle_paste = (_ev) => {
+      event_actions.preventDefault && _ev.preventDefault();
+      event_actions.stopPropagation && _ev.stopPropagation();
+      handle_paste_data(_ev.clipboardData).then(on_paste);
+    };
+    const targets = target_ids
+      .map((id) => document.getElementById(id))
+      .filter((element) => !!element);
+    if (target_ids.length === 0) {
+      targets.push(document);
+    }
+    targets.forEach((target) =>
+      target.addEventListener("paste", handle_paste, false),
+    );
+    return () => {
+      targets.forEach((target) =>
+        target.removeEventListener("paste", handle_paste, false),
+      );
+    };
+  });
+}

+ 1 - 0
reflex/__init__.py

@@ -212,6 +212,7 @@ COMPONENTS_CORE_MAPPING: dict = {
     "components.core.debounce": ["debounce_input"],
     "components.core.html": ["html"],
     "components.core.match": ["match"],
+    "components.core.clipboard": ["clipboard"],
     "components.core.colors": ["color"],
     "components.core.responsive": [
         "desktop_only",

+ 1 - 0
reflex/__init__.pyi

@@ -129,6 +129,7 @@ from .components.core.foreach import foreach as foreach
 from .components.core.debounce import debounce_input as debounce_input
 from .components.core.html import html as html
 from .components.core.match import match as match
+from .components.core.clipboard import clipboard as clipboard
 from .components.core.colors import color as color
 from .components.core.responsive import desktop_only as desktop_only
 from .components.core.responsive import mobile_and_tablet as mobile_and_tablet

+ 1 - 0
reflex/components/core/__init__.py

@@ -17,6 +17,7 @@ _SUBMOD_ATTRS: dict[str, list[str]] = {
         "connection_toaster",
         "connection_pulser",
     ],
+    "clipboard": ["Clipboard", "clipboard"],
     "colors": [
         "color",
     ],

+ 2 - 0
reflex/components/core/__init__.pyi

@@ -13,6 +13,8 @@ from .banner import connection_banner as connection_banner
 from .banner import connection_modal as connection_modal
 from .banner import connection_toaster as connection_toaster
 from .banner import connection_pulser as connection_pulser
+from .clipboard import Clipboard as Clipboard
+from .clipboard import clipboard as clipboard
 from .colors import color as color
 from .cond import Cond as Cond
 from .cond import color_mode_cond as color_mode_cond

+ 94 - 0
reflex/components/core/clipboard.py

@@ -0,0 +1,94 @@
+"""Global on_paste handling for Reflex app."""
+from __future__ import annotations
+
+from typing import Dict, List, Union
+
+from reflex.components.base.fragment import Fragment
+from reflex.components.tags.tag import Tag
+from reflex.event import EventChain, EventHandler
+from reflex.utils.format import format_prop, wrap
+from reflex.utils.imports import ImportVar
+from reflex.vars import Var, get_unique_variable_name
+
+
+class Clipboard(Fragment):
+    """Clipboard component."""
+
+    # The element ids to attach the event listener to. Defaults to all child components or the document.
+    targets: Var[List[str]]
+
+    # Called when the user pastes data into the document. Data is a list of tuples of (mime_type, data). Binary types will be base64 encoded as a data uri.
+    on_paste: EventHandler[lambda data: [data]]
+
+    # Save the original event actions for the on_paste event.
+    on_paste_event_actions: Var[Dict[str, Union[bool, int]]]
+
+    @classmethod
+    def create(cls, *children, **props):
+        """Create a Clipboard component.
+
+        Args:
+            *children: The children of the component.
+            **props: The properties of the component.
+
+        Returns:
+            The Clipboard Component.
+        """
+        if "targets" not in props:
+            # Add all children as targets if not specified.
+            targets = props.setdefault("targets", [])
+            for c in children:
+                if c.id is None:
+                    c.id = f"clipboard_{get_unique_variable_name()}"
+                targets.append(c.id)
+
+        if "on_paste" in props:
+            # Capture the event actions for the on_paste handler if not specified.
+            props.setdefault("on_paste_event_actions", props["on_paste"].event_actions)
+
+        return super().create(*children, **props)
+
+    def _exclude_props(self) -> list[str]:
+        return super()._exclude_props() + ["on_paste", "on_paste_event_actions"]
+
+    def _render(self) -> Tag:
+        tag = super()._render()
+        tag.remove_props("targets")
+        # Ensure a different Fragment component is created whenever targets differ
+        tag.add_props(key=self.targets)
+        return tag
+
+    def add_imports(self) -> dict[str, ImportVar]:
+        """Add the imports for the Clipboard component.
+
+        Returns:
+            The import dict for the component.
+        """
+        return {
+            "/utils/helpers/paste.js": ImportVar(
+                tag="usePasteHandler", is_default=True
+            ),
+        }
+
+    def add_hooks(self) -> list[str]:
+        """Add hook to register paste event listener.
+
+        Returns:
+            The hooks to add to the component.
+        """
+        on_paste = self.event_triggers["on_paste"]
+        if on_paste is None:
+            return []
+        if isinstance(on_paste, EventChain):
+            on_paste = wrap(str(format_prop(on_paste)).strip("{}"), "(")
+        return [
+            "usePasteHandler(%s, %s, %s)"
+            % (
+                self.targets._var_name_unwrapped,
+                self.on_paste_event_actions._var_name_unwrapped,
+                on_paste,
+            )
+        ]
+
+
+clipboard = Clipboard.create

+ 105 - 0
reflex/components/core/clipboard.pyi

@@ -0,0 +1,105 @@
+"""Stub file for reflex/components/core/clipboard.py"""
+# ------------------- DO NOT EDIT ----------------------
+# This file was generated by `reflex/utils/pyi_generator.py`!
+# ------------------------------------------------------
+
+from typing import Any, Dict, Literal, Optional, Union, overload
+from reflex.vars import Var, BaseVar, ComputedVar
+from reflex.event import EventChain, EventHandler, EventSpec
+from reflex.style import Style
+from typing import Dict, List, Union
+from reflex.components.base.fragment import Fragment
+from reflex.components.tags.tag import Tag
+from reflex.event import EventChain, EventHandler
+from reflex.utils.format import format_prop, wrap
+from reflex.utils.imports import ImportVar
+from reflex.vars import Var, get_unique_variable_name
+
+class Clipboard(Fragment):
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        targets: Optional[Union[Var[List[str]], List[str]]] = None,
+        on_paste_event_actions: Optional[
+            Union[Var[Dict[str, Union[bool, int]]], Dict[str, Union[bool, int]]]
+        ] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
+        on_blur: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_click: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_context_menu: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_double_click: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_focus: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mount: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_down: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_enter: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_leave: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_move: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_out: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_over: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_up: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_paste: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_scroll: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_unmount: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        **props
+    ) -> "Clipboard":
+        """Create a Clipboard component.
+
+        Args:
+            *children: The children of the component.
+            targets: The element ids to attach the event listener to. Defaults to all child components or the document.
+            on_paste_event_actions: Save the original event actions for the on_paste event.
+            style: The style of the component.
+            key: A unique key for the component.
+            id: The id for the component.
+            class_name: The class name for the component.
+            autofocus: Whether the component should take the focus once the page is loaded
+            custom_attrs: custom attribute
+            **props: The properties of the component.
+
+        Returns:
+            The Clipboard Component.
+        """
+        ...
+    def add_imports(self) -> dict[str, ImportVar]: ...
+    def add_hooks(self) -> list[str]: ...
+
+clipboard = Clipboard.create

+ 0 - 1
reflex/utils/pyi_generator.py

@@ -960,7 +960,6 @@ class PyiGenerator:
                 target_path.is_file()
                 and target_path.suffix == ".py"
                 and target_path.name not in EXCLUDED_FILES
-                and "reflex/components" in str(target_path)
             ):
                 file_targets.append(target_path)
                 continue