Загрузка данных
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 {
chartContainer: HTMLElement;
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;
}
export class PriceScaleControlsController {
private readonly chartContainer: 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 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;
constructor({
chartContainer,
getPaneElement,
getPriceScale,
getPriceScaleTarget,
hasPriceScaleSeries,
getPriceScaleMode,
togglePriceScaleMode,
}: PriceScaleControlsControllerParams) {
this.chartContainer = chartContainer;
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 = '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.chartContainer
) {
this.update();
return;
}
this.unmount();
this.paneElement = paneElement;
this.chartContainer.appendChild(this.container);
this.chartContainer.addEventListener(
'pointermove',
this.handlePointerMove,
);
this.chartContainer.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, 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 chartRect =
this.chartContainer.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 priceScaleMode = this.getPriceScaleMode(target);
this.renderer.renderComponent(
<PriceScaleControls
isAutoScale={this.isAutoScale(priceScaleId)}
isLogarithmic={
priceScaleMode === PriceScaleMode.Logarithmic
}
onToggleAutoScale={() => {
this.toggleAutoScale(priceScaleId);
this.update();
}}
onToggleLogarithmic={() => {
this.togglePriceScaleMode(
target,
PriceScaleMode.Logarithmic,
);
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.chartContainer.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 toggleAutoScale(priceScaleId: Direction): void {
this.setAutoScale(
priceScaleId,
!this.isAutoScale(priceScaleId),
);
}
private setAutoScale(
priceScaleId: Direction,
autoScale: boolean,
): void {
this.getPriceScale(priceScaleId).setAutoScale(autoScale);
}
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.chartContainer.removeEventListener(
'pointermove',
this.handlePointerMove,
);
this.chartContainer.removeEventListener(
'pointerleave',
this.handlePointerLeave,
);
this.paneElement = null;
this.hoveredPriceScaleId = null;
this.hide();
}
}
this.priceScaleControlsController =
new PriceScaleControlsController({
chartContainer,
getPaneElement: () => this.lwcPane.getHTMLElement(),
getPriceScale: (priceScaleId) =>
this.getPriceScale(priceScaleId),
getPriceScaleTarget: (priceScaleId) =>
this.getPriceScaleTarget(priceScaleId),
hasPriceScaleSeries: (priceScaleId) =>
this.hasPriceScaleSeries(priceScaleId),
getPriceScaleMode,
togglePriceScaleMode,
});
getPriceScaleMode: (target) =>
this.priceScaleModeController.getActiveMode(target),
togglePriceScaleMode: (target, mode) =>
this.priceScaleModeController.toggleSavedMode(target, mode),