Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Clipboard component for handling global on_paste event #3513

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions reflex/.templates/web/utils/helpers/paste.js
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions reflex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
"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",
Expand Down
1 change: 1 addition & 0 deletions reflex/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions reflex/components/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"connection_toaster",
"connection_pulser",
],
"clipboard": ["Clipboard", "clipboard"],
"colors": [
"color",
],
Expand Down
2 changes: 2 additions & 0 deletions reflex/components/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions reflex/components/core/clipboard.py
Original file line number Diff line number Diff line change
@@ -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 changes: 105 additions & 0 deletions reflex/components/core/clipboard.pyi
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion reflex/utils/pyi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,6 @@ def scan_all(self, targets, changed_files: list[Path] | None = None):
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
Expand Down
Loading