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 }} +