Skip to content
Snippets Groups Projects
text-input-group.directive.ts 8.09 KiB
Newer Older
import { Directive, OnDestroy } from '@angular/core';
import { ElementFormGroupDirective } from 'player/src/app/directives/element-form-group.directive';
import { InputElement, UIElement } from 'common/models/elements/element';
import { ElementComponent } from 'common/directives/element-component.directive';
import { FormElementComponent } from 'common/directives/form-element-component.directive';
import { Subscription } from 'rxjs';
import { DeviceService } from 'player/src/app/services/device.service';
import { KeypadService } from 'player/src/app/services/keypad.service';
import { KeyboardService } from 'player/src/app/services/keyboard.service';
import { TextInputComponentType } from 'player/src/app/models/text-input-component.type';

@Directive()
export abstract class TextInputGroupDirective extends ElementFormGroupDirective implements OnDestroy {
  isKeypadOpen: boolean = false;
  inputElement!: HTMLTextAreaElement | HTMLInputElement;
  enterKeySubscription!: Subscription;
  deleteCharactersSubscription!: Subscription;
  selectSubscription!: Subscription;

  abstract deviceService: DeviceService;
  abstract keypadService: KeypadService;
  abstract keyboardService: KeyboardService;

  private shallOpenKeypad(elementModel: InputElement): boolean {
    return !!elementModel.inputAssistancePreset &&
      !(elementModel.showSoftwareKeyboard &&
        elementModel.addInputAssistanceToKeyboard &&
        this.deviceService.isMobileWithoutHardwareKeyboard);
  }

  toggleKeyInput(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
                 elementComponent: TextInputComponentType): void {
    if (this.shallOpenKeypad(elementComponent.elementModel)) {
      this.keypadService.toggle(focusedTextInput, elementComponent);
      this.setInputElement(focusedTextInput.inputElement);
      this.isKeypadOpen = this.keypadService.isOpen;
      if (this.isKeypadOpen) {
        this.subscribeForInputEvents(elementComponent.elementModel, elementComponent);
      } else {
        this.unsubscribeFromInputEvents();
      }
    }
    if (elementComponent.elementModel.showSoftwareKeyboard && !elementComponent.elementModel.readOnly) {
      this.keyboardService
        .toggle(focusedTextInput, elementComponent, this.deviceService.isMobileWithoutHardwareKeyboard);
      if (this.keyboardService.isOpen) {
        this.subscribeForInputEvents(elementComponent.elementModel, elementComponent);
      } else {
        this.unsubscribeFromInputEvents();
      }
    }
  }

  // eslint-disable-next-line class-methods-use-this
  checkInputLimitation(event: {
    keyboardEvent: KeyboardEvent;
    inputElement: HTMLInputElement | HTMLTextAreaElement
  }, elementModel: UIElement): void {
    if (elementModel.maxLength &&
      elementModel.isLimitedToMaxLength &&
      event.inputElement.value.length === elementModel.maxLength &&
      !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp'].includes(event.keyboardEvent.key)) {
      event.keyboardEvent.preventDefault();
    }
  }

  detectHardwareKeyboard(elementModel: UIElement): void {
    if (elementModel.showSoftwareKeyboard) {
      this.deviceService.hasHardwareKeyboard = true;
      this.keyboardService.close();
    }
  }

  private subscribeForInputEvents(elementModel: UIElement, elementComponent: ElementComponent): void {
    this.enterKeySubscription = this.keypadService.enterKey
      .subscribe(key => this.enterKey(key, elementModel, elementComponent));
    this.deleteCharactersSubscription = this.keypadService.deleteCharacters
      .subscribe(isBackspace => this.deleteCharacters(isBackspace, elementComponent));
    this.selectSubscription = this.keypadService.select
      .subscribe(key => this.select(key));
  }

  private unsubscribeFromInputEvents(): void {
    if (this.selectSubscription) this.selectSubscription.unsubscribe();
    if (this.enterKeySubscription) this.enterKeySubscription.unsubscribe();
    if (this.deleteCharactersSubscription) this.deleteCharactersSubscription.unsubscribe();
  }

