Загрузка данных
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);
}
}