CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ionic-native

Native plugin wrappers for Cordova and Ionic with TypeScript, ES6+, Promise and Observable support

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

input-hardware.mddocs/

Input & Hardware

Barcode scanning, NFC communication, keyboard management, and hardware interaction capabilities for advanced input processing and device integration.

Capabilities

Barcode Scanner

Scan various barcode formats including QR codes, UPC codes, and other standard formats with customizable options.

/**
 * Barcode scanner configuration options
 */
interface BarcodeScannerOptions {
  /** Prefer front camera for scanning */
  preferFrontCamera?: boolean;
  /** Show flip camera button */
  showFlipCameraButton?: boolean;
  /** Show torch button */
  showTorchButton?: boolean;
  /** Start with torch on */
  torchOn?: boolean;
  /** Prompt text displayed to user */
  prompt?: string;
  /** Duration to display result in milliseconds */
  resultDisplayDuration?: number;
  /** Supported barcode formats (comma-separated) */
  formats?: string;
  /** Screen orientation (portrait, landscape) */
  orientation?: string;
  /** Disable animations */
  disableAnimations?: boolean;
  /** Disable success beep */
  disableSuccessBeep?: boolean;
}

/**
 * Barcode scan result
 */
interface BarcodeScanResult {
  /** Scanned text content */
  text: string;
  /** Barcode format (QR_CODE, UPC_A, CODE_128, etc.) */
  format: string;
  /** Whether scan was cancelled by user */
  cancelled: boolean;
}

/**
 * BarcodeScanner class for scanning barcodes and QR codes
 */
class BarcodeScanner {
  /**
   * Scan barcode using device camera
   * @param options Scanner configuration options
   * @returns Promise resolving to BarcodeScanResult
   */
  static scan(options?: BarcodeScannerOptions): Promise<BarcodeScanResult>;

  /**
   * Encode text into barcode
   * @param type Barcode type (TEXT_TYPE, EMAIL_TYPE, PHONE_TYPE, etc.)
   * @param data Data to encode
   * @returns Promise resolving to encoded barcode
   */
  static encode(type: string, data: any): Promise<any>;
}

Usage Examples:

import { BarcodeScanner, BarcodeScannerOptions, BarcodeScanResult } from 'ionic-native';

// Basic barcode scanning
async function scanBarcode(): Promise<BarcodeScanResult | null> {
  const options: BarcodeScannerOptions = {
    preferFrontCamera: false,
    showFlipCameraButton: true,
    showTorchButton: true,
    torchOn: false,
    prompt: "Place a barcode inside the scan area",
    resultDisplayDuration: 500,
    formats: "QR_CODE,PDF_417,CODE_128,CODE_39,UPC_A,UPC_E,EAN_13,EAN_8",
    orientation: "portrait",
    disableAnimations: true,
    disableSuccessBeep: false
  };

  try {
    const result = await BarcodeScanner.scan(options);
    
    if (result.cancelled) {
      console.log('Scan cancelled by user');
      return null;
    }
    
    console.log('Scanned result:', {
      text: result.text,
      format: result.format
    });
    
    return result;
  } catch (error) {
    console.error('Scan failed:', error);
    throw error;
  }
}

// QR Code specific scanning
class QRCodeScanner {
  
  async scanQRCode(): Promise<BarcodeScanResult | null> {
    const options: BarcodeScannerOptions = {
      preferFrontCamera: false,
      showFlipCameraButton: false,
      showTorchButton: true,
      prompt: "Scan QR Code",
      formats: "QR_CODE",
      orientation: "portrait"
    };

    try {
      const result = await BarcodeScanner.scan(options);
      
      if (!result.cancelled) {
        return this.parseQRCodeContent(result);
      }
      
      return null;
    } catch (error) {
      console.error('QR Code scan failed:', error);
      throw error;
    }
  }
  
  private parseQRCodeContent(result: BarcodeScanResult): BarcodeScanResult {
    const text = result.text;
    
    // Parse different QR code types
    if (text.startsWith('http://') || text.startsWith('https://')) {
      console.log('URL detected:', text);
    } else if (text.startsWith('wifi:')) {
      console.log('WiFi credentials detected');
      this.parseWiFiQR(text);
    } else if (text.startsWith('mailto:')) {
      console.log('Email detected:', text);
    } else if (text.startsWith('tel:')) {
      console.log('Phone number detected:', text);
    } else if (text.startsWith('geo:')) {
      console.log('Location detected:', text);
    } else {
      console.log('Plain text:', text);
    }
    
    return result;
  }
  
