| <!-- |
| @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-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-focus` | Mixin applied to the input when focused | `{}` |
| `--paper-input-container-input-invalid` | Mixin applied to the input when invalid | `{}` |
| `--paper-input-container-input-webkit-spinner` | Mixin applied to the webkit spinner | `{}` |
| `--paper-input-container-input-webkit-clear` | Mixin applied to the webkit clear button | `{}` |
| `--paper-input-container-ms-clear` | Mixin applied to the Internet Explorer clear button | `{}` |
| `--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.focused ::content input, |
| .input-content.focused ::content textarea, |
| .input-content.focused ::content iron-autogrow-textarea, |
| .input-content.focused ::content .paper-input-input { |
| @apply(--paper-input-container-input-focus); |
| } |
| |
| .input-content.is-invalid ::content input, |
| .input-content.is-invalid ::content textarea, |
| .input-content.is-invalid ::content iron-autogrow-textarea, |
| .input-content.is-invalid ::content .paper-input-input { |
| @apply(--paper-input-container-input-invalid); |
| } |
| |
| .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"> </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'; |
| } |
| if (invalid) { |
| cls += ' is-invalid'; |
| } |
| } |
| } else { |
| if (_inputHasContent) { |
| cls += ' label-is-hidden'; |
| } |
| if (invalid) { |
| cls += ' is-invalid'; |
| } |
| } |
| if (focused) { |
| cls += ' focused'; |
| } |
| 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> |