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