Forráskód Böngészése

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 hónapja
szülő
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.debounce": ["debounce_input"],
     "components.core.html": ["html"],
     "components.core.html": ["html"],
     "components.core.match": ["match"],
     "components.core.match": ["match"],
+    "components.core.clipboard": ["clipboard"],
     "components.core.colors": ["color"],
     "components.core.colors": ["color"],
     "components.core.responsive": [
     "components.core.responsive": [
         "desktop_only",
         "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.debounce import debounce_input as debounce_input
 from .components.core.html import html as html
 from .components.core.html import html as html
 from .components.core.match import match as match
 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.colors import color as color
 from .components.core.responsive import desktop_only as desktop_only
 from .components.core.responsive import desktop_only as desktop_only
 from .components.core.responsive import mobile_and_tablet as mobile_and_tablet
 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_toaster",
         "connection_pulser",
         "connection_pulser",
     ],
     ],
+    "clipboard": ["Clipboard", "clipboard"],
     "colors": [
     "colors": [
         "color",
         "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_modal as connection_modal
 from .banner import connection_toaster as connection_toaster
 from .banner import connection_toaster as connection_toaster
 from .banner import connection_pulser as connection_pulser
 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 .colors import color as color
 from .cond import Cond as Cond
 from .cond import Cond as Cond
 from .cond import color_mode_cond as color_mode_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()
                 target_path.is_file()
                 and target_path.suffix == ".py"
                 and target_path.suffix == ".py"
                 and target_path.name not in EXCLUDED_FILES
                 and target_path.name not in EXCLUDED_FILES
-                and "reflex/components" in str(target_path)
             ):
             ):
                 file_targets.append(target_path)
                 file_targets.append(target_path)
                 continue
                 continue