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.html": ["html"],
|
||||
"components.core.match": ["match"],
|
||||
"components.core.clipboard": ["clipboard"],
|
||||
"components.core.colors": ["color"],
|
||||
"components.core.responsive": [
|
||||
"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.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
|
||||
|
@ -17,6 +17,7 @@ _SUBMOD_ATTRS: dict[str, list[str]] = {
|
||||
"connection_toaster",
|
||||
"connection_pulser",
|
||||
],
|
||||
"clipboard": ["Clipboard", "clipboard"],
|
||||
"colors": [
|
||||
"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_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
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()
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user