  private parseWiFiQR(qrText: string): { ssid: string; password: string; security: string } | null {
    // Parse WiFi QR format: WIFI:T:WPA;S:MyNetwork;P:MyPassword;;
    const wifiMatch = qrText.match(/WIFI:T:([^;]*);S:([^;]*);P:([^;]*);/);
    
    if (wifiMatch) {
      return {
        security: wifiMatch[1],
        ssid: wifiMatch[2],
        password: wifiMatch[3]
      };
    }
    
    return null;
  }
  
  async generateQRCode(data: string, type: string = 'TEXT_TYPE'): Promise<any> {
    try {
      const result = await BarcodeScanner.encode(type, data);
      console.log('QR Code generated:', result);
      return result;
    } catch (error) {
      console.error('QR Code generation failed:', error);
      throw error;
    }
  }
}

// Inventory scanning system
class InventoryScanner {
  private scannedItems: Map<string, any> = new Map();
  
  async scanInventoryItem(): Promise<void> {
    const options: BarcodeScannerOptions = {
      prompt: "Scan product barcode",
      formats: "UPC_A,UPC_E,EAN_13,EAN_8,CODE_128,CODE_39",
      showTorchButton: true,
      disableSuccessBeep: false
    };

    try {
      const result = await BarcodeScanner.scan(options);
      
      if (!result.cancelled) {
        await this.processInventoryItem(result);
      }
    } catch (error) {
      console.error('Inventory scan failed:', error);
    }
  }
  
  private async processInventoryItem(scanResult: BarcodeScanResult): Promise<void> {
    const barcode = scanResult.text;
    
    // Check if item already scanned
    if (this.scannedItems.has(barcode)) {
      const item = this.scannedItems.get(barcode);
      item.quantity += 1;
      console.log(`Updated quantity for ${item.name}: ${item.quantity}`);
    } else {
      // Look up product information
      const productInfo = await this.lookupProduct(barcode);
      
      if (productInfo) {
        this.scannedItems.set(barcode, {
          ...productInfo,
          barcode,
          quantity: 1,
          scannedAt: new Date()
        });
        
        console.log('New item added:', productInfo.name);
      } else {
        console.warn('Product not found for barcode:', barcode);
        // Add unknown item
        this.scannedItems.set(barcode, {
          barcode,
          name: 'Unknown Product',
          quantity: 1,
          scannedAt: new Date()
        });
      }
    }
  }
  
  private async lookupProduct(barcode: string): Promise<any> {
    try {
      // Mock API call to product database
      const response = await fetch(`https://api.example.com/products/${barcode}`);
      
      if (response.ok) {
        return await response.json();
      }
      
      return null;
    } catch (error) {
      console.error('Product lookup failed:', error);
      return null;
    }
  }
  
  getScannedItems(): any[] {
    return Array.from(this.scannedItems.values());
  }
  
  clearScannedItems(): void {
    this.scannedItems.clear();
  }
  
  exportScanResults(): string {
    const items = this.getScannedItems();
    return JSON.stringify(items, null, 2);
  }
}

NFC (Near Field Communication)

Interact with NFC tags and devices for data exchange, payments, and device pairing.

/**
 * NDEF event data
 */
interface NdefEvent {
  /** NFC tag information */
  tag: {
    /** Tag ID */
    id: number[];
    /** Technologies supported by tag */
    techTypes: string[];
    /** Tag type */
    type: string;
    /** Maximum NDEF message size */
    maxSize: number;
    /** Whether tag is writable */
    isWritable: boolean;
    /** Whether tag can be made read-only */
    canMakeReadOnly: boolean;
  };
  /** NDEF message */
  message: NdefRecord[];
}

/**
 * NDEF record structure
 */
interface NdefRecord {
  /** Record ID */
  id: number[];
  /** TNF (Type Name Format) */
  tnf: number;
  /** Record type */
  type: number[];
  /** Record payload */
  payload: number[];
}

