OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * @fileoverview |
| 7 * 'pin-keyboard' is a keyboard that can be used to enter PINs or more generally |
| 8 * numeric values. |
| 9 * |
| 10 * Properties: |
| 11 * value: The value of the PIN keyboard. Writing to this property will adjust |
| 12 * the PIN keyboard's value. |
| 13 * |
| 14 * Events: |
| 15 * pin-change: Fired when the PIN value has changed. The PIN is available at |
| 16 * event.detail.pin. |
| 17 * submit: Fired when the PIN is submitted. The PIN is available at |
| 18 * event.detail.pin. |
| 19 * |
| 20 * Example: |
| 21 * <pin-keyboard on-pin-change="onPinChange" on-submit="onPinSubmit"> |
| 22 * </pin-keyboard> |
| 23 */ |
| 24 |
| 25 (function() { |
| 26 |
| 27 /** |
| 28 * Once auto backspace starts, the time between individual backspaces. |
| 29 * @type {number} |
| 30 * @const |
| 31 */ |
| 32 var REPEAT_BACKSPACE_DELAY_MS = 150; |
| 33 |
| 34 /** |
| 35 * How long the backspace button must be held down before auto backspace |
| 36 * starts. |
| 37 * @type {number} |
| 38 * @const |
| 39 */ |
| 40 var INITIAL_BACKSPACE_DELAY_MS = 500; |
| 41 |
| 42 /** |
| 43 * The key codes of the keys allowed to be used on the pin input, in addition to |
| 44 * number keys. Currently we allow backspace(8), tab(9), left(37) and right(39). |
| 45 * @type {Array<number>} |
| 46 * @const |
| 47 */ |
| 48 var PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES = [8, 9, 37, 39]; |
| 49 |
| 50 Polymer({ |
| 51 is: 'pin-keyboard', |
| 52 |
| 53 behaviors: [ |
| 54 I18nBehavior, |
| 55 ], |
| 56 |
| 57 properties: { |
| 58 /** |
| 59 * Whether or not the keyboard's input element should be numerical |
| 60 * or password. |
| 61 * @private |
| 62 */ |
| 63 enablePassword: { |
| 64 type: Boolean, |
| 65 value: false, |
| 66 }, |
| 67 |
| 68 /** |
| 69 * The password element the pin keyboard is associated with. If this is not |
| 70 * set, then a default input element is shown and used. |
| 71 * @type {?Element} |
| 72 * @private |
| 73 */ |
| 74 passwordElement: { |
| 75 type: Object, |
| 76 value: function() { |
| 77 return this.$.pinInput.inputElement; |
| 78 }, |
| 79 observer: 'onPasswordElementAttached_', |
| 80 }, |
| 81 |
| 82 /** |
| 83 * The intervalID used for the backspace button set/clear interval. |
| 84 * @private |
| 85 */ |
| 86 repeatBackspaceIntervalId_: { |
| 87 type: Number, |
| 88 value: 0, |
| 89 }, |
| 90 |
| 91 /** |
| 92 * The timeoutID used for the auto backspace. |
| 93 * @private |
| 94 */ |
| 95 startAutoBackspaceId_: { |
| 96 type: Number, |
| 97 value: 0, |
| 98 }, |
| 99 |
| 100 /** |
| 101 * Whether or not to show the default pin input. |
| 102 * @private |
| 103 */ |
| 104 showPinInput_: { |
| 105 type: Boolean, |
| 106 value: false, |
| 107 }, |
| 108 |
| 109 /** |
| 110 * The value stored in the keyboard's input element. |
| 111 * @private |
| 112 */ |
| 113 value: { |
| 114 type: String, |
| 115 notify: true, |
| 116 value: '', |
| 117 observer: 'onPinValueChange_', |
| 118 }, |
| 119 }, |
| 120 |
| 121 /** |
| 122 * Called when a password element is attached to the pin keyboard. |
| 123 * @param {HTMLInputElement} inputElement The PIN keyboard's input element. |
| 124 * @private |
| 125 */ |
| 126 onPasswordElementAttached_: function(inputElement) { |
| 127 this.showPinInput_ = inputElement == this.$.pinInput.inputElement; |
| 128 inputElement.addEventListener('input', |
| 129 this.handleInputChanged_.bind(this)); |
| 130 }, |
| 131 |
| 132 /** |
| 133 * Called when the user uses the keyboard to enter a value into the input |
| 134 * element. |
| 135 * @param {Event} event The event object. |
| 136 * @private |
| 137 */ |
| 138 handleInputChanged_: function(event) { |
| 139 this.value = event.target.value; |
| 140 }, |
| 141 |
| 142 /** |
| 143 * Gets the selection start of the input field. |
| 144 * @type {number} |
| 145 * @private |
| 146 */ |
| 147 get selectionStart_() { |
| 148 return this.passwordElement.selectionStart; |
| 149 }, |
| 150 |
| 151 /** |
| 152 * Gets the selection end of the input field. |
| 153 * @type {number} |
| 154 * @private |
| 155 */ |
| 156 get selectionEnd_() { |
| 157 return this.passwordElement.selectionEnd; |
| 158 }, |
| 159 |
| 160 /** |
| 161 * Sets the selection start of the input field. |
| 162 * @param {number} start The new selection start of the input element. |
| 163 * @private |
| 164 */ |
| 165 set selectionStart_(start) { |
| 166 this.passwordElement.selectionStart = start; |
| 167 }, |
| 168 |
| 169 /** |
| 170 * Sets the selection end of the input field. |
| 171 * @param {number} end The new selection end of the input element. |
| 172 * @private |
| 173 */ |
| 174 set selectionEnd_(end) { |
| 175 this.passwordElement.selectionEnd = end; |
| 176 }, |
| 177 |
| 178 /** Transfers focus to the input element. */ |
| 179 focus: function() { |
| 180 this.passwordElement.focus(); |
| 181 }, |
| 182 |
| 183 /** |
| 184 * Called when a keypad number has been tapped. |
| 185 * @param {Event} event The event object. |
| 186 * @private |
| 187 */ |
| 188 onNumberTap_: function(event) { |
| 189 var numberValue = event.target.getAttribute('value'); |
| 190 |
| 191 // Add the number where the caret is, then update the selection range of the |
| 192 // input element. |
| 193 var selectionStart = this.selectionStart_; |
| 194 this.value = this.value.substring(0, this.selectionStart_) + numberValue + |
| 195 this.value.substring(this.selectionEnd_); |
| 196 this.selectionStart_ = selectionStart + 1; |
| 197 this.selectionEnd_ = this.selectionStart_; |
| 198 |
| 199 // If a number button is clicked, we do not want to switch focus to the |
| 200 // button, therefore we transfer focus back to the input, but if a number |
| 201 // button is tabbed into, it should keep focus, so users can use tab and |
| 202 // spacebar/return to enter their PIN. |
| 203 if (!event.target.receivedFocusFromKeyboard) |
| 204 this.focus(); |
| 205 event.preventDefault(); |
| 206 }, |
| 207 |
| 208 /** Fires a submit event with the current PIN value. */ |
| 209 firePinSubmitEvent_: function() { |
| 210 this.fire('submit', { pin: this.value }); |
| 211 }, |
| 212 |
| 213 /** |
| 214 * Fires an update event with the current PIN value. The event will only be |
| 215 * fired if the PIN value has actually changed. |
| 216 * @param {string} value |
| 217 * @param {string} previous |
| 218 */ |
| 219 onPinValueChange_: function(value, previous) { |
| 220 if (value != previous) { |
| 221 this.passwordElement.value = this.value; |
| 222 this.fire('pin-change', { pin: value }); |
| 223 } |
| 224 }, |
| 225 |
| 226 /** |
| 227 * Called when the user wants to erase the last character of the entered |
| 228 * PIN value. |
| 229 * @private |
| 230 */ |
| 231 onPinClear_: function() { |
| 232 // If the input is shown, clear the text based on the caret location or |
| 233 // selected region of the input element. If it is just a caret, remove the |
| 234 // character in front of the caret. |
| 235 var selectionStart = this.selectionStart_; |
| 236 var selectionEnd = this.selectionEnd_; |
| 237 if (selectionStart == selectionEnd) |
| 238 selectionStart--; |
| 239 |
| 240 this.value = this.value.substring(0, selectionStart) + |
| 241 this.value.substring(selectionEnd); |
| 242 |
| 243 // Move the caret or selected region to the correct new place. |
| 244 this.selectionStart_ = selectionStart; |
| 245 this.selectionEnd_ = selectionStart; |
| 246 }, |
| 247 |
| 248 /** |
| 249 * Called when the user presses or touches the backspace button. Starts a |
| 250 * timer which starts an interval to repeatedly backspace the pin value until |
| 251 * the interval is cleared. |
| 252 * @param {Event} event The event object. |
| 253 * @private |
| 254 */ |
| 255 onBackspacePointerDown_: function(event) { |
| 256 this.startAutoBackspaceId_ = setTimeout(function() { |
| 257 this.repeatBackspaceIntervalId_ = setInterval( |
| 258 this.onPinClear_.bind(this), REPEAT_BACKSPACE_DELAY_MS); |
| 259 }.bind(this), INITIAL_BACKSPACE_DELAY_MS); |
| 260 |
| 261 if (!event.target.receivedFocusFromKeyboard) |
| 262 this.focus(); |
| 263 event.preventDefault(); |
| 264 }, |
| 265 |
| 266 /** |
| 267 * Helper function which clears the timer / interval ids and resets them. |
| 268 * @private |
| 269 */ |
| 270 clearAndReset_: function() { |
| 271 clearInterval(this.repeatBackspaceIntervalId_); |
| 272 this.repeatBackspaceIntervalId_ = 0; |
| 273 clearTimeout(this.startAutoBackspaceId_); |
| 274 this.startAutoBackspaceId_ = 0; |
| 275 }, |
| 276 |
| 277 /** |
| 278 * Called when the user exits the backspace button. Stops the interval |
| 279 * callback. |
| 280 * @param {Event} event The event object. |
| 281 * @private |
| 282 */ |
| 283 onBackspacePointerOut_: function(event) { |
| 284 this.clearAndReset_(); |
| 285 |
| 286 if (!event.target.receivedFocusFromKeyboard) |
| 287 this.focus(); |
| 288 event.preventDefault(); |
| 289 }, |
| 290 |
| 291 /** |
| 292 * Called when the user unpresses or untouches the backspace button. Stops the |
| 293 * interval callback and fires a backspace event if there is no interval |
| 294 * running. |
| 295 * @param {Event} event The event object. |
| 296 * @private |
| 297 */ |
| 298 onBackspacePointerUp_: function(event) { |
| 299 // If an interval has started, do not fire event on pointer up. |
| 300 if (!this.repeatBackspaceIntervalId_) |
| 301 this.onPinClear_(); |
| 302 this.clearAndReset_(); |
| 303 |
| 304 if (!event.target.receivedFocusFromKeyboard) |
| 305 this.focus(); |
| 306 event.preventDefault(); |
| 307 }, |
| 308 |
| 309 /** |
| 310 * Helper function to check whether a given |event| should be processed by |
| 311 * the numeric only input. |
| 312 * @param {Event} event The event object. |
| 313 * @private |
| 314 */ |
| 315 isValidEventForInput_: function(event) { |
| 316 // Valid if the key is a number, and shift is not pressed. |
| 317 if ((event.keyCode >= 48 && event.keyCode <= 57) && !event.shiftKey) |
| 318 return true; |
| 319 |
| 320 // Valid if the key is one of the selected special keys defined in |
| 321 // |PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES|. |
| 322 if (PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES.indexOf(event.keyCode) > -1) |
| 323 return true; |
| 324 |
| 325 // Valid if the key is CTRL+A to allow users to quickly select the entire |
| 326 // PIN. |
| 327 if (event.keyCode == 65 && event.ctrlKey) |
| 328 return true; |
| 329 |
| 330 // The rest of the keys are invalid. |
| 331 return false; |
| 332 }, |
| 333 |
| 334 /** |
| 335 * Called when a key event is pressed while the input element has focus. |
| 336 * @param {Event} event The event object. |
| 337 * @private |
| 338 */ |
| 339 onInputKeyDown_: function(event) { |
| 340 // Up/down pressed, swallow the event to prevent the input value from |
| 341 // being incremented or decremented. |
| 342 if (event.keyCode == 38 || event.keyCode == 40) { |
| 343 event.preventDefault(); |
| 344 return; |
| 345 } |
| 346 |
| 347 // Enter pressed. |
| 348 if (event.keyCode == 13) { |
| 349 this.firePinSubmitEvent_(); |
| 350 event.preventDefault(); |
| 351 return; |
| 352 } |
| 353 |
| 354 // Do not pass events that are not numbers or special keys we care about. We |
| 355 // use this instead of input type number because there are several issues |
| 356 // with input type number, such as no selectionStart/selectionEnd and |
| 357 // entered non numbers causes the caret to jump to the left. |
| 358 if (!this.isValidEventForInput_(event)) { |
| 359 event.preventDefault(); |
| 360 return; |
| 361 } |
| 362 }, |
| 363 |
| 364 /** |
| 365 * Disables the backspace button if nothing is entered. |
| 366 * @param {string} value |
| 367 * @private |
| 368 */ |
| 369 hasInput_: function(value) { |
| 370 return value.length > 0; |
| 371 }, |
| 372 |
| 373 /** |
| 374 * Computes the value of the pin input placeholder. |
| 375 * @param {boolean} enablePassword |
| 376 * @private |
| 377 */ |
| 378 getInputPlaceholder_: function(enablePassword) { |
| 379 return enablePassword ? this.i18n('pinKeyboardPlaceholderPinPassword') : |
| 380 this.i18n('pinKeyboardPlaceholderPin'); |
| 381 }, |
| 382 |
| 383 /** |
| 384 * Computes the direction of the pin input. |
| 385 * @param {string} password |
| 386 * @private |
| 387 */ |
| 388 isInputRtl_: function(password) { |
| 389 // +password will convert a string to a number or to NaN if that's not |
| 390 // possible. Number.isInteger will verify the value is not a NaN and that it |
| 391 // does not contain decimals. |
| 392 // This heuristic will fail for inputs like '1.0'. |
| 393 // |
| 394 // Since we still support users entering their passwords through the PIN |
| 395 // keyboard, we swap the input box to rtl when we think it is a password |
| 396 // (just numbers), if the document direction is rtl. |
| 397 return (document.dir == 'rtl') && !Number.isInteger(+password); |
| 398 }, |
| 399 }); |
| 400 })(); |
OLD | NEW |