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)
This commit is contained in:
parent
9d71bcbbb5
commit
956bc0a397
59
reflex/.templates/web/utils/helpers/paste.js
Normal file
59
reflex/.templates/web/utils/helpers/paste.js
Normal file
@ -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),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
],
|
],
|
||||||
|
@ -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
reflex/components/core/clipboard.py
Normal file
94
reflex/components/core/clipboard.py
Normal file
@ -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
reflex/components/core/clipboard.pyi
Normal file
105
reflex/components/core/clipboard.pyi
Normal file
@ -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
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user