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 &#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        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 &#160; instead &nbsp; of to allow this element to be used in XHTML.
353      return _tokens.join('<br/>') + '&#160;';
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, '&amp;').replace(/"/gm, '&quot;').replace(/'/gm, '&#39;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;').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