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


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;
  getPriceScaleMode: (target: PriceScaleTarget) => PriceScaleMode;
  togglePriceScaleMode: (target: PriceScaleTarget, mode: PriceScaleMode) => void;
}

interface RenderedPriceScaleControlsState {
  priceScaleId: Direction;
  isAutoScale: boolean;
  isLogarithmic: boolean;
}

const CONTROLS_CLASS_NAME = 'moex-price-scale-controls';
const VISIBLE_CONTROLS_CLASS_NAME = 'moex-price-scale-controls_visible';

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 getPriceScaleMode: (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;
  private renderedState: RenderedPriceScaleControlsState | null = null;

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

    this.container = document.createElement('div');
    this.container.className = CONTROLS_CLASS_NAME;
    this.container.style.position = 'absolute';
    this.container.style.bottom = '0';
    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 === null) {
      this.hide();
      return;
    }

    this.render(this.hoveredPriceScaleId);
  }

  public resetAutoScale(): void {
    this.setAutoScale(Direction.Left, true);
    this.setAutoScale(Direction.Right, true);
    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;

    this.hoveredPriceScaleId = this.getHoveredPriceScaleId(pointerX, paneRect.width);
    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 activeMode = this.getPriceScaleMode(target);

    const nextState: RenderedPriceScaleControlsState = {
      priceScaleId,
      isAutoScale: this.isAutoScale(priceScaleId),
      isLogarithmic: activeMode === PriceScaleMode.Logarithmic,
    };

    if (!this.isSameState(nextState)) {
      this.renderer.renderComponent(
        <PriceScaleControls
          isAutoScale={nextState.isAutoScale}
          isLogarithmic={nextState.isLogarithmic}
          onToggleAutoScale={() => {
            this.toggleAutoScale(priceScaleId);
            this.update();
          }}
          onToggleLogarithmic={() => {
            this.togglePriceScaleMode(target, PriceScaleMode.Logarithmic);
            this.update();
          }}
        />,
      );

      this.renderedState = nextState;
    }

    this.container.classList.add(VISIBLE_CONTROLS_CLASS_NAME);
  }

  private hide(): void {
    this.container.classList.remove(VISIBLE_CONTROLS_CLASS_NAME);
    this.renderedState = null;
  }

  private toggleAutoScale(priceScaleId: Direction): void {
    this.setAutoScale(priceScaleId, !this.isAutoScale(priceScaleId));
  }

  private setAutoScale(priceScaleId: Direction, value: boolean): void {
    this.getPriceScale(priceScaleId).setAutoScale(value);
  }

  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 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 getPriceScaleWidth(priceScaleId: Direction): number {
    return this.getPriceScale(priceScaleId).width();
  }

  private isSameState(nextState: RenderedPriceScaleControlsState): boolean {
    if (!this.renderedState) {
      return false;
    }

    return (
      this.renderedState.priceScaleId === nextState.priceScaleId &&
      this.renderedState.isAutoScale === nextState.isAutoScale &&
      this.renderedState.isLogarithmic === nextState.isLogarithmic
    );
  }

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

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

    this.paneElement = null;
    this.hoveredPriceScaleId = null;
    this.renderedState = null;

    this.container.classList.remove(VISIBLE_CONTROLS_CLASS_NAME);
  }
}