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