@@ -174,6 +174,8 @@ function labelDirective() {
174
174
* PRESENT. The placeholder text is copied to the aria-label attribute.
175
175
* @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
176
176
* @param md-no-asterisk {boolean=} When present, an asterisk will not be appended to the inputs floating label
177
+ * @param md-no-resize {boolean=} Disables the textarea resize handle.
178
+ * @param {number= } max-rows The maximum amount of rows for a textarea.
177
179
* @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are
178
180
* revealed after being hidden. This is off by default for performance reasons because it
179
181
* guarantees a reflow every digest cycle.
@@ -259,9 +261,21 @@ function labelDirective() {
259
261
* error animation effects. Therefore, it is *not* advised to use the Layout system inside of the
260
262
* `<md-input-container>` tags. Instead, use relative or absolute positioning.
261
263
*
264
+ *
265
+ * <h3>Textarea directive</h3>
266
+ * The `textarea` element within a `md-input-container` has the following specific behavior:
267
+ * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow`
268
+ * attribute.
269
+ * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will
270
+ * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text
271
+ * high initially. If no rows are specified, the directive defaults to 1.
272
+ * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute.
273
+ * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically.
274
+ * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a
275
+ * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute.
262
276
*/
263
277
264
- function inputTextareaDirective ( $mdUtil , $window , $mdAria , $timeout ) {
278
+ function inputTextareaDirective ( $mdUtil , $window , $mdAria , $timeout , $mdGesture ) {
265
279
return {
266
280
restrict : 'E' ,
267
281
require : [ '^?mdInputContainer' , '?ngModel' ] ,
@@ -379,13 +393,16 @@ function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout) {
379
393
}
380
394
381
395
function setupTextarea ( ) {
382
- if ( attr . hasOwnProperty ( 'mdNoAutogrow' ) ) {
383
- return ;
384
- }
396
+ var isAutogrowing = ! attr . hasOwnProperty ( 'mdNoAutogrow' ) ;
397
+
398
+ attachResizeHandle ( ) ;
399
+
400
+ if ( ! isAutogrowing ) return ;
385
401
386
402
// Can't check if height was or not explicity set,
387
403
// so rows attribute will take precedence if present
388
404
var minRows = attr . hasOwnProperty ( 'rows' ) ? parseInt ( attr . rows ) : NaN ;
405
+ var maxRows = attr . hasOwnProperty ( 'maxRows' ) ? parseInt ( attr . maxRows ) : NaN ;
389
406
var lineHeight = null ;
390
407
var node = element [ 0 ] ;
391
408
@@ -395,78 +412,151 @@ function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout) {
395
412
$mdUtil . nextTick ( growTextarea ) ;
396
413
} , 10 , false ) ;
397
414
398
- // We can hook into Angular's pipeline, instead of registering a new listener.
399
- // Note that we should use `$parsers`, as opposed to `$viewChangeListeners` which
400
- // was used before, because `$viewChangeListeners` don't fire if the input is
401
- // invalid.
415
+ // We could leverage ngModel's $parsers here, however it
416
+ // isn't reliable, because Angular trims the input by default,
417
+ // which means that growTextarea won't fire when newlines and
418
+ // spaces are added.
419
+ element . on ( 'input' , growTextarea ) ;
420
+
421
+ // We should still use the $formatters, because they fire when
422
+ // the value was changed from outside the textarea.
402
423
if ( hasNgModel ) {
403
- ngModelCtrl . $formatters . unshift ( pipelineListener ) ;
404
- ngModelCtrl . $parsers . unshift ( pipelineListener ) ;
405
- } else {
406
- // Note that it's safe to use the `input` event since we're not supporting IE9 and below.
407
- element . on ( 'input' , growTextarea ) ;
424
+ ngModelCtrl . $formatters . push ( formattersListener ) ;
408
425
}
409
426
410
427
if ( ! minRows ) {
411
- element
412
- . attr ( 'rows' , 1 )
413
- . on ( 'scroll' , onScroll ) ;
428
+ element . attr ( 'rows' , 1 ) ;
414
429
}
415
430
416
431
angular . element ( $window ) . on ( 'resize' , growTextarea ) ;
417
-
418
- scope . $on ( '$destroy' , function ( ) {
419
- angular . element ( $window ) . off ( 'resize' , growTextarea ) ;
420
- } ) ;
432
+ scope . $on ( '$destroy' , disableAutogrow ) ;
421
433
422
434
function growTextarea ( ) {
423
435
// temporarily disables element's flex so its height 'runs free'
424
436
element
425
- . addClass ( 'md-no-flex' )
426
- . attr ( 'rows' , 1 ) ;
427
-
428
- if ( minRows ) {
429
- if ( ! lineHeight ) {
430
- node . style . minHeight = 0 ;
431
- lineHeight = element . prop ( 'clientHeight' ) ;
432
- node . style . minHeight = null ;
433
- }
437
+ . attr ( 'rows' , 1 )
438
+ . css ( 'height' , 'auto' )
439
+ . addClass ( 'md-no-flex' ) ;
434
440
435
- var newRows = Math . round ( Math . round ( getHeight ( ) / lineHeight ) ) ;
436
- var rowsToSet = Math . min ( newRows , minRows ) ;
441
+ var height = getHeight ( ) ;
437
442
438
- element
439
- . css ( 'height' , lineHeight * rowsToSet + 'px' )
440
- . attr ( 'rows' , rowsToSet )
441
- . toggleClass ( '_md-textarea-scrollable' , newRows >= minRows ) ;
443
+ if ( ! lineHeight ) {
444
+ // offsetHeight includes padding which can throw off our value
445
+ lineHeight = element . css ( 'padding' , 0 ) . prop ( 'offsetHeight' ) ;
446
+ element . css ( 'padding' , null ) ;
447
+ }
442
448
443
- } else {
444
- element . css ( 'height' , 'auto' ) ;
445
- node . scrollTop = 0 ;
446
- var height = getHeight ( ) ;
447
- if ( height ) element . css ( 'height' , height + 'px' ) ;
449
+ if ( minRows && lineHeight ) {
450
+ height = Math . max ( height , lineHeight * minRows ) ;
451
+ }
452
+
453
+ if ( maxRows && lineHeight ) {
454
+ var maxHeight = lineHeight * maxRows ;
455
+
456
+ if ( maxHeight < height ) {
457
+ element . attr ( 'md-no-autogrow' , '' ) ;
458
+ height = maxHeight ;
459
+ } else {
460
+ element . removeAttr ( 'md-no-autogrow' ) ;
461
+ }
462
+ }
463
+
464
+ if ( lineHeight ) {
465
+ element . attr ( 'rows' , Math . round ( height / lineHeight ) ) ;
448
466
}
449
467
450
- element . removeClass ( 'md-no-flex' ) ;
468
+ element
469
+ . css ( 'height' , height + 'px' )
470
+ . removeClass ( 'md-no-flex' ) ;
451
471
}
452
472
453
473
function getHeight ( ) {
454
474
var offsetHeight = node . offsetHeight ;
455
475
var line = node . scrollHeight - offsetHeight ;
456
- return offsetHeight + ( line > 0 ? line : 0 ) ;
476
+ return offsetHeight + Math . max ( line , 0 ) ;
457
477
}
458
478
459
- function onScroll ( e ) {
460
- node . scrollTop = 0 ;
461
- // for smooth new line adding
462
- var line = node . scrollHeight - node . offsetHeight ;
463
- var height = node . offsetHeight + line ;
464
- node . style . height = height + 'px' ;
479
+ function formattersListener ( value ) {
480
+ $mdUtil . nextTick ( growTextarea ) ;
481
+ return value ;
465
482
}
466
483
467
- function pipelineListener ( value ) {
468
- growTextarea ( ) ;
469
- return value ;
484
+ function disableAutogrow ( ) {
485
+ if ( ! isAutogrowing ) return ;
486
+
487
+ isAutogrowing = false ;
488
+ angular . element ( $window ) . off ( 'resize' , growTextarea ) ;
489
+ element
490
+ . attr ( 'md-no-autogrow' , '' )
491
+ . off ( 'input' , growTextarea ) ;
492
+
493
+ if ( hasNgModel ) {
494
+ var listenerIndex = ngModelCtrl . $formatters . indexOf ( formattersListener ) ;
495
+
496
+ if ( listenerIndex > - 1 ) {
497
+ ngModelCtrl . $formatters . splice ( listenerIndex , 1 ) ;
498
+ }
499
+ }
500
+ }
501
+
502
+ function attachResizeHandle ( ) {
503
+ if ( attr . hasOwnProperty ( 'mdNoResize' ) ) return ;
504
+
505
+ var handle = angular . element ( '<div class="md-resize-handle"></div>' ) ;
506
+ var isDragging = false ;
507
+ var dragStart = null ;
508
+ var startHeight = 0 ;
509
+ var container = containerCtrl . element ;
510
+ var dragGestureHandler = $mdGesture . register ( handle , 'drag' , { horizontal : false } ) ;
511
+
512
+ element . after ( handle ) ;
513
+ handle . on ( 'mousedown' , onMouseDown ) ;
514
+
515
+ container
516
+ . on ( '$md.dragstart' , onDragStart )
517
+ . on ( '$md.drag' , onDrag )
518
+ . on ( '$md.dragend' , onDragEnd ) ;
519
+
520
+ scope . $on ( '$destroy' , function ( ) {
521
+ handle
522
+ . off ( 'mousedown' , onMouseDown )
523
+ . remove ( ) ;
524
+
525
+ container
526
+ . off ( '$md.dragstart' , onDragStart )
527
+ . off ( '$md.drag' , onDrag )
528
+ . off ( '$md.dragend' , onDragEnd ) ;
529
+
530
+ dragGestureHandler ( ) ;
531
+ handle = null ;
532
+ container = null ;
533
+ dragGestureHandler = null ;
534
+ } ) ;
535
+
536
+ function onMouseDown ( ev ) {
537
+ ev . preventDefault ( ) ;
538
+ isDragging = true ;
539
+ dragStart = ev . clientY ;
540
+ startHeight = parseFloat ( element . css ( 'height' ) ) || element . prop ( 'offsetHeight' ) ;
541
+ }
542
+
543
+ function onDragStart ( ev ) {
544
+ if ( ! isDragging ) return ;
545
+ ev . preventDefault ( ) ;
546
+ disableAutogrow ( ) ;
547
+ container . addClass ( 'md-input-resized' ) ;
548
+ }
549
+
550
+ function onDrag ( ev ) {
551
+ if ( ! isDragging ) return ;
552
+ element . css ( 'height' , startHeight + ( ev . pointer . y - dragStart ) + 'px' ) ;
553
+ }
554
+
555
+ function onDragEnd ( ev ) {
556
+ if ( ! isDragging ) return ;
557
+ isDragging = false ;
558
+ container . removeClass ( 'md-input-resized' ) ;
559
+ }
470
560
}
471
561
472
562
// Attach a watcher to detect when the textarea gets shown.
0 commit comments