blob: e6ebc86bb3a2df21ebac413755457c63192a03c6 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* 'pin-keyboard' is a keyboard that can be used to enter PINs or more generally
* numeric values.
*
* Properties:
* value: The value of the PIN keyboard. Writing to this property will adjust
* the PIN keyboard's value.
*
* Events:
* pin-change: Fired when the PIN value has changed. The PIN is available at
* event.detail.pin.
* submit: Fired when the PIN is submitted. The PIN is available at
* event.detail.pin.
*
* Example:
* <pin-keyboard on-pin-change="onPinChange" on-submit="onPinSubmit">
* </pin-keyboard>
*/
(function() {
/**
* Once auto backspace starts, the time between individual backspaces.
* @type {number}
* @const
*/
var REPEAT_BACKSPACE_DELAY_MS = 150;
/**
* How long the backspace button must be held down before auto backspace
* starts.
* @type {number}
* @const
*/
var INITIAL_BACKSPACE_DELAY_MS = 500;
/**
* The key codes of the keys allowed to be used on the pin input, in addition to
* number keys. Currently we allow backspace(8), tab(9), left(37) and right(39).
* @type {Array<number>}
* @const
*/
var PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES = [8, 9, 37, 39];
Polymer({
is: 'pin-keyboard',
behaviors: [
I18nBehavior,
],
properties: {
/**
* Whether or not the keyboard's input element should be numerical
* or password.
* @private
*/
enablePassword: {
type: Boolean,
value: false,
},
hasError: Boolean,
/**
* The password element the pin keyboard is associated with. If this is not
* set, then a default input element is shown and used.
* @type {?HTMLElement}
* @private
*/
passwordElement: Object,
/**
* The intervalID used for the backspace button set/clear interval.
* @private
*/
repeatBackspaceIntervalId_: {
type: Number,
value: 0,
},
/**
* The timeoutID used for the auto backspace.
* @private
*/
startAutoBackspaceId_: {
type: Number,
value: 0,
},
/**
* The value stored in the keyboard's input element.
* @private
*/
value: {
type: String,
notify: true,
value: '',
observer: 'onPinValueChange_',
},
/**
* @private
*/
forceUnderline_: {
type: Boolean,
value: false,
},
/**
* Enables pin placeholder.
*/
enablePlaceholder: {
type: Boolean,
value: false,
},
},
listeners: {
'blur': 'onBlur_',
'focus': 'onFocus_',
},
/**
* Gets the selection start of the input field.
* @type {number}
* @private
*/
get selectionStart_() {
return this.passwordElement_().selectionStart;
},
/**
* Gets the selection end of the input field.
* @type {number}
* @private
*/
get selectionEnd_() {
return this.passwordElement_().selectionEnd;
},
/**
* Sets the selection start of the input field.
* @param {number} start The new selection start of the input element.
* @private
*/
set selectionStart_(start) {
this.passwordElement_().selectionStart = start;
},
/**
* Sets the selection end of the input field.
* @param {number} end The new selection end of the input element.
* @private
*/
set selectionEnd_(end) {
this.passwordElement_().selectionEnd = end;
},
/**
* Transfers blur to the input element.
*/
blur: function() {
this.passwordElement_().blur();
},
/**
* Transfers focus to the input element. This should not bring up the virtual
* keyboard, if it is enabled. After focus, moves the caret to the correct
* location if specified.
* @param {number=} opt_selectionStart
* @param {number=} opt_selectionEnd
*/
focus: function(opt_selectionStart, opt_selectionEnd) {
setTimeout(function() {
this.passwordElement_().focus();
this.selectionStart_ = opt_selectionStart || 0;
this.selectionEnd_ = opt_selectionEnd || 0;
}.bind(this), 0);
},
/**
* Transfers focus to the input. Called when a non button element on the
* PIN button area is clicked to prevent focus from leaving the input.
*/
focusInput_: function() {
// Focus the input and place the selected region to its exact previous
// location, as this function will not be called by something that will also
// modify the input value.
this.focus(this.selectionStart_, this.selectionEnd_);
},
/** @private */
onFocus_: function() {
this.forceUnderline_ = true;
},
/** @private */
onBlur_: function() {
this.forceUnderline_ = false;
},
/**
* Called when a keypad number has been tapped.
* @param {Event} event The event object.
* @private
*/
onNumberTap_: function(event) {
var numberValue = event.target.getAttribute('value');
// Add the number where the caret is, then update the selection range of the
// input element.
var selectionStart = this.selectionStart_;
this.value = this.value.substring(0, this.selectionStart_) + numberValue +
this.value.substring(this.selectionEnd_);
// If a number button is clicked, we do not want to switch focus to the
// button, therefore we transfer focus back to the input, but if a number
// button is tabbed into, it should keep focus, so users can use tab and
// spacebar/return to enter their PIN.
if (!event.target.receivedFocusFromKeyboard)
this.focus(selectionStart + 1, selectionStart + 1);
event.stopImmediatePropagation();
},
/** Fires a submit event with the current PIN value. */
firePinSubmitEvent_: function() {
this.fire('submit', {pin: this.value});
},
/**
* Fires an update event with the current PIN value. The event will only be
* fired if the PIN value has actually changed.
* @param {string} value
* @param {string} previous
*/
onPinValueChange_: function(value, previous) {
if (this.passwordElement)
this.passwordElement.value = value;
this.fire('pin-change', {pin: value});
},
/**
* Called when the user wants to erase the last character of the entered
* PIN value.
* @private
*/
onPinClear_: function() {
// If the input is shown, clear the text based on the caret location or
// selected region of the input element. If it is just a caret, remove the
// character in front of the caret.
var selectionStart = this.selectionStart_;
var selectionEnd = this.selectionEnd_;
if (selectionStart == selectionEnd && selectionStart)
selectionStart--;
this.value = this.value.substring(0, selectionStart) +
this.value.substring(selectionEnd);
// Move the caret or selected region to the correct new place.
this.selectionStart_ = selectionStart;
this.selectionEnd_ = selectionStart;
},
/**
* Called when the user presses or touches the backspace button. Starts a
* timer which starts an interval to repeatedly backspace the pin value until
* the interval is cleared.
* @param {Event} event The event object.
* @private
*/
onBackspacePointerDown_: function(event) {
this.startAutoBackspaceId_ = setTimeout(function() {
this.repeatBackspaceIntervalId_ =
setInterval(this.onPinClear_.bind(this), REPEAT_BACKSPACE_DELAY_MS);
}.bind(this), INITIAL_BACKSPACE_DELAY_MS);
if (!event.target.receivedFocusFromKeyboard)
this.focus(this.selectionStart_, this.selectionEnd_);
event.stopImmediatePropagation();
},
/**
* Helper function which clears the timer / interval ids and resets them.
* @private
*/
clearAndReset_: function() {
clearInterval(this.repeatBackspaceIntervalId_);
this.repeatBackspaceIntervalId_ = 0;
clearTimeout(this.startAutoBackspaceId_);
this.startAutoBackspaceId_ = 0;
},
/**
* Called when the user unpresses or untouches the backspace button. Stops the
* interval callback and fires a backspace event if there is no interval
* running.
* @param {Event} event The event object.
* @private
*/
onBackspacePointerUp_: function(event) {
// If an interval has started, do not fire event on pointer up.
if (!this.repeatBackspaceIntervalId_)
this.onPinClear_();
this.clearAndReset_();
// Since on-down gives the input element focus, the input element will
// already have focus when on-up is called. This will actually bring up the
// virtual keyboard, even if focus() is wrapped in a setTimeout. Blur the
// input element first to workaround this.
this.blur();
if (!event.target.receivedFocusFromKeyboard)
this.focus(this.selectionStart_, this.selectionEnd_);
event.stopImmediatePropagation();
},
/**
* Helper function to check whether a given |event| should be processed by
* the numeric only input.
* @param {Event} event The event object.
* @private
*/
isValidEventForInput_: function(event) {
// Valid if the key is a number, and shift is not pressed.
if ((event.keyCode >= 48 && event.keyCode <= 57) && !event.shiftKey)
return true;
// Valid if the key is one of the selected special keys defined in
// |PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES|.
if (PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES.indexOf(event.keyCode) > -1)
return true;
// Valid if the key is CTRL+A to allow users to quickly select the entire
// PIN.
if (event.keyCode == 65 && event.ctrlKey)
return true;
// The rest of the keys are invalid.
return false;
},
/**
* Called when a key event is pressed while the input element has focus.
* @param {Event} event The event object.
* @private
*/
onInputKeyDown_: function(event) {
// Up/down pressed, swallow the event to prevent the input value from
// being incremented or decremented.
if (event.keyCode == 38 || event.keyCode == 40) {
event.preventDefault();
return;
}
// Enter pressed.
if (event.keyCode == 13) {
this.firePinSubmitEvent_();
event.preventDefault();
return;
}
// Do not pass events that are not numbers or special keys we care about. We
// use this instead of input type number because there are several issues
// with input type number, such as no selectionStart/selectionEnd and
// entered non numbers causes the caret to jump to the left.
if (!this.isValidEventForInput_(event)) {
event.preventDefault();
return;
}
},
/**
* Disables the backspace button if nothing is entered.
* @param {string} value
* @private
*/
hasInput_: function(value) {
return value.length > 0 && this.selectionStart_ > 0;
},
/**
* Computes the value of the pin input placeholder.
* @param {boolean} enablePassword
* @param {boolean} enablePlaceholder
* @private
*/
getInputPlaceholder_: function(enablePassword, enablePlaceholder) {
if (!enablePlaceholder)
return '';
return enablePassword ? this.i18n('pinKeyboardPlaceholderPinPassword') :
this.i18n('pinKeyboardPlaceholderPin');
},
/**
* Computes the direction of the pin input.
* @param {string} password
* @private
*/
isInputRtl_: function(password) {
// +password will convert a string to a number or to NaN if that's not
// possible. Number.isInteger will verify the value is not a NaN and that it
// does not contain decimals.
// This heuristic will fail for inputs like '1.0'.
//
// Since we still support users entering their passwords through the PIN
// keyboard, we swap the input box to rtl when we think it is a password
// (just numbers), if the document direction is rtl.
return (document.dir == 'rtl') && !Number.isInteger(+password);
},
/**
* Catch and stop propagation of context menu events since we the backspace
* button can be held down on touch.
* @param {!Event} e
* @private
*/
onContextMenu_: function(e) {
e.preventDefault();
e.stopPropagation();
},
/**
* @return {!HTMLElement} Returns the native input element of |pinInput|.
* @private
*/
passwordElement_: function() {
// |passwordElement| is null by default. It can be set to override the
// input field that will be populated with the keypad.
return this.passwordElement ||
(/** @type {CrInputElement} */ (this.$.pinInput)).inputElement;
},
});
})();