Skip to content

Commit b918122

Browse files
ertglsarahboyce
andcommitted
Introduce several improvements (please see the commit message for details)
1. Implement `BoundFieldWithCharacterCounter` to add character counter specific data to any `CharField` inputs. 2. Implement character counter in JavaScript so we can enhance the page progressively. 3. Make `ProfileForm.bio` field use `BoundFieldWithCharacterCounter`. 4. Set maxlength for `ProfileForm.bio` field. 5. Render `profile.bio` with `urlize` to convert URLs and mail addresses in the plaintext into clickable links. Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
1 parent d4d8873 commit b918122

File tree

9 files changed

+124
-4
lines changed

9 files changed

+124
-4
lines changed

accounts/forms.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.db.models import ProtectedError
44
from django.utils.translation import gettext_lazy as _
55

6+
from contrib.django.forms.boundfields import BoundFieldWithCharacterCounter
7+
68
from .models import Profile
79

810

@@ -21,7 +23,13 @@ class ProfileForm(forms.ModelForm):
2123
required=False, widget=forms.TextInput(attrs={"placeholder": _("Email")})
2224
)
2325
bio = forms.CharField(
24-
required=False, widget=forms.Textarea(attrs={"placeholder": _("Bio")})
26+
bound_field_class=BoundFieldWithCharacterCounter,
27+
required=False,
28+
max_length=3_000,
29+
widget=forms.Textarea(attrs={"placeholder": _("Bio")}),
30+
help_text=_(
31+
"URLs and email addresses are automatically converted into clickable links.",
32+
),
2533
)
2634

2735
class Meta:

contrib/__init__.py

Whitespace-only changes.

contrib/django/__init__.py

Whitespace-only changes.

contrib/django/forms/__init__.py

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from contextlib import suppress
2+
from django.forms.boundfield import BoundField
3+
4+
5+
class BoundFieldWithCharacterCounter(BoundField):
6+
7+
def get_characters_remaining_count(self):
8+
characters_remaining_count = None
9+
max_length = None
10+
with suppress(TypeError, ValueError):
11+
max_length = int(self.field.max_length)
12+
if isinstance(max_length, int):
13+
value = self.value().replace("\r\n", "\n").replace("\r", "\n")
14+
characters_remaining_count = max_length - len(value)
15+
return characters_remaining_count

djangoproject/scss/_style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2967,6 +2967,10 @@ form {
29672967
background: var(--primary);
29682968
}
29692969
}
2970+
2971+
.help-text {
2972+
@include font-size(14);
2973+
}
29702974
}
29712975

29722976
.form-general {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
let CHARACTER_COUNTER_INPUT_SELECTOR_ATTR = 'data-character-counter-input-selector';
2+
3+
let CHARACTER_COUNTER_INDICATOR_SELECTOR_ATTR = 'data-character-counter-indicator-selector';
4+
5+
let CharacterCounter = function (inputElement, indicatorElement) {
6+
this.inputElement = inputElement;
7+
this.indicatorElement = indicatorElement;
8+
};
9+
10+
CharacterCounter.prototype = {
11+
handleInputEvent: function (ev) {
12+
this.updateDOM();
13+
},
14+
15+
updateDOM: function () {
16+
if (typeof this.inputElement.maxLength !== 'number') {
17+
return;
18+
}
19+
20+
if (typeof this.inputElement.value !== 'string') {
21+
return;
22+
}
23+
24+
const remaining = this.inputElement.maxLength - this.inputElement.value.length;
25+
this.indicatorElement.innerText = String(remaining);
26+
},
27+
};
28+
29+
function setupCharacterCounter(counterElement) {
30+
let inputSelector = counterElement.getAttribute(CHARACTER_COUNTER_INPUT_SELECTOR_ATTR);
31+
if (typeof inputSelector !== 'string') {
32+
return;
33+
}
34+
35+
let indicatorSelector = counterElement.getAttribute(CHARACTER_COUNTER_INDICATOR_SELECTOR_ATTR);
36+
if (typeof indicatorSelector !== 'string') {
37+
return;
38+
}
39+
40+
let inputElement = document.querySelector(inputSelector);
41+
if (inputElement == null) {
42+
return;
43+
}
44+
45+
let indicatorElement = document.querySelector(indicatorSelector);
46+
if (indicatorElement == null) {
47+
return;
48+
}
49+
50+
let characterCounter = new CharacterCounter(inputElement, indicatorElement);
51+
52+
inputElement.addEventListener(
53+
'input',
54+
characterCounter.handleInputEvent.bind(characterCounter),
55+
)
56+
}
57+
58+
function setupCharacterCounters() {
59+
let counterElements = document.getElementsByClassName('character-counter');
60+
for (let i = 0; i < counterElements.length; i++) {
61+
setupCharacterCounter(counterElements[i]);
62+
}
63+
}
64+
65+
document.addEventListener('DOMContentLoaded', function () {
66+
setupCharacterCounters();
67+
});

djangoproject/templates/accounts/edit_profile.html

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
{% extends "registration/base.html" %}
2-
{% load i18n %}
2+
{% load i18n static %}
33

44
{% block title %}{% translate "Edit your profile" %}{% endblock %}
55

6+
{% block head_extra %}
7+
<script src="{% static "js/mod/character-counter.js" %}"></script>
8+
{% endblock head_extra %}
9+
610
{% block content %}
711

812
{% if form.errors %}
@@ -29,9 +33,31 @@ <h1>{% translate "Edit your profile" %}</h1>
2933

3034
<div>
3135
{% if form.bio.errors %}
32-
<p class="errors">{{ form.bio.errors.as_text }}</p>
36+
<p id="{{ form.bio.id_for_label }}_error" class="errors">
37+
{{ form.bio.errors.as_text }}
38+
</p>
3339
{% endif %}
3440
<p>{{ form.bio }}</p>
41+
<div id="{{ form.bio.id_for_label }}_helptext" class="help-text">
42+
<div>{{ form.bio.help_text }}</div>
43+
{% with bio_characters_remaining_count=form.bio.get_characters_remaining_count %}
44+
{% if bio_characters_remaining_count is not None %}
45+
<div
46+
class="character-counter"
47+
data-character-counter-input-selector="#{{ form.bio.id_for_label }}"
48+
data-character-counter-indicator-selector="#{{ form.bio.id_for_label }}_characters_remaining_count"
49+
>
50+
<span>{% translate "Characters remaining:" %}</span>
51+
<span
52+
id="{{ form.bio.id_for_label }}_characters_remaining_count"
53+
class="character-counter__indicator"
54+
>
55+
{{ bio_characters_remaining_count }}
56+
</span>
57+
</div>
58+
{% endif %}
59+
{% endwith %}
60+
</div>
3561
</div>
3662

3763
<div class="submit">

djangoproject/templates/accounts/user_profile.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ <h1 class="name">
4343

4444
{% if user_obj.profile.bio %}
4545
<p class="bio">
46-
{{ user_obj.profile.bio }}
46+
{{ user_obj.profile.bio|urlize }}
4747
</p>
4848
{% endif %}
4949

0 commit comments

Comments
 (0)