1<!-- 2@license 3Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11<link rel="import" href="../polymer/polymer.html"> 12<link rel="import" href="../iron-behaviors/iron-control-state.html"> 13<link rel="import" href="../iron-flex-layout/iron-flex-layout.html"> 14<link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html"> 15<link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html"> 16 17<!-- 18`iron-autogrow-textarea` is an element containing a textarea that grows in height as more 19lines of input are entered. Unless an explicit height or the `maxRows` property is set, it will 20never scroll. 21 22Example: 23 24 <iron-autogrow-textarea></iron-autogrow-textarea> 25 26### Styling 27 28The following custom properties and mixins are available for styling: 29 30Custom property | Description | Default 31----------------|-------------|---------- 32`--iron-autogrow-textarea` | Mixin applied to the textarea | `{}` 33`--iron-autogrow-textarea-placeholder` | Mixin applied to the textarea placeholder | `{}` 34 35@group Iron Elements 36@hero hero.svg 37@demo demo/index.html 38--> 39 40<dom-module id="iron-autogrow-textarea"> 41 42 <style> 43 :host { 44 display: inline-block; 45 position: relative; 46 width: 400px; 47 border: 1px solid; 48 padding: 2px; 49 -moz-appearance: textarea; 50 -webkit-appearance: textarea; 51 overflow: hidden; 52 } 53 54 .mirror-text { 55 visibility: hidden; 56 word-wrap: break-word; 57 } 58 59 .fit { 60 @apply(--layout-fit); 61 } 62 63 textarea { 64 position: relative; 65 outline: none; 66 border: none; 67 resize: none; 68 background: inherit; 69 color: inherit; 70 /* see comments in template */ 71 width: 100%; 72 height: 100%; 73 font-size: inherit; 74 font-family: inherit; 75 line-height: inherit; 76 text-align: inherit; 77 @apply(--iron-autogrow-textarea); 78 } 79 80 ::content textarea:invalid { 81 box-shadow: none; 82 } 83 84 textarea::-webkit-input-placeholder { 85 @apply(--iron-autogrow-textarea-placeholder); 86 } 87 88 textarea:-moz-placeholder { 89 @apply(--iron-autogrow-textarea-placeholder); 90 } 91 92 textarea::-moz-placeholder { 93 @apply(--iron-autogrow-textarea-placeholder); 94 } 95 96 textarea:-ms-input-placeholder { 97 @apply(--iron-autogrow-textarea-placeholder); 98 } 99 </style> 100 <template> 101 <!-- the mirror sizes the input/textarea so it grows with typing --> 102 <!-- use   instead of to allow this element to be used in XHTML --> 103 <div id="mirror" class="mirror-text" aria-hidden="true"> </div> 104 105 <!-- size the input/textarea with a div, because the textarea has intrinsic size in ff --> 106 <div class="textarea-container fit"> 107 <textarea id="textarea" 108 name$="[[name]]" 109 autocomplete$="[[autocomplete]]" 110 autofocus$="[[autofocus]]" 111 inputmode$="[[inputmode]]" 112 placeholder$="[[placeholder]]" 113 readonly$="[[readonly]]" 114 required$="[[required]]" 115 disabled$="[[disabled]]" 116 rows$="[[rows]]" 117 maxlength$="[[maxlength]]"></textarea> 118 </div> 119 </template> 120</dom-module> 121 122<script> 123 124 Polymer({ 125 126 is: 'iron-autogrow-textarea', 127 128 behaviors: [ 129 Polymer.IronFormElementBehavior, 130 Polymer.IronValidatableBehavior, 131 Polymer.IronControlState 132 ], 133 134 properties: { 135 136 /** 137 * Use this property instead of `value` for two-way data binding. 138 * This property will be deprecated in the future. Use `value` instead. 139 * @type {string|number} 140 */ 141 bindValue: { 142 observer: '_bindValueChanged', 143 type: String 144 }, 145 146 /** 147 * The initial number of rows. 148 * 149 * @attribute rows 150 * @type number 151 * @default 1 152 */ 153 rows: { 154 type: Number, 155 value: 1, 156 observer: '_updateCached' 157 }, 158 159 /** 160 * The maximum number of rows this element can grow to until it 161 * scrolls. 0 means no maximum. 162 * 163 * @attribute maxRows 164 * @type number 165 * @default 0 166 */ 167 maxRows: { 168 type: Number, 169 value: 0, 170 observer: '_updateCached' 171 }, 172 173 /** 174 * Bound to the textarea's `autocomplete` attribute. 175 */ 176 autocomplete: { 177 type: String, 178 value: 'off' 179 }, 180 181 /** 182 * Bound to the textarea's `autofocus` attribute. 183 */ 184 autofocus: { 185 type: Boolean, 186 value: false 187 }, 188 189 /** 190 * Bound to the textarea's `inputmode` attribute. 191 */ 192 inputmode: { 193 type: String 194 }, 195 196 /** 197 * Bound to the textarea's `placeholder` attribute. 198 */ 199 placeholder: { 200 type: String 201 }, 202 203 /** 204 * Bound to the textarea's `readonly` attribute. 205 */ 206 readonly: { 207 type: String 208 }, 209 210 /** 211 * Set to true to mark the textarea as required. 212 */ 213 required: { 214 type: Boolean 215 }, 216 217 /** 218 * The maximum length of the input value. 219 */ 220 maxlength: { 221 type: Number 222 } 223 224 }, 225 226 listeners: { 227 'input': '_onInput' 228 }, 229 230 observers: [ 231 '_onValueChanged(value)' 232 ], 233 234 /** 235 * Returns the underlying textarea. 236 * @type HTMLTextAreaElement 237 */ 238 get textarea() { 239 return this.$.textarea; 240 }, 241 242 /** 243 * Returns textarea's selection start. 244 * @type Number 245 */ 246 get selectionStart() { 247 return this.$.textarea.selectionStart; 248 }, 249 250 /** 251 * Returns textarea's selection end. 252 * @type Number 253 */ 254 get selectionEnd() { 255 return this.$.textarea.selectionEnd; 256 }, 257 258 /** 259 * Sets the textarea's selection start. 260 */ 261 set selectionStart(value) { 262 this.$.textarea.selectionStart = value; 263 }, 264 265 /** 266 * Sets the textarea's selection end. 267 */ 268 set selectionEnd(value) { 269 this.$.textarea.selectionEnd = value; 270 }, 271 272 /** 273 * Returns true if `value` is valid. The validator provided in `validator` 274 * will be used first, if it exists; otherwise, the `textarea`'s validity 275 * is used. 276 * @return {boolean} True if the value is valid. 277 */ 278 validate: function() { 279 // Empty, non-required input is valid. 280 if (!this.required && this.value == '') { 281 this.invalid = false; 282 return true; 283 } 284 285 var valid; 286 if (this.hasValidator()) { 287 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); 288 } else { 289 valid = this.$.textarea.validity.valid; 290 this.invalid = !valid; 291 } 292 this.fire('iron-input-validate'); 293 return valid; 294 }, 295 296 _bindValueChanged: function() { 297 var textarea = this.textarea; 298 if (!textarea) { 299 return; 300 } 301 302 // If the bindValue changed manually, then we need to also update 303 // the underlying textarea's value. Otherwise this change was probably 304 // generated from the _onInput handler, and the two values are already 305 // the same. 306 if (textarea.value !== this.bindValue) { 307 textarea.value = !(this.bindValue || this.bindValue === 0) ? '' : this.bindValue; 308 } 309 310 this.value = this.bindValue; 311 this.$.mirror.innerHTML = this._valueForMirror(); 312 // manually notify because we don't want to notify until after setting value 313 this.fire('bind-value-changed', {value: this.bindValue}); 314 }, 315 316 _onInput: function(event) { 317 this.bindValue = event.path ? event.path[0].value : event.target.value; 318 }, 319 320 _constrain: function(tokens) { 321 var _tokens; 322 tokens = tokens || ['']; 323 // Enforce the min and max heights for a multiline input to avoid measurement 324 if (this.maxRows > 0 && tokens.length > this.maxRows) { 325 _tokens = tokens.slice(0, this.maxRows); 326 } else { 327 _tokens = tokens.slice(0); 328 } 329 while (this.rows > 0 && _tokens.length < this.rows) { 330 _tokens.push(''); 331 } 332 // Use   instead of to allow this element to be used in XHTML. 333 return _tokens.join('<br/>') + ' '; 334 }, 335 336 _valueForMirror: function() { 337 var input = this.textarea; 338 if (!input) { 339 return; 340 } 341 this.tokens = (input && input.value) ? input.value.replace(/&/gm, '&').replace(/"/gm, '"').replace(/'/gm, ''').replace(/</gm, '<').replace(/>/gm, '>').split('\n') : ['']; 342 return this._constrain(this.tokens); 343 }, 344 345 _updateCached: function() { 346 this.$.mirror.innerHTML = this._constrain(this.tokens); 347 }, 348 349 _onValueChanged: function() { 350 this.bindValue = this.value; 351 } 352 }); 353</script> 354