Skip to content
Snippets Groups Projects
text-input-group.directive.ts 9.28 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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();