<!--
@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="../paper-styles/paper-styles.html">

<!--
`<paper-input-container>` is a container for a `<label>`, an `<input is="iron-input">` or
`<iron-autogrow-textarea>` and optional add-on elements such as an error message or character
counter, used to implement Material Design text fields.

For example:

    <paper-input-container>
      <label>Your name</label>
      <input is="iron-input">
    </paper-input-container>

### Listening for input changes

By default, it listens for changes on the `bind-value` attribute on its children nodes and perform
tasks such as auto-validating and label styling when the `bind-value` changes. You can configure
the attribute it listens to with the `attr-for-value` attribute.

### Using a custom input element

You can use a custom input element in a `<paper-input-container>`, for example to implement a
compound input field like a social security number input. The custom input element should have the
`paper-input-input` class, have a `notify:true` value property and optionally implements
`Polymer.IronValidatableBehavior` if it is validatable.

    <paper-input-container attr-for-value="ssn-value">
      <label>Social security number</label>
      <ssn-input class="paper-input-input"></ssn-input>
    </paper-input-container>

### Validation

If the `auto-validate` attribute is set, the input container will validate the input and update
the container styling when the input value changes.

### Add-ons

Add-ons are child elements of a `<paper-input-container>` with the `add-on` attribute and
implements the `Polymer.PaperInputAddonBehavior` behavior. They are notified when the input value
or validity changes, and may implement functionality such as error messages or character counters.
They appear at the bottom of the input.

### Prefixes and suffixes
These are child elements of a `<paper-input-container>` with the `prefix`
or `suffix` attribute, and are displayed inline with the input, before or after.

    <paper-input-container>
      <div prefix>$</div>
      <label>Total</label>
      <input is="iron-input">
      <paper-icon-button suffix icon="clear"></paper-icon-button>
    </paper-input-container>

### Styling

The following custom properties and mixins are available for styling:

Custom property | Description | Default
----------------|-------------|----------
`--paper-input-container-color` | Label and underline color when the input is not focused | `--secondary-text-color`
`--paper-input-container-focus-color` | Label and underline color when the input is focused | `--default-primary-color`
`--paper-input-container-invalid-color` | Label and underline color when the input is is invalid | `--google-red-500`
`--paper-input-container-input-color` | Input foreground color | `--primary-text-color`
`--paper-input-container` | Mixin applied to the container | `{}`
`--paper-input-container-disabled` | Mixin applied to the container when it's disabled | `{}`
`--paper-input-container-label` | Mixin applied to the label | `{}`
`--paper-input-container-label-focus` | Mixin applied to the label when the input is focused | `{}`
`--paper-input-container-input` | Mixin applied to the input | `{}`
`--paper-input-container-underline` | Mixin applied to the underline | `{}`
`--paper-input-container-underline-focus` | Mixin applied to the underline when the input is focued | `{}`
`--paper-input-container-underline-disabled` | Mixin applied to the underline when the input is disabled | `{}`
`--paper-input-prefix` | Mixin applied to the input prefix | `{}`
`--paper-input-suffix` | Mixin applied to the input suffix | `{}`

