Home | History | Annotate | Download | only in iron-autogrow-textarea
      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 &#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('
'
) + '&#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