ngx-touch-keyboard 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/karma.conf.js +44 -0
- package/ng-package.json +7 -0
- package/package.json +35 -0
- package/src/lib/layouts/index.ts +132 -0
- package/src/lib/layouts/layouts.type.ts +7 -0
- package/src/lib/ngx-touch-keyboard.component.html +26 -0
- package/src/lib/ngx-touch-keyboard.component.scss +103 -0
- package/src/lib/ngx-touch-keyboard.component.spec.ts +22 -0
- package/src/lib/ngx-touch-keyboard.component.ts +723 -0
- package/src/lib/ngx-touch-keyboard.directive.ts +164 -0
- package/src/lib/ngx-touch-keyboard.module.ts +14 -0
- package/src/public-api.ts +7 -0
- package/src/test.ts +27 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +17 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
HostListener,
|
|
7
|
+
OnInit,
|
|
8
|
+
Output,
|
|
9
|
+
ViewEncapsulation,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
|
|
12
|
+
import { KeyboardDisplay, KeyboardLayout } from './layouts/layouts.type';
|
|
13
|
+
import { getDefaultDisplay, getDefaultLayout } from './layouts';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ngx-touch-keyboard',
|
|
17
|
+
templateUrl: './ngx-touch-keyboard.component.html',
|
|
18
|
+
styleUrls: ['./ngx-touch-keyboard.component.scss'],
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class NgxTouchKeyboardComponent implements OnInit {
|
|
23
|
+
layoutMode = 'text';
|
|
24
|
+
layoutName = 'default';
|
|
25
|
+
layout!: KeyboardLayout;
|
|
26
|
+
display!: KeyboardDisplay;
|
|
27
|
+
debug = false;
|
|
28
|
+
|
|
29
|
+
@Output() closePanel = new EventEmitter<void>();
|
|
30
|
+
|
|
31
|
+
private _activeButtonClass = 'active';
|
|
32
|
+
private _holdInteractionTimeout!: number;
|
|
33
|
+
private _holdTimeout!: number;
|
|
34
|
+
private _isMouseHold!: boolean;
|
|
35
|
+
private _caretPosition: number | null = null;
|
|
36
|
+
private _caretPositionEnd: number | null = null;
|
|
37
|
+
private _activeInputElement!: HTMLInputElement | HTMLTextAreaElement | null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Constructor
|
|
41
|
+
*/
|
|
42
|
+
constructor(private _elementRef: ElementRef<HTMLInputElement>) {}
|
|
43
|
+
|
|
44
|
+
// -----------------------------------------------------------------------------------------------------
|
|
45
|
+
// @ Decorated methods
|
|
46
|
+
// -----------------------------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* On keyup
|
|
50
|
+
*/
|
|
51
|
+
@HostListener('window:keyup', ['$event'])
|
|
52
|
+
handleKeyUp(event: KeyboardEvent): void {
|
|
53
|
+
if (event.isTrusted) {
|
|
54
|
+
this._caretEventHandler(event);
|
|
55
|
+
this._handleHighlightKeyUp(event);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* On keydown
|
|
61
|
+
*/
|
|
62
|
+
@HostListener('window:keydown', ['$event'])
|
|
63
|
+
handleKeyDown(event: KeyboardEvent): void {
|
|
64
|
+
if (event.isTrusted) {
|
|
65
|
+
this._handleHighlightKeyDown(event);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* On pointerup (mouseup or touchend)
|
|
71
|
+
*/
|
|
72
|
+
@HostListener('window:pointerup', ['$event'])
|
|
73
|
+
handleMouseUp(event: MouseEvent): void {
|
|
74
|
+
this._caretEventHandler(event);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* On select
|
|
79
|
+
*/
|
|
80
|
+
@HostListener('window:select', ['$event'])
|
|
81
|
+
handleSelect(event: Event): void {
|
|
82
|
+
this._caretEventHandler(event);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* On selectionchange
|
|
87
|
+
*/
|
|
88
|
+
@HostListener('window:selectionchange', ['$event'])
|
|
89
|
+
handleSelectionChange(event: Event): void {
|
|
90
|
+
this._caretEventHandler(event);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// -----------------------------------------------------------------------------------------------------
|
|
94
|
+
// @ Lifecycle hooks
|
|
95
|
+
// -----------------------------------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* On init
|
|
99
|
+
*/
|
|
100
|
+
ngOnInit(): void {
|
|
101
|
+
this.layout = getDefaultLayout();
|
|
102
|
+
this.display = getDefaultDisplay();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// -----------------------------------------------------------------------------------------------------
|
|
106
|
+
// @ Public methods
|
|
107
|
+
// -----------------------------------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Set active input
|
|
111
|
+
*
|
|
112
|
+
* @param input Input native element
|
|
113
|
+
*/
|
|
114
|
+
setActiveInput(input: HTMLInputElement | HTMLTextAreaElement): void {
|
|
115
|
+
this._activeInputElement = input;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Tracking keyboard layout
|
|
119
|
+
*/
|
|
120
|
+
const inputMode = this._activeInputElement?.inputMode;
|
|
121
|
+
if (
|
|
122
|
+
inputMode &&
|
|
123
|
+
['text', 'search', 'email', 'url', 'numeric', 'decimal', 'tel'].some(
|
|
124
|
+
(i) => i === inputMode
|
|
125
|
+
)
|
|
126
|
+
) {
|
|
127
|
+
this.layoutMode = inputMode;
|
|
128
|
+
this.layoutName = 'default';
|
|
129
|
+
} else {
|
|
130
|
+
this.layoutMode = 'text';
|
|
131
|
+
this.layoutName = 'default';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.debug) {
|
|
135
|
+
console.log('Layout:', `${this.layoutMode}_${this.layoutName}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* we must ensure caretPosition doesn't persist once reactivated.
|
|
140
|
+
*/
|
|
141
|
+
this._setCaretPosition(
|
|
142
|
+
this._activeInputElement.selectionStart,
|
|
143
|
+
this._activeInputElement.selectionEnd
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (this.debug) {
|
|
147
|
+
console.log(
|
|
148
|
+
'Caret start at:',
|
|
149
|
+
this._caretPosition,
|
|
150
|
+
this._caretPositionEnd
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// And set focus to input
|
|
155
|
+
this._focusActiveInput();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check whether the button is a standard button
|
|
160
|
+
*/
|
|
161
|
+
isStandardButton = (button: string) =>
|
|
162
|
+
button && !(button[0] === '{' && button[button.length - 1] === '}');
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Retrieve button type
|
|
166
|
+
*
|
|
167
|
+
* @param button The button's layout name
|
|
168
|
+
* @return The button type
|
|
169
|
+
*/
|
|
170
|
+
getButtonType(button: string): string {
|
|
171
|
+
return button.includes('{') && button.includes('}')
|
|
172
|
+
? 'function-key'
|
|
173
|
+
: 'standard-key';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Adds default classes to a given button
|
|
178
|
+
*
|
|
179
|
+
* @param button The button's layout name
|
|
180
|
+
* @return The classes to be added to the button
|
|
181
|
+
*/
|
|
182
|
+
getButtonClass(button: string): string {
|
|
183
|
+
const buttonTypeClass = this.getButtonType(button);
|
|
184
|
+
const buttonWithoutBraces = button.replace('{', '').replace('}', '');
|
|
185
|
+
let buttonNormalized = '';
|
|
186
|
+
|
|
187
|
+
if (buttonTypeClass !== 'standard-key')
|
|
188
|
+
buttonNormalized = `${buttonWithoutBraces}-key`;
|
|
189
|
+
|
|
190
|
+
return `${buttonTypeClass} ${buttonNormalized}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns the display (label) name for a given button
|
|
195
|
+
*
|
|
196
|
+
* @param button The button's layout name
|
|
197
|
+
* @return The display name to be show to the button
|
|
198
|
+
*/
|
|
199
|
+
getButtonDisplayName(button: string): string {
|
|
200
|
+
return this.display[button] || button;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Handles clicks made to keyboard buttons
|
|
205
|
+
*
|
|
206
|
+
* @param button The button layout name.
|
|
207
|
+
* @param event The button event.
|
|
208
|
+
*/
|
|
209
|
+
handleButtonClicked(button: string, e?: Event): void {
|
|
210
|
+
if (this.debug) {
|
|
211
|
+
console.log('Key pressed:', button);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (button === '{shift}') {
|
|
215
|
+
this.layoutName = this.layoutName === 'default' ? 'shift' : 'default';
|
|
216
|
+
return;
|
|
217
|
+
} else if (button === '{abc}') {
|
|
218
|
+
this.layoutName = 'default';
|
|
219
|
+
return;
|
|
220
|
+
} else if (button === '{numbers}') {
|
|
221
|
+
this.layoutName = 'numbers';
|
|
222
|
+
return;
|
|
223
|
+
} else if (button === '{extends}') {
|
|
224
|
+
this.layoutName = 'extends';
|
|
225
|
+
return;
|
|
226
|
+
} else if (button === '{ent}') {
|
|
227
|
+
this.closePanel.emit();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const commonParams: [number, number, boolean] = [
|
|
232
|
+
this._caretPosition || 0,
|
|
233
|
+
this._caretPositionEnd || 0,
|
|
234
|
+
true,
|
|
235
|
+
];
|
|
236
|
+
let output = this._activeInputElement?.value || '';
|
|
237
|
+
|
|
238
|
+
if (button === '{backspace}' && output.length > 0) {
|
|
239
|
+
output = this._removeAt(output, ...commonParams);
|
|
240
|
+
} else if (button === '{space}') {
|
|
241
|
+
output = this._addStringAt(output, ' ', ...commonParams);
|
|
242
|
+
} else if (button === '{tab}') {
|
|
243
|
+
output = this._addStringAt(output, '\t', ...commonParams);
|
|
244
|
+
} else if (button === '{enter}') {
|
|
245
|
+
output = this._addStringAt(output, '\n', ...commonParams);
|
|
246
|
+
} else if (button === '{' || button === '}') {
|
|
247
|
+
output = this._addStringAt(output, button, ...commonParams);
|
|
248
|
+
} else if (!button.includes('{') && !button.includes('}')) {
|
|
249
|
+
output = this._addStringAt(output, button, ...commonParams);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this._activeInputElement) {
|
|
253
|
+
this._activeInputElement.value = output;
|
|
254
|
+
|
|
255
|
+
if (this.debug) {
|
|
256
|
+
console.log(
|
|
257
|
+
'Caret at:',
|
|
258
|
+
this._caretPosition,
|
|
259
|
+
this._caretPositionEnd,
|
|
260
|
+
'Button',
|
|
261
|
+
e
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this._dispatchEvents(button);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Handles button mousedown
|
|
271
|
+
*
|
|
272
|
+
* @param button The button layout name.
|
|
273
|
+
* @param event The button event.
|
|
274
|
+
*/
|
|
275
|
+
handleButtonMouseDown(button: string, e?: Event): void {
|
|
276
|
+
if (e) {
|
|
277
|
+
/**
|
|
278
|
+
* Handle event options
|
|
279
|
+
*/
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
e.stopPropagation();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Add active class
|
|
286
|
+
*/
|
|
287
|
+
this._setActiveButton(button);
|
|
288
|
+
|
|
289
|
+
if (this._holdInteractionTimeout)
|
|
290
|
+
clearTimeout(this._holdInteractionTimeout);
|
|
291
|
+
if (this._holdTimeout) clearTimeout(this._holdTimeout);
|
|
292
|
+
this._isMouseHold = true;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Time to wait until a key hold is detected
|
|
296
|
+
*/
|
|
297
|
+
this._holdTimeout = window.setTimeout(() => {
|
|
298
|
+
if (
|
|
299
|
+
this._isMouseHold &&
|
|
300
|
+
((!button.includes('{') && !button.includes('}')) ||
|
|
301
|
+
button === '{backspace}' ||
|
|
302
|
+
button === '{space}')
|
|
303
|
+
) {
|
|
304
|
+
if (this.debug) {
|
|
305
|
+
console.log('Button held:', button);
|
|
306
|
+
}
|
|
307
|
+
this.handleButtonHold(button);
|
|
308
|
+
}
|
|
309
|
+
clearTimeout(this._holdTimeout);
|
|
310
|
+
}, 500);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handles button mouseup
|
|
315
|
+
*
|
|
316
|
+
* @param button The button layout name.
|
|
317
|
+
* @param event The button event.
|
|
318
|
+
*/
|
|
319
|
+
handleButtonMouseUp(button: string, e?: Event): void {
|
|
320
|
+
if (e) {
|
|
321
|
+
/**
|
|
322
|
+
* Handle event options
|
|
323
|
+
*/
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
e.stopPropagation();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Remove active class
|
|
330
|
+
*/
|
|
331
|
+
this._removeActiveButton();
|
|
332
|
+
|
|
333
|
+
this._isMouseHold = false;
|
|
334
|
+
if (this._holdInteractionTimeout)
|
|
335
|
+
clearTimeout(this._holdInteractionTimeout);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Handles button hold
|
|
340
|
+
*/
|
|
341
|
+
handleButtonHold(button: string): void {
|
|
342
|
+
if (this._holdInteractionTimeout)
|
|
343
|
+
clearTimeout(this._holdInteractionTimeout);
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Timeout dictating the speed of key hold iterations
|
|
347
|
+
*/
|
|
348
|
+
this._holdInteractionTimeout = window.setTimeout(() => {
|
|
349
|
+
if (this._isMouseHold) {
|
|
350
|
+
this.handleButtonClicked(button);
|
|
351
|
+
this.handleButtonHold(button);
|
|
352
|
+
} else {
|
|
353
|
+
clearTimeout(this._holdInteractionTimeout);
|
|
354
|
+
}
|
|
355
|
+
}, 100);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// -----------------------------------------------------------------------------------------------------
|
|
359
|
+
// @ Private methods
|
|
360
|
+
// -----------------------------------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Changes the internal caret position
|
|
364
|
+
*
|
|
365
|
+
* @private
|
|
366
|
+
* @param position The caret's start position
|
|
367
|
+
* @param positionEnd The caret's end position
|
|
368
|
+
*/
|
|
369
|
+
private _setCaretPosition(
|
|
370
|
+
position: number | null,
|
|
371
|
+
endPosition = position
|
|
372
|
+
): void {
|
|
373
|
+
this._caretPosition = position;
|
|
374
|
+
this._caretPositionEnd = endPosition;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Moves the cursor position by a given amount
|
|
379
|
+
*
|
|
380
|
+
* @private
|
|
381
|
+
* @param length Represents by how many characters the input should be moved
|
|
382
|
+
* @param minus Whether the cursor should be moved to the left or not.
|
|
383
|
+
*/
|
|
384
|
+
private _updateCaretPos(length: number, minus = false) {
|
|
385
|
+
const newCaretPos = this._updateCaretPosAction(length, minus);
|
|
386
|
+
this._setCaretPosition(newCaretPos);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Action method of updateCaretPos
|
|
391
|
+
*
|
|
392
|
+
* @private
|
|
393
|
+
* @param length Represents by how many characters the input should be moved
|
|
394
|
+
* @param minus Whether the cursor should be moved to the left or not.
|
|
395
|
+
*/
|
|
396
|
+
private _updateCaretPosAction(length: number, minus = false) {
|
|
397
|
+
let caretPosition = this._caretPosition;
|
|
398
|
+
|
|
399
|
+
if (caretPosition != null) {
|
|
400
|
+
if (minus) {
|
|
401
|
+
if (caretPosition > 0) caretPosition = caretPosition - length;
|
|
402
|
+
} else {
|
|
403
|
+
caretPosition = caretPosition + length;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return caretPosition;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Removes an amount of characters before a given position
|
|
412
|
+
*
|
|
413
|
+
* @private
|
|
414
|
+
* @param source The source input
|
|
415
|
+
* @param position The (cursor) position from where the characters should be removed
|
|
416
|
+
* @param moveCaret Whether to update input cursor
|
|
417
|
+
*/
|
|
418
|
+
private _removeAt(
|
|
419
|
+
source: string,
|
|
420
|
+
position = source.length,
|
|
421
|
+
positionEnd = source.length,
|
|
422
|
+
moveCaret = false
|
|
423
|
+
) {
|
|
424
|
+
if (position === 0 && positionEnd === 0) {
|
|
425
|
+
return source;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let output;
|
|
429
|
+
|
|
430
|
+
if (position === positionEnd) {
|
|
431
|
+
let prevTwoChars;
|
|
432
|
+
let emojiMatched;
|
|
433
|
+
const emojiMatchedReg = /([\uD800-\uDBFF][\uDC00-\uDFFF])/g;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Emojis are made out of two characters, so we must take a custom approach to trim them.
|
|
437
|
+
* For more info: https://mathiasbynens.be/notes/javascript-unicode
|
|
438
|
+
*/
|
|
439
|
+
if (position && position >= 0) {
|
|
440
|
+
prevTwoChars = source.substring(position - 2, position);
|
|
441
|
+
emojiMatched = prevTwoChars.match(emojiMatchedReg);
|
|
442
|
+
|
|
443
|
+
if (emojiMatched) {
|
|
444
|
+
output = source.substr(0, position - 2) + source.substr(position);
|
|
445
|
+
if (moveCaret) this._updateCaretPos(2, true);
|
|
446
|
+
} else {
|
|
447
|
+
output = source.substr(0, position - 1) + source.substr(position);
|
|
448
|
+
if (moveCaret) this._updateCaretPos(1, true);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
prevTwoChars = source.slice(-2);
|
|
452
|
+
emojiMatched = prevTwoChars.match(emojiMatchedReg);
|
|
453
|
+
|
|
454
|
+
if (emojiMatched) {
|
|
455
|
+
output = source.slice(0, -2);
|
|
456
|
+
if (moveCaret) this._updateCaretPos(2, true);
|
|
457
|
+
} else {
|
|
458
|
+
output = source.slice(0, -1);
|
|
459
|
+
if (moveCaret) this._updateCaretPos(1, true);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
output = source.slice(0, position) + source.slice(positionEnd);
|
|
464
|
+
if (moveCaret) {
|
|
465
|
+
this._setCaretPosition(position);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return output;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Adds a string to the input at a given position
|
|
474
|
+
*
|
|
475
|
+
* @private
|
|
476
|
+
* @param source The source input
|
|
477
|
+
* @param str The string to add
|
|
478
|
+
* @param position The (cursor) position where the string should be added
|
|
479
|
+
* @param moveCaret Whether to update virtual-keyboard cursor
|
|
480
|
+
*/
|
|
481
|
+
private _addStringAt(
|
|
482
|
+
source: string,
|
|
483
|
+
str: string,
|
|
484
|
+
position = source.length,
|
|
485
|
+
positionEnd = source.length,
|
|
486
|
+
moveCaret = false
|
|
487
|
+
) {
|
|
488
|
+
let output;
|
|
489
|
+
|
|
490
|
+
if (!position && position !== 0) {
|
|
491
|
+
output = source + str;
|
|
492
|
+
} else {
|
|
493
|
+
output = [source.slice(0, position), str, source.slice(positionEnd)].join(
|
|
494
|
+
''
|
|
495
|
+
);
|
|
496
|
+
if (moveCaret) this._updateCaretPos(str.length, false);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return output;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Method to dispatch necessary keyboard events to current input element.
|
|
504
|
+
* @see https://w3c.github.io/uievents/tools/key-event-viewer.html
|
|
505
|
+
*
|
|
506
|
+
* @param button
|
|
507
|
+
*/
|
|
508
|
+
private _dispatchEvents(button: string) {
|
|
509
|
+
let key, code;
|
|
510
|
+
if (button.includes('{') && button.includes('}')) {
|
|
511
|
+
// Capitalize name
|
|
512
|
+
key = button.slice(1, button.length - 1).toLowerCase();
|
|
513
|
+
key = key.charAt(0).toUpperCase() + key.slice(1);
|
|
514
|
+
code = key;
|
|
515
|
+
} else {
|
|
516
|
+
key = button;
|
|
517
|
+
code = Number.isInteger(Number(button))
|
|
518
|
+
? `Digit${button}`
|
|
519
|
+
: `Key${button.toUpperCase()}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const eventInit: KeyboardEventInit = {
|
|
523
|
+
bubbles: true,
|
|
524
|
+
cancelable: true,
|
|
525
|
+
shiftKey: this.layoutName == 'shift',
|
|
526
|
+
key: key,
|
|
527
|
+
code: code,
|
|
528
|
+
location: 0,
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Simulate all needed events on base element
|
|
532
|
+
this._activeInputElement?.dispatchEvent(
|
|
533
|
+
new KeyboardEvent('keydown', eventInit)
|
|
534
|
+
);
|
|
535
|
+
this._activeInputElement?.dispatchEvent(
|
|
536
|
+
new KeyboardEvent('keypress', eventInit)
|
|
537
|
+
);
|
|
538
|
+
this._activeInputElement?.dispatchEvent(
|
|
539
|
+
new Event('input', { bubbles: true })
|
|
540
|
+
);
|
|
541
|
+
this._activeInputElement?.dispatchEvent(
|
|
542
|
+
new KeyboardEvent('keyup', eventInit)
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// And set focus to input
|
|
546
|
+
this._focusActiveInput();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Called when an event that warrants a cursor position update is triggered
|
|
551
|
+
*
|
|
552
|
+
* @private
|
|
553
|
+
* @param event
|
|
554
|
+
*/
|
|
555
|
+
private _caretEventHandler(event: any) {
|
|
556
|
+
let targetTagName = '';
|
|
557
|
+
if (event.target.tagName) {
|
|
558
|
+
targetTagName = event.target.tagName.toLowerCase();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const isTextInput =
|
|
562
|
+
targetTagName === 'textarea' ||
|
|
563
|
+
(targetTagName === 'input' &&
|
|
564
|
+
['text', 'search', 'email', 'password', 'url', 'tel'].includes(
|
|
565
|
+
event.target.type
|
|
566
|
+
));
|
|
567
|
+
|
|
568
|
+
const isKeyboard =
|
|
569
|
+
event.target === this._elementRef.nativeElement ||
|
|
570
|
+
(event.target && this._elementRef.nativeElement.contains(event.target));
|
|
571
|
+
|
|
572
|
+
if (isTextInput && this._activeInputElement == event.target) {
|
|
573
|
+
/**
|
|
574
|
+
* Tracks current cursor position
|
|
575
|
+
* As keys are pressed, text will be added/removed at that position within the input.
|
|
576
|
+
*/
|
|
577
|
+
this._setCaretPosition(
|
|
578
|
+
event.target.selectionStart,
|
|
579
|
+
event.target.selectionEnd
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (this.debug) {
|
|
583
|
+
console.log(
|
|
584
|
+
'Caret at:',
|
|
585
|
+
this._caretPosition,
|
|
586
|
+
this._caretPositionEnd,
|
|
587
|
+
event && event.target.tagName.toLowerCase(),
|
|
588
|
+
event
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
} else if (
|
|
592
|
+
event.type === 'pointerup' &&
|
|
593
|
+
this._activeInputElement === document.activeElement
|
|
594
|
+
) {
|
|
595
|
+
if (this._isMouseHold) {
|
|
596
|
+
this.handleButtonMouseUp('');
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
} else if (!isKeyboard && event?.type !== 'selectionchange') {
|
|
600
|
+
/**
|
|
601
|
+
* we must ensure caretPosition doesn't persist once reactivated.
|
|
602
|
+
*/
|
|
603
|
+
this._setCaretPosition(null);
|
|
604
|
+
|
|
605
|
+
if (this.debug) {
|
|
606
|
+
console.log(
|
|
607
|
+
`Caret position reset due to "${event?.type}" event`,
|
|
608
|
+
event
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Close panel
|
|
614
|
+
*/
|
|
615
|
+
this.closePanel.emit();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Focus to input
|
|
621
|
+
*
|
|
622
|
+
* @private
|
|
623
|
+
*/
|
|
624
|
+
private _focusActiveInput(): void {
|
|
625
|
+
this._activeInputElement?.focus();
|
|
626
|
+
this._activeInputElement?.setSelectionRange(
|
|
627
|
+
this._caretPosition,
|
|
628
|
+
this._caretPositionEnd
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Handel highlight on key down
|
|
634
|
+
*
|
|
635
|
+
* @private
|
|
636
|
+
* @param event The KeyboardEvent
|
|
637
|
+
*/
|
|
638
|
+
private _handleHighlightKeyDown(event: KeyboardEvent): void {
|
|
639
|
+
const buttonPressed = this._getKeyboardLayoutKey(event);
|
|
640
|
+
/**
|
|
641
|
+
* Add active class
|
|
642
|
+
*/
|
|
643
|
+
this._setActiveButton(buttonPressed);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Handel highlight on key up
|
|
648
|
+
*
|
|
649
|
+
* @private
|
|
650
|
+
* @param event The KeyboardEvent
|
|
651
|
+
*/
|
|
652
|
+
private _handleHighlightKeyUp(event: KeyboardEvent): void {
|
|
653
|
+
const buttonPressed = this._getKeyboardLayoutKey(event);
|
|
654
|
+
/**
|
|
655
|
+
* Remove active class
|
|
656
|
+
*/
|
|
657
|
+
this._removeActiveButton(buttonPressed);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Transforms a KeyboardEvent's "key.code" string into a virtual-keyboard layout format
|
|
662
|
+
*
|
|
663
|
+
* @private
|
|
664
|
+
* @param event The KeyboardEvent
|
|
665
|
+
*/
|
|
666
|
+
private _getKeyboardLayoutKey(event: KeyboardEvent) {
|
|
667
|
+
let output = '';
|
|
668
|
+
const keyId = event.code || event.key;
|
|
669
|
+
|
|
670
|
+
if (
|
|
671
|
+
keyId?.includes('Space') ||
|
|
672
|
+
keyId?.includes('Numpad') ||
|
|
673
|
+
keyId?.includes('Backspace') ||
|
|
674
|
+
keyId?.includes('CapsLock') ||
|
|
675
|
+
keyId?.includes('Meta')
|
|
676
|
+
) {
|
|
677
|
+
output = `{${event.code}}` || '';
|
|
678
|
+
} else if (
|
|
679
|
+
keyId?.includes('Control') ||
|
|
680
|
+
keyId?.includes('Shift') ||
|
|
681
|
+
keyId?.includes('Alt')
|
|
682
|
+
) {
|
|
683
|
+
output = `{${event.key}}` || '';
|
|
684
|
+
} else {
|
|
685
|
+
output = event.key || '';
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return output.length > 1 ? output?.toLowerCase() : output;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Set active class in button
|
|
693
|
+
*
|
|
694
|
+
* @param buttonName
|
|
695
|
+
*/
|
|
696
|
+
private _setActiveButton(buttonName: string): void {
|
|
697
|
+
const node = this._elementRef.nativeElement
|
|
698
|
+
.getElementsByTagName('button')
|
|
699
|
+
.namedItem(buttonName);
|
|
700
|
+
if (node && node.classList) {
|
|
701
|
+
node.classList.add(this._activeButtonClass);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Remove active button
|
|
707
|
+
*
|
|
708
|
+
* @param buttonName
|
|
709
|
+
*/
|
|
710
|
+
private _removeActiveButton(buttonName?: string): void {
|
|
711
|
+
const nodes = this._elementRef.nativeElement.getElementsByTagName('button');
|
|
712
|
+
if (buttonName) {
|
|
713
|
+
const node = nodes.namedItem(buttonName);
|
|
714
|
+
if (node && node.classList) {
|
|
715
|
+
node.classList.remove(this._activeButtonClass);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
719
|
+
nodes[i].classList.remove(this._activeButtonClass);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|