/**
 * Tag discovery event
 */
interface TagEvent {
  /** Tag information */
  tag: {
    id: number[];
    techTypes: string[];
  };
}

/**
 * NFC class for Near Field Communication
 */
class NFC {
  /**
   * Add NDEF listener for NDEF-formatted tags
   * @returns Observable emitting NDEF events
   */
  static addNdefListener(): Observable<NdefEvent>;

  /**
   * Add listener for any NFC tag discovery
   * @returns Observable emitting tag discovery events
   */
  static addTagDiscoveredListener(): Observable<TagEvent>;

  /**
   * Add listener for specific MIME type NDEF records
   * @param mimeType MIME type to listen for
   * @returns Observable emitting matching NDEF events
   */
  static addMimeTypeListener(mimeType: string): Observable<NdefEvent>;

  /**
   * Add listener for NDEF formatable tags
   * @returns Observable emitting formatable tag events
   */
  static addNdefFormatableListener(): Observable<TagEvent>;

  /**
   * Write NDEF message to tag
   * @param message Array of NDEF records
   * @returns Promise indicating write completion
   */
  static write(message: any[]): Promise<any>;

  /**
   * Make tag read-only
   * @returns Promise indicating completion
   */
  static makeReadOnly(): Promise<any>;

  /**
   * Share NDEF message via Android Beam
   * @param message Array of NDEF records
   * @returns Promise indicating share setup completion
   */
  static share(message: any[]): Promise<any>;

  /**
   * Stop sharing via Android Beam
   * @returns Promise indicating stop completion
   */
  static unshare(): Promise<any>;

  /**
   * Erase NDEF tag
   * @returns Promise indicating erase completion
   */
  static erase(): Promise<any>;

  /**
   * Handover to other device via NFC
   * @param uris Array of URIs to handover
   * @returns Promise indicating handover completion
   */
  static handover(uris: string[]): Promise<any>;

  /**
   * Stop handover
   * @returns Promise indicating stop completion
   */
  static stopHandover(): Promise<any>;

  /**
   * Show NFC settings
   * @returns Promise indicating settings display
   */
  static showSettings(): Promise<any>;

  /**
   * Check if NFC is enabled
   * @returns Promise resolving to NFC status
   */
  static enabled(): Promise<any>;
}

Usage Examples:

import { NFC, NdefEvent, TagEvent } from 'ionic-native';

// NFC service for tag operations
class NFCService {
  private ndefListener: any;
  private tagListener: any;
  
  async initialize(): Promise<boolean> {
    try {
      const isEnabled = await NFC.enabled();
      
      if (!isEnabled) {
        console.log('NFC not enabled');
        await NFC.showSettings();
        return false;
      }
      
      this.setupListeners();
      console.log('NFC service initialized');
      return true;
    } catch (error) {
      console.error('NFC initialization failed:', error);
      return false;
    }
  }
  
  private setupListeners(): void {
    // Listen for NDEF tags
    this.ndefListener = NFC.addNdefListener().subscribe(
      (event: NdefEvent) => {
        console.log('NDEF tag detected:', event);
        this.handleNdefTag(event);
      },
      (error) => {
        console.error('NDEF listener error:', error);
      }
    );
    
    // Listen for any NFC tags
    this.tagListener = NFC.addTagDiscoveredListener().subscribe(
      (event: TagEvent) => {
        console.log('NFC tag discovered:', event);
        this.handleTagDiscovery(event);
      },
      (error) => {
        console.error('Tag listener error:', error);
      }
    );
  }
  
  private handleNdefTag(event: NdefEvent): void {
    const message = event.message;
    
    message.forEach((record, index) => {
      const payload = this.parseNdefRecord(record);
      console.log(`NDEF Record ${index}:`, payload);
    });
  }
  
  private parseNdefRecord(record: NdefRecord): any {
    // Convert payload to string (simplified parsing)
    const payload = String.fromCharCode.apply(null, record.payload);
    
    // Handle different TNF types
    switch (record.tnf) {
      case 1: // Well Known Type
        return this.parseWellKnownType(record.type, payload);
      case 2: // MIME Media Type
        return { type: 'mime', payload };
      case 3: // Absolute URI
        return { type: 'uri', payload };
      case 4: // External Type
        return { type: 'external', payload };
      default:
        return { type: 'unknown', payload };
    }
  }
  
