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


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 {
  chartElement: HTMLElement;
  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 chartElement: HTMLElement;
  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({
    chartElement,
    getPaneElement,
    getPriceScale,
    getPriceScaleTarget,
    hasPriceScaleSeries,
    getSavedPriceScaleMode,
    togglePriceScaleMode,
  }: PriceScaleControlsControllerParams) {
    this.chartElement = chartElement;
    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.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 === this.chartElement
    ) {
      this.update();
      return;
    }

    this.unmount();

    this.paneElement = paneElement;

    this.chartElement.appendChild(this.container);
    this.chartElement.addEventListener(
      'pointermove',
      this.handlePointerMove,
    );
    this.chartElement.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 chartRect = this.chartElement.getBoundingClientRect();
    const paneRect = this.paneElement.getBoundingClientRect();

    const isInsidePane =
      event.clientY >= paneRect.top &&
      event.clientY < paneRect.bottom;

    const nextPriceScaleId = isInsidePane
      ? this.getHoveredPriceScaleId(
          event.clientX - chartRect.left,
          chartRect.width,
        )
      : null;

    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,
    chartWidth: 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 >= chartWidth - rightWidth &&
      pointerX <= chartWidth
    ) {
      return Direction.Right;
    }

    return null;
  }

  private updatePosition(priceScaleId: Direction): void {
    if (!this.paneElement) {
      return;
    }

    const chartRect = this.chartElement.getBoundingClientRect();
    const paneRect = this.paneElement.getBoundingClientRect();
    const bottomOffset = Math.max(
      0,
      chartRect.bottom - paneRect.bottom,
    );

    this.container.style.width = `${this.getPriceScaleWidth(
      priceScaleId,
    )}px`;
    this.container.style.bottom = `${bottomOffset}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 {
    this.chartElement.removeEventListener(
      'pointermove',
      this.handlePointerMove,
    );
    this.chartElement.removeEventListener(
      'pointerleave',
      this.handlePointerLeave,
    );

    this.paneElement = null;
    this.hoveredPriceScaleId = null;
    this.hide();
  }
}




this.priceScaleControlsController =
  new PriceScaleControlsController({
    chartElement: chartContainer,
    getPaneElement: () => this.lwcPane.getHTMLElement(),
    getPriceScale: (priceScaleId) =>
      this.getPriceScale(priceScaleId),
    getPriceScaleTarget: (priceScaleId) =>
      this.getPriceScaleTarget(priceScaleId),
    hasPriceScaleSeries: (priceScaleId) =>
      this.hasPriceScaleSeries(priceScaleId),
    getSavedPriceScaleMode,
    togglePriceScaleMode,
  });