Загрузка данных


import { PriceScaleMode } from 'lightweight-charts';

import { PriceScaleControls as PriceScaleControlsView } from '@components/PriceScaleControls';
import { ReactRenderer } from '@core/ReactRenderer';
import { Direction } from '@src/types';

import { PriceScale } from './PriceScale';

interface PriceScaleControlsParams {
  leftPriceScale: PriceScale;
  rightPriceScale: PriceScale;
  onPriceScaleChange: () => void;
}

interface PriceScaleElements {
  left: HTMLTableCellElement;
  right: HTMLTableCellElement;
}

export class PriceScaleControls {
  private readonly leftPriceScale: PriceScale;
  private readonly rightPriceScale: PriceScale;
  private readonly onPriceScaleChange: () => void;
  private readonly container: HTMLElement;
  private readonly renderer: ReactRenderer;

  private paneElement: HTMLElement | null = null;
  private leftPriceScaleElement: HTMLTableCellElement | null = null;
  private rightPriceScaleElement: HTMLTableCellElement | null = null;
  private hoveredPriceScale: PriceScale | null = null;

  constructor({
    leftPriceScale,
    rightPriceScale,
    onPriceScaleChange,
  }: PriceScaleControlsParams) {
    this.leftPriceScale = leftPriceScale;
    this.rightPriceScale = rightPriceScale;
    this.onPriceScaleChange = onPriceScaleChange;

    this.container = document.createElement('div');
    this.container.className = 'moex-price-scale-controls';
    this.renderer = new ReactRenderer(this.container);
  }

  public mount(paneElement: HTMLElement): void {
    const priceScaleElements = this.getPriceScaleElements(paneElement);

    if (!priceScaleElements) {
      this.unmount();
      return;
    }

    if (this.paneElement !== paneElement) {
      this.unmount();

      this.paneElement = paneElement;
      this.paneElement.addEventListener('pointermove', this.handlePointerMove);
      this.paneElement.addEventListener('pointerleave', this.handlePointerLeave);
    }

    this.leftPriceScaleElement = priceScaleElements.left;
    this.rightPriceScaleElement = priceScaleElements.right;

    this.leftPriceScaleElement.style.position = 'relative';
    this.rightPriceScaleElement.style.position = 'relative';

    this.refresh();
  }

  public refresh(): void {
    if (!this.hoveredPriceScale) {
      this.hide();
      return;
    }

    this.render(this.hoveredPriceScale);
  }

  public destroy(): void {
    this.unmount();
    this.renderer.destroy();
  }

  private handlePointerMove = (event: PointerEvent): void => {
    const nextPriceScale = this.getHoveredPriceScale(event.clientX);

    if (this.hoveredPriceScale === nextPriceScale) {
      return;
    }

    this.hoveredPriceScale = nextPriceScale;
    this.refresh();
  };

  private handlePointerLeave = (): void => {
    this.hoveredPriceScale = null;
    this.refresh();
  };

  private render(priceScale: PriceScale): void {
    const priceScaleElement = this.getPriceScaleElement(priceScale);

    if (!priceScaleElement || !this.canShowControls(priceScale, priceScaleElement)) {
      this.hide();
      return;
    }

    if (this.container.parentElement !== priceScaleElement) {
      priceScaleElement.appendChild(this.container);
    }

    this.renderer.renderComponent(
      <PriceScaleControlsView
        isAutoScale={priceScale.isAutoScaleEnabled()}
        isLogarithmic={priceScale.getMode() === PriceScaleMode.Logarithmic}
        onToggleAutoScale={() => {
          priceScale.toggleAutoScale();
          this.refresh();
          this.onPriceScaleChange();
        }}
        onToggleLogarithmic={() => {
          priceScale.toggleLogarithmic();
          this.refresh();
          this.onPriceScaleChange();
        }}
      />,
    );

    this.container.classList.add('moex-price-scale-controls_visible');
  }

  private hide(): void {
    this.container.classList.remove('moex-price-scale-controls_visible');
  }

  private getHoveredPriceScale(clientX: number): PriceScale | null {
    if (this.isPointerInsidePriceScale(this.leftPriceScale, clientX)) {
      return this.leftPriceScale;
    }

    if (this.isPointerInsidePriceScale(this.rightPriceScale, clientX)) {
      return this.rightPriceScale;
    }

    return null;
  }

  private isPointerInsidePriceScale(priceScale: PriceScale, clientX: number): boolean {
    const priceScaleElement = this.getPriceScaleElement(priceScale);

    if (!priceScaleElement || !this.canShowControls(priceScale, priceScaleElement)) {
      return false;
    }

    const priceScaleRect = priceScaleElement.getBoundingClientRect();

    return clientX >= priceScaleRect.left && clientX <= priceScaleRect.right;
  }

  private canShowControls(priceScale: PriceScale, priceScaleElement: HTMLTableCellElement): boolean {
    return priceScale.isVisible() && priceScale.hasVisibleSeriesData() && priceScaleElement.clientWidth > 0;
  }

  private getPriceScaleElement(priceScale: PriceScale): HTMLTableCellElement | null {
    return priceScale.side === Direction.Left
      ? this.leftPriceScaleElement
      : this.rightPriceScaleElement;
  }

  private getPriceScaleElements(paneElement: HTMLElement): PriceScaleElements | null {
    if (paneElement.children.length < 3) {
      return null;
    }

    const leftPriceScaleElement = paneElement.children.item(0);
    const rightPriceScaleElement = paneElement.children.item(paneElement.children.length - 1);

    if (
      !(leftPriceScaleElement instanceof HTMLTableCellElement) ||
      !(rightPriceScaleElement instanceof HTMLTableCellElement)
    ) {
      return null;
    }

    return {
      left: leftPriceScaleElement,
      right: rightPriceScaleElement,
    };
  }

  private unmount(): void {
    if (this.paneElement) {
      this.paneElement.removeEventListener('pointermove', this.handlePointerMove);
      this.paneElement.removeEventListener('pointerleave', this.handlePointerLeave);
    }

    this.paneElement = null;
    this.leftPriceScaleElement = null;
    this.rightPriceScaleElement = null;
    this.hoveredPriceScale = null;

    this.container.remove();
    this.hide();
  }
}


.moex-price-scale-controls {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  box-sizing: border-box;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
}

.moex-price-scale-controls_visible {
  opacity: 1;
}

.moex-price-scale-controls_visible .moex-price-scale-controls-content {
  pointer-events: auto;
}