<!– @license Copyright © 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at polymer.github.io/LICENSE.txt The complete set of authors may be found at polymer.github.io/AUTHORS.txt The complete set of contributors may be found at 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 polymer.github.io/PATENTS.txt –>

<link rel=“import” href=“../polymer/polymer.html”> <link rel=“import” href=“../iron-flex-layout/iron-flex-layout.html”> <link rel=“import” href=“../paper-styles/default-theme.html”> <link rel=“import” href=“../paper-styles/typography.html”>

<!– `<paper-input-container>` is a container for a `<label>`, an `<input is=“iron-input”>` or `<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>

Do not wrap `<paper-input-container>` around elements that already include it, such as `<paper-input>`. Doing so may cause events to bounce infintely between the container and its contained element.

### 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>

If you're using a `<paper-input-container>` imperatively, it's important to make sure that you attach its children (the `iron-input` and the optional `label`) before you attach the `<paper-input-container>` itself, so that it can be set up correctly.

### 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 | `–primary-color` `–paper-input-container-invalid-color` | Label and underline color when the input is is invalid | `–error-color` `–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-label-floating` | Mixin applied to the label when floating | `{}` `–paper-input-container-input` | Mixin applied to the input | `{}` `–paper-input-container-input-webkit-spinner` | Mixin applied to the webkit spinner | `{}` `–paper-input-container-underline` | Mixin applied to the underline | `{}` `–paper-input-container-underline-focus` | Mixin applied to the underline when the input is focused | `{}` `–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);
    }

    :host([hidden]) {
      display: none !important;
    }

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

    .underline {
      height: 2px;
      position: relative;
    }

    .focused-line {
      @apply(--layout-fit);

      border-bottom: 2px solid var(--paper-input-container-focus-color, --primary-color);

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

      @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 {
      border-color: var(--paper-input-container-invalid-color, --error-color);
      -webkit-transform: none;
      transform: none;
      -webkit-transition: -webkit-transform 0.25s;
      transition: transform 0.25s;

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

    .unfocused-line {
      @apply(--layout-fit);

      border-bottom: 1px solid 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);

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

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

      width: 100%;
      max-width: 100%;
    }

    .input-content {
      @apply(--layout-horizontal);
      @apply(--layout-center);

      position: relative;
    }

    .input-content ::content label,
    .input-content ::content .paper-input-label {
      position: absolute;
      top: 0;
      right: 0;
      left: 0;
      width: 100%;
      font: inherit;
      color: var(--paper-input-container-color, --secondary-text-color);
      -webkit-transition: -webkit-transform 0.25s, width 0.25s;
      transition: transform 0.25s, width 0.25s;
      -webkit-transform-origin: left top;
      transform-origin: left top;

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

    .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);

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

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

    :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, --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, --error-color);
    }

    .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%;
      max-width: 100%;
      background: transparent;
      border: none;
      color: var(--paper-input-container-input-color, --primary-text-color);
      -webkit-appearance: none;
      text-align: inherit;
      vertical-align: bottom;

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

    .input-content ::content input::-webkit-outer-spin-button,
    .input-content ::content input::-webkit-inner-spin-button {
      @apply(--paper-input-container-input-webkit-spinner);
    }

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

      @apply(--paper-input-prefix);
      @apply(--layout-flex-none);
    }

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

      @apply(--paper-input-suffix);
      @apply(--layout-flex-none);
    }

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

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

  <template is="dom-if" if="[[!noLabelFloat]]">
    <div class="floated-label-placeholder" aria-hidden="true">&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"></div>
    <div class="focused-line"></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);
  },

  attached: function() {
    if (this.attrForValue) {
      this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
    } else {
      this.addEventListener('input', this._onInput);
    }

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

  _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 the label is floating, ignore any offsets that may have been
        // applied from a prefix element.
        this.$.labelAndInputContainer.style.position = 'static';

        if (invalid) {
          cls += ' is-invalid';
        } else if (focused) {
          cls += " label-is-highlighted";
        }
      } 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>