  private parseWellKnownType(type: number[], payload: string): any {
    const typeString = String.fromCharCode.apply(null, type);
    
    switch (typeString) {
      case 'T': // Text
        return { type: 'text', text: payload.substring(3) }; // Skip language code
      case 'U': // URI
        return { type: 'uri', uri: this.decodeUri(payload) };
      default:
        return { type: typeString, payload };
    }
  }
  
  private decodeUri(payload: string): string {
    // URI prefixes for NDEF URI records
    const uriPrefixes = [
      '', 'http://www.', 'https://www.', 'http://', 'https://',
      'tel:', 'mailto:', 'ftp://anonymous:anonymous@', 'ftp://ftp.',
      'ftps://', 'sftp://', 'smb://', 'nfs://', 'ftp://', 'dav://',
      'news:', 'telnet://', 'imap:', 'rtsp://', 'urn:', 'pop:',
      'sip:', 'sips:', 'tftp:', 'btspp://', 'btl2cap://', 'btgoep://',
      'tcpobex://', 'irdaobex://', 'file://', 'urn:epc:id:', 'urn:epc:tag:',
      'urn:epc:pat:', 'urn:epc:raw:', 'urn:epc:', 'urn:nfc:'
    ];
    
    const prefixIndex = payload.charCodeAt(0);
    const prefix = uriPrefixes[prefixIndex] || '';
    
    return prefix + payload.substring(1);
  }
  
  private handleTagDiscovery(event: TagEvent): void {
    console.log('Tag technologies:', event.tag.techTypes);
    
    // Handle different tag types
    if (event.tag.techTypes.includes('android.nfc.tech.Ndef')) {
      console.log('NDEF-compatible tag detected');
    }
    
    if (event.tag.techTypes.includes('android.nfc.tech.NdefFormatable')) {
      console.log('Formatable tag detected');
    }
  }
  
  async writeTextToTag(text: string, language: string = 'en'): Promise<void> {
    try {
      const textRecord = {
        tnf: 1, // Well Known Type
        type: [0x54], // 'T' for text
        payload: this.encodeTextPayload(text, language),
        id: []
      };
      
      await NFC.write([textRecord]);
      console.log('Text written to NFC tag:', text);
    } catch (error) {
      console.error('NFC write failed:', error);
      throw error;
    }
  }
  
  async writeUriToTag(uri: string): Promise<void> {
    try {
      const uriRecord = {
        tnf: 1, // Well Known Type
        type: [0x55], // 'U' for URI
        payload: this.encodeUriPayload(uri),
        id: []
      };
      
      await NFC.write([uriRecord]);
      console.log('URI written to NFC tag:', uri);
    } catch (error) {
      console.error('NFC URI write failed:', error);
      throw error;
    }
  }
  
  private encodeTextPayload(text: string, language: string): number[] {
    const langBytes = Array.from(language).map(c => c.charCodeAt(0));
    const textBytes = Array.from(text).map(c => c.charCodeAt(0));
    
    // Format: [flags, lang_length, ...lang_bytes, ...text_bytes]
    return [0x02, langBytes.length, ...langBytes, ...textBytes];
  }
  
  private encodeUriPayload(uri: string): number[] {
    // Find best URI prefix
    const prefixes = ['http://www.', 'https://www.', 'http://', 'https://'];
    let prefixIndex = 0;
    let remainder = uri;
    
    for (let i = 0; i < prefixes.length; i++) {
      if (uri.startsWith(prefixes[i])) {
        prefixIndex = i + 1;
        remainder = uri.substring(prefixes[i].length);
        break;
      }
    }
    
    const remainderBytes = Array.from(remainder).map(c => c.charCodeAt(0));
    return [prefixIndex, ...remainderBytes];
  }
  
  async makeTagReadOnly(): Promise<void> {
    try {
      await NFC.makeReadOnly();
      console.log('NFC tag made read-only');
    } catch (error) {
      console.error('Failed to make tag read-only:', error);
      throw error;
    }
  }
  