This element is `display:block` by default, but you can set the `inline` attribute to make it
`display:inline-block`.
-->
<dom-module id="paper-input-container">
  <template>

    <style>
      :host {
        display: block;
        padding: 8px 0;

        @apply(--paper-input-container);
      }

      :host[inline] {
        display: inline-block;
      }

      :host([disabled]) {
        pointer-events: none;
        opacity: 0.33;

        @apply(--paper-input-container-disabled);
      }

      .floated-label-placeholder {
        @apply(--paper-font-caption);
      }

      .underline {
        position: relative;
      }

      .focused-line {
        height: 2px;

        -webkit-transform-origin: center center;
        transform-origin: center center;
        -webkit-transform: scale3d(0,1,1);
        transform: scale3d(0,1,1);

        background: var(--paper-input-container-focus-color, --default-primary-color);

        @apply(--paper-input-container-underline-focus);
      }

      .underline.is-highlighted .focused-line {
        -webkit-transform: none;
        transform: none;
        -webkit-transition: -webkit-transform 0.25s;
        transition: transform 0.25s;

        @apply(--paper-transition-easing);
      }

      .underline.is-invalid .focused-line {
        background: var(--paper-input-container-invalid-color, --google-red-500);

        -webkit-transform: none;
        transform: none;
        -webkit-transition: -webkit-transform 0.25s;
        transition: transform 0.25s;

        @apply(--paper-transition-easing);
      }

      .unfocused-line {
        height: 1px;
        background: var(--paper-input-container-color, --secondary-text-color);

        @apply(--paper-input-container-underline);
      }

      :host([disabled]) .unfocused-line {
        border-bottom: 1px dashed;
        border-color: var(--paper-input-container-color, --secondary-text-color);
        background: transparent;

        @apply(--paper-input-container-underline-disabled);
      }

      .label-and-input-container {
        @apply(--layout-flex);
        @apply(--layout-relative);
      }

      .input-content {
        position: relative;
        @apply(--layout-horizontal);
        @apply(--layout-end);
      }

      .input-content ::content label,
      .input-content ::content .paper-input-label {
        position: absolute;
        top: 0;
        right: 0;
        left: 0;
        font: inherit;
        color: var(--paper-input-container-color, --secondary-text-color);

        @apply(--paper-font-common-nowrap);
        @apply(--paper-font-subhead);
        @apply(--paper-input-container-label);
      }

      .input-content.label-is-floating ::content label,
      .input-content.label-is-floating ::content .paper-input-label {
        -webkit-transform: translateY(-75%) scale(0.75);
        transform: translateY(-75%) scale(0.75);
        -webkit-transition: -webkit-transform 0.25s;
        transition: transform 0.25s;

        -webkit-transform-origin: left top;
        transform-origin: left top;

        /* Since we scale to 75/100 of the size, we actually have 100/75 of the
        original space now available */
        width: 133%;

        @apply(--paper-transition-easing);
      }

      :host-context([dir="rtl"]) .input-content.label-is-floating ::content label,
      :host-context([dir="rtl"]) .input-content.label-is-floating ::content .paper-input-label {
        /* TODO(noms): Figure out why leaving the width at 133% before the animation
         * actually makes
         * it wider on the right side, not left side, as you would expect in RTL */
        width: 100%;
        -webkit-transform-origin: right top;
        transform-origin: right top;
      }

      .input-content.label-is-highlighted ::content label,
      .input-content.label-is-highlighted ::content .paper-input-label {
        color: var(--paper-input-container-focus-color, --default-primary-color);

        @apply(--paper-input-container-label-focus);
      }

      .input-content.is-invalid ::content label,
      .input-content.is-invalid ::content .paper-input-label {
        color: var(--paper-input-container-invalid-color, --google-red-500);
      }

      .input-content.label-is-hidden ::content label,
      .input-content.label-is-hidden ::content .paper-input-label {
        visibility: hidden;
      }

      .input-content ::content input,
      .input-content ::content textarea,
      .input-content ::content iron-autogrow-textarea,
      .input-content ::content .paper-input-input {
        position: relative; /* to make a stacking context */
        outline: none;
        box-shadow: none;
        padding: 0;
        width: 100%;
        background: transparent;
        border: none;
        color: var(--paper-input-container-input-color, --primary-text-color);
        -webkit-appearance: none;
        text-align: inherit;

        @apply(--paper-font-subhead);
        @apply(--paper-input-container-input);
      }

      ::content [prefix] {
        @apply(--paper-font-subhead);
        @apply(--paper-input-prefix);
      }

      ::content [suffix] {
        @apply(--paper-font-subhead);
        @apply(--paper-input-suffix);
      }

      /* Firefox sets a min-width on the input, which can cause layout issues */
      .input-content ::content input {
        min-width: 0;
      }

      .input-content ::content textarea {
        resize: none;
      }

      .add-on-content {
        position: relative;
      }

      .add-on-content.is-invalid ::content * {
        color: var(--paper-input-container-invalid-color, --google-red-500);
      }

      .add-on-content.is-highlighted ::content * {
        color: var(--paper-input-container-focus-color, --default-primary-color);
      }
    </style>

    <template is="dom-if" if="[[!noLabelFloat]]">
      <div class="floated-label-placeholder">&nbsp;</div>
    </template>

    <div class$="[[_computeInputContentClass(noLabelFloat,alwaysFloatLabel,focused,invalid,_inputHasContent)]]">
      <content select="[prefix]" id="prefix"></content>

      <div class="label-and-input-container" id="labelAndInputContainer">
        <content select=":not([add-on]):not([prefix]):not([suffix])"></content>
      </div>
      <content select="[suffix]"></content>
    </div>

    <div class$="[[_computeUnderlineClass(focused,invalid)]]">
      <div class="unfocused-line fit"></div>
      <div class="focused-line fit"></div>
    </div>

    <div class$="[[_computeAddOnContentClass(focused,invalid)]]">
      <content id="addOnContent" select="[add-on]"></content>
    </div>

  </template>
