diff --git a/forms/__init__.py b/forms/__init__.py
index 81cae57cf..62e907a5a 100644
--- a/forms/__init__.py
+++ b/forms/__init__.py
@@ -356,3 +356,22 @@ def clean(self, value):
return html.unescape(cleaned_value)
return cleaned_value
+
+
+class AutoGrowTextareaWidget(forms.Textarea):
+ """A textarea widget whose minimum height is automatically adjusted to fit its content."""
+
+ template_name = "admission/widgets/autogrow_textarea.html"
+
+ def __init__(self, attrs=None):
+ if not attrs:
+ attrs = {}
+
+ attrs['onInput'] = 'this.parentNode.dataset.value = this.value'
+
+ super().__init__(attrs)
+
+ class Media:
+ css = {
+ 'all': ('admission/autogrow_textarea.css',),
+ }
diff --git a/forms/admission/checklist.py b/forms/admission/checklist.py
index 80e6d818e..9687827d2 100644
--- a/forms/admission/checklist.py
+++ b/forms/admission/checklist.py
@@ -47,12 +47,6 @@
)
from admission.constants import CONTEXT_GENERAL, CONTEXT_DOCTORATE
-from admission.models import GeneralEducationAdmission, DoctorateAdmission
-from admission.models.base import training_campus_subquery
-from admission.models.checklist import (
- RefusalReason,
- AdditionalApprovalCondition,
-)
from admission.ddd import DUREE_MINIMALE_PROGRAMME, DUREE_MAXIMALE_PROGRAMME
from admission.ddd.admission.domain.model.enums.authentification import EtatAuthentificationParcours
from admission.ddd.admission.domain.model.enums.condition_acces import recuperer_conditions_acces_par_formation
@@ -79,9 +73,16 @@
EMPTY_CHOICE_AS_LIST,
get_initial_choices_for_additional_approval_conditions,
AdmissionHTMLCharField,
+ AutoGrowTextareaWidget,
)
from admission.forms import get_academic_year_choices
from admission.forms.admission.document import ChangeRequestDocumentForm
+from admission.models import GeneralEducationAdmission, DoctorateAdmission
+from admission.models.base import training_campus_subquery
+from admission.models.checklist import (
+ RefusalReason,
+ AdditionalApprovalCondition,
+)
from admission.views.autocomplete.learning_unit_years import LearningUnitYearAutocomplete
from admission.views.common.detail_tabs.comments import (
COMMENT_TAG_SIC,
@@ -114,7 +115,7 @@
class CommentForm(forms.Form):
comment = forms.CharField(
- widget=forms.Textarea(
+ widget=AutoGrowTextareaWidget(
attrs={
'rows': 2,
'hx-trigger': 'keyup changed delay:2s',
diff --git a/static/admission/autogrow_textarea.css b/static/admission/autogrow_textarea.css
new file mode 100644
index 000000000..1a9673c4a
--- /dev/null
+++ b/static/admission/autogrow_textarea.css
@@ -0,0 +1,42 @@
+/*
+ *
+ * OSIS stands for Open Student Information System. It's an application
+ * designed to manage the core business of higher education institutions,
+ * such as universities, faculties, institutes and professional schools.
+ * The core business involves the administration of students, teachers,
+ * courses, programs and so on.
+ *
+ * Copyright (C) 2015-2024 Université catholique de Louvain (http://www.uclouvain.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * A copy of this license - GNU General Public License - is available
+ * at the root of the source code of this program. If not,
+ * see http://www.gnu.org/licenses/.
+ *
+ */
+
+.autogrow-textarea-wrapper {
+ display: grid;
+}
+
+.autogrow-textarea-wrapper:after {
+ content: attr(data-value) ' ';
+ visibility: hidden;
+ display: block;
+ white-space: pre-wrap;
+}
+
+.autogrow-textarea-wrapper textarea, .autogrow-textarea-wrapper:after {
+ min-height: 100% !important;
+ grid-area: 1 / 1 / 1 / 1;
+ padding: 0.5em;
+}
diff --git a/templates/admission/continuing_education/checklist.html b/templates/admission/continuing_education/checklist.html
index 1eb56f14e..54a47ed78 100644
--- a/templates/admission/continuing_education/checklist.html
+++ b/templates/admission/continuing_education/checklist.html
@@ -268,16 +268,6 @@
$(this).find('.next-option-button').prop('disabled', optionsNumber <=1);
});
- function autogrow () {
- if (this.scrollHeight > this.clientHeight) {
- this.style.height = `${this.scrollHeight}px`;
- }
- }
-
- $('#tabs-content').on('input', 'textarea[name$="-comment"]', function () {
- autogrow.call(this);
- });
-
const menuItems = $('#checklist-menu *[data-toggle="tab"]');
menuItems.on('show.bs.tab', function (e) {
@@ -300,9 +290,6 @@
const tabPaneId = $(this).attr('href');
window.location.hash = tabPaneId;
- // refresh comment height
- autogrow.call($(tabPaneId).find('textarea[name$="-comment"]')[0]);
-
refreshDocuments(tabPaneId);
// refresh info viewer
@@ -352,8 +339,6 @@
// Activate tab from hash on first load
$(`#checklist-menu *[data-toggle="tab"][href="${defaultTab}"]`).click();
-
- $('textarea[name="comment"]:visible').each(autogrow);
});
let bound = false;
@@ -453,10 +438,6 @@
}
});
- $('#tabs-content').on('htmx:afterSettle', function(event){
- $('textarea[name$="-comment"]:visible').each(autogrow);
- });
-
$('#tabs-content').on('htmx:afterRequest', function(event) {
// Do not remove spinner if we are going to refresh the page
if (event.originalEvent.detail.xhr.getResponseHeader('hx-refresh') === 'true') {
@@ -543,6 +524,7 @@
{{ block.super }}
+