1 <!--
2 @license
3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7 Code distributed by Google as part of the polymer project is also
8 subject 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
19 lines of input are entered. Unless an explicit height or the `maxRows` property is set, it will
20 never scroll.
21
22 Example:
23
24 <iron-autogrow-textarea></iron-autogrow-textarea>
25
26 ### Styling
27
28 The following custom properties and mixins are available for styling:
29
30 Custom 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('
') + ' ';
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