</dom-module>

<script>
  Polymer({
    is: 'paper-input-container',

    properties: {
      /**
       * Set to true to disable the floating label. The label disappears when the input value is
       * not null.
       */
      noLabelFloat: {
        type: Boolean,
        value: false
      },

      /**
       * Set to true to always float the floating label.
       */
      alwaysFloatLabel: {
        type: Boolean,
        value: false
      },

      /**
       * The attribute to listen for value changes on.
       */
      attrForValue: {
        type: String,
        value: 'bind-value'
      },

      /**
       * Set to true to auto-validate the input value when it changes.
       */
      autoValidate: {
        type: Boolean,
        value: false
      },

      /**
       * True if the input is invalid. This property is set automatically when the input value
       * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
       */
      invalid: {
        observer: '_invalidChanged',
        type: Boolean,
        value: false
      },

      /**
       * True if the input has focus.
       */
      focused: {
        readOnly: true,
        type: Boolean,
        value: false,
        notify: true
      },

      _addons: {
        type: Array
        // do not set a default value here intentionally - it will be initialized lazily when a
        // distributed child is attached, which may occur before configuration for this element
        // in polyfill.
      },

      _inputHasContent: {
        type: Boolean,
        value: false
      },

      _inputSelector: {
        type: String,
        value: 'input,textarea,.paper-input-input'
      },

      _boundOnFocus: {
        type: Function,
        value: function() {
          return this._onFocus.bind(this);
        }
      },

      _boundOnBlur: {
        type: Function,
        value: function() {
          return this._onBlur.bind(this);
        }
      },

      _boundOnInput: {
        type: Function,
        value: function() {
          return this._onInput.bind(this);
        }
      },

      _boundValueChanged: {
        type: Function,
        value: function() {
          return this._onValueChanged.bind(this);
        }
      }
    },

    listeners: {
      'addon-attached': '_onAddonAttached',
      'iron-input-validate': '_onIronInputValidate'
    },

    get _valueChangedEvent() {
      return this.attrForValue + '-changed';
    },

    get _propertyForValue() {
      return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
    },

    get _inputElement() {
      return Polymer.dom(this).querySelector(this._inputSelector);
    },

    get _inputElementValue() {
      return this._inputElement[this._propertyForValue] || this._inputElement.value;
    },

    ready: function() {
      if (!this._addons) {
        this._addons = [];
      }
      this.addEventListener('focus', this._boundOnFocus, true);
      this.addEventListener('blur', this._boundOnBlur, true);
      if (this.attrForValue) {
        this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
      } else {
        this.addEventListener('input', this._onInput);
      }
    },

    attached: function() {
      // Only validate when attached if the input already has a value.
      if (this._inputElementValue != '') {
        this._handleValueAndAutoValidate(this._inputElement);
      } else {
        this._handleValue(this._inputElement);
      }

      this._numberOfPrefixNodes = 0;
      this._prefixObserver = Polymer.dom(this.$.prefix).observeNodes(
          function(mutations) {
            // Keep track whether there's at least one prefix node, since it
            // affects laying out the floating label.
            this._numberOfPrefixNodes += mutations.addedNodes.length -
                mutations.removedNodes.length;
          }.bind(this));
    },

    detached: function() {
      if (this._prefixObserver) {
        Polymer.dom(this.$.prefix).unobserveNodes(this._prefixObserver);
      }
    },

    _onAddonAttached: function(event) {
      if (!this._addons) {
        this._addons = [];
      }
      var target = event.target;
      if (this._addons.indexOf(target) === -1) {
        this._addons.push(target);
        if (this.isAttached) {
          this._handleValue(this._inputElement);
        }
      }
    },

    _onFocus: function() {
      this._setFocused(true);
    },

    _onBlur: function() {
      this._setFocused(false);
      this._handleValueAndAutoValidate(this._inputElement);
    },

    _onInput: function(event) {
      this._handleValueAndAutoValidate(event.target);
    },

    _onValueChanged: function(event) {
      this._handleValueAndAutoValidate(event.target);
    },

    _handleValue: function(inputElement) {
      var value = this._inputElementValue;

      // type="number" hack needed because this.value is empty until it's valid
      if (value || value === 0 || (inputElement.type === 'number' && !inputElement.checkValidity())) {
        this._inputHasContent = true;
      } else {
        this._inputHasContent = false;
      }

      this.updateAddons({
        inputElement: inputElement,
        value: value,
        invalid: this.invalid
      });
    },

    _handleValueAndAutoValidate: function(inputElement) {
      if (this.autoValidate) {
        var valid;
        if (inputElement.validate) {
          valid = inputElement.validate(this._inputElementValue);
        } else {
          valid = inputElement.checkValidity();
        }
        this.invalid = !valid;
      }

      // Call this last to notify the add-ons.
      this._handleValue(inputElement);
    },

    _onIronInputValidate: function(event) {
      this.invalid = this._inputElement.invalid;
    },

    _invalidChanged: function() {
      if (this._addons) {
        this.updateAddons({invalid: this.invalid});
      }
    },

    /**
     * Call this to update the state of add-ons.
     * @param {Object} state Add-on state.
     */
    updateAddons: function(state) {
      for (var addon, index = 0; addon = this._addons[index]; index++) {
        addon.update(state);
      }
    },

    _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
      var cls = 'input-content';
      if (!noLabelFloat) {
        var label = this.querySelector('label');

        if (alwaysFloatLabel || _inputHasContent) {
          cls += ' label-is-floating';
          if (invalid) {
            cls += ' is-invalid';
          } else if (focused) {
            cls += " label-is-highlighted";
          }
          // If a prefix element exists, the label has a horizontal offset
          // which needs to be undone when displayed as a floating label.
          if (this._numberOfPrefixNodes > 0) {
            this.$.labelAndInputContainer.style.position = 'static';
          }
        } else {
          // When the label is not floating, it should overlap the input element.
          if (label) {
            this.$.labelAndInputContainer.style.position = 'relative';
          }
        }
      } else {
        if (_inputHasContent) {
          cls += ' label-is-hidden';
        }
      }
      return cls;
    },

    _computeUnderlineClass: function(focused, invalid) {
      var cls = 'underline';
      if (invalid) {
        cls += ' is-invalid';
      } else if (focused) {
        cls += ' is-highlighted'
      }
      return cls;
    },

    _computeAddOnContentClass: function(focused, invalid) {
      var cls = 'add-on-content';
      if (invalid) {
        cls += ' is-invalid';
      } else if (focused) {
        cls += ' is-highlighted'
      }
      return cls;
    }
  });
</script>