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