From c00ebb5b2d00c7a195e7b51531e19780c8a759b7 Mon Sep 17 00:00:00 2001 From: michaldev0 <125133223+michaldev0@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:53:25 +0200 Subject: [PATCH 1/2] OLMIS-8191: long-text wrap mechanism --- CHANGELOG.md | 1 + src/common/_variables.scss | 4 + src/openlmis-table-form/_table-forms.scss | 19 +++- ...ive.js => openlmis-long-text.directive.js} | 61 +++++++------ .../openlmis-long-text.directive.spec.js | 86 +++++++++++++++++++ .../textarea-auto-resize.directive.spec.js | 79 ----------------- 6 files changed, 140 insertions(+), 110 deletions(-) rename src/openlmis-table-form/{textarea-auto-resize.directive.js => openlmis-long-text.directive.js} (58%) create mode 100644 src/openlmis-table-form/openlmis-long-text.directive.spec.js delete mode 100644 src/openlmis-table-form/textarea-auto-resize.directive.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b2eabb14..e1750dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 7.2.17-SNAPSHOT (WIP) ================== +* [OLMIS-8191](https://openlmis.atlassian.net/browse/OLMIS-8191): Add openlmis-long-text class and directive so long free-text in table cells grows with content then wraps — for both the editable textarea and its read-only display. 7.2.16 / 2026-06-09 ================== diff --git a/src/common/_variables.scss b/src/common/_variables.scss index a7948a82..02987684 100644 --- a/src/common/_variables.scss +++ b/src/common/_variables.scss @@ -65,6 +65,10 @@ $padding-small: 0.5em; $padding: (2 * $padding-small); $padding-large: (2 * $padding); +// openlmis-long-text widen-then-wrap: cap = where wrapping starts, floor = editable typing room. +$openlmis-long-text-max-width: 35em !default; +$openlmis-long-text-min-width: 14em !default; + $margin: 1em; $padding2px:2px; diff --git a/src/openlmis-table-form/_table-forms.scss b/src/openlmis-table-form/_table-forms.scss index 38193352..ea3865e4 100644 --- a/src/openlmis-table-form/_table-forms.scss +++ b/src/openlmis-table-form/_table-forms.scss @@ -142,11 +142,24 @@ td > label.checkbox { width: 100%; } -.openlmis-table td > div.input-control > textarea { - min-width: 14em; - max-width: 20em; +// Long free-text in a table column: widens with content, then wraps. Opt-in class on the +// editable textarea (sized by the openlmisLongText directive) and the read-only span. +.openlmis-long-text { + display: inline-block; + max-width: $openlmis-long-text-max-width; white-space: pre-wrap; + overflow-wrap: break-word; overflow: hidden; resize: none; vertical-align: middle; } + +// Editable: minimum typing width (the directive sets the actual width). +textarea.openlmis-long-text { + min-width: $openlmis-long-text-min-width; +} + +// Read-only: size to content up to the cap, so short cells stay narrow and long text wraps. +span.openlmis-long-text { + width: max-content; +} diff --git a/src/openlmis-table-form/textarea-auto-resize.directive.js b/src/openlmis-table-form/openlmis-long-text.directive.js similarity index 58% rename from src/openlmis-table-form/textarea-auto-resize.directive.js rename to src/openlmis-table-form/openlmis-long-text.directive.js index ab28a548..5859399a 100644 --- a/src/openlmis-table-form/textarea-auto-resize.directive.js +++ b/src/openlmis-table-form/openlmis-long-text.directive.js @@ -18,53 +18,67 @@ /** * @ngdoc directive - * @restrict E - * @name openlmis-table-form.directive:textareaAutoResize + * @restrict C + * @name openlmis-table-form.directive:openlmisLongText * * @description - * Adds auto-resize to textarea elements inside table cells. The textarea grows - * horizontally with its content up to the column max-width (set in CSS); once the - * content reaches that width it wraps and the textarea grows vertically instead. + * Shared behavior for long free-text fields shown in a constrained table column. + * Add the `openlmis-long-text` class to opt a field in: it grows horizontally with + * its content up to the column max-width (set in CSS), then wraps and grows + * vertically. The class is the single opt-in flag and covers both the editable side + * (a textarea, resized here) and the read-only side (a span, handled purely by CSS). * * @example * ``` - * + * + * {{model}} * ``` */ - var GHOST_ID = 'openlmisTextareaAutoResizeGhost'; + var GHOST_ID = 'openlmisLongTextGhost'; var GHOST_STYLE = 'display:inline-block;position:absolute;top:0;left:-9999px;' + 'visibility:hidden;height:0;overflow:hidden;white-space:pre;'; var MEASURED_PROPERTIES = [ 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth', 'boxSizing' ]; - angular .module('openlmis-table-form') - .directive('textarea', textareaAutoResize); + .directive('openlmisLongText', openlmisLongText); - function textareaAutoResize() { + function openlmisLongText() { var directive = { link: link, - restrict: 'E' + restrict: 'C' }; return directive; + // A textarea's size comes from its cols/rows, not its text, so measure the content and + // set the width and height. (The read-only span does the same purely in CSS.) function link(scope, element) { - if (shouldSkipElement(element)) { + var el = element[0]; + + if (el.tagName !== 'TEXTAREA') { return; } - var el = element[0]; scope.$watch(function() { return el.value; }, function() { - el.style.width = measureContentWidth(el) + 'px'; - el.style.height = 'auto'; - el.style.height = el.scrollHeight + 'px'; + resize(el); }); } + function resize(el) { + // Skip while hidden: a display:none textarea has scrollHeight 0, which would stick it + // at height:0 once shown. Typing re-runs this when it is visible. + if (el.offsetParent === null) { + return; + } + el.style.width = measureContentWidth(el) + 'px'; + el.style.height = 'auto'; + el.style.height = el.scrollHeight + 'px'; + } + function measureContentWidth(el) { var ghost = getGhost(); var style = window.getComputedStyle(el); @@ -74,7 +88,9 @@ }); ghost.textContent = el.value || el.getAttribute('placeholder') || ''; - return ghost.offsetWidth; + // Sub-pixel width rounded up. offsetWidth rounds down and can wrap the last word + // mid-typing; ceil of the real width is the smallest integer that still fits. + return Math.ceil(ghost.getBoundingClientRect().width); } function getGhost() { @@ -87,17 +103,6 @@ } return ghost; } - - function shouldSkipElement(element) { - return element.parents().length < 2 || isOutsideTd(element); - } - - function isOutsideTd(element) { - var parents = element.parents(); - - return parents[1].localName !== 'td' || parents[0].localName !== 'div' || - !parents[0].classList.contains('input-control'); - } } })(); diff --git a/src/openlmis-table-form/openlmis-long-text.directive.spec.js b/src/openlmis-table-form/openlmis-long-text.directive.spec.js new file mode 100644 index 00000000..eebee838 --- /dev/null +++ b/src/openlmis-table-form/openlmis-long-text.directive.spec.js @@ -0,0 +1,86 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('openlmisLongText directive', function() { + + beforeEach(function() { + module('openlmis-table-form'); + + inject(function($injector) { + this.$compile = $injector.get('$compile'); + this.$rootScope = $injector.get('$rootScope'); + }); + + this.$scope = this.$rootScope.$new(); + + this.compileMarkup = function(markup) { + const element = this.$compile(markup)(this.$scope); + angular.element('body').append(element); + this.$scope.$apply(); + return element; + }; + }); + + it('should set an explicit width and height on a textarea with the class', function() { + const textarea = this.compileMarkup(''); + + expect(Number.parseInt(textarea[0].style.width, 10)).toBeGreaterThan(0); + expect(Number.parseInt(textarea[0].style.height, 10)).toBeGreaterThan(0); + }); + + it('should grow width when the value becomes longer', function() { + const textarea = this.compileMarkup(''); + const previousWidth = Number.parseInt(textarea[0].style.width, 10); + + textarea[0].value = 'a value that is clearly longer than the initial one'; + this.$scope.$apply(); + + expect(Number.parseInt(textarea[0].style.width, 10)).toBeGreaterThan(previousWidth); + }); + + it('should grow height when the value spans multiple lines', function() { + const textarea = this.compileMarkup(''); + const previousHeight = Number.parseInt(textarea[0].style.height, 10); + + textarea[0].value = 'first line\nsecond line\nthird line\nfourth line'; + this.$scope.$apply(); + + expect(Number.parseInt(textarea[0].style.height, 10)).toBeGreaterThan(previousHeight); + }); + + it('should not resize a textarea that does not have the class', function() { + const textarea = this.compileMarkup(''); + + expect(textarea[0].style.width).toEqual(''); + expect(textarea[0].style.height).toEqual(''); + }); + + it('should not set an inline size on a read-only span with the class', function() { + const span = this.compileMarkup('read only'); + + expect(span[0].style.width).toEqual(''); + expect(span[0].style.height).toEqual(''); + }); + + it('should not collapse a textarea that is hidden when linked', function() { + const textarea = this.compileMarkup( + '' + ); + + // A hidden textarea reports scrollHeight 0; sizing it then would bake height:0 and + // leave it collapsed once shown (e.g. an ng-show free-text comment). It must be skipped. + expect(textarea[0].style.height).not.toEqual('0px'); + }); +}); diff --git a/src/openlmis-table-form/textarea-auto-resize.directive.spec.js b/src/openlmis-table-form/textarea-auto-resize.directive.spec.js deleted file mode 100644 index cd5879c6..00000000 --- a/src/openlmis-table-form/textarea-auto-resize.directive.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * This program is part of the OpenLMIS logistics management information system platform software. - * Copyright © 2017 VillageReach - * - * This program is free software: you can redistribute it and/or modify it under the terms - * of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of - * the GNU Affero General Public License along with this program. If not, see - * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  - */ - -describe('Textarea automatic resize directive', function() { - - beforeEach(function() { - module('openlmis-table-form'); - - inject(function($injector) { - this.$compile = $injector.get('$compile'); - this.$rootScope = $injector.get('$rootScope'); - }); - - this.compileMarkup = function(markup) { - const element = this.$compile(markup)(this.$scope); - angular.element('body').append(element); - this.$scope.$apply(); - return element; - }; - - this.$scope = this.$rootScope.$new(); - - const markup = - '
' + - '' + - '
'; - - this.html = this.compileMarkup(markup); - this.textarea = this.html.find('textarea'); - }); - - it('should set an explicit width and height when textarea has a value', function() { - expect(Number.parseInt(this.textarea[0].style.width, 10)).toBeGreaterThan(0); - expect(Number.parseInt(this.textarea[0].style.height, 10)).toBeGreaterThan(0); - }); - - it('should grow width when value becomes longer', function() { - const previousWidth = Number.parseInt(this.textarea[0].style.width, 10); - - this.textarea[0].value = 'a value that is clearly longer than the initial one'; - this.$scope.$apply(); - - expect(Number.parseInt(this.textarea[0].style.width, 10)).toBeGreaterThan(previousWidth); - }); - - it('should grow height when value spans multiple lines', function() { - const previousHeight = Number.parseInt(this.textarea[0].style.height, 10); - - this.textarea[0].value = 'first line\nsecond line\nthird line\nfourth line'; - this.$scope.$apply(); - - expect(Number.parseInt(this.textarea[0].style.height, 10)).toBeGreaterThan(previousHeight); - }); - - it('should do nothing for a textarea outside of a table cell', function() { - const markup = - '
' + - '' + - '
'; - - const element = this.compileMarkup(markup); - const textarea = element.find('textarea'); - - expect(textarea[0].style.width).toEqual(''); - expect(textarea[0].style.height).toEqual(''); - }); -}); From d95eb3ba92982436e11b2ba657fad2eadab9edd3 Mon Sep 17 00:00:00 2001 From: Denys Mamchura Date: Tue, 21 Jul 2026 13:35:58 +0200 Subject: [PATCH 2/2] OLMIS-8191: size openlmis-long-text field when shown after being hidden The openlmisLongText directive only re-measured on value change, so a pre-filled textarea that starts hidden (ng-show/ng-if) and is shown later stayed at one row until edited. Watch visibility (offsetParent) too, so the field is sized once it becomes visible. Adds a regression test and notes OLMIS-8192 in the changelog. --- CHANGELOG.md | 2 +- .../openlmis-long-text.directive.js | Bin 4246 -> 4585 bytes .../openlmis-long-text.directive.spec.js | 16 ++++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1750dfe..d93296b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ 7.2.17-SNAPSHOT (WIP) ================== -* [OLMIS-8191](https://openlmis.atlassian.net/browse/OLMIS-8191): Add openlmis-long-text class and directive so long free-text in table cells grows with content then wraps — for both the editable textarea and its read-only display. +* [OLMIS-8191](https://openlmis.atlassian.net/browse/OLMIS-8191) / [OLMIS-8192](https://openlmis.atlassian.net/browse/OLMIS-8192): Add openlmis-long-text class and directive so long free-text in table cells grows with content then wraps — for both the editable textarea and its read-only display. 7.2.16 / 2026-06-09 ================== diff --git a/src/openlmis-table-form/openlmis-long-text.directive.js b/src/openlmis-table-form/openlmis-long-text.directive.js index 5859399a1c5265144e6629fbc7b671d1844b45cf..18374bd6cfc12d092306d034641bac579a06af13 100644 GIT binary patch delta 379 zcmZ8dJ5Iwu5T&3p9Vd8EiX=j|d!)3~2+>lk?Xh>_%`Vy<$1zO~5s3?M0VHY;!V$Ox zVr`I!*kZJrH}gL8Jo}u#ZjwWugZM0+sv-KLbGiG&sNxt&9esY9fNvRP%#e0r{IYh~lMk@uC LZcA_P7a!-}1O0#n delta 49 zcmaE