or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

analytics-monetization.mdcamera-media.mddevice-sensors.mddevice-system.mdindex.mdinput-hardware.mdlocation-maps.mdnetwork-communication.mdnotifications-ui.mdsecurity-auth.mdsocial-sharing.mdstorage-files.md
tile.json

input-hardware.mddocs/

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

```