  async eraseTag(): Promise<void> {
    try {
      await NFC.erase();
      console.log('NFC tag erased');
    } catch (error) {
      console.error('Failed to erase tag:', error);
      throw error;
    }
  }
  
  destroy(): void {
    if (this.ndefListener) {
      this.ndefListener.unsubscribe();
    }
    
    if (this.tagListener) {
      this.tagListener.unsubscribe();
    }
  }
}

// NFC-based app launcher
class NFCAppLauncher extends NFCService {
  
  async createAppLaunchTag(appId: string, additionalData?: any): Promise<void> {
    const launchData = {
      action: 'launch_app',
      appId,
      data: additionalData,
      timestamp: new Date().toISOString()
    };
    
    const jsonString = JSON.stringify(launchData);
    await this.writeTextToTag(jsonString);
  }
  
  async createWiFiTag(ssid: string, password: string, security: string = 'WPA'): Promise<void> {
    // Create WiFi configuration URI
    const wifiUri = `WIFI:T:${security};S:${ssid};P:${password};;`;
    await this.writeTextToTag(wifiUri);
  }
  
  async createContactTag(contact: {
    name: string;
    phone?: string;
    email?: string;
    url?: string;
  }): Promise<void> {
    // Create vCard format
    let vcard = 'BEGIN:VCARD\nVERSION:3.0\n';
    vcard += `FN:${contact.name}\n`;
    
    if (contact.phone) {
      vcard += `TEL:${contact.phone}\n`;
    }
    
    if (contact.email) {
      vcard += `EMAIL:${contact.email}\n`;
    }
    
    if (contact.url) {
      vcard += `URL:${contact.url}\n`;
    }
    
    vcard += 'END:VCARD';
    
    await this.writeTextToTag(vcard);
  }
}

Keyboard Management

Control and monitor device keyboard behavior, especially for input optimization and UI adjustments.

/**
 * Keyboard class for keyboard management and events
 */
class Keyboard {
  /**
   * Hide keyboard programmatically
   */
  static hideKeyboard(): void;

  /**
   * Close keyboard (alias for hideKeyboard)
   */
  static close(): void;

  /**
   * Show keyboard programmatically
   */
  static show(): void;

  /**
   * Disable scroll when keyboard is shown
   * @param disable Whether to disable scroll
   */
  static disableScroll(disable: boolean): void;

  /**
   * Observable for keyboard show events
   * @returns Observable emitting keyboard show events
   */
  static onKeyboardShow(): Observable<any>;

  /**
   * Observable for keyboard hide events
   * @returns Observable emitting keyboard hide events
   */
  static onKeyboardHide(): Observable<any>;
}

Usage Examples:

import { Keyboard } from 'ionic-native';

// Keyboard management service
class KeyboardManager {
  private showSubscription: any;
  private hideSubscription: any;
  private keyboardHeight = 0;
  private isKeyboardVisible = false;
  
  initialize(): void {
    this.setupKeyboardListeners();
    console.log('Keyboard manager initialized');
  }
  
  private setupKeyboardListeners(): void {
    // Listen for keyboard show events
    this.showSubscription = Keyboard.onKeyboardShow().subscribe(
      (event) => {
        console.log('Keyboard shown:', event);
        this.isKeyboardVisible = true;
        this.keyboardHeight = event.keyboardHeight || 0;
        this.handleKeyboardShow(event);
      }
    );
    
    // Listen for keyboard hide events
    this.hideSubscription = Keyboard.onKeyboardHide().subscribe(
      (event) => {
        console.log('Keyboard hidden:', event);
        this.isKeyboardVisible = false;
        this.keyboardHeight = 0;
        this.handleKeyboardHide(event);
      }
    );
  }
  
  private handleKeyboardShow(event: any): void {
    // Adjust UI layout for keyboard
    this.adjustLayoutForKeyboard(event.keyboardHeight);
    
    // Disable page scrolling if needed
    Keyboard.disableScroll(true);
    
    // Ensure input field is visible
    this.ensureInputVisible();
  }
  
  private handleKeyboardHide(event: any): void {
    // Reset UI layout
    this.resetLayoutAfterKeyboard();
    
    // Re-enable page scrolling
    Keyboard.disableScroll(false);
  }
  
