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 <template> 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 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 minlength$="[[minlength]]" 118 maxlength$="[[maxlength]]"></textarea> 119 </div> 120 </template> 121</dom-module> 122 123<script> 124 125 Polymer({ 126 127 is: 'iron-autogrow-textarea', 128 129 behaviors: [ 130 Polymer.IronFormElementBehavior, 131 Polymer.IronValidatableBehavior, 132 Polymer.IronControlState 133 ], 134 135 properties: { 136 137 /** 138 * Use this property instead of `value` for two-way data binding. 139 * This property will be deprecated in the future. Use `value` instead. 140 * @type {string|number} 141 */ 142 bindValue: { 143 observer: '_bindValueChanged', 144 type: String 145 }, 146 147 /** 148 * The initial number of rows. 149 * 150 * @attribute rows 151 * @type number 152 * @default 1 153 */ 154 rows: { 155 type: Number, 156 value: 1, 157 observer: '_updateCached' 158 }, 159 160 /** 161 * The maximum number of rows this element can grow to until it 162 * scrolls. 0 means no maximum. 163 * 164 * @attribute maxRows 165 * @type number 166 * @default 0 167 */ 168 maxRows: { 169 type: Number, 170 value: 0, 171 observer: '_updateCached' 172 }, 173 174 /** 175 * Bound to the textarea's `autocomplete` attribute. 176 */ 177 autocomplete: { 178 type: String, 179 value: 'off' 180 }, 181 182 /** 183 * Bound to the textarea's `autofocus` attribute. 184 */ 185 autofocus: { 186 type: Boolean, 187 value: false 188 }, 189 190 /** 191 * Bound to the textarea's `inputmode` attribute. 192 */ 193 inputmode: { 194 type: String 195 }, 196 197 /** 198 * Bound to the textarea's `placeholder` attribute. 199 */ 200 placeholder: { 201 type: String 202 }, 203 204 /** 205 * Bound to the textarea's `readonly` attribute. 206 */ 207 readonly: { 208 type: String 209 }, 210 211 /** 212 * Set to true to mark the textarea as required. 213 */ 214 required: { 215 type: Boolean 216 }, 217 218 /** 219 * The minimum length of the input value. 220 */ 221 minlength: { 222 type: Number 223 }, 224 225 /** 226 * The maximum length of the input value. 227 */ 228 maxlength: { 229 type: Number 230 } 231 232 }, 233 234 listeners: { 235 'input': '_onInput' 236 }, 237 238 observers: [ 239 '_onValueChanged(value)' 240 ], 241 242 /** 243 * Returns the underlying textarea. 244 * @type HTMLTextAreaElement 245 */ 246 get textarea() { 247 return this.$.textarea; 248 }, 249 250 /** 251 * Returns textarea's selection start. 252 * @type Number 253 */ 254 get selectionStart() { 255 return this.$.textarea.selectionStart; 256 }, 257 258 /** 259 * Returns textarea's selection end. 260 * @type Number 261 */ 262 get selectionEnd() { 263 return this.$.textarea.selectionEnd; 264 }, 265 266 /** 267 * Sets the textarea's selection start. 268 */ 269 set selectionStart(value) { 270 this.$.textarea.selectionStart = value; 271 }, 272 273 /** 274 * Sets the textarea's selection end. 275 */ 276 set selectionEnd(value) { 277 this.$.textarea.selectionEnd = value; 278 }, 279 280 attached: function() { 281 /* iOS has an arbitrary left margin of 3px that isn't present 282 * in any other browser, and means that the paper-textarea's cursor 283 * overlaps the label. 284 * See https://github.com/PolymerElements/paper-input/issues/468. 285 */ 286 var IS_IOS = navigator.userAgent.match(/iP(?:[oa]d|hone)/); 287 if (IS_IOS) { 288 this.$.textarea.style.marginLeft = '-3px'; 289 } 290 }, 291 292 /** 293 * Returns true if `value` is valid. The validator provided in `validator` 294 * will be used first, if it exists; otherwise, the `textarea`'s validity 295 * is used. 296 * @return {boolean} True if the value is valid. 297 */ 298 validate: function() { 299 // Empty, non-required input is valid. 300 if (!this.required && this.value == '') { 301 this.invalid = false; 302 return true; 303 } 304 305 var valid; 306 if (this.hasValidator()) { 307 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); 308 } else { 309 valid = this.$.textarea.validity.valid; 310 this.invalid = !valid; 311 } 312 this.fire('iron-input-validate'); 313 return valid; 314 }, 315 316 _bindValueChanged: function() { 317 var textarea = this.textarea; 318 if (!textarea) { 319 return; 320 } 321 322 // If the bindValue changed manually, then we need to also update 323 // the underlying textarea's value. Otherwise this change was probably 324 // generated from the _onInput handler, and the two values are already 325 // the same. 326 if (textarea.value !== this.bindValue) { 327 textarea.value = !(this.bindValue || this.bindValue === 0) ? '' : this.bindValue; 328 } 329 330 this.value = this.bindValue; 331 this.$.mirror.innerHTML = this._valueForMirror(); 332 // manually notify because we don't want to notify until after setting value 333 this.fire('bind-value-changed', {value: this.bindValue}); 334 }, 335 336 _onInput: function(event) { 337 this.bindValue = event.path ? event.path[0].value : event.target.value; 338 }, 339 340 _constrain: function(tokens) { 341 var _tokens; 342 tokens = tokens || ['']; 343 // Enforce the min and max heights for a multiline input to avoid measurement 344 if (this.maxRows > 0 && tokens.length > this.maxRows) { 345 _tokens = tokens.slice(0, this.maxRows); 346 } else { 347 _tokens = tokens.slice(0); 348 } 349 while (this.rows > 0 && _tokens.length < this.rows) { 350 _tokens.push(''); 351 } 352 // Use   instead of to allow this element to be used in XHTML. 353 return _tokens.join('<br/>') + ' '; 354 }, 355 356 _valueForMirror: function() { 357 var input = this.textarea; 358 if (!input) { 359 return; 360 } 361 this.tokens = (input && input.value) ? input.value.replace(/&/gm, '&').replace(/"/gm, '"').replace(/'/gm, ''').replace(/</gm, '<').replace(/>/gm, '>').split('\n') : ['']; 362 return this._constrain(this.tokens); 363 }, 364 365 _updateCached: function() { 366 this.$.mirror.innerHTML = this._constrain(this.tokens); 367 }, 368 369 _onValueChanged: function() { 370 this.bindValue = this.value; 371 } 372 }); 373</script> 374