• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &#160; instead &nbsp; of to allow this element to be used in XHTML -->
103    <div id="mirror" class="mirror-text" aria-hidden="true">&#160;</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 &#160; instead &nbsp; of to allow this element to be used in XHTML.
333      return _tokens.join('<br/>') + '&#160;';
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, '&amp;').replace(/"/gm, '&quot;').replace(/'/gm, '&#39;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;').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