  private adjustLayoutForKeyboard(keyboardHeight: number): void {
    // Adjust content padding/margin to account for keyboard
    const content = document.querySelector('.main-content') as HTMLElement;
    if (content) {
      content.style.paddingBottom = `${keyboardHeight}px`;
    }
    
    // Adjust floating elements
    const floatingElements = document.querySelectorAll('.floating-element');
    floatingElements.forEach((element: HTMLElement) => {
      element.style.bottom = `${keyboardHeight}px`;
    });
  }
  
  private resetLayoutAfterKeyboard(): void {
    // Reset content padding
    const content = document.querySelector('.main-content') as HTMLElement;
    if (content) {
      content.style.paddingBottom = '';
    }
    
    // Reset floating elements
    const floatingElements = document.querySelectorAll('.floating-element');
    floatingElements.forEach((element: HTMLElement) => {
      element.style.bottom = '';
    });
  }
  
  private ensureInputVisible(): void {
    // Scroll active input into view
    const activeElement = document.activeElement as HTMLElement;
    if (activeElement && this.isInputElement(activeElement)) {
      setTimeout(() => {
        activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, 300);
    }
  }
  
  private isInputElement(element: HTMLElement): boolean {
    const inputTypes = ['input', 'textarea', 'select'];
    return inputTypes.includes(element.tagName.toLowerCase());
  }
  
  // Public methods
  showKeyboard(): void {
    Keyboard.show();
  }
  
  hideKeyboard(): void {
    Keyboard.hideKeyboard();
  }
  
  closeKeyboard(): void {
    Keyboard.close();
  }
  
  isVisible(): boolean {
    return this.isKeyboardVisible;
  }
  
  getHeight(): number {
    return this.keyboardHeight;
  }
  
  setScrollDisabled(disabled: boolean): void {
    Keyboard.disableScroll(disabled);
  }
  
  destroy(): void {
    if (this.showSubscription) {
      this.showSubscription.unsubscribe();
    }
    
    if (this.hideSubscription) {
      this.hideSubscription.unsubscribe();
    }
  }
}

// Enhanced keyboard service with form optimization
class SmartKeyboardService extends KeyboardManager {
  private activeForm: HTMLFormElement | null = null;
  private inputQueue: HTMLElement[] = [];
  private currentInputIndex = 0;
  
  initialize(): void {
    super.initialize();
    this.setupFormHandling();
  }
  
  private setupFormHandling(): void {
    // Monitor form focus events
    document.addEventListener('focusin', (event) => {
      const target = event.target as HTMLElement;
      
      if (this.isInputElement(target)) {
        this.handleInputFocus(target);
      }
    });
    
    // Monitor form submit events
    document.addEventListener('submit', (event) => {
      this.handleFormSubmit(event.target as HTMLFormElement);
    });
  }
  
  private handleInputFocus(input: HTMLElement): void {
    // Find parent form
    this.activeForm = input.closest('form');
    
    if (this.activeForm) {
      this.buildInputQueue();
      this.currentInputIndex = this.inputQueue.indexOf(input);
      this.setupFormNavigation();
    }
  }
  
  private buildInputQueue(): void {
    if (!this.activeForm) return;
    
    // Get all form inputs in tab order
    const inputs = Array.from(this.activeForm.querySelectorAll('input, textarea, select'))
      .filter((input: HTMLElement) => {
        return !input.hasAttribute('disabled') && 
               !input.hasAttribute('readonly') &&
               input.offsetParent !== null; // Visible elements only
      }) as HTMLElement[];
    
    this.inputQueue = inputs.sort((a, b) => {
      const aIndex = parseInt(a.getAttribute('tabindex') || '0');
      const bIndex = parseInt(b.getAttribute('tabindex') || '0');
      return aIndex - bIndex;
    });
  }
  
  private setupFormNavigation(): void {
    // Add next/previous buttons to keyboard toolbar if supported
    this.addKeyboardToolbar();
  }
  
