Skip to content

Commit 48ae61d

Browse files
committed
add: ajax-search field
1 parent cb38765 commit 48ae61d

File tree

8 files changed

+290
-3137
lines changed

8 files changed

+290
-3137
lines changed

basxbread/forms/forms.py

+4
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ def _generate_formset_class(
240240

241241
def _formfield_callback_with_request(field, request, model, instance, cache_querysets):
242242
kwargs = getattr(field, "formfield_kwargs", {})
243+
if hasattr(field, "widget"):
244+
kwargs["widget"] = field.widget
243245
choices = None
244246
if hasattr(field, "lazy_choices"):
245247
choices = field.lazy_choices(field, request, instance)
@@ -250,6 +252,8 @@ def _formfield_callback_with_request(field, request, model, instance, cache_quer
250252
kwargs["initial"] = field.lazy_initial(field, request, instance)
251253

252254
ret = field.formfield(**kwargs)
255+
if hasattr(field, "breadwidget"):
256+
ret.breadwidget = field.breadwidget
253257
if isinstance(choices, models.QuerySet):
254258
ret.queryset = choices
255259

basxbread/layout/components/forms/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ..notification import InlineNotification
1010
from .fields import DEFAULT_FORM_CONTEXTNAME, FormField, FormFieldMarker
1111
from .helpers import HelpText, Submit # noqa
12+
from .widgets import *
1213

1314

1415
class Form(hg.FORM):

basxbread/layout/components/forms/fields.py

+3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ def wrapper(context):
172172
if suggested_widgetclass is not None:
173173
return suggested_widgetclass
174174

175+
if hasattr(realform[fieldname].field, "breadwidget"):
176+
return realform[fieldname].field.breadwidget
177+
175178
# Auto-detection declared by the bread widget has third priority
176179
return (
177180
django2bread_widgetclass(widgetclass, type(realform[fieldname].field))

basxbread/layout/components/forms/widgets.py

+108-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from _strptime import TimeRE
99
from django.conf import settings
1010
from django.forms import widgets
11+
from django.urls import reverse
1112
from django.utils import formats
1213
from django.utils.translation import gettext_lazy as _
1314
from phonenumber_field.formfields import PhoneNumberField
@@ -857,9 +858,7 @@ def __init__(
857858

858859
def format_date_value(context):
859860
bfield = hg.resolve_lazy(boundfield, context)
860-
return bfield.field.widget.format_value(
861-
hg.resolve_lazy(inputelement_attrs, context).get("value")
862-
)
861+
return bfield.field.widget.format_value(bfield.value())
863862

864863
super().__init__(
865864
hg.DIV(
@@ -1105,6 +1104,112 @@ class LazySelect(Select):
11051104
django_widget = django_countries.widgets.LazySelect
11061105

11071106

1107+
class AjaxSearchWidget(BaseWidget):
1108+
carbon_input_error_class = "bx--text-input--invalid"
1109+
1110+
def __init__(
1111+
self,
1112+
label=None,
1113+
help_text=None,
1114+
errors=None,
1115+
inputelement_attrs=None,
1116+
boundfield=None,
1117+
**attributes,
1118+
):
1119+
inputelement_attrs = inputelement_attrs or {}
1120+
searchresult_id = hg.format("{}-searchresult", inputelement_attrs.get("id"))
1121+
super().__init__(
1122+
label,
1123+
hg.DIV(
1124+
hg.If(
1125+
getattr(errors, "condition", None),
1126+
Icon(
1127+
"warning--filled",
1128+
size=16,
1129+
_class="bx--text-input__invalid-icon",
1130+
),
1131+
),
1132+
hg.DIV(
1133+
_("Loading..."),
1134+
id=hg.format("{}-loader", inputelement_attrs.get("id")),
1135+
_class="htmx-indicator",
1136+
style="position: absolute;z-index: 1;right: 8px; pointer-events: none",
1137+
),
1138+
hg.INPUT(type="hidden", lazy_attributes=inputelement_attrs),
1139+
hg.INPUT(
1140+
_class=hg.BaseElement(
1141+
"bx--text-input",
1142+
hg.If(
1143+
getattr(errors, "condition", False),
1144+
" bx--text-input--invalid",
1145+
),
1146+
),
1147+
data_invalid=hg.If(getattr(errors, "condition", False), True),
1148+
name="query",
1149+
type="text",
1150+
hx_get=reverse(self.url),
1151+
hx_trigger="input changed delay:100ms",
1152+
hx_target=hg.format("#{}", searchresult_id),
1153+
hx_indicator=hg.format("#{}-loader", inputelement_attrs.get("id")),
1154+
onfocusin="this.parentElement.nextElementSibling.nextElementSibling.style.display = 'block'",
1155+
style="padding-right: 2.5rem",
1156+
),
1157+
hg.SCRIPT(
1158+
hg.mark_safe(
1159+
"""
1160+
let elem = document.currentScript;
1161+
document.addEventListener('click', (ev) => {
1162+
if(!elem.parentElement.parentElement.contains(ev.target))
1163+
elem.parentElement.nextElementSibling.nextElementSibling.style.display = 'none'
1164+
});
1165+
1166+
document.addEventListener('htmx:load', (ev) => {
1167+
$$('.result-item', ev.target)._.bind({'click': (e) => {
1168+
elem.previousElementSibling.previousElementSibling.value = e.target.value;
1169+
elem.previousElementSibling.value = '';
1170+
elem.parentElement.nextElementSibling.firstElementChild.innerText = e.target.innerText;
1171+
elem.parentElement.nextElementSibling.style.display = 'flex';
1172+
elem.parentElement.nextElementSibling.nextElementSibling.style.display = 'none';
1173+
}})
1174+
})"""
1175+
)
1176+
),
1177+
_class="bx--text-input__field-wrapper",
1178+
data_invalid=hg.If(getattr(errors, "condition", None), True),
1179+
),
1180+
Tag(
1181+
hg.F(
1182+
lambda c: hg.resolve_lazy(boundfield, c).field.to_python(
1183+
hg.resolve_lazy(boundfield, c).value()
1184+
)
1185+
),
1186+
can_delete=hg.F(
1187+
lambda c: not hg.resolve_lazy(boundfield, c).field.required
1188+
),
1189+
style=hg.If(
1190+
hg.F(lambda c: not hg.resolve_lazy(boundfield, c).value()),
1191+
"display: none",
1192+
),
1193+
ondelete=hg.format(
1194+
"""document.getElementById('{}').value = ''; this.parentElement.style.display = 'none'""",
1195+
inputelement_attrs.get("id"),
1196+
),
1197+
),
1198+
hg.DIV(
1199+
hg.SPAN("...", style="padding: 8px"),
1200+
id=searchresult_id,
1201+
style="border-left: solid 1px gray; border-right: solid 1px gray; border-bottom: solid 1px gray; background: white; z-index: 99; display: none",
1202+
),
1203+
errors,
1204+
help_text,
1205+
**hg.merge_html_attrs(attributes, {"_class": "bx--text-input-wrapper"}),
1206+
)
1207+
1208+
1209+
def AjaxSearch(url):
1210+
return type("SubclassedAjaxSearchWidget", (AjaxSearchWidget,), {"url": url})
1211+
1212+
11081213
class MultiWidget(BaseWidget):
11091214
django_widget = widgets.MultiWidget
11101215

basxbread/layout/components/tag.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,18 @@ def __init__(self, *label, can_delete=False, tag_color=None, **kwargs):
2727
kwargs.setdefault(
2828
"type", "button"
2929
) # prevents this from trying to submit a form when inside a FORM element
30-
kwargs["_class"] = (
31-
kwargs.get("_class", "")
32-
+ " bx--tag"
33-
+ (" bx--tag--filter" if can_delete else "")
34-
+ (f" bx--tag--{tag_color}" if tag_color else "")
30+
kwargs["_class"] = hg.BaseElement(
31+
kwargs.get("_class", ""),
32+
" bx--tag",
33+
hg.If(can_delete, " bx--tag--filter"),
34+
(f" bx--tag--{tag_color}" if tag_color else ""),
3535
)
36-
if can_delete:
37-
kwargs.setdefault("title", _("Remove"))
3836

37+
on_del = kwargs.pop("ondelete", None)
3938
super().__init__(
4039
hg.SPAN(*label, _class="bx--tag__label"),
41-
*([Icon("close", size=16)] if can_delete else []),
40+
hg.If(
41+
can_delete, Icon("close", size=16, onclick=on_del, title=_("Remove"))
42+
),
4243
**kwargs,
4344
)

basxbread/static/js/basxbread.min.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)