From d7b0faead30328e9d6d519ec77ec2c932564577f Mon Sep 17 00:00:00 2001 From: maxim-f1 <“maxconal228@gmail.com”> Date: Fri, 10 Jul 2026 16:17:59 +0300 Subject: [PATCH 1/4] Add auto-resizing textarea widget on input. --- sqladmin/fields.py | 13 +++++++++++++ sqladmin/forms.py | 2 +- sqladmin/statics/css/main.css | 9 +++++++++ sqladmin/statics/js/main.js | 30 ++++++++++++++++++++++++++++++ sqladmin/widgets.py | 29 ++++++++++++++++++++++++++++- tests/test_fields.py | 10 ++++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) diff --git a/sqladmin/fields.py b/sqladmin/fields.py index fcea7d90..4347638f 100644 --- a/sqladmin/fields.py +++ b/sqladmin/fields.py @@ -25,6 +25,10 @@ "QuerySelectMultipleField", "SelectField", "Select2TagsField", + "BooleanField", + "FileField", + "UuidField", + "TextAreaField", ] @@ -439,3 +443,12 @@ 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. + """ + + widget = sqladmin_widgets.TextAreaWidget() diff --git a/sqladmin/forms.py b/sqladmin/forms.py index 302037a0..53b144b5 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 49cad898..4903eeb2 100644 --- a/sqladmin/statics/css/main.css +++ b/sqladmin/statics/css/main.css @@ -8,3 +8,12 @@ margin-right: 5px; } +.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 34e59178..d71f43b0 100644 --- a/sqladmin/statics/js/main.js +++ b/sqladmin/statics/js/main.js @@ -164,3 +164,33 @@ $(':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); + const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0; + + const $charsCountLabel = $textarea.next('.chars-count-label'); + const $charsCountElement = $charsCountLabel.length ? $charsCountLabel : $charsCountLabel.first(); + + if (!$charsCountElement.length) return; + + const updateTextareaHeight = () => { + $textarea.css('height', 'auto'); + $textarea.css('height', $textarea[0].scrollHeight + 'px'); + }; + + const updateCharsCount = () => { + const $currentLength = $textarea.val().length; + $charsCountElement.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`); + $charsCountElement.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength); + }; + + $textarea.on('input propertychange', function() { + updateTextareaHeight(); + updateCharsCount(); + }); + + updateTextareaHeight(); // Resize on start + updateCharsCount(); // Resize on start +}); diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py index f5c10f3b..3402cf72 100644 --- a/sqladmin/widgets.py +++ b/sqladmin/widgets.py @@ -3,7 +3,7 @@ import json 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 @@ -122,3 +122,30 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: return Markup( '
%s
' ) % Markup.escape(super().__call__(field, **kwargs)) + + +class TextAreaWidget(widgets.Input): + """ + 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_ = " ".join(filter(None, (kwargs.get("class"), "autoresize-textarea"))) + kwargs["class"] = class_ + kwargs["rows"] = "1" + if hasattr(field, "_value") and callable(field._value): + kwargs["value"] = field._value() + + return Markup( + "" + % (html_params(name=field.name, **kwargs), escape(kwargs.get("value", ""))) + + '
' + ) # nosec: markupsafe_markup_xss diff --git a/tests/test_fields.py b/tests/test_fields.py index bc368a57..c92296c7 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -16,6 +16,7 @@ QuerySelectMultipleField, Select2TagsField, SelectField, + TextAreaField, UuidField, ) from tests.common import DummyData @@ -202,3 +203,12 @@ class F(Form): form = F(DummyData(uuid=["00000000-0000-000000000001"])) assert form.validate() is False + + +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() From 66f30877f702d70a3322adc885348fbcaa2e764f Mon Sep 17 00:00:00 2001 From: maxim-f1 <“maxconal228@gmail.com”> Date: Fri, 10 Jul 2026 18:01:24 +0300 Subject: [PATCH 2/4] Add `show_chars_count` field to disable chars count --- sqladmin/fields.py | 22 ++++++++++++++++++++++ sqladmin/statics/js/main.js | 6 ++++-- sqladmin/widgets.py | 13 +++++++++---- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/sqladmin/fields.py b/sqladmin/fields.py index e987bb7a..d0dee9f2 100644 --- a/sqladmin/fields.py +++ b/sqladmin/fields.py @@ -449,6 +449,28 @@ class TextAreaField(fields.StringField): """ This field represents an HTML `textarea` and can be used to take multi-line input. + + 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, + *, + show_chars_count: bool = True, + **kwargs: Any, + ): + super().__init__(label, validators, **kwargs) + self.show_chars_count = show_chars_count + widget = sqladmin_widgets.TextAreaWidget() diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js index 03e1695d..602b5577 100644 --- a/sqladmin/statics/js/main.js +++ b/sqladmin/statics/js/main.js @@ -210,19 +210,21 @@ $(':input[data-role="select2-tags"]').each(function () { // Automatically resize textarea on input events. $('.autoresize-textarea').each(function() { const $textarea = $(this); + if (!$textarea) return; + const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0; const $charsCountLabel = $textarea.next('.chars-count-label'); const $charsCountElement = $charsCountLabel.length ? $charsCountLabel : $charsCountLabel.first(); - if (!$charsCountElement.length) return; - const updateTextareaHeight = () => { $textarea.css('height', 'auto'); $textarea.css('height', $textarea[0].scrollHeight + 'px'); }; const updateCharsCount = () => { + if (!$charsCountElement.length || !$charsCountLabel) return; + const $currentLength = $textarea.val().length; $charsCountElement.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`); $charsCountElement.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength); diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py index 4db2398b..15726049 100644 --- a/sqladmin/widgets.py +++ b/sqladmin/widgets.py @@ -138,16 +138,21 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: flags = getattr(field, "flags", {}) for k in dir(flags): if k in self.validation_attrs and k not in kwargs: - kwargs.setdefault(k, getattr(flags, k)) + kwargs[k] = getattr(flags, k) class_ = " ".join(filter(None, (kwargs.get("class"), "autoresize-textarea"))) - kwargs.setdefault("class", class_) + kwargs["class"] = class_ kwargs.setdefault("rows", "1") if hasattr(field, "_value") and callable(field._value): - kwargs.setdefault("value", field._value()) + kwargs["value"] = field._value() + + if getattr(field, "show_chars_count", True) is True: + chars_count = '
' + else: + chars_count = "" return Markup( "" % (html_params(name=field.name, **kwargs), escape(kwargs.get("value", ""))) - + '
' + + chars_count ) # nosec: markupsafe_markup_xss From b6445fe7b28803e9ea83ce830def85d1d5bbd9e3 Mon Sep 17 00:00:00 2001 From: maxim-f1 <“maxconal228@gmail.com”> Date: Fri, 10 Jul 2026 18:32:03 +0300 Subject: [PATCH 3/4] Added `enable_autoresize` field to control resizing behavior. Now the parameters are independent of each other --- sqladmin/fields.py | 12 ++++++++++++ sqladmin/statics/js/main.js | 37 ++++++++++++++++++++++++------------- sqladmin/widgets.py | 8 ++++++-- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/sqladmin/fields.py b/sqladmin/fields.py index d0dee9f2..9c34cbde 100644 --- a/sqladmin/fields.py +++ b/sqladmin/fields.py @@ -450,6 +450,16 @@ 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. @@ -467,10 +477,12 @@ def __init__( 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/statics/js/main.js b/sqladmin/statics/js/main.js index 602b5577..0052e21a 100644 --- a/sqladmin/statics/js/main.js +++ b/sqladmin/statics/js/main.js @@ -212,29 +212,40 @@ $('.autoresize-textarea').each(function() { const $textarea = $(this); if (!$textarea) return; - const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0; - - const $charsCountLabel = $textarea.next('.chars-count-label'); - const $charsCountElement = $charsCountLabel.length ? $charsCountLabel : $charsCountLabel.first(); - const updateTextareaHeight = () => { $textarea.css('height', 'auto'); $textarea.css('height', $textarea[0].scrollHeight + 'px'); }; - const updateCharsCount = () => { - if (!$charsCountElement.length || !$charsCountLabel) return; + $textarea.on('input propertychange', function() { + updateTextareaHeight(); + }); + + updateTextareaHeight(); // Resize on start +}); + + +// Shows the number of characters in a textarea.. +$('.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; - $charsCountElement.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`); - $charsCountElement.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength); + $charsCountLabel.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`); + $charsCountLabel.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength); }; $textarea.on('input propertychange', function() { - updateTextareaHeight(); - updateCharsCount(); + updateCharsCountLabel(); }); - updateTextareaHeight(); // Resize on start - updateCharsCount(); // Resize on start + updateCharsCountLabel(); // Show on start }); diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py index 15726049..87a3be6d 100644 --- a/sqladmin/widgets.py +++ b/sqladmin/widgets.py @@ -140,9 +140,13 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: if k in self.validation_attrs and k not in kwargs: kwargs[k] = getattr(flags, k) - class_ = " ".join(filter(None, (kwargs.get("class"), "autoresize-textarea"))) + class_ = kwargs.get("class") + if getattr(field, "enable_autoresize", True) is True: + class_ = " ".join(filter(None, (class_, "autoresize-textarea"))) + kwargs.setdefault("rows", "1") + kwargs["class"] = class_ - kwargs.setdefault("rows", "1") + if hasattr(field, "_value") and callable(field._value): kwargs["value"] = field._value() From c6771171ed865df8d0705d6b8d2898ba6ccffc1f Mon Sep 17 00:00:00 2001 From: maxim-f1 <“maxconal228@gmail.com”> Date: Mon, 13 Jul 2026 11:50:24 +0300 Subject: [PATCH 4/4] Fix value --- sqladmin/statics/js/main.js | 2 +- sqladmin/widgets.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js index 0052e21a..e93cbb13 100644 --- a/sqladmin/statics/js/main.js +++ b/sqladmin/statics/js/main.js @@ -225,7 +225,7 @@ $('.autoresize-textarea').each(function() { }); -// Shows the number of characters in a textarea.. +// Displays the number of characters in the text field. $('.chars-count-label').each(function() { const $charsCountLabel = $(this); if (!$charsCountLabel) return; diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py index 87a3be6d..8741b750 100644 --- a/sqladmin/widgets.py +++ b/sqladmin/widgets.py @@ -126,7 +126,7 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: ) -class TextAreaWidget(widgets.Input): +class TextAreaWidget(widgets.TextArea): """ Render a textarea that automatically resizes on input. """ @@ -145,7 +145,8 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: class_ = " ".join(filter(None, (class_, "autoresize-textarea"))) kwargs.setdefault("rows", "1") - kwargs["class"] = class_ + if class_: + kwargs["class"] = class_ if hasattr(field, "_value") and callable(field._value): kwargs["value"] = field._value() @@ -155,8 +156,10 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup: else: chars_count = "" + value = kwargs.pop("value", "") + return Markup( "" - % (html_params(name=field.name, **kwargs), escape(kwargs.get("value", ""))) + % (html_params(name=field.name, **kwargs), escape(value)) + chars_count ) # nosec: markupsafe_markup_xss