  private addKeyboardToolbar(): void {
    // Create navigation buttons above keyboard
    const toolbar = document.createElement('div');
    toolbar.className = 'keyboard-toolbar';
    toolbar.innerHTML = `
      <button id="prev-input" ${this.currentInputIndex === 0 ? 'disabled' : ''}>Previous</button>
      <button id="next-input" ${this.currentInputIndex === this.inputQueue.length - 1 ? 'disabled' : ''}>Next</button>
      <button id="done-input">Done</button>
    `;
    
    // Position toolbar above keyboard
    toolbar.style.cssText = `
      position: fixed;
      bottom: ${this.getHeight()}px;
      left: 0;
      right: 0;
      background: #f0f0f0;
      border-top: 1px solid #ccc;
      padding: 8px;
      display: flex;
      justify-content: space-between;
      z-index: 9999;
    `;
    
    document.body.appendChild(toolbar);
    
    // Add event listeners
    document.getElementById('prev-input')?.addEventListener('click', () => this.goToPreviousInput());
    document.getElementById('next-input')?.addEventListener('click', () => this.goToNextInput());
    document.getElementById('done-input')?.addEventListener('click', () => this.hideKeyboard());
    
    // Remove toolbar when keyboard hides
    const hideSubscription = Keyboard.onKeyboardHide().subscribe(() => {
      toolbar.remove();
      hideSubscription.unsubscribe();
    });
  }
  
  private goToPreviousInput(): void {
    if (this.currentInputIndex > 0) {
      this.currentInputIndex--;
      this.inputQueue[this.currentInputIndex].focus();
    }
  }
  
  private goToNextInput(): void {
    if (this.currentInputIndex < this.inputQueue.length - 1) {
      this.currentInputIndex++;
      this.inputQueue[this.currentInputIndex].focus();
    }
  }
  
  private handleFormSubmit(form: HTMLFormElement): void {
    // Hide keyboard on form submit
    this.hideKeyboard();
  }
  
  // Auto-resize text areas
  setupAutoResizeTextarea(textarea: HTMLTextAreaElement): void {
    const adjust = () => {
      textarea.style.height = 'auto';
      textarea.style.height = textarea.scrollHeight + 'px';
    };
    
    textarea.addEventListener('input', adjust);
    textarea.addEventListener('focus', adjust);
    
    // Initial adjustment
    adjust();
  }
  
  // Smart input validation
  setupSmartValidation(input: HTMLInputElement): void {
    input.addEventListener('blur', () => {
      this.validateInput(input);
    });
    
    input.addEventListener('input', () => {
      // Real-time validation for certain types
      if (input.type === 'email' || input.type === 'tel') {
        this.validateInput(input);
      }
    });
  }
  
  private validateInput(input: HTMLInputElement): void {
    const value = input.value.trim();
    
    switch (input.type) {
      case 'email':
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        this.setValidationState(input, emailRegex.test(value) || value === '');
        break;
        
      case 'tel':
        const phoneRegex = /^[\d\s\-\+\(\)]+$/;
        this.setValidationState(input, phoneRegex.test(value) || value === '');
        break;
        
      case 'url':
        try {
          new URL(value);
          this.setValidationState(input, true);
        } catch {
          this.setValidationState(input, value === '');
        }
        break;
    }
  }
  
  private setValidationState(input: HTMLInputElement, isValid: boolean): void {
    if (isValid) {
      input.classList.remove('invalid');
      input.classList.add('valid');
    } else {
      input.classList.remove('valid');
      input.classList.add('invalid');
    }
  }
}

// Usage
const keyboardManager = new SmartKeyboardService();
keyboardManager.initialize();

// Auto-setup for forms
document.addEventListener('DOMContentLoaded', () => {
  // Setup auto-resize for all textareas
  document.querySelectorAll('textarea').forEach(textarea => {
    keyboardManager.setupAutoResizeTextarea(textarea as HTMLTextAreaElement);
  });
  
  // Setup smart validation for inputs
  document.querySelectorAll('input[type="email"], input[type="tel"], input[type="url"]').forEach(input => {
    keyboardManager.setupSmartValidation(input as HTMLInputElement);
  });
});

docs

analytics-monetization.md

camera-media.md

device-sensors.md

device-system.md

index.md

input-hardware.md

location-maps.md

network-communication.md

notifications-ui.md

security-auth.md

social-sharing.md

storage-files.md

tile.json