Skip to content
Snippets Groups Projects
text-input-group.directive.ts 9.28 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;

  keypadEnterKeySubscription!: Subscription;
  keypadDeleteCharactersSubscription!: Subscription;
  keypadSelectSubscription!: Subscription;
  keyboardEnterKeySubscription!: Subscription;
  keyboardDeleteCharactersSubscription!: 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);
  }

  async toggleKeyInput(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
                       elementComponent: TextInputComponentType): Promise<void> {
    const promises: Promise<boolean>[] = [];
    if (elementComponent.elementModel.showSoftwareKeyboard && !elementComponent.elementModel.readOnly) {
      promises.push(this.keyboardService
        .toggleAsync(focusedTextInput, elementComponent, this.deviceService.isMobileWithoutHardwareKeyboard));
    }
    if (this.shallOpenKeypad(elementComponent.elementModel)) {
      promises.push(this.keypadService.toggleAsync(focusedTextInput, elementComponent));
    if (promises.length) {
      await Promise.all(promises).then(() => {
        if (this.keyboardService.isOpen) {
          this.subscribeForKeyboardEvents(elementComponent.elementModel, elementComponent);
        } else {
          this.unsubscribeFromKeyboardEvents();
        }
        if (this.keypadService.isOpen) {
          this.subscribeForKeypadEvents(elementComponent.elementModel, elementComponent);
        } else {
          this.unsubscribeFromKeypadEvents();
        }
        this.isKeypadOpen = this.keypadService.isOpen;
        if (this.keyboardService.isOpen || this.keypadService.isOpen) {
          this.setInputElement(focusedTextInput.inputElement);
        }
      });
    }
  }

  // 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 subscribeForKeypadEvents(elementModel: UIElement, elementComponent: ElementComponent): void {
    this.keypadEnterKeySubscription = this.keypadService.enterKey
      .subscribe(key => this.enterKey(key, elementModel, elementComponent));
    this.keypadDeleteCharactersSubscription = this.keypadService.deleteCharacters
      .subscribe(isBackspace => this.deleteCharacters(isBackspace, elementComponent));
    this.keypadSelectSubscription = this.keypadService.select
      .subscribe(key => this.select(key));
  }

  private unsubscribeFromKeypadEvents(): void {
    if (this.keypadSelectSubscription) this.keypadSelectSubscription.unsubscribe();
    if (this.keypadEnterKeySubscription) this.keypadEnterKeySubscription.unsubscribe();
    if (this.keypadDeleteCharactersSubscription) this.keypadDeleteCharactersSubscription.unsubscribe();
  }

  private subscribeForKeyboardEvents(elementModel: UIElement, elementComponent: ElementComponent): void {
    this.keyboardEnterKeySubscription = this.keyboardService.enterKey
      .subscribe(key => this.enterKey(key, elementModel, elementComponent));
    this.keyboardDeleteCharactersSubscription = this.keyboardService.deleteCharacters
      .subscribe(isBackspace => this.deleteCharacters(isBackspace, elementComponent));
  }

  private unsubscribeFromKeyboardEvents(): void {
    if (this.keyboardEnterKeySubscription) this.keyboardEnterKeySubscription.unsubscribe();
    if (this.keyboardDeleteCharactersSubscription) this.keyboardDeleteCharactersSubscription.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 === 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.unsubscribeFromKeypadEvents();
    this.unsubscribeFromKeyboardEvents();