@@ -49,3 +49,124 @@ export function onDomReady(cb) {
4949 cb ( ) ;
5050 }
5151}
52+
53+ // autosize a textarea to fit content. Based on
54+ // https://github.com/github/textarea-autosize
55+ // ---------------------------------------------------------------------
56+ // Copyright (c) 2018 GitHub, Inc.
57+ //
58+ // Permission is hereby granted, free of charge, to any person obtaining
59+ // a copy of this software and associated documentation files (the
60+ // "Software"), to deal in the Software without restriction, including
61+ // without limitation the rights to use, copy, modify, merge, publish,
62+ // distribute, sublicense, and/or sell copies of the Software, and to
63+ // permit persons to whom the Software is furnished to do so, subject to
64+ // the following conditions:
65+ //
66+ // The above copyright notice and this permission notice shall be
67+ // included in all copies or substantial portions of the Software.
68+ // ---------------------------------------------------------------------
69+ export function autosize ( textarea , { viewportMarginBottom = 0 } = { } ) {
70+ let isUserResized = false ;
71+ // lastStyleHeight and initialStyleHeight are CSS values like '100px'
72+ let lastMouseX , lastMouseY , lastStyleHeight , initialStyleHeight ;
73+
74+ function onUserResize ( event ) {
75+ if ( isUserResized ) return ;
76+ if ( lastMouseX !== event . clientX || lastMouseY !== event . clientY ) {
77+ const newStyleHeight = textarea . style . height ;
78+ if ( lastStyleHeight && lastStyleHeight !== newStyleHeight ) {
79+ isUserResized = true ;
80+ }
81+ lastStyleHeight = newStyleHeight ;
82+ }
83+
84+ lastMouseX = event . clientX ;
85+ lastMouseY = event . clientY ;
86+ }
87+
88+ function overflowOffset ( ) {
89+ let offsetTop = 0 ;
90+ let el = textarea ;
91+
92+ while ( el !== document . body && el !== null ) {
93+ offsetTop += el . offsetTop || 0 ;
94+ el = el . offsetParent ;
95+ }
96+
97+ const top = offsetTop - document . defaultView . scrollY ;
98+ const bottom = document . documentElement . clientHeight - ( top + textarea . offsetHeight ) ;
99+ return { top, bottom} ;
100+ }
101+
102+ function resizeToFit ( ) {
103+ if ( isUserResized ) return ;
104+ if ( textarea . offsetWidth <= 0 && textarea . offsetHeight <= 0 ) return ;
105+
106+ try {
107+ const { top, bottom} = overflowOffset ( ) ;
108+ const isOutOfViewport = top < 0 || bottom < 0 ;
109+
110+ const computedStyle = getComputedStyle ( textarea ) ;
111+ const topBorderWidth = parseFloat ( computedStyle . borderTopWidth ) ;
112+ const bottomBorderWidth = parseFloat ( computedStyle . borderBottomWidth ) ;
113+ const isBorderBox = computedStyle . boxSizing === 'border-box' ;
114+ const borderAddOn = isBorderBox ? topBorderWidth + bottomBorderWidth : 0 ;
115+
116+ const adjustedViewportMarginBottom = bottom < viewportMarginBottom ? bottom : viewportMarginBottom ;
117+ const curHeight = parseFloat ( computedStyle . height ) ;
118+ const maxHeight = curHeight + bottom - adjustedViewportMarginBottom ;
119+
120+ textarea . style . height = 'auto' ;
121+ let newHeight = textarea . scrollHeight + borderAddOn ;
122+
123+ if ( isOutOfViewport ) {
124+ // it is already out of the viewport:
125+ // * if the textarea is expanding: do not resize it
126+ if ( newHeight > curHeight ) {
127+ newHeight = curHeight ;
128+ }
129+ // * if the textarea is shrinking, shrink line by line (just use the
130+ // scrollHeight). do not apply max-height limit, otherwise the page
131+ // flickers and the textarea jumps
132+ } else {
133+ // * if it is in the viewport, apply the max-height limit
134+ newHeight = Math . min ( maxHeight , newHeight ) ;
135+ }
136+
137+ textarea . style . height = `${ newHeight } px` ;
138+ lastStyleHeight = textarea . style . height ;
139+ } finally {
140+ // ensure that the textarea is fully scrolled to the end, when the cursor
141+ // is at the end during an input event
142+ if ( textarea . selectionStart === textarea . selectionEnd &&
143+ textarea . selectionStart === textarea . value . length ) {
144+ textarea . scrollTop = textarea . scrollHeight ;
145+ }
146+ }
147+ }
148+
149+ function onFormReset ( ) {
150+ isUserResized = false ;
151+ if ( initialStyleHeight !== undefined ) {
152+ textarea . style . height = initialStyleHeight ;
153+ } else {
154+ textarea . style . removeProperty ( 'height' ) ;
155+ }
156+ }
157+
158+ textarea . addEventListener ( 'mousemove' , onUserResize ) ;
159+ textarea . addEventListener ( 'input' , resizeToFit ) ;
160+ textarea . form ?. addEventListener ( 'reset' , onFormReset ) ;
161+ initialStyleHeight = textarea . style . height ?? undefined ;
162+ if ( textarea . value ) resizeToFit ( ) ;
163+
164+ return {
165+ resizeToFit,
166+ destroy ( ) {
167+ textarea . removeEventListener ( 'mousemove' , onUserResize ) ;
168+ textarea . removeEventListener ( 'input' , resizeToFit ) ;
169+ textarea . form ?. removeEventListener ( 'reset' , onFormReset ) ;
170+ }
171+ } ;
172+ }
0 commit comments