0
# Input & Hardware
1
2
Barcode scanning, NFC communication, keyboard management, and hardware interaction capabilities for advanced input processing and device integration.
3
4
## Capabilities
5
6
### Barcode Scanner
7
8
Scan various barcode formats including QR codes, UPC codes, and other standard formats with customizable options.
9
10
```typescript { .api }
11
/**
12
* Barcode scanner configuration options
13
*/
14
interface BarcodeScannerOptions {
15
/** Prefer front camera for scanning */
16
preferFrontCamera?: boolean;
17
/** Show flip camera button */
18
showFlipCameraButton?: boolean;
19
/** Show torch button */
20
showTorchButton?: boolean;
21
/** Start with torch on */
22
torchOn?: boolean;
23
/** Prompt text displayed to user */
24
prompt?: string;
25
/** Duration to display result in milliseconds */
26
resultDisplayDuration?: number;
27
/** Supported barcode formats (comma-separated) */
28
formats?: string;
29
/** Screen orientation (portrait, landscape) */
30
orientation?: string;
31
/** Disable animations */
32
disableAnimations?: boolean;
33
/** Disable success beep */
34
disableSuccessBeep?: boolean;
35
}
36
37
/**
38
* Barcode scan result
39
*/
40
interface BarcodeScanResult {
41
/** Scanned text content */
42
text: string;
43
/** Barcode format (QR_CODE, UPC_A, CODE_128, etc.) */
44
format: string;
45
/** Whether scan was cancelled by user */
46
cancelled: boolean;
47
}
48
49
/**
50
* BarcodeScanner class for scanning barcodes and QR codes
51
*/
52
class BarcodeScanner {
53
/**
54
* Scan barcode using device camera
55
* @param options Scanner configuration options
56
* @returns Promise resolving to BarcodeScanResult
57
*/
58
static scan(options?: BarcodeScannerOptions): Promise<BarcodeScanResult>;
59
60
/**
61
* Encode text into barcode
62
* @param type Barcode type (TEXT_TYPE, EMAIL_TYPE, PHONE_TYPE, etc.)
63
* @param data Data to encode
64
* @returns Promise resolving to encoded barcode
65
*/
66
static encode(type: string, data: any): Promise<any>;
67
}
68
```
69
70
**Usage Examples:**
71
72
```typescript
73
import { BarcodeScanner, BarcodeScannerOptions, BarcodeScanResult } from 'ionic-native';
74
75
// Basic barcode scanning
76
async function scanBarcode(): Promise<BarcodeScanResult | null> {
77
const options: BarcodeScannerOptions = {
78
preferFrontCamera: false,
79
showFlipCameraButton: true,
80
showTorchButton: true,
81
torchOn: false,
82
prompt: "Place a barcode inside the scan area",
83
resultDisplayDuration: 500,
84
formats: "QR_CODE,PDF_417,CODE_128,CODE_39,UPC_A,UPC_E,EAN_13,EAN_8",
85
orientation: "portrait",
86
disableAnimations: true,
87
disableSuccessBeep: false
88
};
89
90
try {
91
const result = await BarcodeScanner.scan(options);
92
93
if (result.cancelled) {
94
console.log('Scan cancelled by user');
95
return null;
96
}
97
98
console.log('Scanned result:', {
99
text: result.text,
100
format: result.format
101
});
102
103
return result;
104
} catch (error) {
105
console.error('Scan failed:', error);
106
throw error;
107
}
108
}
109
110
// QR Code specific scanning
111
class QRCodeScanner {
112
113
async scanQRCode(): Promise<BarcodeScanResult | null> {
114
const options: BarcodeScannerOptions = {
115
preferFrontCamera: false,
116
showFlipCameraButton: false,
117
showTorchButton: true,
118
prompt: "Scan QR Code",
119
formats: "QR_CODE",
120
orientation: "portrait"
121
};
122
123
try {
124
const result = await BarcodeScanner.scan(options);
125
126
if (!result.cancelled) {
127
return this.parseQRCodeContent(result);
128
}
129
130
return null;
131
} catch (error) {
132
console.error('QR Code scan failed:', error);
133
throw error;
134
}
135
}
136
137
private parseQRCodeContent(result: BarcodeScanResult): BarcodeScanResult {
138
const text = result.text;
139
140
// Parse different QR code types
141
if (text.startsWith('http://') || text.startsWith('https://')) {
142
console.log('URL detected:', text);
143
} else if (text.startsWith('wifi:')) {
144
console.log('WiFi credentials detected');
145
this.parseWiFiQR(text);
146
} else if (text.startsWith('mailto:')) {
147
console.log('Email detected:', text);
148
} else if (text.startsWith('tel:')) {
149
console.log('Phone number detected:', text);
150
} else if (text.startsWith('geo:')) {
151
console.log('Location detected:', text);
152
} else {
153
console.log('Plain text:', text);
154
}
155
156
return result;
157
}
158
159
private parseWiFiQR(qrText: string): { ssid: string; password: string; security: string } | null {
160
// Parse WiFi QR format: WIFI:T:WPA;S:MyNetwork;P:MyPassword;;
161
const wifiMatch = qrText.match(/WIFI:T:([^;]*);S:([^;]*);P:([^;]*);/);
162
163
if (wifiMatch) {
164
return {
165
security: wifiMatch[1],
166
ssid: wifiMatch[2],
167
password: wifiMatch[3]
168
};
169
}
170
171
return null;
172
}
173
174
async generateQRCode(data: string, type: string = 'TEXT_TYPE'): Promise<any> {
175
try {
176
const result = await BarcodeScanner.encode(type, data);
177
console.log('QR Code generated:', result);
178
return result;
179
} catch (error) {
180
console.error('QR Code generation failed:', error);
181
throw error;
182
}
183
}
184
}
185
186
// Inventory scanning system
187
class InventoryScanner {
188
private scannedItems: Map<string, any> = new Map();
189
190
async scanInventoryItem(): Promise<void> {
191
const options: BarcodeScannerOptions = {
192
prompt: "Scan product barcode",
193
formats: "UPC_A,UPC_E,EAN_13,EAN_8,CODE_128,CODE_39",
194
showTorchButton: true,
195
disableSuccessBeep: false
196
};
197
198
try {
199
const result = await BarcodeScanner.scan(options);
200
201
if (!result.cancelled) {
202
await this.processInventoryItem(result);
203
}
204
} catch (error) {
205
console.error('Inventory scan failed:', error);
206
}
207
}
208
209
private async processInventoryItem(scanResult: BarcodeScanResult): Promise<void> {
210
const barcode = scanResult.text;
211
212
// Check if item already scanned
213
if (this.scannedItems.has(barcode)) {
214
const item = this.scannedItems.get(barcode);
215
item.quantity += 1;
216
console.log(`Updated quantity for ${item.name}: ${item.quantity}`);
217
} else {
218
// Look up product information
219
const productInfo = await this.lookupProduct(barcode);
220
221
if (productInfo) {
222
this.scannedItems.set(barcode, {
223
...productInfo,
224
barcode,
225
quantity: 1,
226
scannedAt: new Date()
227
});
228
229
console.log('New item added:', productInfo.name);
230
} else {
231
console.warn('Product not found for barcode:', barcode);
232
// Add unknown item
233
this.scannedItems.set(barcode, {
234
barcode,
235
name: 'Unknown Product',
236
quantity: 1,
237
scannedAt: new Date()
238
});
239
}
240
}
241
}
242
243
private async lookupProduct(barcode: string): Promise<any> {
244
try {
245
// Mock API call to product database
246
const response = await fetch(`https://api.example.com/products/${barcode}`);
247
248
if (response.ok) {
249
return await response.json();
250
}
251
252
return null;
253
} catch (error) {
254
console.error('Product lookup failed:', error);
255
return null;
256
}
257
}
258
259
getScannedItems(): any[] {
260
return Array.from(this.scannedItems.values());
261
}
262
263
clearScannedItems(): void {
264
this.scannedItems.clear();
265
}
266
267
exportScanResults(): string {
268
const items = this.getScannedItems();
269
return JSON.stringify(items, null, 2);
270
}
271
}
272
```
273
274
### NFC (Near Field Communication)
275
276
Interact with NFC tags and devices for data exchange, payments, and device pairing.
277
278
```typescript { .api }
279
/**
280
* NDEF event data
281
*/
282
interface NdefEvent {
283
/** NFC tag information */
284
tag: {
285
/** Tag ID */
286
id: number[];
287
/** Technologies supported by tag */
288
techTypes: string[];
289
/** Tag type */
290
type: string;
291
/** Maximum NDEF message size */
292
maxSize: number;
293
/** Whether tag is writable */
294
isWritable: boolean;
295
/** Whether tag can be made read-only */
296
canMakeReadOnly: boolean;
297
};
298
/** NDEF message */
299
message: NdefRecord[];
300
}
301
302
/**
303
* NDEF record structure
304
*/
305
interface NdefRecord {
306
/** Record ID */
307
id: number[];
308
/** TNF (Type Name Format) */
309
tnf: number;
310
/** Record type */
311
type: number[];
312
/** Record payload */
313
payload: number[];
314
}
315
316
/**
317
* Tag discovery event
318
*/
319
interface TagEvent {
320
/** Tag information */
321
tag: {
322
id: number[];
323
techTypes: string[];
324
};
325
}
326
327
/**
328
* NFC class for Near Field Communication
329
*/
330
class NFC {
331
/**
332
* Add NDEF listener for NDEF-formatted tags
333
* @returns Observable emitting NDEF events
334
*/
335
static addNdefListener(): Observable<NdefEvent>;
336
337
/**
338
* Add listener for any NFC tag discovery
339
* @returns Observable emitting tag discovery events
340
*/
341
static addTagDiscoveredListener(): Observable<TagEvent>;
342
343
/**
344
* Add listener for specific MIME type NDEF records
345
* @param mimeType MIME type to listen for
346
* @returns Observable emitting matching NDEF events
347
*/
348
static addMimeTypeListener(mimeType: string): Observable<NdefEvent>;
349
350
/**
351
* Add listener for NDEF formatable tags
352
* @returns Observable emitting formatable tag events
353
*/
354
static addNdefFormatableListener(): Observable<TagEvent>;
355
356
/**
357
* Write NDEF message to tag
358
* @param message Array of NDEF records
359
* @returns Promise indicating write completion
360
*/
361
static write(message: any[]): Promise<any>;
362
363
/**
364
* Make tag read-only
365
* @returns Promise indicating completion
366
*/
367
static makeReadOnly(): Promise<any>;
368
369
/**
370
* Share NDEF message via Android Beam
371
* @param message Array of NDEF records
372
* @returns Promise indicating share setup completion
373
*/
374
static share(message: any[]): Promise<any>;
375
376
/**
377
* Stop sharing via Android Beam
378
* @returns Promise indicating stop completion
379
*/
380
static unshare(): Promise<any>;
381
382
/**
383
* Erase NDEF tag
384
* @returns Promise indicating erase completion
385
*/
386
static erase(): Promise<any>;
387
388
/**
389
* Handover to other device via NFC
390
* @param uris Array of URIs to handover
391
* @returns Promise indicating handover completion
392
*/
393
static handover(uris: string[]): Promise<any>;
394
395
/**
396
* Stop handover
397
* @returns Promise indicating stop completion
398
*/
399
static stopHandover(): Promise<any>;
400
401
/**
402
* Show NFC settings
403
* @returns Promise indicating settings display
404
*/
405
static showSettings(): Promise<any>;
406
407
/**
408
* Check if NFC is enabled
409
* @returns Promise resolving to NFC status
410
*/
411
static enabled(): Promise<any>;
412
}
413
```
414
415
**Usage Examples:**
416
417
```typescript
418
import { NFC, NdefEvent, TagEvent } from 'ionic-native';
419
420
// NFC service for tag operations
421
class NFCService {
422
private ndefListener: any;
423
private tagListener: any;
424
425
async initialize(): Promise<boolean> {
426
try {
427
const isEnabled = await NFC.enabled();
428
429
if (!isEnabled) {
430
console.log('NFC not enabled');
431
await NFC.showSettings();
432
return false;
433
}
434
435
this.setupListeners();
436
console.log('NFC service initialized');
437
return true;
438
} catch (error) {
439
console.error('NFC initialization failed:', error);
440
return false;
441
}
442
}
443
444
private setupListeners(): void {
445
// Listen for NDEF tags
446
this.ndefListener = NFC.addNdefListener().subscribe(
447
(event: NdefEvent) => {
448
console.log('NDEF tag detected:', event);
449
this.handleNdefTag(event);
450
},
451
(error) => {
452
console.error('NDEF listener error:', error);
453
}
454
);
455
456
// Listen for any NFC tags
457
this.tagListener = NFC.addTagDiscoveredListener().subscribe(
458
(event: TagEvent) => {
459
console.log('NFC tag discovered:', event);
460
this.handleTagDiscovery(event);
461
},
462
(error) => {
463
console.error('Tag listener error:', error);
464
}
465
);
466
}
467
468
private handleNdefTag(event: NdefEvent): void {
469
const message = event.message;
470
471
message.forEach((record, index) => {
472
const payload = this.parseNdefRecord(record);
473
console.log(`NDEF Record ${index}:`, payload);
474
});
475
}
476
477
private parseNdefRecord(record: NdefRecord): any {
478
// Convert payload to string (simplified parsing)
479
const payload = String.fromCharCode.apply(null, record.payload);
480
481
// Handle different TNF types
482
switch (record.tnf) {
483
case 1: // Well Known Type
484
return this.parseWellKnownType(record.type, payload);
485
case 2: // MIME Media Type
486
return { type: 'mime', payload };
487
case 3: // Absolute URI
488
return { type: 'uri', payload };
489
case 4: // External Type
490
return { type: 'external', payload };
491
default:
492
return { type: 'unknown', payload };
493
}
494
}
495
496
private parseWellKnownType(type: number[], payload: string): any {
497
const typeString = String.fromCharCode.apply(null, type);
498
499
switch (typeString) {
500
case 'T': // Text
501
return { type: 'text', text: payload.substring(3) }; // Skip language code
502
case 'U': // URI
503
return { type: 'uri', uri: this.decodeUri(payload) };
504
default:
505
return { type: typeString, payload };
506
}
507
}
508
509
private decodeUri(payload: string): string {
510
// URI prefixes for NDEF URI records
511
const uriPrefixes = [
512
'', 'http://www.', 'https://www.', 'http://', 'https://',
513
'tel:', 'mailto:', 'ftp://anonymous:anonymous@', 'ftp://ftp.',
514
'ftps://', 'sftp://', 'smb://', 'nfs://', 'ftp://', 'dav://',
515
'news:', 'telnet://', 'imap:', 'rtsp://', 'urn:', 'pop:',
516
'sip:', 'sips:', 'tftp:', 'btspp://', 'btl2cap://', 'btgoep://',
517
'tcpobex://', 'irdaobex://', 'file://', 'urn:epc:id:', 'urn:epc:tag:',
518
'urn:epc:pat:', 'urn:epc:raw:', 'urn:epc:', 'urn:nfc:'
519
];
520
521
const prefixIndex = payload.charCodeAt(0);
522
const prefix = uriPrefixes[prefixIndex] || '';
523
524
return prefix + payload.substring(1);
525
}
526
527
private handleTagDiscovery(event: TagEvent): void {
528
console.log('Tag technologies:', event.tag.techTypes);
529
530
// Handle different tag types
531
if (event.tag.techTypes.includes('android.nfc.tech.Ndef')) {
532
console.log('NDEF-compatible tag detected');
533
}
534
535
if (event.tag.techTypes.includes('android.nfc.tech.NdefFormatable')) {
536
console.log('Formatable tag detected');
537
}
538
}
539
540
async writeTextToTag(text: string, language: string = 'en'): Promise<void> {
541
try {
542
const textRecord = {
543
tnf: 1, // Well Known Type
544
type: [0x54], // 'T' for text
545
payload: this.encodeTextPayload(text, language),
546
id: []
547
};
548
549
await NFC.write([textRecord]);
550
console.log('Text written to NFC tag:', text);
551
} catch (error) {
552
console.error('NFC write failed:', error);
553
throw error;
554
}
555
}
556
557
async writeUriToTag(uri: string): Promise<void> {
558
try {
559
const uriRecord = {
560
tnf: 1, // Well Known Type
561
type: [0x55], // 'U' for URI
562
payload: this.encodeUriPayload(uri),
563
id: []
564
};
565
566
await NFC.write([uriRecord]);
567
console.log('URI written to NFC tag:', uri);
568
} catch (error) {
569
console.error('NFC URI write failed:', error);
570
throw error;
571
}
572
}
573
574
private encodeTextPayload(text: string, language: string): number[] {
575
const langBytes = Array.from(language).map(c => c.charCodeAt(0));
576
const textBytes = Array.from(text).map(c => c.charCodeAt(0));
577
578
// Format: [flags, lang_length, ...lang_bytes, ...text_bytes]
579
return [0x02, langBytes.length, ...langBytes, ...textBytes];
580
}
581
582
private encodeUriPayload(uri: string): number[] {
583
// Find best URI prefix
584
const prefixes = ['http://www.', 'https://www.', 'http://', 'https://'];
585
let prefixIndex = 0;
586
let remainder = uri;
587
588
for (let i = 0; i < prefixes.length; i++) {
589
if (uri.startsWith(prefixes[i])) {
590
prefixIndex = i + 1;
591
remainder = uri.substring(prefixes[i].length);
592
break;
593
}
594
}
595
596
const remainderBytes = Array.from(remainder).map(c => c.charCodeAt(0));
597
return [prefixIndex, ...remainderBytes];
598
}
599
600
async makeTagReadOnly(): Promise<void> {
601
try {
602
await NFC.makeReadOnly();
603
console.log('NFC tag made read-only');
604
} catch (error) {
605
console.error('Failed to make tag read-only:', error);
606
throw error;
607
}
608
}
609
610
async eraseTag(): Promise<void> {
611
try {
612
await NFC.erase();
613
console.log('NFC tag erased');
614
} catch (error) {
615
console.error('Failed to erase tag:', error);
616
throw error;
617
}
618
}
619
620
destroy(): void {
621
if (this.ndefListener) {
622
this.ndefListener.unsubscribe();
623
}
624
625
if (this.tagListener) {
626
this.tagListener.unsubscribe();
627
}
628
}
629
}
630
631
// NFC-based app launcher
632
class NFCAppLauncher extends NFCService {
633
634
async createAppLaunchTag(appId: string, additionalData?: any): Promise<void> {
635
const launchData = {
636
action: 'launch_app',
637
appId,
638
data: additionalData,
639
timestamp: new Date().toISOString()
640
};
641
642
const jsonString = JSON.stringify(launchData);
643
await this.writeTextToTag(jsonString);
644
}
645
646
async createWiFiTag(ssid: string, password: string, security: string = 'WPA'): Promise<void> {
647
// Create WiFi configuration URI
648
const wifiUri = `WIFI:T:${security};S:${ssid};P:${password};;`;
649
await this.writeTextToTag(wifiUri);
650
}
651
652
async createContactTag(contact: {
653
name: string;
654
phone?: string;
655
email?: string;
656
url?: string;
657
}): Promise<void> {
658
// Create vCard format
659
let vcard = 'BEGIN:VCARD\nVERSION:3.0\n';
660
vcard += `FN:${contact.name}\n`;
661
662
if (contact.phone) {
663
vcard += `TEL:${contact.phone}\n`;
664
}
665
666
if (contact.email) {
667
vcard += `EMAIL:${contact.email}\n`;
668
}
669
670
if (contact.url) {
671
vcard += `URL:${contact.url}\n`;
672
}
673
674
vcard += 'END:VCARD';
675
676
await this.writeTextToTag(vcard);
677
}
678
}
679
```
680
681
### Keyboard Management
682
683
Control and monitor device keyboard behavior, especially for input optimization and UI adjustments.
684
685
```typescript { .api }
686
/**
687
* Keyboard class for keyboard management and events
688
*/
689
class Keyboard {
690
/**
691
* Hide keyboard programmatically
692
*/
693
static hideKeyboard(): void;
694
695
/**
696
* Close keyboard (alias for hideKeyboard)
697
*/
698
static close(): void;
699
700
/**
701
* Show keyboard programmatically
702
*/
703
static show(): void;
704
705
/**
706
* Disable scroll when keyboard is shown
707
* @param disable Whether to disable scroll
708
*/
709
static disableScroll(disable: boolean): void;
710
711
/**
712
* Observable for keyboard show events
713
* @returns Observable emitting keyboard show events
714
*/
715
static onKeyboardShow(): Observable<any>;
716
717
/**
718
* Observable for keyboard hide events
719
* @returns Observable emitting keyboard hide events
720
*/
721
static onKeyboardHide(): Observable<any>;
722
}
723
```
724
725
**Usage Examples:**
726
727
```typescript
728
import { Keyboard } from 'ionic-native';
729
730
// Keyboard management service
731
class KeyboardManager {
732
private showSubscription: any;
733
private hideSubscription: any;
734
private keyboardHeight = 0;
735
private isKeyboardVisible = false;
736
737
initialize(): void {
738
this.setupKeyboardListeners();
739
console.log('Keyboard manager initialized');
740
}
741
742
private setupKeyboardListeners(): void {
743
// Listen for keyboard show events
744
this.showSubscription = Keyboard.onKeyboardShow().subscribe(
745
(event) => {
746
console.log('Keyboard shown:', event);
747
this.isKeyboardVisible = true;
748
this.keyboardHeight = event.keyboardHeight || 0;
749
this.handleKeyboardShow(event);
750
}
751
);
752
753
// Listen for keyboard hide events
754
this.hideSubscription = Keyboard.onKeyboardHide().subscribe(
755
(event) => {
756
console.log('Keyboard hidden:', event);
757
this.isKeyboardVisible = false;
758
this.keyboardHeight = 0;
759
this.handleKeyboardHide(event);
760
}
761
);
762
}
763
764
private handleKeyboardShow(event: any): void {
765
// Adjust UI layout for keyboard
766
this.adjustLayoutForKeyboard(event.keyboardHeight);
767
768
// Disable page scrolling if needed
769
Keyboard.disableScroll(true);
770
771
// Ensure input field is visible
772
this.ensureInputVisible();
773
}
774
775
private handleKeyboardHide(event: any): void {
776
// Reset UI layout
777
this.resetLayoutAfterKeyboard();
778
779
// Re-enable page scrolling
780
Keyboard.disableScroll(false);
781
}
782
783
private adjustLayoutForKeyboard(keyboardHeight: number): void {
784
// Adjust content padding/margin to account for keyboard
785
const content = document.querySelector('.main-content') as HTMLElement;
786
if (content) {
787
content.style.paddingBottom = `${keyboardHeight}px`;
788
}
789
790
// Adjust floating elements
791
const floatingElements = document.querySelectorAll('.floating-element');
792
floatingElements.forEach((element: HTMLElement) => {
793
element.style.bottom = `${keyboardHeight}px`;
794
});
795
}
796
797
private resetLayoutAfterKeyboard(): void {
798
// Reset content padding
799
const content = document.querySelector('.main-content') as HTMLElement;
800
if (content) {
801
content.style.paddingBottom = '';
802
}
803
804
// Reset floating elements
805
const floatingElements = document.querySelectorAll('.floating-element');
806
floatingElements.forEach((element: HTMLElement) => {
807
element.style.bottom = '';
808
});
809
}
810
811
private ensureInputVisible(): void {
812
// Scroll active input into view
813
const activeElement = document.activeElement as HTMLElement;
814
if (activeElement && this.isInputElement(activeElement)) {
815
setTimeout(() => {
816
activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
817
}, 300);
818
}
819
}
820
821
private isInputElement(element: HTMLElement): boolean {
822
const inputTypes = ['input', 'textarea', 'select'];
823
return inputTypes.includes(element.tagName.toLowerCase());
824
}
825
826
// Public methods
827
showKeyboard(): void {
828
Keyboard.show();
829
}
830
831
hideKeyboard(): void {
832
Keyboard.hideKeyboard();
833
}
834
835
closeKeyboard(): void {
836
Keyboard.close();
837
}
838
839
isVisible(): boolean {
840
return this.isKeyboardVisible;
841
}
842
843
getHeight(): number {
844
return this.keyboardHeight;
845
}
846
847
setScrollDisabled(disabled: boolean): void {
848
Keyboard.disableScroll(disabled);
849
}
850
851
destroy(): void {
852
if (this.showSubscription) {
853
this.showSubscription.unsubscribe();
854
}
855
856
if (this.hideSubscription) {
857
this.hideSubscription.unsubscribe();
858
}
859
}
860
}
861
862
// Enhanced keyboard service with form optimization
863
class SmartKeyboardService extends KeyboardManager {
864
private activeForm: HTMLFormElement | null = null;
865
private inputQueue: HTMLElement[] = [];
866
private currentInputIndex = 0;
867
868
initialize(): void {
869
super.initialize();
870
this.setupFormHandling();
871
}
872
873
private setupFormHandling(): void {
874
// Monitor form focus events
875
document.addEventListener('focusin', (event) => {
876
const target = event.target as HTMLElement;
877
878
if (this.isInputElement(target)) {
879
this.handleInputFocus(target);
880
}
881
});
882
883
// Monitor form submit events
884
document.addEventListener('submit', (event) => {
885
this.handleFormSubmit(event.target as HTMLFormElement);
886
});
887
}
888
889
private handleInputFocus(input: HTMLElement): void {
890
// Find parent form
891
this.activeForm = input.closest('form');
892
893
if (this.activeForm) {
894
this.buildInputQueue();
895
this.currentInputIndex = this.inputQueue.indexOf(input);
896
this.setupFormNavigation();
897
}
898
}
899
900
private buildInputQueue(): void {
901
if (!this.activeForm) return;
902
903
// Get all form inputs in tab order
904
const inputs = Array.from(this.activeForm.querySelectorAll('input, textarea, select'))
905
.filter((input: HTMLElement) => {
906
return !input.hasAttribute('disabled') &&
907
!input.hasAttribute('readonly') &&
908
input.offsetParent !== null; // Visible elements only
909
}) as HTMLElement[];
910
911
this.inputQueue = inputs.sort((a, b) => {
912
const aIndex = parseInt(a.getAttribute('tabindex') || '0');
913
const bIndex = parseInt(b.getAttribute('tabindex') || '0');
914
return aIndex - bIndex;
915
});
916
}
917
918
private setupFormNavigation(): void {
919
// Add next/previous buttons to keyboard toolbar if supported
920
this.addKeyboardToolbar();
921
}
922
923
private addKeyboardToolbar(): void {
924
// Create navigation buttons above keyboard
925
const toolbar = document.createElement('div');
926
toolbar.className = 'keyboard-toolbar';
927
toolbar.innerHTML = `
928
<button id="prev-input" ${this.currentInputIndex === 0 ? 'disabled' : ''}>Previous</button>
929
<button id="next-input" ${this.currentInputIndex === this.inputQueue.length - 1 ? 'disabled' : ''}>Next</button>
930
<button id="done-input">Done</button>
931
`;
932
933
// Position toolbar above keyboard
934
toolbar.style.cssText = `
935
position: fixed;
936
bottom: ${this.getHeight()}px;
937
left: 0;
938
right: 0;
939
background: #f0f0f0;
940
border-top: 1px solid #ccc;
941
padding: 8px;
942
display: flex;
943
justify-content: space-between;
944
z-index: 9999;
945
`;
946
947
document.body.appendChild(toolbar);
948
949
// Add event listeners
950
document.getElementById('prev-input')?.addEventListener('click', () => this.goToPreviousInput());
951
document.getElementById('next-input')?.addEventListener('click', () => this.goToNextInput());
952
document.getElementById('done-input')?.addEventListener('click', () => this.hideKeyboard());
953
954
// Remove toolbar when keyboard hides
955
const hideSubscription = Keyboard.onKeyboardHide().subscribe(() => {
956
toolbar.remove();
957
hideSubscription.unsubscribe();
958
});
959
}
960
961
private goToPreviousInput(): void {
962
if (this.currentInputIndex > 0) {
963
this.currentInputIndex--;
964
this.inputQueue[this.currentInputIndex].focus();
965
}
966
}
967
968
private goToNextInput(): void {
969
if (this.currentInputIndex < this.inputQueue.length - 1) {
970
this.currentInputIndex++;
971
this.inputQueue[this.currentInputIndex].focus();
972
}
973
}
974
975
private handleFormSubmit(form: HTMLFormElement): void {
976
// Hide keyboard on form submit
977
this.hideKeyboard();
978
}
979
980
// Auto-resize text areas
981
setupAutoResizeTextarea(textarea: HTMLTextAreaElement): void {
982
const adjust = () => {
983
textarea.style.height = 'auto';
984
textarea.style.height = textarea.scrollHeight + 'px';
985
};
986
987
textarea.addEventListener('input', adjust);
988
textarea.addEventListener('focus', adjust);
989
990
// Initial adjustment
991
adjust();
992
}
993
994
// Smart input validation
995
setupSmartValidation(input: HTMLInputElement): void {
996
input.addEventListener('blur', () => {
997
this.validateInput(input);
998
});
999
1000
input.addEventListener('input', () => {
1001
// Real-time validation for certain types
1002
if (input.type === 'email' || input.type === 'tel') {
1003
this.validateInput(input);
1004
}
1005
});
1006
}
1007
1008
private validateInput(input: HTMLInputElement): void {
1009
const value = input.value.trim();
1010
1011
switch (input.type) {
1012
case 'email':
1013
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1014
this.setValidationState(input, emailRegex.test(value) || value === '');
1015
break;
1016
1017
case 'tel':
1018
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
1019
this.setValidationState(input, phoneRegex.test(value) || value === '');
1020
break;
1021
1022
case 'url':
1023
try {
1024
new URL(value);
1025
this.setValidationState(input, true);
1026
} catch {
1027
this.setValidationState(input, value === '');
1028
}
1029
break;
1030
}
1031
}
1032
1033
private setValidationState(input: HTMLInputElement, isValid: boolean): void {
1034
if (isValid) {
1035
input.classList.remove('invalid');
1036
input.classList.add('valid');
1037
} else {
1038
input.classList.remove('valid');
1039
input.classList.add('invalid');
1040
}
1041
}
1042
}
1043
1044
// Usage
1045
const keyboardManager = new SmartKeyboardService();
1046
keyboardManager.initialize();
1047
1048
// Auto-setup for forms
1049
document.addEventListener('DOMContentLoaded', () => {
1050
// Setup auto-resize for all textareas
1051
document.querySelectorAll('textarea').forEach(textarea => {
1052
keyboardManager.setupAutoResizeTextarea(textarea as HTMLTextAreaElement);
1053
});
1054
1055
// Setup smart validation for inputs
1056
document.querySelectorAll('input[type="email"], input[type="tel"], input[type="url"]').forEach(input => {
1057
keyboardManager.setupSmartValidation(input as HTMLInputElement);
1058
});
1059
});
1060
```