diff --git a/reflex/components/__init__.py b/reflex/components/__init__.py index 075700d69c..f9a580032e 100644 --- a/reflex/components/__init__.py +++ b/reflex/components/__init__.py @@ -100,6 +100,7 @@ editable_input = EditableInput.create editable_preview = EditablePreview.create editable_textarea = EditableTextarea.create +editor = Editor.create form = Form.create form_control = FormControl.create form_error_message = FormErrorMessage.create diff --git a/reflex/components/forms/__init__.py b/reflex/components/forms/__init__.py index 1982319920..d5ff4a164c 100644 --- a/reflex/components/forms/__init__.py +++ b/reflex/components/forms/__init__.py @@ -12,6 +12,7 @@ from .date_time_picker import DateTimePicker from .debounce import DebounceInput from .editable import Editable, EditableInput, EditablePreview, EditableTextarea +from .editor import Editor, EditorButtonList, EditorOptions from .email import Email from .form import Form, FormControl, FormErrorMessage, FormHelperText, FormLabel from .iconbutton import IconButton diff --git a/reflex/components/forms/editor.py b/reflex/components/forms/editor.py new file mode 100644 index 0000000000..9e3276f231 --- /dev/null +++ b/reflex/components/forms/editor.py @@ -0,0 +1,206 @@ +"""A Rich Text Editor based on SunEditor.""" +from __future__ import annotations + +import enum +from typing import Any, Dict, List, Optional, Union + +from reflex.base import Base +from reflex.components.component import Component, NoSSRComponent +from reflex.constants import EventTriggers +from reflex.utils.format import to_camel_case +from reflex.vars import ImportVar, Var + + +class EditorButtonList(list, enum.Enum): + """List enum that provides three predefined button lists.""" + + BASIC = [ + ["font", "fontSize"], + ["fontColor"], + ["horizontalRule"], + ["link", "image"], + ] + FORMATTING = [ + ["undo", "redo"], + ["bold", "underline", "italic", "strike", "subscript", "superscript"], + ["removeFormat"], + ["outdent", "indent"], + ["fullScreen", "showBlocks", "codeView"], + ["preview", "print"], + ] + COMPLEX = [ + ["undo", "redo"], + ["font", "fontSize", "formatBlock"], + ["bold", "underline", "italic", "strike", "subscript", "superscript"], + ["removeFormat"], + "/", + ["fontColor", "hiliteColor"], + ["outdent", "indent"], + ["align", "horizontalRule", "list", "table"], + ["link", "image", "video"], + ["fullScreen", "showBlocks", "codeView"], + ["preview", "print"], + ["save", "template"], + ] + + +class EditorOptions(Base): + """Some of the additional options to configure the Editor. + Complete list of options found here: + https://github.com/JiHong88/SunEditor/blob/master/README.md#options. + """ + + # Specifies default tag name of the editor. + # default: 'p' {String} + default_tag: Optional[str] = None + + # The mode of the editor ('classic', 'inline', 'balloon', 'balloon-always'). + # default: 'classic' {String} + mode: Optional[str] = None + + # If true, the editor is set to RTL(Right To Left) mode. + # default: false {Boolean} + rtl: Optional[bool] = None + + # List of buttons to use in the toolbar. + button_list: Optional[List[Union[List[str], str]]] + + +class Editor(NoSSRComponent): + """A Rich Text Editor component based on SunEditor. + Not every JS prop is listed here (some are not easily usable from python), + refer to the library docs for a complete list. + """ + + library = "suneditor-react" + + tag = "SunEditor" + + is_default = True + + lib_dependencies: List[str] = ["suneditor"] + + # Language of the editor. + # Alternatively to a string, a dict of your language can be passed to this prop. + # Please refer to the library docs for this. + # options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | + # "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" + # default : "en" + lang: Var[Union[str, dict]] + + # This is used to set the HTML form name of the editor. + # This means on HTML form submission, + # it will be submitted together with contents of the editor by the name provided. + name: Var[str] + + # Sets the default value of the editor. + # This is useful if you don't want the on_change method to be called on render. + # If you want the on_change method to be called on render please use the set_contents prop + default_value: Var[str] + + # Sets the width of the editor. + # px and percentage values are accepted, eg width="100%" or width="500px" + # default: 100% + width: Var[str] + + # Sets the height of the editor. + # px and percentage values are accepted, eg height="100%" or height="100px" + height: Var[str] + + # Sets the placeholder of the editor. + placeholder: Var[str] + + # Should the editor receive focus when initialized? + auto_focus: Var[bool] + + # Pass an EditorOptions instance to modify the behaviour of Editor even more. + set_options: Var[Dict] + + # Whether all SunEditor plugins should be loaded. + # default: True + set_all_plugins: Var[bool] + + # Set the content of the editor. + # Note: To set the initial contents of the editor + # without calling the on_change event, + # please use the default_value prop. + # set_contents is used to set the contents of the editor programmatically. + # You must be aware that, when the set_contents's prop changes, + # the on_change event is triggered. + set_contents: Var[str] + + # Append editor content + append_contents: Var[str] + + # Sets the default style of the editor's edit area + set_default_style: Var[str] + + # Disable the editor + # default: False + disable: Var[bool] + + # Hide the editor + # default: False + hide: Var[bool] + + # Hide the editor toolbar + # default: False + hide_toolbar: Var[bool] + + # Disable the editor toolbar + # default: False + disable_toolbar: Var[bool] + + def _get_imports(self): + imports = super()._get_imports() + imports[""] = { + ImportVar(tag="suneditor/dist/css/suneditor.min.css", install=False) + } + return imports + + def get_event_triggers(self) -> Dict[str, Any]: + """Get the event triggers that pass the component's value to the handler. + + Returns: + A dict mapping the event trigger to the var that is passed to the handler. + """ + return { + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda content: [content], + "on_input": lambda _e: [_e], + EventTriggers.ON_BLUR: lambda _e, content: [content], + "on_load": lambda reload: [reload], + "on_resize_editor": lambda height, prev_height: [height, prev_height], + "on_copy": lambda _e, clipboard_data: [clipboard_data], + "on_cut": lambda _e, clipboard_data: [clipboard_data], + "on_paste": lambda _e, clean_data, max_char_count: [ + clean_data, + max_char_count, + ], + "toggle_code_view": lambda is_code_view: [is_code_view], + "toggle_full_screen": lambda is_full_screen: [is_full_screen], + } + + @classmethod + def create(cls, set_options: Optional[EditorOptions] = None, **props) -> Component: + """Create an instance of Editor. No children allowed. + + Args: + set_options(Optional[EditorOptions]): Configuration object to further configure the instance. + **props: Any properties to be passed to the Editor + + Returns: + An Editor instance. + + Raises: + ValueError: If set_options is a state Var. + """ + if set_options is not None: + if isinstance(set_options, Var): + raise ValueError("EditorOptions cannot be a state Var") + props["set_options"] = { + to_camel_case(k): v + for k, v in set_options.dict().items() + if v is not None + } + return super().create(*[], **props) diff --git a/reflex/components/forms/editor.pyi b/reflex/components/forms/editor.pyi new file mode 100644 index 0000000000..240ca20b85 --- /dev/null +++ b/reflex/components/forms/editor.pyi @@ -0,0 +1,33 @@ +"""Stub file for editor.py""" +# ------------------- DO NOT EDIT ---------------------- +# This file was generated by `scripts/pyi_generator.py`! +# ------------------------------------------------------ + +import enum +from typing import Any, Dict, List, Optional, Union, overload +from reflex.base import Base +from reflex.components.component import Component +from reflex.components.component import NoSSRComponent +from reflex.vars import Var, BaseVar, ComputedVar +from reflex.event import EventHandler, EventChain, EventSpec + +class EditorButtonList(list, enum.Enum): ... +class EditorOptions(Base): ... + +class Editor(NoSSRComponent): + @overload + @classmethod + def create(cls, *children, lib_dependencies: Optional[List[str]] = None, lang: Optional[Union[Var[Union[str, dict]], Union[str, dict]]] = None, name: Optional[Union[Var[str], str]] = None, default_value: Optional[Union[Var[str], str]] = None, width: Optional[Union[Var[str], str]] = None, height: Optional[Union[Var[str], str]] = None, placeholder: Optional[Union[Var[str], str]] = None, auto_focus: Optional[Union[Var[bool], bool]] = None, set_options: Optional[Union[Var[Dict], Dict]] = None, set_all_plugins: Optional[Union[Var[bool], bool]] = None, set_contents: Optional[Union[Var[str], str]] = None, append_contents: Optional[Union[Var[str], str]] = None, set_default_style: Optional[Union[Var[str], str]] = None, disable: Optional[Union[Var[bool], bool]] = None, hide: Optional[Union[Var[bool], bool]] = None, hide_toolbar: Optional[Union[Var[bool], bool]] = None, disable_toolbar: Optional[Union[Var[bool], bool]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_change: 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_copy: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_cut: 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_input: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_load: 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_resize_editor: 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, toggle_code_view: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, toggle_full_screen: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Editor": # type: ignore + """Create an instance of Editor. No children allowed. + + Args: + set_options(Optional[EditorOptions]): Configuration object to further configure the instance. + **props: Any properties to be passed to the Editor + + Returns: + An Editor instance. + + Raises: + ValueError: If set_options is a state Var. + """ + ... diff --git a/reflex/utils/types.py b/reflex/utils/types.py index a0c9a1f8c0..136839ee46 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -100,6 +100,9 @@ def _issubclass(cls: GenericType, cls_check: GenericType) -> bool: Returns: Whether the class is a subclass of the other class. + + Raises: + TypeError: If the base class is not valid for issubclass. """ # Special check for Any. if cls_check == Any: @@ -116,7 +119,12 @@ def _issubclass(cls: GenericType, cls_check: GenericType) -> bool: return False # Check if the types match. - return cls_check_base == Any or issubclass(cls_base, cls_check_base) + try: + return cls_check_base == Any or issubclass(cls_base, cls_check_base) + except TypeError as te: + # These errors typically arise from bad annotations and are hard to + # debug without knowing the type that we tried to compare. + raise TypeError(f"Invalid type for issubclass: {cls_base}") from te def _isinstance(obj: Any, cls: GenericType) -> bool: