Загрузка данных
import { PriceScaleMode } from 'lightweight-charts';
import { PriceScaleControls as PriceScaleControlsView } from '@components/PriceScaleControls';
import { ReactRenderer } from '@core/ReactRenderer';
import { Direction } from '@src/types';
import { PriceScale } from './PriceScale';
interface PriceScaleControlsParams {
leftPriceScale: PriceScale;
rightPriceScale: PriceScale;
onPriceScaleChange: () => void;
}
interface PriceScaleElements {
left: HTMLTableCellElement;
right: HTMLTableCellElement;
}
export class PriceScaleControls {
private readonly leftPriceScale: PriceScale;
private readonly rightPriceScale: PriceScale;
private readonly onPriceScaleChange: () => void;
private readonly container: HTMLElement;
private readonly renderer: ReactRenderer;
private paneElement: HTMLElement | null = null;
private leftPriceScaleElement: HTMLTableCellElement | null = null;
private rightPriceScaleElement: HTMLTableCellElement | null = null;
private hoveredPriceScale: PriceScale | null = null;
private isPriceScaleInteractionActive = false;
private refreshFrameId: number | null = null;
private notifyPriceScaleChangeAfterRefresh = false;
constructor({ leftPriceScale, rightPriceScale, onPriceScaleChange }: PriceScaleControlsParams) {
this.leftPriceScale = leftPriceScale;
this.rightPriceScale = rightPriceScale;
this.onPriceScaleChange = onPriceScaleChange;
this.container = document.createElement('div');
this.container.className = 'moex-price-scale-controls';
this.renderer = new ReactRenderer(this.container);
}
public mount(paneElement: HTMLElement): void {
const priceScaleElements = this.getPriceScaleElements(paneElement);
if (!priceScaleElements) {
this.unmount();
return;
}
if (this.paneElement !== paneElement) {
this.unmount();
this.paneElement = paneElement;
this.paneElement.addEventListener('pointerdown', this.handlePointerDown);
this.paneElement.addEventListener('pointermove', this.handlePointerMove);
this.paneElement.addEventListener('pointerleave', this.handlePointerLeave);
this.paneElement.addEventListener('dblclick', this.handleDoubleClick);
window.addEventListener('pointerup', this.handlePointerUp);
window.addEventListener('pointercancel', this.handlePointerUp);
}
this.leftPriceScaleElement = priceScaleElements.left;
this.rightPriceScaleElement = priceScaleElements.right;
this.leftPriceScaleElement.style.position = 'relative';
this.rightPriceScaleElement.style.position = 'relative';
this.refresh();
}
public refresh(): void {
if (!this.hoveredPriceScale) {
this.hide();
return;
}
this.render(this.hoveredPriceScale);
}
public destroy(): void {
this.unmount();
this.renderer.destroy();
}
private handlePointerDown = (event: PointerEvent): void => {
this.isPriceScaleInteractionActive = this.getHoveredPriceScale(event.clientX) !== null;
};
private handlePointerMove = (event: PointerEvent): void => {
const nextPriceScale = this.getHoveredPriceScale(event.clientX);
if (this.hoveredPriceScale !== nextPriceScale) {
this.hoveredPriceScale = nextPriceScale;
this.refresh();
}
if (this.isPriceScaleInteractionActive && nextPriceScale) {
this.scheduleRefresh();
}
};
private handlePointerUp = (): void => {
if (!this.isPriceScaleInteractionActive) {
return;
}
this.isPriceScaleInteractionActive = false;
this.scheduleRefresh(true);
};
private handlePointerLeave = (): void => {
this.hoveredPriceScale = null;
this.refresh();
};
private handleDoubleClick = (event: MouseEvent): void => {
const priceScale = this.getHoveredPriceScale(event.clientX);
if (!priceScale) {
return;
}
this.hoveredPriceScale = priceScale;
this.scheduleRefresh(true);
};
private scheduleRefresh(notifyPriceScaleChange = false): void {
if (notifyPriceScaleChange) {
this.notifyPriceScaleChangeAfterRefresh = true;
}
if (this.refreshFrameId !== null) {
return;
}
this.refreshFrameId = requestAnimationFrame(() => {
this.refreshFrameId = null;
this.refresh();
if (!this.notifyPriceScaleChangeAfterRefresh) {
return;
}
this.notifyPriceScaleChangeAfterRefresh = false;
this.onPriceScaleChange();
});
}
private render(priceScale: PriceScale): void {
const priceScaleElement = this.getPriceScaleElement(priceScale);
if (!priceScaleElement || !this.canShowControls(priceScale, priceScaleElement)) {
this.hide();
return;
}
if (this.container.parentElement !== priceScaleElement) {
priceScaleElement.appendChild(this.container);
}
this.renderer.renderComponent(
<PriceScaleControlsView
isAutoScale={priceScale.isAutoScaleEnabled()}
isLogarithmic={priceScale.getMode() === PriceScaleMode.Logarithmic}
onToggleAutoScale={() => {
priceScale.toggleAutoScale();
this.refresh();
this.onPriceScaleChange();
}}
onToggleLogarithmic={() => {
priceScale.toggleLogarithmic();
this.refresh();
this.onPriceScaleChange();
}}
/>,
);
this.container.classList.add('moex-price-scale-controls_visible');
}
private hide(): void {
this.container.classList.remove('moex-price-scale-controls_visible');
}
private getHoveredPriceScale(clientX: number): PriceScale | null {
if (this.isPointerInsidePriceScale(this.leftPriceScale, clientX)) {
return this.leftPriceScale;
}
if (this.isPointerInsidePriceScale(this.rightPriceScale, clientX)) {
return this.rightPriceScale;
}
return null;
}
private isPointerInsidePriceScale(priceScale: PriceScale, clientX: number): boolean {
const priceScaleElement = this.getPriceScaleElement(priceScale);
if (!priceScaleElement || !this.canShowControls(priceScale, priceScaleElement)) {
return false;
}
const priceScaleRect = priceScaleElement.getBoundingClientRect();
return clientX >= priceScaleRect.left && clientX <= priceScaleRect.right;
}
private canShowControls(priceScale: PriceScale, priceScaleElement: HTMLTableCellElement): boolean {
return priceScale.isVisible() && priceScale.hasVisibleSeriesData() && priceScaleElement.clientWidth > 0;
}
private getPriceScaleElement(priceScale: PriceScale): HTMLTableCellElement | null {
return priceScale.side === Direction.Left ? this.leftPriceScaleElement : this.rightPriceScaleElement;
}
private getPriceScaleElements(paneElement: HTMLElement): PriceScaleElements | null {
if (paneElement.children.length < 3) {
return null;
}
const leftPriceScaleElement = paneElement.children.item(0);
const rightPriceScaleElement = paneElement.children.item(paneElement.children.length - 1);
if (
!(leftPriceScaleElement instanceof HTMLTableCellElement) ||
!(rightPriceScaleElement instanceof HTMLTableCellElement)
) {
return null;
}
return {
left: leftPriceScaleElement,
right: rightPriceScaleElement,
};
}
private unmount(): void {
if (this.paneElement) {
this.paneElement.removeEventListener('pointerdown', this.handlePointerDown);
this.paneElement.removeEventListener('pointermove', this.handlePointerMove);
this.paneElement.removeEventListener('pointerleave', this.handlePointerLeave);
this.paneElement.removeEventListener('dblclick', this.handleDoubleClick);
window.removeEventListener('pointerup', this.handlePointerUp);
window.removeEventListener('pointercancel', this.handlePointerUp);
}
if (this.refreshFrameId !== null) {
cancelAnimationFrame(this.refreshFrameId);
this.refreshFrameId = null;
}
this.paneElement = null;
this.leftPriceScaleElement = null;
this.rightPriceScaleElement = null;
this.hoveredPriceScale = null;
this.isPriceScaleInteractionActive = false;
this.notifyPriceScaleChangeAfterRefresh = false;
this.container.remove();
this.hide();
}
}