1 <!-- 2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 3 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE 4 The complete set of authors may be found at http://polymer.github.io/AUTHORS 5 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS 6 Code distributed by Google as part of the polymer project is also 7 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS 8 --> 9 10 <!-- 11 `paper-input` is a single- or multi-line text field where user can enter input. 12 It can optionally have a label. 13 14 Example: 15 16 <paper-input label="Your Name"></paper-input> 17 <paper-input multiline label="Enter multiple lines here"></paper-input> 18 19 Theming 20 -------- 21 22 Set `CoreStyle.g.paperInput.focusedColor` and `CoreStyle.g.paperInput.invalidColor` to theme 23 the focused and invalid states. 24 25 @group Paper Elements 26 @element paper-input 27 @extends core-input 28 @homepage github.io 29 --> 30 <link href="../polymer/polymer.html" rel="import"> 31 <link href="../core-input/core-input.html" rel="import"> 32 <link href="../core-style/core-style.html" rel="import"> 33 34 <core-style id="paper-input"> 35 36 #label.focused, 37 #floatedLabel.focused { 38 color: {{g.paperInput.focusedColor}}; 39 } 40 41 #underlineHighlight.focused, 42 #caretInner { 43 background-color: {{g.paperInput.focusedColor}}; 44 } 45 46 #error, 47 :host(.invalid) #label.focused, 48 :host(.invalid) #floatedLabel.focused { 49 color: {{g.paperInput.invalidColor}}; 50 } 51 :host(.invalid) #underlineHighlight.focused, 52 :host(.invalid) #caretInner { 53 background-color: {{g.paperInput.invalidColor}}; 54 } 55 56 </core-style> 57 58 <polymer-element name="paper-input" extends="core-input" attributes="label floatingLabel maxRows error" on-down="{{downAction}}" on-up="{{upAction}}"> 59 60 <template> 61 62 <link href="paper-input.css" rel="stylesheet"> 63 64 <core-style ref="paper-input"></core-style> 65 66 <div id="floatedLabel" class="hidden" hidden?="{{!floatingLabel}}"><span id="floatedLabelSpan">{{label}}</span></div> 67 68 <div id="container" on-transitionend="{{transitionEndAction}}" on-webkitTransitionEnd="{{transitionEndAction}}"> 69 70 <div id="label"><span id="labelSpan">{{label}}</span></div> 71 72 <div id="inputContainer"> 73 74 <div id="inputClone"> 75 <span id="inputCloneSpan" aria-hidden="true"> </span> 76 </div> 77 78 <template if="{{multiline}}"> 79 <div id="minInputHeight"></div> 80 <div id="maxInputHeight"></div> 81 </template> 82 83 <shadow></shadow> 84 85 </div> 86 87 <div id="underlineContainer"> 88 <div id="underline"></div> 89 <div id="underlineHighlight" class="focusedColor"></div> 90 </div> 91 92 <div id="caret"> 93 <div id="caretInner" class="focusedColor"></div> 94 </div> 95 96 </div> 97 98 <div id="errorContainer"> 99 <div id="error" role="alert" aria-hidden="{{!invalid}}">{{error || validationMessage}}</div> 100 <div id="errorIcon"></div> 101 </div> 102 103 </template> 104 105 <script> 106 107 (function() { 108 109 var paperInput = CoreStyle.g.paperInput = CoreStyle.g.paperInput || {}; 110 paperInput.focusedColor = '#4059a9'; 111 paperInput.invalidColor = '#d34336'; 112 113 Polymer('paper-input', { 114 115 /** 116 * The label for this input. It normally appears as grey text inside 117 * the text input and disappears once the user enters text. 118 * 119 * @attribute label 120 * @type string 121 * @default '' 122 */ 123 label: '', 124 125 /** 126 * If true, the label will "float" above the text input once the 127 * user enters text instead of disappearing. 128 * 129 * @attribute floatingLabel 130 * @type boolean 131 * @default false 132 */ 133 floatingLabel: false, 134 135 /** 136 * (multiline only) If set to a non-zero value, the height of this 137 * text input will grow with the value changes until it is maxRows 138 * rows tall. If the maximum size does not fit the value, the text 139 * input will scroll internally. 140 * 141 * @attribute maxRows 142 * @type number 143 * @default 0 144 */ 145 maxRows: 0, 146 147 /** 148 * The message to display if the input value fails validation. If this 149 * is unset or the empty string, a default message is displayed depending 150 * on the type of validation error. 151 * 152 * @attribute error 153 * @type string 154 */ 155 error: '', 156 157 focused: false, 158 pressed: false, 159 160 attached: function() { 161 if (this.multiline) { 162 this.resizeInput(); 163 window.requestAnimationFrame(function() { 164 this.$.underlineContainer.classList.add('animating'); 165 }.bind(this)); 166 } 167 }, 168 169 resizeInput: function() { 170 var height = this.$.inputClone.getBoundingClientRect().height; 171 var bounded = this.maxRows > 0 || this.rows > 0; 172 if (bounded) { 173 var minHeight = this.$.minInputHeight.getBoundingClientRect().height; 174 var maxHeight = this.$.maxInputHeight.getBoundingClientRect().height; 175 height = Math.max(minHeight, Math.min(height, maxHeight)); 176 } 177 this.$.inputContainer.style.height = height + 'px'; 178 this.$.underlineContainer.style.top = height + 'px'; 179 }, 180 181 prepareLabelTransform: function() { 182 var toRect = this.$.floatedLabelSpan.getBoundingClientRect(); 183 var fromRect = this.$.labelSpan.getBoundingClientRect(); 184 if (toRect.width !== 0) { 185 this.$.label.cachedTransform = 'scale(' + (toRect.width / fromRect.width) + ') ' + 186 'translateY(' + (toRect.bottom - fromRect.bottom) + 'px)'; 187 } 188 }, 189 190 toggleLabel: function(force) { 191 var v = force !== undefined ? force : this.inputValue; 192 193 if (!this.floatingLabel) { 194 this.$.label.classList.toggle('hidden', v); 195 } 196 197 if (this.floatingLabel && !this.focused) { 198 this.$.label.classList.toggle('hidden', v); 199 this.$.floatedLabel.classList.toggle('hidden', !v); 200 } 201 }, 202 203 rowsChanged: function() { 204 if (this.multiline && !isNaN(parseInt(this.rows))) { 205 this.$.minInputHeight.innerHTML = ''; 206 for (var i = 0; i < this.rows; i++) { 207 this.$.minInputHeight.appendChild(document.createElement('br')); 208 } 209 this.resizeInput(); 210 } 211 }, 212 213 maxRowsChanged: function() { 214 if (this.multiline && !isNaN(parseInt(this.maxRows))) { 215 this.$.maxInputHeight.innerHTML = ''; 216 for (var i = 0; i < this.maxRows; i++) { 217 this.$.maxInputHeight.appendChild(document.createElement('br')); 218 } 219 this.resizeInput(); 220 } 221 }, 222 223 inputValueChanged: function() { 224 this.super(); 225 226 if (this.multiline) { 227 var escaped = this.inputValue.replace(/\n/gm, '<br>'); 228 if (!escaped || escaped.lastIndexOf('<br>') === escaped.length - 4) { 229 escaped += ' '; 230 } 231 this.$.inputCloneSpan.innerHTML = escaped; 232 this.resizeInput(); 233 } 234 235 this.toggleLabel(); 236 }, 237 238 labelChanged: function() { 239 if (this.floatingLabel && this.$.floatedLabel && this.$.label) { 240 // If the element is created programmatically, labelChanged is called before 241 // binding. Run the measuring code in async so the DOM is ready. 242 this.async(function() { 243 this.prepareLabelTransform(); 244 }); 245 } 246 }, 247 248 placeholderChanged: function() { 249 this.label = this.placeholder; 250 }, 251 252 inputFocusAction: function() { 253 if (!this.pressed) { 254 if (this.floatingLabel) { 255 this.$.floatedLabel.classList.remove('hidden'); 256 this.$.floatedLabel.classList.add('focused'); 257 this.$.floatedLabel.classList.add('focusedColor'); 258 } 259 this.$.label.classList.add('hidden'); 260 this.$.underlineHighlight.classList.add('focused'); 261 this.$.caret.classList.add('focused'); 262 263 this.super(arguments); 264 } 265 this.focused = true; 266 }, 267 268 shouldFloatLabel: function() { 269 // if type = number, the input value is the empty string until a valid number 270 // is entered so we must do some hacks here 271 return this.inputValue || (this.type === 'number' && !this.validity.valid); 272 }, 273 274 inputBlurAction: function() { 275 this.super(arguments); 276 277 this.$.underlineHighlight.classList.remove('focused'); 278 this.$.caret.classList.remove('focused'); 279 280 if (this.floatingLabel) { 281 this.$.floatedLabel.classList.remove('focused'); 282 this.$.floatedLabel.classList.remove('focusedColor'); 283 if (!this.shouldFloatLabel()) { 284 this.$.floatedLabel.classList.add('hidden'); 285 } 286 } 287 288 // type = number hack. see core-input for more info 289 if (!this.shouldFloatLabel()) { 290 this.$.label.classList.remove('hidden'); 291 this.$.label.classList.add('animating'); 292 this.async(function() { 293 this.$.label.style.webkitTransform = 'none'; 294 this.$.label.style.transform = 'none'; 295 }); 296 } 297 298 this.focused = false; 299 }, 300 301 downAction: function(e) { 302 if (this.disabled) { 303 return; 304 } 305 306 if (this.focused) { 307 return; 308 } 309 310 this.pressed = true; 311 var rect = this.$.underline.getBoundingClientRect(); 312 var right = e.x - rect.left; 313 this.$.underlineHighlight.style.webkitTransformOriginX = right + 'px'; 314 this.$.underlineHighlight.style.transformOriginX = right + 'px'; 315 this.$.underlineHighlight.classList.remove('focused'); 316 this.underlineAsync = this.async(function() { 317 this.$.underlineHighlight.classList.add('pressed'); 318 }, null, 200); 319 320 // No caret animation if there is text in the input. 321 if (!this.inputValue) { 322 this.$.caret.classList.remove('focused'); 323 } 324 }, 325 326 upAction: function(e) { 327 if (this.disabled) { 328 return; 329 } 330 331 if (!this.pressed) { 332 return; 333 } 334 335 // if a touchevent caused the up, the synthentic mouseevents will blur 336 // the input, make sure to prevent those from being generated. 337 if (e._source === 'touch') { 338 e.preventDefault(); 339 } 340 341 if (this.underlineAsync) { 342 clearTimeout(this.underlineAsync); 343 this.underlineAsync = null; 344 } 345 346 // Focus the input here to bring up the virtual keyboard. 347 this.$.input.focus(); 348 this.pressed = false; 349 this.animating = true; 350 351 this.$.underlineHighlight.classList.remove('pressed'); 352 this.$.underlineHighlight.classList.add('animating'); 353 this.async(function() { 354 this.$.underlineHighlight.classList.add('focused'); 355 }); 356 357 // No caret animation if there is text in the input. 358 if (!this.inputValue) { 359 this.$.caret.classList.add('animating'); 360 this.async(function() { 361 this.$.caret.classList.add('focused'); 362 }, null, 100); 363 } 364 365 if (this.floatingLabel) { 366 this.$.label.classList.add('focusedColor'); 367 this.$.label.classList.add('animating'); 368 if (!this.$.label.cachedTransform) { 369 this.prepareLabelTransform(); 370 } 371 this.$.label.style.webkitTransform = this.$.label.cachedTransform; 372 this.$.label.style.transform = this.$.label.cachedTransform; 373 } 374 }, 375 376 keydownAction: function() { 377 this.super(); 378 379 // more type = number hacks. see core-input for more info 380 if (this.type === 'number') { 381 this.async(function() { 382 if (!this.inputValue) { 383 this.toggleLabel(!this.validity.valid); 384 } 385 }); 386 } 387 }, 388 389 keypressAction: function() { 390 if (this.animating) { 391 this.transitionEndAction(); 392 } 393 }, 394 395 transitionEndAction: function(e) { 396 this.animating = false; 397 if (this.pressed) { 398 return; 399 } 400 401 if (this.focused) { 402 403 if (this.floatingLabel || this.inputValue) { 404 this.$.label.classList.add('hidden'); 405 } 406 407 if (this.floatingLabel) { 408 this.$.label.classList.remove('focusedColor'); 409 this.$.label.classList.remove('animating'); 410 this.$.floatedLabel.classList.remove('hidden'); 411 this.$.floatedLabel.classList.add('focused'); 412 this.$.floatedLabel.classList.add('focusedColor'); 413 } 414 415 this.async(function() { 416 this.$.underlineHighlight.classList.remove('animating'); 417 this.$.caret.classList.remove('animating'); 418 }, null, 100); 419 420 } else { 421 422 this.$.label.classList.remove('animating'); 423 424 } 425 } 426 427 }); 428 429 }()); 430 431 </script> 432 433 </polymer-element> 434