<!--
Copyright (c) 2014 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
The complete set of authors may be found at http://polymer.github.io/AUTHORS
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS
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
-->
<!--
`paper-input` is a single- or multi-line text field where user can enter input.
It can optionally have a label.
Example:
<paper-input label="Your Name"></paper-input>
<paper-input multiline label="Enter multiple lines here"></paper-input>
Theming
--------
Set `CoreStyle.g.paperInput.focusedColor` and `CoreStyle.g.paperInput.invalidColor` to theme
the focused and invalid states.
@group Paper Elements
@element paper-input
@extends core-input
@homepage github.io
-->
<link href="../polymer/polymer.html" rel="import">
<link href="../core-input/core-input.html" rel="import">
<link href="../core-style/core-style.html" rel="import">
<core-style id="paper-input">
#label.focused,
#floatedLabel.focused {
color: {{g.paperInput.focusedColor}};
}
#underlineHighlight.focused,
#caretInner {
background-color: {{g.paperInput.focusedColor}};
}
#error,
:host(.invalid) #label.focused,
:host(.invalid) #floatedLabel.focused {
color: {{g.paperInput.invalidColor}};
}
:host(.invalid) #underlineHighlight.focused,
:host(.invalid) #caretInner {
background-color: {{g.paperInput.invalidColor}};
}
</core-style>
<polymer-element name="paper-input" extends="core-input" attributes="label floatingLabel maxRows error" on-down="{{downAction}}" on-up="{{upAction}}">
<template>
<link href="paper-input.css" rel="stylesheet">
<core-style ref="paper-input"></core-style>
<div id="floatedLabel" class="hidden" hidden?="{{!floatingLabel}}"><span id="floatedLabelSpan">{{label}}</span></div>
<div id="container" on-transitionend="{{transitionEndAction}}" on-webkitTransitionEnd="{{transitionEndAction}}">
<div id="label"><span id="labelSpan">{{label}}</span></div>
<div id="inputContainer">
<div id="inputClone">
<span id="inputCloneSpan" aria-hidden="true"> </span>
</div>
<template if="{{multiline}}">
<div id="minInputHeight"></div>
<div id="maxInputHeight"></div>
</template>
<shadow></shadow>
</div>
<div id="underlineContainer">
<div id="underline"></div>
<div id="underlineHighlight" class="focusedColor"></div>
</div>
<div id="caret">
<div id="caretInner" class="focusedColor"></div>
</div>
</div>
<div id="errorContainer">
<div id="error" role="alert" aria-hidden="{{!invalid}}">{{error || validationMessage}}</div>
<div id="errorIcon"></div>
</div>
</template>
<script>
(function() {
var paperInput = CoreStyle.g.paperInput = CoreStyle.g.paperInput || {};
paperInput.focusedColor = '#4059a9';
paperInput.invalidColor = '#d34336';
Polymer('paper-input', {
/**
* The label for this input. It normally appears as grey text inside
* the text input and disappears once the user enters text.
*
* @attribute label
* @type string
* @default ''
*/
label: '',
/**
* If true, the label will "float" above the text input once the
* user enters text instead of disappearing.
*
* @attribute floatingLabel
* @type boolean
* @default false
*/
floatingLabel: false,
/**
* (multiline only) If set to a non-zero value, the height of this
* text input will grow with the value changes until it is maxRows
* rows tall. If the maximum size does not fit the value, the text
* input will scroll internally.
*
* @attribute maxRows
* @type number
* @default 0
*/
maxRows: 0,
/**
* The message to display if the input value fails validation. If this
* is unset or the empty string, a default message is displayed depending
* on the type of validation error.
*
* @attribute error
* @type string
*/
error: '',
focused: false,
pressed: false,
attached: function() {
if (this.multiline) {
this.resizeInput();
window.requestAnimationFrame(function() {
this.$.underlineContainer.classList.add('animating');
}.bind(this));
}
},
resizeInput: function() {
var height = this.$.inputClone.getBoundingClientRect().height;
var bounded = this.maxRows > 0 || this.rows > 0;
if (bounded) {
var minHeight = this.$.minInputHeight.getBoundingClientRect().height;
var maxHeight = this.$.maxInputHeight.getBoundingClientRect().height;
height = Math.max(minHeight, Math.min(height, maxHeight));
}
this.$.inputContainer.style.height = height + 'px';
this.$.underlineContainer.style.top = height + 'px';
},
prepareLabelTransform: function() {
var toRect = this.$.floatedLabelSpan.getBoundingClientRect();
var fromRect = this.$.labelSpan.getBoundingClientRect();
if (toRect.width !== 0) {
this.$.label.cachedTransform = 'scale(' + (toRect.width / fromRect.width) + ') ' +
'translateY(' + (toRect.bottom - fromRect.bottom) + 'px)';
}
},
toggleLabel: function(force) {
var v = force !== undefined ? force : this.inputValue;
if (!this.floatingLabel) {
this.$.label.classList.toggle('hidden', v);
}
if (this.floatingLabel && !this.focused) {
this.$.label.classList.toggle('hidden', v);
this.$.floatedLabel.classList.toggle('hidden', !v);
}
},
rowsChanged: function() {
if (this.multiline && !isNaN(parseInt(this.rows))) {
this.$.minInputHeight.innerHTML = '';
for (var i = 0; i < this.rows; i++) {
this.$.minInputHeight.appendChild(document.createElement('br'));
}
this.resizeInput();
}
},
maxRowsChanged: function() {
if (this.multiline && !isNaN(parseInt(this.maxRows))) {
this.$.maxInputHeight.innerHTML = '';
for (var i = 0; i < this.maxRows; i++) {
this.$.maxInputHeight.appendChild(document.createElement('br'));
}
this.resizeInput();
}
},
inputValueChanged: function() {
this.super();
if (this.multiline) {
var escaped = this.inputValue.replace(/\n/gm, '<br>');
if (!escaped || escaped.lastIndexOf('<br>') === escaped.length - 4) {
escaped += ' ';
}
this.$.inputCloneSpan.innerHTML = escaped;
this.resizeInput();
}
this.toggleLabel();
},
labelChanged: function() {
if (this.floatingLabel && this.$.floatedLabel && this.$.label) {
// If the element is created programmatically, labelChanged is called before
// binding. Run the measuring code in async so the DOM is ready.
this.async(function() {
this.prepareLabelTransform();
});
}
},
placeholderChanged: function() {
this.label = this.placeholder;
},
inputFocusAction: function() {
if (!this.pressed) {
if (this.floatingLabel) {
this.$.floatedLabel.classList.remove('hidden');
this.$.floatedLabel.classList.add('focused');
this.$.floatedLabel.classList.add('focusedColor');
}
this.$.label.classList.add('hidden');
this.$.underlineHighlight.classList.add('focused');
this.$.caret.classList.add('focused');
this.super(arguments);
}
this.focused = true;
},
shouldFloatLabel: function() {
// if type = number, the input value is the empty string until a valid number
// is entered so we must do some hacks here
return this.inputValue || (this.type === 'number' && !this.validity.valid);
},
inputBlurAction: function() {
this.super(arguments);
this.$.underlineHighlight.classList.remove('focused');
this.$.caret.classList.remove('focused');
if (this.floatingLabel) {
this.$.floatedLabel.classList.remove('focused');
this.$.floatedLabel.classList.remove('focusedColor');
if (!this.shouldFloatLabel()) {
this.$.floatedLabel.classList.add('hidden');
}
}
// type = number hack. see core-input for more info
if (!this.shouldFloatLabel()) {
this.$.label.classList.remove('hidden');
this.$.label.classList.add('animating');
this.async(function() {
this.$.label.style.webkitTransform = 'none';
this.$.label.style.transform = 'none';
});
}
this.focused = false;
},
downAction: function(e) {
if (this.disabled) {
return;
}
if (this.focused) {
return;
}
this.pressed = true;
var rect = this.$.underline.getBoundingClientRect();
var right = e.x - rect.left;
this.$.underlineHighlight.style.webkitTransformOriginX = right + 'px';
this.$.underlineHighlight.style.transformOriginX = right + 'px';
this.$.underlineHighlight.classList.remove('focused');
this.underlineAsync = this.async(function() {
this.$.underlineHighlight.classList.add('pressed');
}, null, 200);
// No caret animation if there is text in the input.
if (!this.inputValue) {
this.$.caret.classList.remove('focused');
}
},
upAction: function(e) {
if (this.disabled) {
return;
}
if (!this.pressed) {
return;
}
// if a touchevent caused the up, the synthentic mouseevents will blur
// the input, make sure to prevent those from being generated.
if (e._source === 'touch') {
e.preventDefault();
}
if (this.underlineAsync) {
clearTimeout(this.underlineAsync);
this.underlineAsync = null;
}
// Focus the input here to bring up the virtual keyboard.
this.$.input.focus();
this.pressed = false;
this.animating = true;
this.$.underlineHighlight.classList.remove('pressed');
this.$.underlineHighlight.classList.add('animating');
this.async(function() {
this.$.underlineHighlight.classList.add('focused');
});
// No caret animation if there is text in the input.
if (!this.inputValue) {
this.$.caret.classList.add('animating');
this.async(function() {
this.$.caret.classList.add('focused');
}, null, 100);
}
if (this.floatingLabel) {
this.$.label.classList.add('focusedColor');
this.$.label.classList.add('animating');
if (!this.$.label.cachedTransform) {
this.prepareLabelTransform();
}
this.$.label.style.webkitTransform = this.$.label.cachedTransform;
this.$.label.style.transform = this.$.label.cachedTransform;
}
},
keydownAction: function() {
this.super();
// more type = number hacks. see core-input for more info
if (this.type === 'number') {
this.async(function() {
if (!this.inputValue) {
this.toggleLabel(!this.validity.valid);
}
});
}
},
keypressAction: function() {
if (this.animating) {
this.transitionEndAction();
}
},
transitionEndAction: function(e) {
this.animating = false;
if (this.pressed) {
return;
}
if (this.focused) {
if (this.floatingLabel || this.inputValue) {
this.$.label.classList.add('hidden');
}
if (this.floatingLabel) {
this.$.label.classList.remove('focusedColor');
this.$.label.classList.remove('animating');
this.$.floatedLabel.classList.remove('hidden');
this.$.floatedLabel.classList.add('focused');
this.$.floatedLabel.classList.add('focusedColor');
}
this.async(function() {
this.$.underlineHighlight.classList.remove('animating');
this.$.caret.classList.remove('animating');
}, null, 100);
} else {
this.$.label.classList.remove('animating');
}
}
});
}());
</script>
</polymer-element>