Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
text-group-element.component.ts 6.10 KiB
import {
  AfterViewInit, Component, OnDestroy, OnInit, ViewChild
} from '@angular/core';
import { first, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TextComponent } from 'common/components/text/text.component';
import { TextElement } from 'common/models/elements/text/text';
import { ValueChangeElement } from 'common/models/elements/element';
import { AnchorService } from 'player/src/app/services/anchor.service';
import { TextMarkingService } from '../../../services/text-marking.service';
import { NativeEventService } from '../../../services/native-event.service';
import { UnitStateService } from '../../../services/unit-state.service';
import { ElementGroupDirective } from '../../../directives/element-group.directive';
import { ElementModelElementCodeMappingService } from '../../../services/element-model-element-code-mapping.service';

@Component({
  selector: 'aspect-text-group-element',
  templateUrl: './text-group-element.component.html',
  styleUrls: ['./text-group-element.component.scss']
})
export class TextGroupElementComponent extends ElementGroupDirective implements OnInit, AfterViewInit, OnDestroy {
  @ViewChild('elementComponent') elementComponent!: TextComponent;
  TextElement!: TextElement;

  initialValue: string = '';
  isMarkingBarOpen: boolean = false;
  selectedColor: string | null = null;
  selectedMode: 'mark' | 'delete' | null = null;
  markingBarPosition: { top: number, left: number } = { top: 0, left: 0 };
  textComponentContainerScrollTop: number = 0;
  textComponentRect!: DOMRect;

  private ngUnsubscribe = new Subject<void>();

  constructor(
    private nativeEventService: NativeEventService,
    private unitStateElementMapperService: ElementModelElementCodeMappingService,
    public unitStateService: UnitStateService,
    private anchorService: AnchorService
  ) {
    super();
  }

  ngOnInit(): void {
    this.initialValue = this.unitStateElementMapperService
      .mapToElementModelValue(
        this.unitStateService.getElementCodeById(this.elementModel.id)?.value, this.elementModel
      ) as string;
  }

  ngAfterViewInit(): void {
    this.registerAtUnitStateService(
      this.elementModel.id,
      ElementModelElementCodeMappingService.mapToElementCodeValue(this.initialValue, this.elementModel.type),
      this.elementComponent,
      this.pageIndex);
  }

  changeElementCodeValue(value: ValueChangeElement): void {
    this.unitStateService.changeElementCodeValue({
      id: value.id,
      value: ElementModelElementCodeMappingService
        .mapToElementCodeValue(value.value, this.elementModel.type)
    });
  }

  applyMarkingData(
    selection: { active: boolean; mode: 'mark' | 'delete'; color: string; colorName: string | undefined }
  ): void {
    if (selection.active) {
      this.selectedColor = selection.color;
      this.selectedMode = selection.mode;
      this.applyMarkingDataToText(selection.mode, selection.color);
    } else {
      this.selectedColor = null;
      this.selectedMode = null;
    }
  }

  startTextSelection(pointerDown: PointerEvent): void {
    this.isMarkingBarOpen = false;
    this.anchorService.reset();
    this.nativeEventService.pointerUp
      .pipe(takeUntil(this.ngUnsubscribe), first())
      .subscribe((pointerUp: PointerEvent) => {
        this.stopTextSelection(
          {
            startSelectionX: pointerDown.clientX,
            startSelectionY: pointerDown.clientY,
            stopSelectionX: pointerUp.clientX,
            stopSelectionY: pointerUp.clientY
          },
          pointerUp.ctrlKey
        );
        pointerUp.preventDefault();
        pointerUp.stopPropagation();
      });
  }

  applyMarkingDataToText(mode: 'mark' | 'delete', color: string): void {
    TextMarkingService
      .applyMarkingDataToText(
        mode,
        color,
        this.elementComponent
      );
    this.isMarkingBarOpen = false;
  }

  private stopTextSelection(
    textSelectionPosition: {
      stopSelectionX: number, stopSelectionY: number, startSelectionX: number, startSelectionY: number
    },
    isControlKeyPressed: boolean // do not allow multiple selections (FF)
  ) {
    const selection = window.getSelection();
    if (selection && TextMarkingService.isSelectionValid(selection) && selection.rangeCount > 0) {
      if (!TextMarkingService
        .isRangeInside(
          selection.getRangeAt(0), this.elementComponent.textContainerRef.nativeElement
        ) || (isControlKeyPressed)) {
        selection.removeAllRanges();
      } else if (this.selectedMode && this.selectedColor) {
        this.applyMarkingDataToText(this.selectedMode, this.selectedColor);
      } else if (!this.isMarkingBarOpen) {
        // hack for windows mobile to prevent opening marking bar while selection is removed on FF
        setTimeout(() => {
          const selectionTest = window.getSelection();
          if (selectionTest && TextMarkingService.isSelectionValid(selectionTest) && selectionTest.rangeCount > 0) {
            this.openMarkingBar(textSelectionPosition);
          }
        }, 100);
      }
    }
  }

  private openMarkingBar(
    textSelectionPosition: {
      stopSelectionX: number, stopSelectionY: number, startSelectionX: number, startSelectionY: number
    }
  ) {
    this.markingBarPosition.left =
      textSelectionPosition.startSelectionX > textSelectionPosition.stopSelectionX ?
        textSelectionPosition.startSelectionX : textSelectionPosition.stopSelectionX;
    this.markingBarPosition.top =
      textSelectionPosition.startSelectionY > textSelectionPosition.stopSelectionY ?
        textSelectionPosition.startSelectionY : textSelectionPosition.stopSelectionY;
    this.textComponentContainerScrollTop =
      this.elementComponent.domElement.closest('.fixed-size-content')?.scrollTop || 0;
    this.textComponentRect = this.elementComponent.domElement.getBoundingClientRect();
    this.isMarkingBarOpen = true;
    this.nativeEventService.pointerDown
      .pipe(takeUntil(this.ngUnsubscribe), first())
      .subscribe(() => this.closeMarkingBar());
  }

  private closeMarkingBar() {
    this.isMarkingBarOpen = false;
  }

  ngOnDestroy(): void {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
}