2015-06-26 20:27:38 -07:00
|
|
|
<!--
|
|
|
|
@license
|
|
|
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
|
|
Code distributed by Google as part of the polymer project is also
|
|
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
|
|
-->
|
|
|
|
|
|
|
|
<link rel="import" href="../polymer/polymer.html">
|
|
|
|
<link rel="import" href="../iron-behaviors/iron-control-state.html">
|
2015-12-14 08:43:03 -07:00
|
|
|
<link rel="import" href="../iron-flex-layout/iron-flex-layout.html">
|
2015-06-26 20:27:38 -07:00
|
|
|
<link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html">
|
2015-07-13 14:26:11 -07:00
|
|
|
<link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html">
|
2015-06-26 20:27:38 -07:00
|
|
|
|
|
|
|
<!--
|
|
|
|
`iron-autogrow-textarea` is an element containing a textarea that grows in height as more
|
|
|
|
lines of input are entered. Unless an explicit height or the `maxRows` property is set, it will
|
|
|
|
never scroll.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
2015-08-05 18:21:18 -07:00
|
|
|
<iron-autogrow-textarea></iron-autogrow-textarea>
|
2015-06-26 20:27:38 -07:00
|
|
|
|
|
|
|
Because the `textarea`'s `value` property is not observable, you should use
|
|
|
|
this element's `bind-value` instead for imperative updates.
|
|
|
|
|
2015-10-07 18:49:40 -07:00
|
|
|
### Styling
|
2015-12-14 08:43:03 -07:00
|
|
|
|
2015-10-07 18:49:40 -07:00
|
|
|
The following custom properties and mixins are available for styling:
|
2015-12-14 08:43:03 -07:00
|
|
|
|
2015-10-07 18:49:40 -07:00
|
|
|
Custom property | Description | Default
|
|
|
|
----------------|-------------|----------
|
|
|
|
`--iron-autogrow-textarea` | Mixin applied to the textarea | `{}`
|
2016-02-18 11:20:10 -07:00
|
|
|
`--iron-autogrow-textarea-placeholder` | Mixin applied to the textarea placeholder | `{}`
|
2015-10-07 18:49:40 -07:00
|
|
|
|
2015-06-26 20:27:38 -07:00
|
|
|
@group Iron Elements
|
|
|
|
@hero hero.svg
|
|
|
|
@demo demo/index.html
|
|
|
|
-->
|
|
|
|
|
|
|
|
<dom-module id="iron-autogrow-textarea">
|
|
|
|
|
|
|
|
<style>
|
|
|
|
:host {
|
|
|
|
display: inline-block;
|
|
|
|
position: relative;
|
|
|
|
width: 400px;
|
|
|
|
border: 1px solid;
|
|
|
|
padding: 2px;
|
|
|
|
-moz-appearance: textarea;
|
|
|
|
-webkit-appearance: textarea;
|
2016-02-18 11:20:10 -07:00
|
|
|
overflow: hidden;
|
2015-06-26 20:27:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
.mirror-text {
|
|
|
|
visibility: hidden;
|
|
|
|
word-wrap: break-word;
|
|
|
|
}
|
|
|
|
|
2015-12-14 08:43:03 -07:00
|
|
|
.fit {
|
|
|
|
@apply(--layout-fit);
|
|
|
|
}
|
|
|
|
|
2015-06-26 20:27:38 -07:00
|
|
|
textarea {
|
|
|
|
position: relative;
|
|
|
|
outline: none;
|
|
|
|
border: none;
|
|
|
|
resize: none;
|
|
|
|
background: inherit;
|
2015-07-13 14:26:11 -07:00
|
|
|
color: inherit;
|
2015-06-26 20:27:38 -07:00
|
|
|
/* see comments in template */
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
font-size: inherit;
|
|
|
|
font-family: inherit;
|
2015-08-05 18:21:18 -07:00
|
|
|
line-height: inherit;
|
2015-12-14 08:43:03 -07:00
|
|
|
text-align: inherit;
|
2015-10-07 18:49:40 -07:00
|
|
|
@apply(--iron-autogrow-textarea);
|
2015-06-26 20:27:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
::content textarea:invalid {
|
|
|
|
box-shadow: none;
|
|
|
|
}
|
|
|
|
|
2016-02-18 11:20:10 -07:00
|
|
|
textarea::-webkit-input-placeholder {
|
|
|
|
@apply(--iron-autogrow-textarea-placeholder);
|
|
|
|
}
|
|
|
|
|
|
|
|
textarea:-moz-placeholder {
|
|
|
|
@apply(--iron-autogrow-textarea-placeholder);
|
|
|
|
}
|
|
|
|
|
|
|
|
textarea::-moz-placeholder {
|
|
|
|
@apply(--iron-autogrow-textarea-placeholder);
|
|
|
|
}
|
|
|
|
|
|
|
|
textarea:-ms-input-placeholder {
|
|
|
|
@apply(--iron-autogrow-textarea-placeholder);
|
|
|
|
}
|
2015-06-26 20:27:38 -07:00
|
|
|
</style>
|
|
|
|
<template>
|
|
|
|
<!-- the mirror sizes the input/textarea so it grows with typing -->
|
2015-12-14 08:43:03 -07:00
|
|
|
<!-- use   instead of to allow this element to be used in XHTML -->
|
|
|
|
<div id="mirror" class="mirror-text" aria-hidden="true"> </div>
|
2015-06-26 20:27:38 -07:00
|
|
|
|
|
|
|
<!-- size the input/textarea with a div, because the textarea has intrinsic size in ff -->
|
|
|
|
<div class="textarea-container fit">
|
|
|
|
<textarea id="textarea"
|
|
|
|
autocomplete$="[[autocomplete]]"
|
|
|
|
autofocus$="[[autofocus]]"
|
|
|
|
inputmode$="[[inputmode]]"
|
|
|
|
placeholder$="[[placeholder]]"
|
|
|
|
readonly$="[[readonly]]"
|
|
|
|
required$="[[required]]"
|
2015-10-07 18:49:40 -07:00
|
|
|
disabled$="[[disabled]]"
|
2015-06-26 20:27:38 -07:00
|
|
|
rows$="[[rows]]"
|
|
|
|
maxlength$="[[maxlength]]"></textarea>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</dom-module>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
Polymer({
|
|
|
|
|
|
|
|
is: 'iron-autogrow-textarea',
|
|
|
|
|
|
|
|
behaviors: [
|
2015-07-13 14:26:11 -07:00
|
|
|
Polymer.IronFormElementBehavior,
|
2015-06-26 20:27:38 -07:00
|
|
|
Polymer.IronValidatableBehavior,
|
|
|
|
Polymer.IronControlState
|
|
|
|
],
|
|
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use this property instead of `value` for two-way data binding.
|
|
|
|
*/
|
|
|
|
bindValue: {
|
|
|
|
observer: '_bindValueChanged',
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The initial number of rows.
|
|
|
|
*
|
|
|
|
* @attribute rows
|
|
|
|
* @type number
|
|
|
|
* @default 1
|
|
|
|
*/
|
|
|
|
rows: {
|
|
|
|
type: Number,
|
|
|
|
value: 1,
|
|
|
|
observer: '_updateCached'
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The maximum number of rows this element can grow to until it
|
|
|
|
* scrolls. 0 means no maximum.
|
|
|
|
*
|
|
|
|
* @attribute maxRows
|
|
|
|
* @type number
|
|
|
|
* @default 0
|
|
|
|
*/
|
|
|
|
maxRows: {
|
|
|
|
type: Number,
|
|
|
|
value: 0,
|
|
|
|
observer: '_updateCached'
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to the textarea's `autocomplete` attribute.
|
|
|
|
*/
|
|
|
|
autocomplete: {
|
|
|
|
type: String,
|
|
|
|
value: 'off'
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to the textarea's `autofocus` attribute.
|
|
|
|
*/
|
|
|
|
autofocus: {
|
2015-08-05 18:21:18 -07:00
|
|
|
type: Boolean,
|
|
|
|
value: false
|
2015-06-26 20:27:38 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to the textarea's `inputmode` attribute.
|
|
|
|
*/
|
|
|
|
inputmode: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to the textarea's `name` attribute.
|
|
|
|
*/
|
|
|
|
name: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
|
2015-07-13 14:26:11 -07:00
|
|
|
/**
|
|
|
|
* The value for this input, same as `bindValue`
|
|
|
|
*/
|
|
|
|
value: {
|
|
|
|
notify: true,
|
|
|
|
type: String,
|
2016-02-18 11:20:10 -07:00
|
|
|
value: '',
|
2015-07-13 14:26:11 -07:00
|
|
|
computed: '_computeValue(bindValue)'
|
|
|
|
},
|
|
|
|
|
2015-06-26 20:27:38 -07:00
|
|
|
/**
|
|
|
|
* Bound to the textarea's `placeholder` attribute.
|
|
|
|
*/
|
|
|
|
placeholder: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bound to the textarea's `readonly` attribute.
|
|
|
|
*/
|
|
|
|
readonly: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set to true to mark the textarea as required.
|
|
|
|
*/
|
|
|
|
required: {
|
|
|
|
type: Boolean
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The maximum length of the input value.
|
|
|
|
*/
|
|
|
|
maxlength: {
|
|
|
|
type: Number
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
listeners: {
|
|
|
|
'input': '_onInput'
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the underlying textarea.
|
2015-07-13 14:26:11 -07:00
|
|
|
* @type HTMLTextAreaElement
|
2015-06-26 20:27:38 -07:00
|
|
|
*/
|
|
|
|
get textarea() {
|
|
|
|
return this.$.textarea;
|
|
|
|
},
|
|
|
|
|
2015-09-24 10:08:10 -07:00
|
|
|
/**
|
|
|
|
* Returns textarea's selection start.
|
|
|
|
* @type Number
|
|
|
|
*/
|
|
|
|
get selectionStart() {
|
|
|
|
return this.$.textarea.selectionStart;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns textarea's selection end.
|
|
|
|
* @type Number
|
|
|
|
*/
|
|
|
|
get selectionEnd() {
|
|
|
|
return this.$.textarea.selectionEnd;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the textarea's selection start.
|
|
|
|
*/
|
|
|
|
set selectionStart(value) {
|
|
|
|
this.$.textarea.selectionStart = value;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the textarea's selection end.
|
|
|
|
*/
|
|
|
|
set selectionEnd(value) {
|
|
|
|
this.$.textarea.selectionEnd = value;
|
|
|
|
},
|
2016-02-18 11:20:10 -07:00
|
|
|
|
|
|
|
ready: function() {
|
|
|
|
this.bindValue = this.value;
|
|
|
|
},
|
2015-09-24 10:08:10 -07:00
|
|
|
|
2015-07-13 14:26:11 -07:00
|
|
|
/**
|
|
|
|
* Returns true if `value` is valid. The validator provided in `validator`
|
|
|
|
* will be used first, if it exists; otherwise, the `textarea`'s validity
|
|
|
|
* is used.
|
|
|
|
* @return {boolean} True if the value is valid.
|
|
|
|
*/
|
|
|
|
validate: function() {
|
|
|
|
// Empty, non-required input is valid.
|
|
|
|
if (!this.required && this.value == '') {
|
|
|
|
this.invalid = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
var valid;
|
|
|
|
if (this.hasValidator()) {
|
|
|
|
valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
|
|
|
|
} else {
|
|
|
|
valid = this.$.textarea.validity.valid;
|
|
|
|
this.invalid = !valid;
|
|
|
|
}
|
|
|
|
this.fire('iron-input-validate');
|
|
|
|
return valid;
|
|
|
|
},
|
|
|
|
|
2015-06-26 20:27:38 -07:00
|
|
|
_bindValueChanged: function() {
|
|
|
|
var textarea = this.textarea;
|
|
|
|
if (!textarea) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-10-07 18:49:40 -07:00
|
|
|
// If the bindValue changed manually, then we need to also update
|
|
|
|
// the underlying textarea's value. Otherwise this change was probably
|
|
|
|
// generated from the _onInput handler, and the two values are already
|
|
|
|
// the same.
|
|
|
|
if (textarea.value !== this.bindValue) {
|
|
|
|
textarea.value = !(this.bindValue || this.bindValue === 0) ? '' : this.bindValue;
|
|
|
|
}
|
|
|
|
|
2015-09-03 21:33:31 -07:00
|
|
|
this.$.mirror.innerHTML = this._valueForMirror();
|
2015-06-26 20:27:38 -07:00
|
|
|
// manually notify because we don't want to notify until after setting value
|
|
|
|
this.fire('bind-value-changed', {value: this.bindValue});
|
|
|
|
},
|
|
|
|
|
|
|
|
_onInput: function(event) {
|
|
|
|
this.bindValue = event.path ? event.path[0].value : event.target.value;
|
|
|
|
},
|
|
|
|
|
|
|
|
_constrain: function(tokens) {
|
|
|
|
var _tokens;
|
|
|
|
tokens = tokens || [''];
|
|
|
|
// Enforce the min and max heights for a multiline input to avoid measurement
|
|
|
|
if (this.maxRows > 0 && tokens.length > this.maxRows) {
|
|
|
|
_tokens = tokens.slice(0, this.maxRows);
|
|
|
|
} else {
|
|
|
|
_tokens = tokens.slice(0);
|
|
|
|
}
|
|
|
|
while (this.rows > 0 && _tokens.length < this.rows) {
|
|
|
|
_tokens.push('');
|
|
|
|
}
|
2015-12-14 08:43:03 -07:00
|
|
|
// Use   instead of to allow this element to be used in XHTML.
|
|
|
|
return _tokens.join('<br/>') + ' ';
|
2015-06-26 20:27:38 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
_valueForMirror: function() {
|
|
|
|
var input = this.textarea;
|
|
|
|
if (!input) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.tokens = (input && input.value) ? input.value.replace(/&/gm, '&').replace(/"/gm, '"').replace(/'/gm, ''').replace(/</gm, '<').replace(/>/gm, '>').split('\n') : [''];
|
|
|
|
return this._constrain(this.tokens);
|
|
|
|
},
|
|
|
|
|
|
|
|
_updateCached: function() {
|
|
|
|
this.$.mirror.innerHTML = this._constrain(this.tokens);
|
2015-07-13 14:26:11 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
_computeValue: function() {
|
|
|
|
return this.bindValue;
|
2015-06-26 20:27:38 -07:00
|
|
|
}
|
2015-08-05 18:21:18 -07:00
|
|
|
});
|
2015-06-26 20:27:38 -07:00
|
|
|
</script>
|