flask-construct-ui 0.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flask_construct_ui/__init__.py +82 -0
- flask_construct_ui/auth.py +290 -0
- flask_construct_ui/base.py +1259 -0
- flask_construct_ui/components.py +812 -0
- flask_construct_ui/crud.py +1641 -0
- flask_construct_ui/csrf.py +39 -0
- flask_construct_ui/data.py +257 -0
- flask_construct_ui/data_sqlalchemy.py +304 -0
- flask_construct_ui/forms.py +130 -0
- flask_construct_ui/headless.py +110 -0
- flask_construct_ui/icons.py +158 -0
- flask_construct_ui/l10n/en/main.flt +82 -0
- flask_construct_ui/l10n/sv/main.flt +82 -0
- flask_construct_ui/language.py +100 -0
- flask_construct_ui/navbar.py +54 -0
- flask_construct_ui/navigation.py +58 -0
- flask_construct_ui/orgmode.py +338 -0
- flask_construct_ui/pagination.py +39 -0
- flask_construct_ui/py.typed +0 -0
- flask_construct_ui/sanitize.py +25 -0
- flask_construct_ui/schema.py +507 -0
- flask_construct_ui/static/css/prosemirror-bundle.min.css +1 -0
- flask_construct_ui/static/css/unpoly.min.css +6 -0
- flask_construct_ui/static/js/construct-core.min.js +1 -0
- flask_construct_ui/static/js/prosemirror-bundle.min.js +34 -0
- flask_construct_ui/static/js/unpoly.min.js +1 -0
- flask_construct_ui/static/themes/whitelabel/css/whitelabel.css +720 -0
- flask_construct_ui/static/themes/whitelabel/designs/adminlte.css +94 -0
- flask_construct_ui/static/themes/whitelabel/designs/sandstone.css +64 -0
- flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/LICENSE.txt +21 -0
- flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/bootstrap-icons.min.css +5 -0
- flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/LICENSE.txt +202 -0
- flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
- flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/material-symbols.css +26 -0
- flask_construct_ui/templates/structure/base.html +5 -0
- flask_construct_ui/templates/structure/flashes.html +7 -0
- flask_construct_ui/templates/structure/public.html +18 -0
- flask_construct_ui/templates/themes/whitelabel/components/_form_field.html +69 -0
- flask_construct_ui/templates/themes/whitelabel/components/button.html +16 -0
- flask_construct_ui/templates/themes/whitelabel/components/calendar.html +30 -0
- flask_construct_ui/templates/themes/whitelabel/components/calendarmini.html +30 -0
- flask_construct_ui/templates/themes/whitelabel/components/card.html +25 -0
- flask_construct_ui/templates/themes/whitelabel/components/chart.html +5 -0
- flask_construct_ui/templates/themes/whitelabel/components/form.html +61 -0
- flask_construct_ui/templates/themes/whitelabel/components/grid_layout.html +5 -0
- flask_construct_ui/templates/themes/whitelabel/components/horizontal_layout.html +5 -0
- flask_construct_ui/templates/themes/whitelabel/components/live.html +40 -0
- flask_construct_ui/templates/themes/whitelabel/components/main.html +4 -0
- flask_construct_ui/templates/themes/whitelabel/components/markup.html +1 -0
- flask_construct_ui/templates/themes/whitelabel/components/rich_text.html +19 -0
- flask_construct_ui/templates/themes/whitelabel/components/table.html +30 -0
- flask_construct_ui/templates/themes/whitelabel/components/tabs.html +30 -0
- flask_construct_ui/templates/themes/whitelabel/components/text.html +26 -0
- flask_construct_ui/templates/themes/whitelabel/components/vertical_layout.html +5 -0
- flask_construct_ui/templates/themes/whitelabel/crud/_macros.html +962 -0
- flask_construct_ui/templates/themes/whitelabel/crud/_many_related_options.html +26 -0
- flask_construct_ui/templates/themes/whitelabel/crud/_related_child_form.html +98 -0
- flask_construct_ui/templates/themes/whitelabel/crud/_related_child_list.html +136 -0
- flask_construct_ui/templates/themes/whitelabel/crud/_related_options.html +15 -0
- flask_construct_ui/templates/themes/whitelabel/crud/create.html +45 -0
- flask_construct_ui/templates/themes/whitelabel/crud/edit.html +125 -0
- flask_construct_ui/templates/themes/whitelabel/crud/listindex.html +238 -0
- flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_child_view.html +33 -0
- flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_form.html +125 -0
- flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_list.html +140 -0
- flask_construct_ui/templates/themes/whitelabel/crud/tab_view.html +38 -0
- flask_construct_ui/templates/themes/whitelabel/errors/forbidden.html +16 -0
- flask_construct_ui/templates/themes/whitelabel/navbar/notifications.html +26 -0
- flask_construct_ui/templates/themes/whitelabel/navbar/search_results.html +31 -0
- flask_construct_ui/templates/themes/whitelabel/style/_flashes.html +16 -0
- flask_construct_ui/templates/themes/whitelabel/style/base_full.html +181 -0
- flask_construct_ui/templates/themes/whitelabel/style/base_partial.html +16 -0
- flask_construct_ui/validation.py +422 -0
- flask_construct_ui-0.9.5.dist-info/METADATA +60 -0
- flask_construct_ui-0.9.5.dist-info/RECORD +80 -0
- flask_construct_ui-0.9.5.dist-info/WHEEL +5 -0
- flask_construct_ui-0.9.5.dist-info/licenses/LICENSE +165 -0
- flask_construct_ui-0.9.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import datetime
|
|
3
|
+
from .validation import ValidationError, parse_rule
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SchemaField:
|
|
7
|
+
def __init__(self, field_type, name, **kwargs):
|
|
8
|
+
self.field_type = field_type
|
|
9
|
+
self.name = name
|
|
10
|
+
self.readonly = kwargs.get("readonly", False)
|
|
11
|
+
self.required = kwargs.get("required", False)
|
|
12
|
+
self.nullable = kwargs.get("nullable", not self.required)
|
|
13
|
+
self.skipfilter = kwargs.get("skipfilter", False)
|
|
14
|
+
self.visible_list = kwargs.get("visible_list", True)
|
|
15
|
+
self.visible_form = kwargs.get("visible_form", True)
|
|
16
|
+
self.secret = kwargs.get("secret", False)
|
|
17
|
+
# None = follow the app-wide CONSTRUCT_ENFORCE_MATCHES default;
|
|
18
|
+
# True/False = explicit per-field override for server-side `matches`
|
|
19
|
+
# enforcement. Structural checks (length, type, required) are always
|
|
20
|
+
# enforced server-side regardless of this flag.
|
|
21
|
+
self.enforce = kwargs.get("enforce", None)
|
|
22
|
+
self.default = kwargs.get("default", None)
|
|
23
|
+
self.kwargs = kwargs
|
|
24
|
+
matches = kwargs.get("matches", None)
|
|
25
|
+
self._matches_fn = None
|
|
26
|
+
self._validation_node = None
|
|
27
|
+
if callable(matches):
|
|
28
|
+
self._matches_fn = matches
|
|
29
|
+
elif matches:
|
|
30
|
+
self.matches = matches
|
|
31
|
+
self._validation_node = parse_rule(matches)
|
|
32
|
+
|
|
33
|
+
def form_input_type(self) -> str:
|
|
34
|
+
return "text"
|
|
35
|
+
|
|
36
|
+
def form_extra_attrs(self) -> str:
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
def format_value(self, field, row):
|
|
40
|
+
return getattr(row, field)
|
|
41
|
+
|
|
42
|
+
def parse_value(self, value):
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
def validate_value(self, val, row=None):
|
|
46
|
+
if val is None or val == "":
|
|
47
|
+
if self.required:
|
|
48
|
+
return ValidationError(
|
|
49
|
+
"validation-field-required", {}, "This field is required"
|
|
50
|
+
)
|
|
51
|
+
return None
|
|
52
|
+
return self._validate_matches(val, row)
|
|
53
|
+
|
|
54
|
+
def _validate_matches(self, val, row=None):
|
|
55
|
+
if self._matches_fn is not None:
|
|
56
|
+
rule_str = self._matches_fn(row)
|
|
57
|
+
if rule_str:
|
|
58
|
+
node = parse_rule(rule_str)
|
|
59
|
+
ok, err = node.validate_detail(val)
|
|
60
|
+
if not ok:
|
|
61
|
+
return err
|
|
62
|
+
elif self._validation_node:
|
|
63
|
+
ok, err = self._validation_node.validate_detail(val)
|
|
64
|
+
if not ok:
|
|
65
|
+
return err
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def validate_structural(self, val):
|
|
69
|
+
"""Type/size invariants that are always enforced server-side."""
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def validate_submission(self, val, row=None, enforce_default=False):
|
|
73
|
+
"""Validate a submitted form value before it is written.
|
|
74
|
+
|
|
75
|
+
Structural checks and ``required`` always apply; ``matches`` rules
|
|
76
|
+
apply when the field's ``enforce`` flag (or the app default) is on.
|
|
77
|
+
"""
|
|
78
|
+
if (
|
|
79
|
+
val is None
|
|
80
|
+
or val == ""
|
|
81
|
+
or (isinstance(val, (set, list, tuple)) and not val)
|
|
82
|
+
):
|
|
83
|
+
if self.required:
|
|
84
|
+
return ValidationError(
|
|
85
|
+
"validation-field-required", {}, "This field is required"
|
|
86
|
+
)
|
|
87
|
+
return None
|
|
88
|
+
err = self.validate_structural(val)
|
|
89
|
+
if err:
|
|
90
|
+
return err
|
|
91
|
+
enforce = self.enforce if self.enforce is not None else enforce_default
|
|
92
|
+
if enforce:
|
|
93
|
+
return self._validate_matches(val, row)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PK(SchemaField):
|
|
98
|
+
def __init__(self, name, **kwargs):
|
|
99
|
+
super().__init__("pk", name, **kwargs)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class String(SchemaField):
|
|
103
|
+
def __init__(self, name, length, **kwargs):
|
|
104
|
+
super().__init__("string", name, **kwargs)
|
|
105
|
+
self.length = length
|
|
106
|
+
|
|
107
|
+
def form_input_type(self):
|
|
108
|
+
return "text"
|
|
109
|
+
|
|
110
|
+
def validate_structural(self, val):
|
|
111
|
+
if self.length and len(str(val)) > self.length:
|
|
112
|
+
return ValidationError(
|
|
113
|
+
"validation-string-len-at-most",
|
|
114
|
+
{"limit": self.length},
|
|
115
|
+
f"Length must be at most {self.length} characters",
|
|
116
|
+
)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Text(SchemaField):
|
|
121
|
+
def __init__(self, name, **kwargs):
|
|
122
|
+
super().__init__("text", name, **kwargs)
|
|
123
|
+
self.markdown = kwargs.get("markdown", False)
|
|
124
|
+
self.orgmode = kwargs.get("orgmode", False)
|
|
125
|
+
|
|
126
|
+
def form_input_type(self):
|
|
127
|
+
if self.markdown:
|
|
128
|
+
return "richtext"
|
|
129
|
+
return "textarea"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Integer(SchemaField):
|
|
133
|
+
def __init__(self, name, **kwargs):
|
|
134
|
+
super().__init__("integer", name, **kwargs)
|
|
135
|
+
|
|
136
|
+
def form_input_type(self):
|
|
137
|
+
return "number"
|
|
138
|
+
|
|
139
|
+
def parse_value(self, value):
|
|
140
|
+
if value == "":
|
|
141
|
+
return None
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
def validate_structural(self, val):
|
|
145
|
+
try:
|
|
146
|
+
int(str(val))
|
|
147
|
+
except (ValueError, TypeError):
|
|
148
|
+
return ValidationError(
|
|
149
|
+
"validation-must-be-integer", {}, "Must be an integer"
|
|
150
|
+
)
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Decimal(SchemaField):
|
|
155
|
+
def __init__(self, name, **kwargs):
|
|
156
|
+
super().__init__("decimal", name, **kwargs)
|
|
157
|
+
|
|
158
|
+
def form_input_type(self):
|
|
159
|
+
return "number"
|
|
160
|
+
|
|
161
|
+
def parse_value(self, value):
|
|
162
|
+
if value == "":
|
|
163
|
+
return None
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
def form_extra_attrs(self):
|
|
167
|
+
return 'step="any"'
|
|
168
|
+
|
|
169
|
+
def validate_structural(self, val):
|
|
170
|
+
try:
|
|
171
|
+
float(val)
|
|
172
|
+
except (ValueError, TypeError):
|
|
173
|
+
return ValidationError("validation-must-be-number", {}, "Must be a number")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Boolean(SchemaField):
|
|
178
|
+
def __init__(self, name, **kwargs):
|
|
179
|
+
super().__init__("bool", name, **kwargs)
|
|
180
|
+
|
|
181
|
+
def form_input_type(self):
|
|
182
|
+
return "checkbox"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DateTime(SchemaField):
|
|
186
|
+
def __init__(self, name, **kwargs):
|
|
187
|
+
super().__init__("datetime", name, **kwargs)
|
|
188
|
+
|
|
189
|
+
def form_input_type(self):
|
|
190
|
+
return "datetime-local"
|
|
191
|
+
|
|
192
|
+
def parse_value(self, value):
|
|
193
|
+
if value == "":
|
|
194
|
+
return None
|
|
195
|
+
return value
|
|
196
|
+
|
|
197
|
+
def validate_structural(self, val):
|
|
198
|
+
|
|
199
|
+
if isinstance(val, (datetime.date, datetime.datetime)):
|
|
200
|
+
return None
|
|
201
|
+
try:
|
|
202
|
+
datetime.datetime.fromisoformat(str(val))
|
|
203
|
+
except (ValueError, TypeError):
|
|
204
|
+
return ValidationError(
|
|
205
|
+
"validation-must-be-datetime", {}, "Must be a valid date/time"
|
|
206
|
+
)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Related(SchemaField):
|
|
211
|
+
def __init__(self, name, relcls, relation, **kwargs):
|
|
212
|
+
super().__init__("related", name, **kwargs)
|
|
213
|
+
self.related_class = relcls
|
|
214
|
+
self.related_field = relation
|
|
215
|
+
|
|
216
|
+
def form_input_type(self):
|
|
217
|
+
return "select"
|
|
218
|
+
|
|
219
|
+
def format_value(self, field, row):
|
|
220
|
+
curr = row
|
|
221
|
+
for item in self.related_field.split("."):
|
|
222
|
+
if curr is None:
|
|
223
|
+
return None
|
|
224
|
+
curr = getattr(curr, item, None)
|
|
225
|
+
return curr
|
|
226
|
+
|
|
227
|
+
def form_value(self, row):
|
|
228
|
+
"""The related record itself, for form rendering.
|
|
229
|
+
|
|
230
|
+
``format_value`` resolves ``related_field`` as a (possibly dotted)
|
|
231
|
+
display path for list cells — e.g. ``"category.name"`` yields a
|
|
232
|
+
string. Form templates instead need the related record to compare
|
|
233
|
+
primary keys when preselecting the current option, so this always
|
|
234
|
+
resolves just the relationship attribute (the first path segment).
|
|
235
|
+
A raw primary key on the row (a form re-rendering submitted values
|
|
236
|
+
after a validation failure) is read back into the record.
|
|
237
|
+
"""
|
|
238
|
+
value = getattr(row, self.related_field.split(".")[0], None)
|
|
239
|
+
if value is None or value == "":
|
|
240
|
+
return None
|
|
241
|
+
model = getattr(self.related_class, "model", None)
|
|
242
|
+
if model is not None and isinstance(value, model):
|
|
243
|
+
return value
|
|
244
|
+
if isinstance(value, (str, int)):
|
|
245
|
+
return self.related_class().read(value)
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _RecordMap:
|
|
250
|
+
"""Mapping over a record's attributes for ``str.format_map`` — missing
|
|
251
|
+
attributes render as empty strings instead of raising."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, record):
|
|
254
|
+
self._record = record
|
|
255
|
+
|
|
256
|
+
def __getitem__(self, key):
|
|
257
|
+
value = getattr(self._record, key, None)
|
|
258
|
+
return "" if value is None else value
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ManyRelated(SchemaField):
|
|
262
|
+
# Sub-field types allowed in the inline create panel (create="form").
|
|
263
|
+
# Relational types would need recursive pickers and richtext needs a
|
|
264
|
+
# live editor instance — neither works inside a cloned <template>.
|
|
265
|
+
INLINE_FORM_TYPES = (
|
|
266
|
+
"string",
|
|
267
|
+
"text",
|
|
268
|
+
"integer",
|
|
269
|
+
"decimal",
|
|
270
|
+
"datetime",
|
|
271
|
+
"bool",
|
|
272
|
+
"set",
|
|
273
|
+
"enum",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def __init__(self, name, relcls, relation, **kwargs):
|
|
277
|
+
super().__init__("manyrelated", name, **kwargs)
|
|
278
|
+
self.related_class = relcls
|
|
279
|
+
self.related_field = relation
|
|
280
|
+
self.search_fields = kwargs.get("search_fields", None)
|
|
281
|
+
self.display = kwargs.get("display", None)
|
|
282
|
+
self.create = kwargs.get("create", None)
|
|
283
|
+
if self.create not in (None, "quick", "form"):
|
|
284
|
+
raise ValueError(
|
|
285
|
+
f"ManyRelated create= must be None, 'quick' or 'form',"
|
|
286
|
+
f" got {self.create!r}"
|
|
287
|
+
)
|
|
288
|
+
self.quick_field = kwargs.get("quick_field", None)
|
|
289
|
+
self._quick_field_resolved = None
|
|
290
|
+
|
|
291
|
+
def form_input_type(self):
|
|
292
|
+
return "manyrelated"
|
|
293
|
+
|
|
294
|
+
def render_display(self, record):
|
|
295
|
+
"""Render a related record for chips and search results.
|
|
296
|
+
|
|
297
|
+
``display`` mirrors the ``matches`` contract: a format string
|
|
298
|
+
interpolating related-record fields (``"{name} ({city})"``) or a
|
|
299
|
+
``callable(record) -> str``. Defaults to ``str(record)``.
|
|
300
|
+
"""
|
|
301
|
+
if record is None:
|
|
302
|
+
return ""
|
|
303
|
+
if callable(self.display):
|
|
304
|
+
return str(self.display(record))
|
|
305
|
+
if self.display:
|
|
306
|
+
return self.display.format_map(_RecordMap(record))
|
|
307
|
+
return str(record)
|
|
308
|
+
|
|
309
|
+
def resolve_quick_field(self):
|
|
310
|
+
"""The related field that receives a quick-created value.
|
|
311
|
+
|
|
312
|
+
Resolved lazily — ``related_class._schema`` only exists once
|
|
313
|
+
``@dataschema`` has run on the related class.
|
|
314
|
+
"""
|
|
315
|
+
if self._quick_field_resolved is not None:
|
|
316
|
+
return self._quick_field_resolved
|
|
317
|
+
sub_schema = self.related_class._schema
|
|
318
|
+
if self.quick_field is not None:
|
|
319
|
+
ftype = sub_schema.get(self.quick_field)
|
|
320
|
+
if ftype is None or ftype.field_type not in ("string", "text"):
|
|
321
|
+
raise ValueError(
|
|
322
|
+
f"ManyRelated {self.name}: quick_field"
|
|
323
|
+
f" {self.quick_field!r} must be a string or text field"
|
|
324
|
+
f" of the related schema"
|
|
325
|
+
)
|
|
326
|
+
self._quick_field_resolved = self.quick_field
|
|
327
|
+
return self.quick_field
|
|
328
|
+
candidates = [
|
|
329
|
+
sub_name
|
|
330
|
+
for sub_name, ftype in sub_schema.items()
|
|
331
|
+
if ftype.field_type in ("string", "text")
|
|
332
|
+
and ftype.visible_form
|
|
333
|
+
and not ftype.readonly
|
|
334
|
+
and not ftype.secret
|
|
335
|
+
]
|
|
336
|
+
required = [c for c in candidates if sub_schema[c].required]
|
|
337
|
+
if len(required) == 1:
|
|
338
|
+
self._quick_field_resolved = required[0]
|
|
339
|
+
elif len(candidates) == 1:
|
|
340
|
+
self._quick_field_resolved = candidates[0]
|
|
341
|
+
else:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"ManyRelated {self.name}: quick_field is ambiguous;"
|
|
344
|
+
f" pass quick_field= explicitly"
|
|
345
|
+
)
|
|
346
|
+
return self._quick_field_resolved
|
|
347
|
+
|
|
348
|
+
def inline_form_fields(self):
|
|
349
|
+
"""The related-schema fields editable in the inline create panel."""
|
|
350
|
+
return [
|
|
351
|
+
(sub_name, ftype)
|
|
352
|
+
for sub_name, ftype in self.related_class._schema.items()
|
|
353
|
+
if ftype.field_type in self.INLINE_FORM_TYPES
|
|
354
|
+
and ftype.visible_form
|
|
355
|
+
and not ftype.readonly
|
|
356
|
+
and not ftype.secret
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
def format_value(self, field, row):
|
|
360
|
+
return list(getattr(row, self.related_field, None) or [])
|
|
361
|
+
|
|
362
|
+
def form_value(self, row):
|
|
363
|
+
"""The related records, for form rendering (see Related.form_value).
|
|
364
|
+
|
|
365
|
+
Raw primary keys on the row (a form re-rendering submitted values
|
|
366
|
+
after a validation failure) are read back into records.
|
|
367
|
+
"""
|
|
368
|
+
model = getattr(self.related_class, "model", None)
|
|
369
|
+
resolved = []
|
|
370
|
+
for item in getattr(row, self.related_field, None) or []:
|
|
371
|
+
if model is not None and isinstance(item, model):
|
|
372
|
+
resolved.append(item)
|
|
373
|
+
elif isinstance(item, (str, int)):
|
|
374
|
+
rec = self.related_class().read(item)
|
|
375
|
+
if rec is not None:
|
|
376
|
+
resolved.append(rec)
|
|
377
|
+
else:
|
|
378
|
+
resolved.append(item)
|
|
379
|
+
return resolved
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class Set(SchemaField):
|
|
383
|
+
def __init__(self, name, sets, **kwargs):
|
|
384
|
+
super().__init__("set", name, **kwargs)
|
|
385
|
+
self.sets = sets
|
|
386
|
+
|
|
387
|
+
def parse_value(self, value):
|
|
388
|
+
if isinstance(value, (list, set)):
|
|
389
|
+
return ",".join(sorted(list(value)))
|
|
390
|
+
return value
|
|
391
|
+
|
|
392
|
+
def form_input_type(self):
|
|
393
|
+
return "multiselect"
|
|
394
|
+
|
|
395
|
+
def validate_structural(self, val):
|
|
396
|
+
values = val if isinstance(val, (set, list, tuple)) else str(val).split(",")
|
|
397
|
+
invalid = [v for v in values if v and v not in self.sets]
|
|
398
|
+
if invalid:
|
|
399
|
+
options = ", ".join(f"'{k}'" for k in self.sets)
|
|
400
|
+
return ValidationError(
|
|
401
|
+
"validation-must-be-one-of",
|
|
402
|
+
{"options": options},
|
|
403
|
+
f"Must be one of: {options}",
|
|
404
|
+
)
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class List(SchemaField):
|
|
409
|
+
def __init__(self, name, relcls, relation, columns=None, **kwargs):
|
|
410
|
+
super().__init__("list", name, **kwargs)
|
|
411
|
+
self.list_class = relcls
|
|
412
|
+
self.list_field = relation
|
|
413
|
+
self.columns = columns
|
|
414
|
+
self.layout = kwargs.get("layout", "table")
|
|
415
|
+
self.can_add = kwargs.get("can_add", True)
|
|
416
|
+
self.can_delete = kwargs.get("can_delete", True)
|
|
417
|
+
self.template = kwargs.get("template", None)
|
|
418
|
+
|
|
419
|
+
def form_input_type(self):
|
|
420
|
+
return "list"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class Enum(String):
|
|
424
|
+
class ValueWrapper(dict):
|
|
425
|
+
def __init__(self, v, display=None):
|
|
426
|
+
super().__init__(id=v)
|
|
427
|
+
self.v = v
|
|
428
|
+
self.id = v
|
|
429
|
+
self.display = display if display is not None else v
|
|
430
|
+
|
|
431
|
+
def __str__(self):
|
|
432
|
+
from flask import has_app_context, current_app
|
|
433
|
+
|
|
434
|
+
if has_app_context() and hasattr(current_app, "l10n"):
|
|
435
|
+
translated = current_app.l10n.t(self.v)
|
|
436
|
+
if translated != self.v:
|
|
437
|
+
return str(translated)
|
|
438
|
+
return str(self.display)
|
|
439
|
+
|
|
440
|
+
def __init__(self, name, enum_class, **kwargs):
|
|
441
|
+
super().__init__(name, 50, **kwargs)
|
|
442
|
+
self.enum_class = enum_class
|
|
443
|
+
self.field_type = "enum"
|
|
444
|
+
self._primary_key = "id"
|
|
445
|
+
|
|
446
|
+
def related_class(self):
|
|
447
|
+
return self
|
|
448
|
+
|
|
449
|
+
related_class._primary_key = "id"
|
|
450
|
+
|
|
451
|
+
def search(self, q=None):
|
|
452
|
+
return [self.ValueWrapper(t.name, t.value) for t in self.enum_class]
|
|
453
|
+
|
|
454
|
+
def validate_structural(self, val):
|
|
455
|
+
if str(val) not in {member.name for member in self.enum_class}:
|
|
456
|
+
options = ", ".join(f"'{member.name}'" for member in self.enum_class)
|
|
457
|
+
return ValidationError(
|
|
458
|
+
"validation-must-be-one-of",
|
|
459
|
+
{"options": options},
|
|
460
|
+
f"Must be one of: {options}",
|
|
461
|
+
)
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
def parse_value(self, value):
|
|
465
|
+
if value == "":
|
|
466
|
+
return None
|
|
467
|
+
return value
|
|
468
|
+
|
|
469
|
+
def format_value(self, field, row):
|
|
470
|
+
val = getattr(row, field, None)
|
|
471
|
+
if val is None:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
if isinstance(val, enum.Enum):
|
|
475
|
+
return self.ValueWrapper(val.name, val.value)
|
|
476
|
+
return self.ValueWrapper(str(val), str(val))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class Schema:
|
|
480
|
+
def __init__(self):
|
|
481
|
+
self.PK = PK
|
|
482
|
+
self.String = String
|
|
483
|
+
self.Text = Text
|
|
484
|
+
self.Integer = Integer
|
|
485
|
+
self.Decimal = Decimal
|
|
486
|
+
self.Boolean = Boolean
|
|
487
|
+
self.DateTime = DateTime
|
|
488
|
+
self.Related = Related
|
|
489
|
+
self.ManyRelated = ManyRelated
|
|
490
|
+
self.List = List
|
|
491
|
+
self.Set = Set
|
|
492
|
+
self.Enum = Enum
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
schema = Schema()
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def dataschema(cls):
|
|
499
|
+
cls._schema = {}
|
|
500
|
+
cls._primary_key = None
|
|
501
|
+
for name, attribute in list(cls.__dict__.items()):
|
|
502
|
+
if isinstance(attribute, SchemaField):
|
|
503
|
+
cls._schema[name] = attribute
|
|
504
|
+
delattr(cls, name)
|
|
505
|
+
if isinstance(attribute, PK):
|
|
506
|
+
cls._primary_key = name
|
|
507
|
+
return cls
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.ProseMirror{position:relative}.ProseMirror{word-wrap:break-word;white-space:pre-wrap;white-space:break-spaces;-webkit-font-variant-ligatures:none;font-variant-ligatures:none;font-feature-settings:"liga" 0}.ProseMirror pre{white-space:pre-wrap}.ProseMirror li{position:relative}.ProseMirror-hideselection *::selection{background:transparent}.ProseMirror-hideselection *::-moz-selection{background:transparent}.ProseMirror-hideselection{caret-color:transparent}.ProseMirror [draggable][contenteditable=false]{user-select:text}.ProseMirror-selectednode{outline:2px solid #8cf}li.ProseMirror-selectednode{outline:none}li.ProseMirror-selectednode:after{content:"";position:absolute;inset:-2px -2px -2px -32px;border:2px solid #8cf;pointer-events:none}img.ProseMirror-separator{display:inline!important;border:none!important;margin:0!important}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[hidden][hidden]{display:none !important}
|
|
2
|
+
up-wrapper{display:inline-block}
|
|
3
|
+
up-bounds{position:absolute}.up-focus-hidden:focus-visible{outline-color:rgba(0,0,0,0) !important;outline-style:none !important}body.up-scrollbar-away{padding-right:calc(var(--up-scrollbar-width) + var(--up-original-padding-right)) !important}body.up-scrollbar-away,html:has(>body.up-scrollbar-away){overflow-y:hidden !important;overflow-y:clip !important}body.up-scrollbar-away .up-scrollbar-away{right:calc(var(--up-scrollbar-width) + var(--up-original-right)) !important}
|
|
4
|
+
.up-request-loader{display:none}up-progress-bar{position:fixed;top:0;left:0;z-index:999999999;height:3px;background-color:#007bff}
|
|
5
|
+
up-focus-trap{position:fixed;top:0;left:0;width:0;height:0}up-cover-viewport,up-drawer-viewport,up-modal-viewport,up-drawer-backdrop,up-modal-backdrop,up-cover,up-drawer,up-modal{top:0;left:0;bottom:0;right:0}up-drawer-box,up-modal-box{box-shadow:0 0 10px 1px rgba(0,0,0,.3)}up-popup{box-shadow:0 0 4px rgba(0,0,0,.3)}up-popup:focus,up-cover-box:focus,up-drawer-box:focus,up-modal-box:focus,up-cover:focus,up-drawer:focus,up-modal:focus,up-popup:focus-visible,up-cover-box:focus-visible,up-drawer-box:focus-visible,up-modal-box:focus-visible,up-cover:focus-visible,up-drawer:focus-visible,up-modal:focus-visible{outline:none}up-cover,up-drawer,up-modal{z-index:2000;position:fixed}up-drawer-backdrop,up-modal-backdrop{position:absolute;background:rgba(0,0,0,.4)}up-cover-viewport,up-drawer-viewport,up-modal-viewport{position:absolute;overflow-y:scroll;overflow-x:hidden;overscroll-behavior:contain;display:flex;align-items:flex-start;justify-content:center}up-popup,up-cover-box,up-drawer-box,up-modal-box{position:relative;box-sizing:border-box;max-width:100%;background-color:#fff;padding:20px;overflow-x:hidden}up-popup-content,up-cover-content,up-drawer-content,up-modal-content{display:block}up-popup{z-index:1000}up-popup-dismiss,up-cover-dismiss,up-drawer-dismiss,up-modal-dismiss{color:#888;position:absolute;top:10px;right:10px;font-size:1.7rem;line-height:.5;cursor:pointer}up-modal[nesting="0"] up-modal-viewport{padding:25px 15px}up-modal[nesting="1"] up-modal-viewport{padding:50px 30px}up-modal[nesting="2"] up-modal-viewport{padding:75px 45px}up-modal[nesting="3"] up-modal-viewport{padding:100px 60px}up-modal[nesting="4"] up-modal-viewport{padding:125px 75px}up-modal[size=small] up-modal-box{width:350px}up-modal[size=medium] up-modal-box{width:650px}up-modal[size=large] up-modal-box{width:1000px}up-modal[size=grow] up-modal-box{width:auto}up-modal[size=full] up-modal-box{width:100%}up-drawer-viewport{justify-content:flex-start}up-drawer[position=right] up-drawer-viewport{justify-content:flex-end}up-drawer-box{min-height:100vh}up-drawer[size=small] up-drawer-box{width:150px}up-drawer[size=medium] up-drawer-box{width:340px}up-drawer[size=large] up-drawer-box{width:600px}up-drawer[size=grow] up-drawer-box{width:auto}up-drawer[size=full] up-drawer-box{width:100%}up-cover-box{width:100%;min-height:100vh;padding:0}up-popup{padding:15px;text-align:left}up-popup[size=small]{width:180px}up-popup[size=medium]{width:300px}up-popup[size=large]{width:550px}up-popup[size=grow] up-popup{width:auto}up-popup[size=full] up-popup{width:100%}
|
|
6
|
+
[up-clickable][role=link]{cursor:pointer}[up-expand]:not([role]),[up-expand][role=link]{cursor:pointer}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{var x={};if(typeof up<"u"){let d=function(r){return String(r).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")};A=d,up.status&&up.status.config&&up.status.config.currentClasses&&(up.status.config.currentClasses.includes("active")||up.status.config.currentClasses.push("active")),up.feedback&&up.feedback.config&&up.feedback.config.currentClasses&&(up.feedback.config.currentClasses.includes("active")||up.feedback.config.currentClasses.push("active")),up.on("up:location:changed",function(){setTimeout(()=>{document.querySelectorAll(".fcu-submenu-link.active, .fcu-submenu-link.up-current").forEach(function(r){let a=r.closest("details");a&&(a.open=!0)})},0)}),up.compiler(".fcu-menu-link, .fcu-submenu-link",function(r){let a=r.getAttribute("href");if(a&&a!=="#"){let u=a.replace(/\/+$/,"");u&&u!=="/"&&r.setAttribute("up-alias",`${u} ${u}/ ${u}/*`)}}),up.compiler("[data-live-sse-url], [data-live-poll-url]",function(r){let a=r.getAttribute("data-live-sse-url"),u=r.getAttribute("data-live-poll-url"),e=parseInt(r.getAttribute("data-live-interval")||"10000",10),n=r.getAttribute("data-live-content-id")||"live-content-"+r.getAttribute("data-live-source"),o=document.getElementById(n),s=r.getAttribute("data-live-format")||"default",t=r.getAttribute("data-live-limit")?parseInt(r.getAttribute("data-live-limit"),10):200;if(!o)return;let c=null,i=null,p=!1;setTimeout(()=>{p=!0},2*e);function g(l){if(l&&!p)return;r.classList.toggle("live-stale",l);let f=r.querySelector(".live-stale-indicator");f&&(f.style.display=l?"":"none")}function v(l){let f=l||location.href;typeof up<"u"&&up.visit?(up.cache&&up.cache.expire&&up.cache.expire(),up.visit(f,{cache:!1})):window.location.href=f}function T(){let l=e*.15*(Math.random()*2-1);return Math.max(1e3,e+l)}function b(){document.hidden||fetch(u).then(l=>l.status===205?(y(),v(),null):l.text()).then(l=>{if(!l)return;let m=new DOMParser().parseFromString(l,"text/html").querySelector(".live-content");m&&(o.innerHTML=m.innerHTML),g(!1)}).catch(()=>g(!0))}function E(){c=setTimeout(()=>{b(),E()},T())}function w(){document.hidden||b()}function L(){c||!u||(document.addEventListener("visibilitychange",w),E())}function y(){c&&(clearTimeout(c),c=null),document.removeEventListener("visibilitychange",w)}return a?(i=new EventSource(a),i.addEventListener("update",function(l){if(s==="raw"){o.innerHTML=l.data,o.scrollTop=o.scrollHeight,g(!1),r.dispatchEvent(new CustomEvent("fcu:live:update",{detail:l.data}));return}try{let f=JSON.parse(l.data);r.dispatchEvent(new CustomEvent("fcu:live:update",{detail:f}));let m=Object.entries(f).map(([I,h])=>I==="line"?`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${d(h)}</div>`:Array.isArray(h)?h.map(k=>`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${d(k)}</div>`).join(""):`<div><strong>${d(I)}</strong>: ${d(h)}</div>`).join("");if(s==="append"){if(o.insertAdjacentHTML("beforeend",m),t>0)for(;o.children.length>t;)o.removeChild(o.firstChild)}else o.innerHTML=m;o.scrollTop=o.scrollHeight,g(!1)}catch{if(s==="append"){let m=document.createElement("div");if(m.textContent=l.data,o.appendChild(m),t>0)for(;o.children.length>t;)o.removeChild(o.firstChild)}else o.textContent=l.data;o.scrollTop=o.scrollHeight,r.dispatchEvent(new CustomEvent("fcu:live:update",{detail:l.data}))}}),i.addEventListener("done",function(l){i.close(),i=null,y();let f;try{f=JSON.parse(l.data).redirect}catch{}r.dispatchEvent(new CustomEvent("fcu:live:done",{detail:f})),v(f)}),i.onerror=function(){i.close(),i=null,g(!0),L()}):L(),function(){i&&i.close(),y()}})}var A;if(typeof up<"u"){let a=function(e){let n=document.getElementById("manyrelated-results-"+e);n&&(n.innerHTML="");let o=document.getElementById("manyrelated-search-"+e);o&&(o.value="",o.focus())},u=function(e,n,o,s){let t=document.createElement("span");t.style.cssText=`display: inline-flex; align-items: center; gap: 4px; background-color: var(--fcu-surface-alt); border: 1px ${o?"dashed":"solid"} var(--fcu-border); font-size: var(--fcu-font-size-sm); font-weight: 500; padding: 2px 8px; border-radius: var(--fcu-border-radius);`,t.appendChild(document.createTextNode(n));let c=document.createElement("button");c.type="button",c.style.cssText="margin-left: 4px; border: none; background: transparent; padding: 0; cursor: pointer; color: var(--fcu-text-muted); display: inline-flex; align-items: center;";let i=document.getElementById("manyrelated-search-"+e),p=i&&i.parentNode.querySelector("span button");return p?c.innerHTML=p.innerHTML:c.textContent="\xD7",c.addEventListener("click",function(g){g.stopPropagation(),s(t)}),t.appendChild(c),i&&i.parentNode.insertBefore(t,i),t};q=a,C=u,up.compiler(".js-hide",function(e){e.querySelectorAll("input, textarea, select").forEach(n=>n.disabled=!0)}),up.compiler(".secret-toggle-btn",function(e){let n=o=>{let s=e.closest(".form-group"),t=s.querySelector(".secret-input-container"),c=s.querySelector(".secret-change-checkbox");if(!t||!c)return;let i=t.querySelectorAll("input, textarea, select");t.style.display==="none"?(t.style.display="block",i.forEach(p=>p.disabled=!1),c.checked=!0,e.classList.add("active"),e.querySelector(".bi")?e.innerHTML='<i class="bi bi-x-lg"></i>':e.querySelector(".material-symbols-outlined")&&(e.innerHTML='<span class="material-symbols-outlined text-xl">close</span>'),i.length>0&&i[0].focus()):(t.style.display="none",i.forEach(p=>p.disabled=!0),c.checked=!1,e.classList.remove("active"),e.querySelector(".bi")?e.innerHTML='<i class="bi bi-pencil-fill"></i>':e.querySelector(".material-symbols-outlined")&&(e.innerHTML='<span class="material-symbols-outlined text-xl">edit</span>'))};return e.addEventListener("click",n),()=>e.removeEventListener("click",n)}),up.compiler("[data-auto-dismiss]",function(e){let n=(o,s)=>{let t=s||o.target;t&&!t.contains(e)&&document.contains(e)&&(up.destroy(e,{transition:"fade-out"}),up.off("up:fragment:inserted",n))};return up.on("up:fragment:inserted",n),()=>up.off("up:fragment:inserted",n)});let d={},r={};window.addItem=function(e){if(d[e]===void 0){let s=document.querySelectorAll(`[name^="${e}-new-"]`).length;d[e]=s}let n=document.getElementById(`${e}-new-item-template`),o=document.getElementById(`${e}-new-items-container`);if(n&&o){let s=n.content.cloneNode(!0),t=document.createElement("div");t.appendChild(s);let c=t.innerHTML.replace(/__INDEX__/g,d[e]);o.insertAdjacentHTML("beforeend",c),d[e]++}},window.selectItem=function(e,n,o){let s=document.getElementById(e);s&&(s.value=n);let t=document.getElementById(`selected-item-display-${e}`);t&&(t.value=o);let c=document.getElementById(`search-results-${e}`);c&&(c.innerHTML="");let i=document.getElementById(`search-input-${e}`);i&&(i.value="")},window.addManyRelatedItem=function(e,n,o){let s=document.getElementById(e+"-hidden-inputs");if(!s)return;if(s.querySelector(`input[value="${n}"]`)){a(e);return}let t=document.createElement("input");t.type="hidden",t.name=e,t.value=n,s.appendChild(t);let c=u(e,o,!1,function(i){t.remove(),i.remove()});c.id=e+"-badge-"+n,a(e)},window.addManyRelatedQuickItem=function(e,n){if(Array.from(document.querySelectorAll(`input[name="${e}-quicknew"]`)).some(function(p){return!p.disabled&&p.value===n})){a(e);return}let s=document.getElementById(e+"-hidden-inputs"),t=s&&s.dataset.pendingLabel||"",c=document.createElement("input");c.type="hidden",c.name=e+"-quicknew",c.value=n;let i=u(e,n,!0,function(p){p.remove()});if(i.insertBefore(c,i.firstChild),t){let p=document.createElement("span");p.style.cssText="color: var(--fcu-text-muted); font-weight: normal;",p.textContent=t,i.insertBefore(p,i.lastChild)}a(e)},window.addManyRelatedNewPanel=function(e){let n=`${e}-newrel-`;if(r[e]===void 0){let t=-1;document.querySelectorAll(`[name^="${n}"]`).forEach(function(c){let i=parseInt(c.name.slice(n.length).split("-")[0],10);!isNaN(i)&&i>t&&(t=i)}),r[e]=t+1}let o=document.getElementById(`${e}-newrel-item-template`),s=document.getElementById(`${e}-newrel-items-container`);if(o&&s){let t=o.content.cloneNode(!0),c=document.createElement("div");c.appendChild(t);let i=c.innerHTML.replace(/__INDEX__/g,r[e]);s.insertAdjacentHTML("beforeend",i),r[e]++}},window.removeManyRelatedItem=function(e,n){let o=document.getElementById(e+"-hidden-inputs");if(o){let t=o.querySelector(`input[value="${n}"]`);t&&t.remove()}let s=document.getElementById(e+"-badge-"+n);s&&s.remove()},up.compiler("[data-js-enable]",function(e){e.disabled=!1}),up.compiler(".js-hide select",function(e){e.disabled=!0}),up.compiler('input[id^="search-input-"]',function(e){e.addEventListener("keydown",function(n){let o=e.id.replace("search-input-",""),s=document.getElementById("search-results-"+o);if(!s)return;if(n.key==="Escape"){n.preventDefault(),s.innerHTML="",e.blur();return}let t=s.querySelectorAll("a.fcu-dropdown-item, a.list-group-item");if(t.length===0)return;let c=s.querySelector("a.fcu-dropdown-item.active, a.list-group-item.active"),i=c?Array.from(t).indexOf(c):-1;n.key==="ArrowDown"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i+1)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="ArrowUp"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i-1+t.length)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="Enter"&&(n.preventDefault(),c&&c.click())})}),up.compiler('input[id^="manyrelated-search-"]',function(e){e.addEventListener("keydown",function(n){let o=e.id.replace("manyrelated-search-",""),s=document.getElementById("manyrelated-results-"+o);if(!s)return;let t=s.querySelectorAll("a.fcu-dropdown-item, a.list-group-item");if(t.length===0)return;let c=s.querySelector("a.fcu-dropdown-item.active, a.list-group-item.active"),i=c?Array.from(t).indexOf(c):-1;n.key==="ArrowDown"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i+1)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="ArrowUp"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i-1+t.length)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="Enter"?(n.preventDefault(),c&&c.click()):n.key==="Escape"&&(s.innerHTML="")})})}var q,C;document.addEventListener("input",function(d){let r=d.target;if(r.matches('input[id^="manyrelated-search-"]')){let a=r.id.replace("manyrelated-search-",""),u=r.getAttribute("up-href"),e=r.value,n=parseInt(r.getAttribute("up-delay"))||0,o=parseInt(r.getAttribute("up-min-length"))||0;if(clearTimeout(window.manyRelatedTimer),u&&e.length>=o)window.manyRelatedTimer=setTimeout(function(){typeof up<"u"&&up.render({url:u,target:"#manyrelated-results-"+a,params:{q:e}})},n);else{let s=document.getElementById("manyrelated-results-"+a);s&&(s.innerHTML="")}return}if(r.matches('input[id^="search-input-"]')){let a=r.id.replace("search-input-",""),u=r.getAttribute("up-href"),e=r.getAttribute("up-target"),n=r.value,o=parseInt(r.getAttribute("up-delay"))||0,s=parseInt(r.getAttribute("up-min-length"))||0;if(clearTimeout(window.searchTimer),u&&e)if(n.length>=s)window.searchTimer=setTimeout(function(){typeof up<"u"&&up.render({url:u,target:e,params:{q:n}})},o);else{let t=document.querySelector(e);t&&(t.innerHTML="")}}});document.addEventListener("focusin",function(d){let r=d.target;if(r.matches('input[id^="manyrelated-search-"]')){let a=r.id.replace("manyrelated-search-","");clearTimeout(x[a]),r.dispatchEvent(new Event("input",{bubbles:!0}))}r.matches('input[id^="search-input-"]')&&r.dispatchEvent(new Event("input",{bubbles:!0}))});document.addEventListener("keydown",function(d){let r=d.target;if(d.key==="ArrowDown"&&r.matches('input[id^="selected-item-display-"]')){let a=document.getElementById("search-input-"+r.id.replace("selected-item-display-",""));a&&(d.preventDefault(),a.focus())}});document.addEventListener("focusout",function(d){let r=d.target;if(r.matches('input[id^="manyrelated-search-"]')){let a=r.id.replace("manyrelated-search-","");x[a]=setTimeout(function(){let u=document.getElementById("manyrelated-results-"+a);u&&(u.innerHTML="")},200)}});})();
|