  private setInputElement(inputElement: HTMLElement): void {
    this.inputElement = this.elementModel.type === 'text-area' ?
      inputElement as HTMLTextAreaElement :
      inputElement as HTMLInputElement;
  }

  private select(direction: string): void {
    let lastBreak = 0;
    const inputValueKeys = this.inputElement.value.split('');
    const lineBreaks = inputValueKeys
      .reduce(
        (previousValue: number[][],
         currentValue,
         currentIndex) => {
          if (currentValue === '\n') {
            const d = [lastBreak, currentIndex + 1];
            lastBreak = currentIndex + 1;
            return [...previousValue, d];
          }
          if (currentIndex === inputValueKeys.length - 1) {
            return [...previousValue, [lastBreak, currentIndex + 2]];
          }
          return previousValue;
        }, []);
    const selectionStart = this.inputElement.selectionStart || 0;
    const selectionEnd = this.inputElement.selectionEnd || 0;
    let newSelection = selectionStart;

    switch (direction) {
      case 'ArrowLeft': {
        newSelection -= 1;
        break;
      }
      case 'ArrowRight': {
        newSelection += 1;
        break;
      }
      case 'ArrowUp': {
        const targetLine = lineBreaks.reverse().find(line => line[1] <= selectionStart);
        if (targetLine) {
          const posInLine = selectionStart - targetLine[1];
          newSelection = targetLine[0] + posInLine < targetLine[1] ? targetLine[0] + posInLine : targetLine[1] - 1;
        } else {
          newSelection = 0;
        }
        break;
      }
      case 'ArrowDown': {
        const targetLine = lineBreaks.find(line => line[0] > selectionEnd);
        if (targetLine) {
          const currentLine = lineBreaks.find(line => line[1] === targetLine[0]) || [0, 1];
          const posInLine = selectionEnd - currentLine[0];
          newSelection = targetLine[0] + posInLine < targetLine[1] ? targetLine[0] + posInLine : targetLine[1] - 1;
        } else {
          newSelection = inputValueKeys.length;
        }
        break;
      }
      default: {
        newSelection = selectionStart;
      }
    }
    this.inputElement.setSelectionRange(newSelection, newSelection);
  }

  private enterKey(key: string, elementModel: UIElement, elementComponent: ElementComponent): void {
    if (!(elementModel.maxLength &&
      elementModel.isLimitedToMaxLength &&
      this.inputElement.value.length === this.elementModel.maxLength)) {
      const selectionStart = this.inputElement.selectionStart || 0;
      const selectionEnd = this.inputElement.selectionEnd || 0;
      const newSelection = selectionStart ? selectionStart + 1 : 1;
      this.insert({
        selectionStart, selectionEnd, newSelection, key
      }, elementComponent);
    }
  }

  private deleteCharacters(backspace: boolean, elementComponent: ElementComponent): void {
    let selectionStart = this.inputElement.selectionStart || 0;
    let selectionEnd = this.inputElement.selectionEnd || 0;
    if (backspace && selectionEnd > 0) {
      if (selectionStart === selectionEnd) {
        selectionStart -= 1;
      }
      this.insert({
        selectionStart, selectionEnd, newSelection: selectionStart, key: ''
      }, elementComponent);
    }
    if (!backspace && selectionEnd <= this.inputElement.value.length) {
      if (selectionStart === selectionEnd) {
        selectionEnd += 1;
      }
      this.insert({
        selectionStart, selectionEnd, newSelection: selectionStart, key: ''
      }, elementComponent);
    }
  }

  private insert(keyAtPosition: {
    selectionStart: number;
    selectionEnd: number;
    newSelection: number;
    key: string
  }, elementComponent: ElementComponent): void {
    const startText = this.inputElement.value.substring(0, keyAtPosition.selectionStart);
    const endText = this.inputElement.value.substring(keyAtPosition.selectionEnd);
    (elementComponent as FormElementComponent).elementFormControl
      .setValue(startText + keyAtPosition.key + endText);
    this.inputElement.setSelectionRange(keyAtPosition.newSelection, keyAtPosition.newSelection);
  }

  ngOnDestroy(): void {
    this.unsubscribeFromInputEvents();
    super.ngOnDestroy();
  }
}