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


import { IPriceScaleApi, PriceScaleMode } from 'lightweight-charts';

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

import type { PriceScaleTarget } from './controller';

interface PriceScaleControlsControllerParams {
  getPaneElement: () => HTMLElement | null;
  getPriceScale: (priceScaleId: Direction) => IPriceScaleApi;
  getPriceScaleTarget: (priceScaleId: Direction) => PriceScaleTarget;
  hasPriceScaleSeries: (priceScaleId: Direction) => boolean;
  getSavedPriceScaleMode: (target: PriceScaleTarget) => PriceScaleMode;
  togglePriceScaleMode: (target: PriceScaleTarget, mode: PriceScaleMode) => void;
}

export class PriceScaleControlsController {
  private readonly getPaneElement: () => HTMLElement | null;
  private readonly getPriceScale: (priceScaleId: Direction) => IPriceScaleApi;
  private readonly getPriceScaleTarget: (priceScaleId: Direction) => PriceScaleTarget;
  private readonly hasPriceScaleSeries: (priceScaleId: Direction) => boolean;
  private readonly getSavedPriceScaleMode: (target: PriceScaleTarget) => PriceScaleMode;
  private readonly togglePriceScaleMode: (target: PriceScaleTarget, mode: PriceScaleMode) => void;

  private readonly container: HTMLElement;
  private readonly renderer: ReactRenderer;

  private paneElement: HTMLElement | null = null;
  private hoveredPriceScaleId: Direction | null = null;

  constructor({
    getPaneElement,
    getPriceScale,
    getPriceScaleTarget,
    hasPriceScaleSeries,
    getSavedPriceScaleMode,
    togglePriceScaleMode,
  }: PriceScaleControlsControllerParams) {
    this.getPaneElement = getPaneElement;
    this.getPriceScale = getPriceScale;
    this.getPriceScaleTarget = getPriceScaleTarget;
    this.hasPriceScaleSeries = hasPriceScaleSeries;
    this.getSavedPriceScaleMode = getSavedPriceScaleMode;
    this.togglePriceScaleMode = togglePriceScaleMode;

    this.container = document.createElement('div');
    this.container.className = 'moex-price-scale-controls';
    this.container.style.position = 'absolute';
    this.container.style.bottom = 'var(--space-0500)';
    this.container.style.zIndex = '10';
    this.container.style.pointerEvents = 'none';

    this.renderer = new ReactRenderer(this.container);
  }

  public mount(): void {
    const paneElement = this.getPaneElement();

    if (!paneElement) {
      return;
    }

    if (this.paneElement === paneElement && this.container.parentElement === paneElement) {
      this.update();
      return;
    }

    this.unmount();

    this.paneElement = paneElement;
    this.paneElement.style.position = 'relative';
    this.paneElement.appendChild(this.container);
    this.paneElement.addEventListener('pointermove', this.handlePointerMove);
    this.paneElement.addEventListener('pointerleave', this.handlePointerLeave);

    this.update();
  }

  public update(): void {
    if (!this.hoveredPriceScaleId) {
      this.hide();
      return;
    }

    this.render(this.hoveredPriceScaleId);
  }

  public resetAutoScale(): void {
    this.setAutoScale(Direction.Left);
    this.setAutoScale(Direction.Right);
    this.update();
  }

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

  private handlePointerMove = (event: PointerEvent): void => {
    if (!this.paneElement) {
      return;
    }

    const paneRect = this.paneElement.getBoundingClientRect();
    const pointerX = event.clientX - paneRect.left;
    const nextPriceScaleId = this.getHoveredPriceScaleId(pointerX, paneRect.width);

    if (this.hoveredPriceScaleId === nextPriceScaleId) {
      if (nextPriceScaleId) {
        this.updatePosition(nextPriceScaleId);
      }

      return;
    }

    this.hoveredPriceScaleId = nextPriceScaleId;
    this.update();
  };

  private handlePointerLeave = (): void => {
    this.hoveredPriceScaleId = null;
    this.update();
  };

  private render(priceScaleId: Direction): void {
    if (!this.isPriceScaleAvailable(priceScaleId)) {
      this.hide();
      return;
    }

    this.updatePosition(priceScaleId);

    const target = this.getPriceScaleTarget(priceScaleId);
    const savedMode = this.getSavedPriceScaleMode(target);

    this.renderer.renderComponent(
      <PriceScaleControls
        isAutoScale={this.isAutoScale(priceScaleId)}
        isLogarithmic={savedMode === PriceScaleMode.Logarithmic}
        onAutoScale={() => {
          this.setAutoScale(priceScaleId);
          this.update();
        }}
        onToggleLogarithmic={() => {
          this.togglePriceScaleMode(target, PriceScaleMode.Logarithmic);
          this.update();
        }}
        onRefresh={() => {
          this.update();
        }}
      />,
    );

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

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

  private getHoveredPriceScaleId(pointerX: number, paneWidth: number): Direction | null {
    const leftWidth = this.getPriceScaleWidth(Direction.Left);
    const rightWidth = this.getPriceScaleWidth(Direction.Right);

    if (this.isPriceScaleAvailable(Direction.Left) && pointerX >= 0 && pointerX <= leftWidth) {
      return Direction.Left;
    }

    if (this.isPriceScaleAvailable(Direction.Right) && pointerX >= paneWidth - rightWidth && pointerX <= paneWidth) {
      return Direction.Right;
    }

    return null;
  }

  private updatePosition(priceScaleId: Direction): void {
    const width = this.getPriceScaleWidth(priceScaleId);

    this.container.style.width = `${width}px`;

    if (priceScaleId === Direction.Left) {
      this.container.style.left = '0';
      this.container.style.right = '';
      return;
    }

    this.container.style.left = '';
    this.container.style.right = '0';
  }

  private setAutoScale(priceScaleId: Direction): void {
    this.getPriceScale(priceScaleId).setAutoScale(true);
  }

  private isAutoScale(priceScaleId: Direction): boolean {
    return this.getPriceScale(priceScaleId).options().autoScale ?? true;
  }

  private isPriceScaleAvailable(priceScaleId: Direction): boolean {
    const priceScale = this.getPriceScale(priceScaleId);
    const options = priceScale.options();

    return options.visible !== false && priceScale.width() > 0 && this.hasPriceScaleSeries(priceScaleId);
  }

  private getPriceScaleWidth(priceScaleId: Direction): number {
    return this.getPriceScale(priceScaleId).width();
  }

  private unmount(): void {
    if (!this.paneElement) {
      return;
    }

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








public hasPriceScaleSeries(priceScaleId: Direction): boolean {
  const mainSeries = this.mainSeries.value;

  if (mainSeries) {
    const series = Array.from(mainSeries.getSeriesMap().values());

    for (let i = 0; i < series.length; i += 1) {
      if (this.isSeriesOnPriceScale(series[i], priceScaleId)) {
        return true;
      }
    }
  }

  const indicators = Array.from(this.indicatorsMap.value.values());

  for (let i = 0; i < indicators.length; i += 1) {
    const series = Array.from(indicators[i].getSeriesMap().values());

    for (let j = 0; j < series.length; j += 1) {
      if (this.isSeriesOnPriceScale(series[j], priceScaleId)) {
        return true;
      }
    }
  }

  return false;
}

private isSeriesOnPriceScale(
  series: {
    options: () => {
      priceScaleId?: string;
      visible?: boolean;
    };
  },
  priceScaleId: Direction,
): boolean {
  const options = series.options();
  const seriesPriceScaleId = options.priceScaleId ?? Direction.Right;

  return options.visible !== false && seriesPriceScaleId === priceScaleId;
}