diff --git a/sqladmin/fields.py b/sqladmin/fields.py index 9865b0fb..eff2f48d 100644 --- a/sqladmin/fields.py +++ b/sqladmin/fields.py @@ -36,6 +36,10 @@ "SelectField", "Select2TagsField", "file_display_formatter", + "BooleanField", + "FileField", + "UuidField", + "TextAreaField", ] @@ -533,3 +537,46 @@ def process_data(self, value: str | UUID | None) -> None: self.data = None else: self.data = None + + +class TextAreaField(fields.StringField): + """ + This field represents an HTML `textarea` and can be used to take + multi-line input. + + To disable automatic updating of the input size, + you need to pass enable_autoresize = False to the field arguments. + ```python + form_args = { + 'field_name': { + 'enable_autoresize': False + } + } + ``` + + To disable the display of the number of characters, + you need to pass the value show_chars_count = False to the field arguments. + + ```python + form_args = { + 'field_name': { + 'show_chars_count': False + } + } + ``` + """ + + def __init__( + self, + label: str | None = None, + validators: list | None = None, + *, + enable_autoresize: bool = True, + show_chars_count: bool = True, + **kwargs: Any, + ): + super().__init__(label, validators, **kwargs) + self.enable_autoresize = enable_autoresize + self.show_chars_count = show_chars_count + + widget = sqladmin_widgets.TextAreaWidget() diff --git a/sqladmin/forms.py b/sqladmin/forms.py index 076e9fd9..cbce2519 100644 --- a/sqladmin/forms.py +++ b/sqladmin/forms.py @@ -29,7 +29,6 @@ Form, IntegerField, StringField, - TextAreaField, TimeField, validators, ) @@ -57,6 +56,7 @@ QuerySelectMultipleField, Select2TagsField, SelectField, + TextAreaField, UuidField, ) from sqladmin.helpers import ( diff --git a/sqladmin/statics/css/main.css b/sqladmin/statics/css/main.css index 74f3141a..199056b7 100644 --- a/sqladmin/statics/css/main.css +++ b/sqladmin/statics/css/main.css @@ -45,3 +45,12 @@ border: 0; } +.autoresize-textarea { + min-height: 25px; + overflow-y: hidden; + height: 25px; +} + +.chars-count-label.warning { + color: red; +} diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js index bb080371..a7f9559a 100644 --- a/sqladmin/statics/js/main.js +++ b/sqladmin/statics/js/main.js @@ -538,3 +538,46 @@ $(':input[data-role="select2-tags"]').each(function () { $(this).append(option).trigger('change'); } }); + +// Automatically resize textarea on input events. +$('.autoresize-textarea').each(function() { + const $textarea = $(this); + if (!$textarea) return; + + const updateTextareaHeight = () => { + $textarea.css('height', 'auto'); + $textarea.css('height', $textarea[0].scrollHeight + 'px'); + }; + + $textarea.on('input propertychange', function() { + updateTextareaHeight(); + }); + + updateTextareaHeight(); // Resize on start +}); + + +// Displays the number of characters in the text field. +$('.chars-count-label').each(function() { + const $charsCountLabel = $(this); + if (!$charsCountLabel) return; + + const $textarea = $charsCountLabel.prev('textarea'); + if (!$textarea.length) return; + + const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0; + + const updateCharsCountLabel = () => { + if (!$charsCountLabel.length) return; + + const $currentLength = $textarea.val().length; + $charsCountLabel.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`); + $charsCountLabel.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength); + }; + + $textarea.on('input propertychange', function() { + updateCharsCountLabel(); + }); + + updateCharsCountLabel(); // Show on start +}); diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py index 4d936484..cdda11e0 100644 --- a/sqladmin/widgets.py +++ b/sqladmin/widgets.py @@ -4,7 +4,7 @@ import logging from typing import TYPE_CHECKING, Any -from markupsafe import Markup +from markupsafe import Markup, escape from wtforms import Field, SelectFieldBase, widgets from wtforms.widgets import html_params @@ -170,3 +170,42 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: ) return template.format(text=super().__call__(field, **kwargs)) + + +class TextAreaWidget(widgets.TextArea): + """ + Render a textarea that automatically resizes on input. + """ + + validation_attrs = ["required", "disabled", "readonly", "maxlength", "minlength"] + + def __call__(self, field: Field, **kwargs: Any) -> Markup: + kwargs.setdefault("id", field.id) + flags = getattr(field, "flags", {}) + for k in dir(flags): + if k in self.validation_attrs and k not in kwargs: + kwargs[k] = getattr(flags, k) + + class_ = kwargs.get("class") + if getattr(field, "enable_autoresize", True) is True: + class_ = " ".join(filter(None, (class_, "autoresize-textarea"))) + kwargs.setdefault("rows", "1") + + if class_: + kwargs["class"] = class_ + + if hasattr(field, "_value") and callable(field._value): + kwargs["value"] = field._value() + + if getattr(field, "show_chars_count", True) is True: + chars_count = '
' + else: + chars_count = "" + + value = kwargs.pop("value", "") + + return Markup( + "" + % (html_params(name=field.name, **kwargs), escape(value)) + + chars_count + ) # nosec: markupsafe_markup_xss diff --git a/tests/test_fields.py b/tests/test_fields.py index 48a3a092..97beb0c9 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -18,6 +18,7 @@ QuerySelectMultipleField, Select2TagsField, SelectField, + TextAreaField, UuidField, ) from tests.common import DummyData @@ -226,3 +227,12 @@ class FRequired(Form): form = FRequired() html = form.boolean() assert "required" in html + + +def test_textarea_field() -> None: + class F(Form): + text = TextAreaField() + + form = F() + assert "autoresize-textarea" in form.text() + assert "chars-count-label" in form.text()