Coverage for src / lexigram / admin / schema / text_area.py: 36%
72 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1from __future__ import annotations
3from dataclasses import dataclass
4from typing import Any
6from lexigram.admin.schema.text import TextField
7from lexigram.ui import Element, MarkdownEditor, RichEditor, TextArea, raw
10@dataclass(frozen=True, kw_only=True)
11class TextAreaField(TextField):
12 """A multi-line text input field.
14 Renders a ``<textarea>`` form control. Column display truncates
15 long content at 200 characters.
16 """
18 rows: int = 5
20 def render_form(
21 self, value: str | None, *, errors: list[str] | None = None
22 ) -> Element:
23 kwargs: dict[str, Any] = {
24 "name": self.name,
25 "value": value if value is not None else "",
26 "rows": self.rows,
27 }
28 if self.placeholder is not None:
29 kwargs["placeholder"] = self.placeholder
30 if self.label is not None:
31 kwargs["label"] = self.label
32 if errors:
33 kwargs["error"] = errors[0]
34 return TextArea(**kwargs).render()
36 def render_column(self, record: Any, value: str | None) -> Element:
37 if value is None:
38 return Element("span", "\u2014", class_="text-muted")
39 if len(value) > 200:
40 return Element("span", value[:200] + "...")
41 return Element("span", value)
44@dataclass(frozen=True, kw_only=True)
45class MarkdownField(TextAreaField):
46 """A markdown editor field with optional preview toggle.
48 Renders a ``MarkdownEditor`` form control with Alpine.js-powered
49 preview mode switching. Column display strips markdown syntax
50 for plain-text preview.
51 """
53 preview: bool = True
54 min_height: int = 300
56 def render_form(
57 self, value: str | None, *, errors: list[str] | None = None
58 ) -> Element:
59 kwargs: dict[str, Any] = {
60 "name": self.name,
61 "value": value if value is not None else "",
62 "rows": self.rows,
63 "preview": self.preview,
64 "min_height": self.min_height,
65 }
66 if self.placeholder is not None:
67 kwargs["placeholder"] = self.placeholder
68 if self.label is not None:
69 kwargs["label"] = self.label
70 if errors:
71 kwargs["error"] = errors[0]
72 return MarkdownEditor(**kwargs).render()
74 def render_column(self, record: Any, value: str | None) -> Element:
75 if value is None:
76 return Element("span", "\u2014", class_="text-muted")
77 text = value
78 text = text.replace("**", "").replace("__", "")
79 text = text.replace("*", "").replace("_", "")
80 text = text.replace("```", "").replace("`", "")
81 text = text.replace("#", "")
82 text = text.replace("> ", "")
83 text = text.replace("[", "").replace("]", "")
84 text = text.replace("(", "").replace(")", "")
85 return Element("span", text.strip())
88@dataclass(frozen=True, kw_only=True)
89class RichTextField(TextAreaField):
90 """A WYSIWYG rich-text field.
92 Renders a ``RichEditor`` form control backed by Trix.js.
93 Column display renders the HTML value as trusted (unescaped)
94 content.
96 Use ``render_assets()`` to include the required Trix.js CSS and
97 JS when this field appears on a page.
98 """
100 toolbar: str | None = None
101 min_height: int = 300
103 _trix_css_url: str = "https://unpkg.com/trix@2.0.8/dist/trix.css"
104 _trix_js_url: str = "https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"
106 @classmethod
107 def render_assets(cls) -> Element:
108 """Render the Trix.js CSS and ``<script>`` tags.
110 Include the result in ``<head>`` once per page to enable
111 RichEditor instances.
112 """
113 return Element(
114 "div",
115 Element(
116 "link",
117 rel="stylesheet",
118 href=cls._trix_css_url,
119 ),
120 Element(
121 "script",
122 src=cls._trix_js_url,
123 defer="",
124 ),
125 )
127 def render_form(
128 self, value: str | None, *, errors: list[str] | None = None
129 ) -> Element:
130 kwargs: dict[str, Any] = {
131 "name": self.name,
132 "value": value if value is not None else "",
133 "min_height": self.min_height,
134 }
135 if self.placeholder is not None:
136 kwargs["placeholder"] = self.placeholder
137 if self.label is not None:
138 kwargs["label"] = self.label
139 if self.toolbar is not None:
140 kwargs["toolbar"] = self.toolbar
141 if errors:
142 kwargs["error"] = errors[0]
143 return RichEditor(**kwargs).render()
145 def render_column(self, record: Any, value: str | None) -> Element:
146 if value is None:
147 return Element("span", "\u2014", class_="text-muted")
148 return Element("div", raw(value))