From 9d5d843d5b354e376723e91893d9e92eda90f973 Mon Sep 17 00:00:00 2001 From: Ales Rechtorik Date: Thu, 27 Oct 2016 15:10:28 +0200 Subject: [PATCH] feat(textarea): add autosize directive --- src/demo-app/input/input-demo.html | 25 ++++++ src/lib/input/input.html | 1 + src/lib/input/input.ts | 131 ++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 2130e211064e..c1a265b808d1 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -75,6 +75,31 @@

Textarea

+ + Expanding textarea + +
+ + +
+ +

With `rows` explicitly set

+
+ + +
+
+
+ Hello , diff --git a/src/lib/input/input.html b/src/lib/input/input.html index 5b6924d94813..10368dc3d34c 100644 --- a/src/lib/input/input.html +++ b/src/lib/input/input.html @@ -51,6 +51,7 @@ [attr.rows]="rows" [attr.wrap]="wrap" [autofocus]="autofocus" + [mdAutosize]="autosize" [disabled]="disabled" [id]="inputId" [attr.maxlength]="maxlength" diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 3d8b10a3c613..b11cbccc6e6a 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -5,6 +5,8 @@ import { Input, Directive, AfterContentInit, + OnInit, + HostListener, ContentChild, SimpleChange, ContentChildren, @@ -87,6 +89,126 @@ export class MdHint { @Input() align: 'start' | 'end' = 'start'; } + +/** + * Enables auto expanding behaviour for the textarea + * based on: https://github.com/stevepapa/angular2-autosize + */ +@Directive({ + selector: 'textarea[mdAutosize]' +}) +export class MdAutosize implements OnInit { + // public because of AOT + @HostListener('input') + _onChange() { + if (this._isActive) { + this._adjust(); + } + } + + // public because of AOT + @Input('mdAutosize') + _isActive: boolean; + + // public because of AOT + // TODO: change to a setter, to recalculate on change + @Input('attr.rows') + _rows: string; + + private _height: number; + private _lineHeight: number; + + constructor(private _elRef: ElementRef) {} + + ngOnInit() { + console.log(this._isActive); + + if (this._isActive) { + this._elRef.nativeElement.style.overflow = 'hidden'; + + // check is rows is explicitly set, otherwise we don't need to + // care about it(and getting the actual line-height) at all + if (this._rows) { + this._lineHeight = this._getLineHeight(); + this._elRef.nativeElement.style.minHeight = `${+this._rows * this._lineHeight}px`; + } + + this._adjust(); + } + } + + private _adjust() { + // reset height(and rows if it was set by the user) + // to the default values to properly calculate new height + this._elRef.nativeElement.style.height = 'auto'; + + // only need to do this if user explicitly set rows + if (this._rows) { + this._elRef.nativeElement.rows = this._rows; + } + + this._height = +this._elRef.nativeElement.scrollHeight; + + // only need to do this if user explicitly set rows + if (this._rows) { + this._elRef.nativeElement.rows = this._getCalculatedRows(); + } + + this._elRef.nativeElement.style.height = `${this._height}px`; + } + + private _getCalculatedRows() { + return Math.round(this._height / this._lineHeight); + } + + private _getLineHeight(): number { + const el: HTMLTextAreaElement = this._elRef.nativeElement; + + // get the actual computed styles for the element + const computedStyles = window.getComputedStyle(el); + + // if line height is explicitly set(to a pixel value), use that + if (computedStyles.lineHeight && computedStyles.lineHeight.toLowerCase().indexOf('px') >= 0) { + // return stripped of the "px" and as a number + return parseFloat(computedStyles.lineHeight); + } + + // create temporary element + const tempEl = document.createElement(el.nodeName); + + // reset styling, visually hiden the element + // and set its font styles to match the ones of our textarea + tempEl.setAttribute('rows', '1'); + tempEl.setAttribute( + 'style', + ` + margin: 0px; + padding: 0px; + visibility: hidden; + opacity: 0; + font-family: ${computedStyles.fontFamily}; + font-size: ${computedStyles.fontSize}; + ` + ); + + // fill in one row + tempEl.value = '-'; + + // append to parent element to correctly inherit same values + // as the actual textarea + el.parentNode.appendChild(tempEl); + + // get the actual line height + const lineHeight = tempEl.clientHeight; + + // cleanup + tempEl.parentNode.removeChild(tempEl); + + return lineHeight; + } +} + + /** * Component that represents a text input. It encapsulates the HTMLElement and * improve on its behaviour, along with styling it according to the Material Design. @@ -174,6 +296,7 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange private _floatingPlaceholder: boolean = true; private _autofocus: boolean = false; + private _autosize: boolean = false; private _disabled: boolean = false; private _readonly: boolean = false; private _required: boolean = false; @@ -203,6 +326,10 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange get spellcheck(): boolean { return this._spellcheck; } set spellcheck(value) { this._spellcheck = coerceBooleanProperty(value); } + // textarea-specific + @Input() + get autosize(): boolean { return this._autosize; } + set autosize(value) { this._autosize = coerceBooleanProperty(value); } private _blurEmitter: EventEmitter = new EventEmitter(); private _focusEmitter: EventEmitter = new EventEmitter(); @@ -360,9 +487,9 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange @NgModule({ - declarations: [MdPlaceholder, MdInput, MdHint], + declarations: [MdPlaceholder, MdInput, MdHint, MdAutosize], imports: [CommonModule, FormsModule], - exports: [MdPlaceholder, MdInput, MdHint], + exports: [MdPlaceholder, MdInput, MdHint, MdAutosize], }) export class MdInputModule { static forRoot(): ModuleWithProviders {