iron-autogrow-textarea.html 7.76 KB
<!--
@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">
<link rel="import" href="../iron-flex-layout/classes/iron-flex-layout.html">
<link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html">
<link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html">

<!--
`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:

    <iron-autogrow-textarea></iron-autogrow-textarea>

Because the `textarea`'s `value` property is not observable, you should use
this element's `bind-value` instead for imperative updates.

@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;
    }

    .mirror-text {
      visibility: hidden;
      word-wrap: break-word;
    }

    textarea {
      position: relative;
      outline: none;
      border: none;
      resize: none;
      background: inherit;
      color: inherit;
      /* see comments in template */
      width: 100%;
      height: 100%;
      font-size: inherit;
      font-family: inherit;
      line-height: inherit;
    }

    ::content textarea:invalid {
      box-shadow: none;
    }

  </style>
  <template>
    <!-- the mirror sizes the input/textarea so it grows with typing -->
    <div id="mirror" class="mirror-text" aria-hidden="true">&nbsp;</div>

    <!-- 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]]"
        rows$="[[rows]]"
        maxlength$="[[maxlength]]"></textarea>
    </div>
  </template>
</dom-module>

<script>

  Polymer({

    is: 'iron-autogrow-textarea',

    behaviors: [
      Polymer.IronFormElementBehavior,
      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: {
        type: Boolean,
        value: false
      },

      /**
       * Bound to the textarea's `inputmode` attribute.
       */
      inputmode: {
        type: String
      },

      /**
       * Bound to the textarea's `name` attribute.
       */
      name: {
        type: String
      },

      /**
       * The value for this input, same as `bindValue`
       */
      value: {
        notify: true,
        type: String,
        computed: '_computeValue(bindValue)'
      },

      /**
       * 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.
     * @type HTMLTextAreaElement
     */
    get textarea() {
      return this.$.textarea;
    },

    /**
     * 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;
    },

    /**
     * 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;
    },

    _bindValueChanged: function() {
      var textarea = this.textarea;
      if (!textarea) {
        return;
      }

      textarea.value = this.bindValue;
      this.$.mirror.innerHTML = this._valueForMirror();
      // 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('');
      }
      return _tokens.join('<br>') + '&nbsp;';
    },

    _valueForMirror: function() {
      var input = this.textarea;
      if (!input) {
        return;
      }
      this.tokens = (input && input.value) ? input.value.replace(/&/gm, '&amp;').replace(/"/gm, '&quot;').replace(/'/gm, '&#39;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;').split('\n') : [''];
      return this._constrain(this.tokens);
    },

    _updateCached: function() {
      this.$.mirror.innerHTML = this._constrain(this.tokens);
    },

    _computeValue: function() {
      return this.bindValue;
    }
  });
</script>