Загрузка данных
import { SyntheticEvent } from 'react';
import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import styles from './index.module.scss';
interface PriceScaleControlsProps {
isAutoScale: boolean;
isLogarithmic: boolean;
onAutoScale: () => void;
onToggleLogarithmic: () => void;
onRefresh: () => void;
}
export function PriceScaleControls({
isAutoScale,
isLogarithmic,
onAutoScale,
onToggleLogarithmic,
onRefresh,
}: PriceScaleControlsProps) {
const stopPropagation = (event: SyntheticEvent) => {
event.stopPropagation();
};
return (
<section
className={classNames(styles.root, 'moex-price-scale-controls-content')}
onMouseEnter={onRefresh}
onFocus={onRefresh}
onPointerDown={stopPropagation}
onMouseDown={stopPropagation}
onClick={stopPropagation}
>
<Button
size="sm"
className={classNames(styles.button, {
[styles.button_active]: isAutoScale,
})}
onClick={onAutoScale}
label="A"
/>
<Button
size="sm"
className={classNames(styles.button, {
[styles.button_active]: isLogarithmic,
})}
onClick={onToggleLogarithmic}
label="L"
/>
</section>
);
}
.root {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-0250);
pointer-events: none;
}
.button {
min-width: 24px;
height: 24px;
padding: 0 var(--space-0250);
pointer-events: auto;
}
.button_active {
font-weight: 600;
}
import { IPriceScaleApi, PriceScaleMode } from 'lightweight-charts';
import { ReactRenderer } from '@core/ReactRenderer';
import { PriceScaleControls } from '@src/components/PriceScaleControls';
import { PriceScaleTarget } from '@src/core/PriceScale';
import { Direction } from '@src/types';
interface PriceScaleControlsControllerParams {
paneId: number;
getPaneElement: () => HTMLElement | null;
getPriceScale: (priceScaleId: Direction) => IPriceScaleApi;
getPriceScaleTarget: (priceScaleId: Direction) => PriceScaleTarget;
getSavedPriceScaleMode: (target: PriceScaleTarget) => PriceScaleMode;
togglePriceScaleMode: (target: PriceScaleTarget, mode: PriceScaleMode) => void;
}
export class PriceScaleControlsController {
private readonly paneId: number;
private readonly getPaneElement: () => HTMLElement | null;
private readonly getPriceScale: (priceScaleId: Direction) => IPriceScaleApi;
private readonly getPriceScaleTarget: (priceScaleId: Direction) => PriceScaleTarget;
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({
paneId,
getPaneElement,
getPriceScale,
getPriceScaleTarget,
getSavedPriceScaleMode,
togglePriceScaleMode,
}: PriceScaleControlsControllerParams) {
this.paneId = paneId;
this.getPaneElement = getPaneElement;
this.getPriceScale = getPriceScale;
this.getPriceScaleTarget = getPriceScaleTarget;
this.getSavedPriceScaleMode = getSavedPriceScaleMode;
this.togglePriceScaleMode = togglePriceScaleMode;
this.container = this.createContainer();
this.renderer = new ReactRenderer(this.container);
}
public mount(): void {
const nextPaneElement = this.getPaneElement();
if (!nextPaneElement) {
return;
}
if (this.paneElement === nextPaneElement && this.container.parentElement === nextPaneElement) {
return;
}
this.unmountEvents();
this.paneElement = nextPaneElement;
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.unmountEvents();
this.renderer.destroy();
this.container.remove();
}
private createContainer(): HTMLElement {
const container = document.createElement('div');
container.className = 'moex-price-scale-controls';
container.style.position = 'absolute';
container.style.bottom = 'var(--space-0500)';
container.style.zIndex = '10';
container.style.pointerEvents = 'none';
return container;
}
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.isAvailable(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.getWidth(Direction.Left);
const rightWidth = this.getWidth(Direction.Right);
if (this.isAvailable(Direction.Left) && pointerX >= 0 && pointerX <= leftWidth) {
return Direction.Left;
}
if (this.isAvailable(Direction.Right) && pointerX >= paneWidth - rightWidth && pointerX <= paneWidth) {
return Direction.Right;
}
return null;
}
private updatePosition(priceScaleId: Direction): void {
const width = this.getWidth(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 {
try {
this.getPriceScale(priceScaleId).setAutoScale(true);
} catch {
// ignore inactive price scale
}
}
private isAutoScale(priceScaleId: Direction): boolean {
try {
return this.getPriceScale(priceScaleId).options().autoScale ?? true;
} catch {
return true;
}
}
private isAvailable(priceScaleId: Direction): boolean {
try {
const priceScale = this.getPriceScale(priceScaleId);
const options = priceScale.options();
if (options.visible === false) {
return false;
}
return this.getWidth(priceScaleId) > 0;
} catch {
return false;
}
}
private getWidth(priceScaleId: Direction): number {
try {
return this.getPriceScale(priceScaleId).width();
} catch {
return 0;
}
}
private unmountEvents(): void {
if (!this.paneElement) {
return;
}
this.paneElement.removeEventListener('pointermove', this.handlePointerMove);
this.paneElement.removeEventListener('pointerleave', this.handlePointerLeave);
this.paneElement = null;
}
}
.moex-price-scale-controls {
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.moex-price-scale-controls_visible {
opacity: 1;
}
.moex-price-scale-controls_visible .moex-price-scale-controls-content {
pointer-events: auto;
}