Загрузка данных


import {
  AutoscaleInfo,
  CrosshairMode,
  IChartApi,
  IPrimitivePaneView,
  ISeriesApi,
  ISeriesPrimitive,
  ISeriesPrimitiveAxisView,
  Logical,
  PrimitiveHoveredItem,
  SeriesAttachedParameter,
  SeriesOptionsMap,
  SeriesType,
  Time,
} from 'lightweight-charts';

import { BehaviorSubject, distinctUntilChanged, Subscription } from 'rxjs';

import { getPointerPoint as getPointerPointFromEvent } from '@core/Drawings/helpers';
import { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';

import { SettingsTab, SettingsValues } from '@src/types';

export interface ISeriesDrawing extends ISeriesPrimitive<Time> {
  show(): void;
  hide(): void;
  rebind(series: ISeriesApi<SeriesType>): void;
  destroy(): void;
  isCreationPending(): boolean;
  waitTillReady(): Promise<void>;

  shouldShowInObjectTree(): boolean;

  getState(): unknown;
  setState(state: unknown): void;

  getSettings(): SettingsValues;
  updateSettings(settings: SettingsValues): void;
  getSettingsTabs(): SettingsTab[];

  subscribeIsSelected(callback: (isSelected: boolean) => void): Subscription;
  subscribeIsLocked(callback: (isLocked: boolean) => void): Subscription;

  isSelected(): boolean;
  isLocked(): boolean;
  setLocked(isLocked: boolean): void;

  getRenderData(): unknown;
}

interface SeriesDrawingBaseParams {
  container: HTMLElement;
  chart: IChartApi;
  series: SeriesApi;
}

export abstract class SeriesDrawingBase<TSettings extends SettingsValues = SettingsValues> implements ISeriesDrawing {
  protected hidden = false;
  protected chart: IChartApi;
  protected series: SeriesApi;
  protected subscriptions = new Subscription();
  protected abstract mode: unknown; // todo: хочется иметь единый mode
  protected abstract settings: TSettings;
  protected readonly container: HTMLElement;
  protected isActive = new BehaviorSubject(false);
  protected isBound = false;

  private readonly isLockedSubject = new BehaviorSubject(false);

  protected readyPromise: Promise<void> | null = null;
  protected resolveReady: (() => void) | null = null;

  protected requestUpdate: (() => void) | null = null;

  constructor({ chart, series, container }: SeriesDrawingBaseParams) {
    this.chart = chart;
    this.series = series;
    this.container = container;
  }

  public subscribeIsSelected(callback: (isSelected: boolean) => void): Subscription {
    return this.isActive.pipe(distinctUntilChanged()).subscribe(callback);
  }

  public subscribeIsLocked(callback: (isLocked: boolean) => void): Subscription {
    return this.isLockedSubject.pipe(distinctUntilChanged()).subscribe(callback);
  }

  public isSelected(): boolean {
    return this.isActive.value;
  }

  public isLocked(): boolean {
    return this.isLockedSubject.value;
  }

  public setLocked(isLocked: boolean): void {
    if (this.isLockedSubject.value === isLocked) {
      return;
    }

    this.isLockedSubject.next(isLocked);

    if (isLocked) {
      this.showCrosshair();
    }

    this.render();
  }

  public show(): void {
    this.hidden = false;
    this.render();
  }

  public hide(): void {
    this.hidden = true;
    this.showCrosshair();
    this.render();
  }

  public rebind(series: SeriesApi): void {
    if (this.series === series) {
      return;
    }

    this.showCrosshair();
    this.unbindEvents();
    this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);

    this.series = series;
    this.requestUpdate = null;

    this.series.attachPrimitive(this as unknown as ISeriesPrimitive<Time>);
    this.render();
  }

  public destroy(): void {
    this.showCrosshair();
    this.unbindEvents();
    this.subscriptions.unsubscribe();
    this.isLockedSubject.complete();
    this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);
    this.requestUpdate = null;
    this.resolveReady?.();
  }

  public waitTillReady(): Promise<void> {
    if (this.mode === 'ready') {
      return Promise.resolve();
    }

    if (!this.readyPromise) {
      this.readyPromise = new Promise((resolve) => {
        this.resolveReady = resolve;
      });
    }

    return this.readyPromise;
  }

  public shouldShowInObjectTree(): boolean {
    return this.mode !== 'idle';
  }

  public getSettings(): SettingsValues {
    return { ...this.settings };
  }

  public updateSettings(settings: SettingsValues): void {
    this.settings = {
      ...this.settings,
      ...settings,
    };

    this.render();
  }

  public attached(param: SeriesAttachedParameter<Time, keyof SeriesOptionsMap>): void {
    this.requestUpdate = param.requestUpdate;
    this.bindEvents();
  }

  public detached(): void {
    this.showCrosshair();
    this.unbindEvents();
    this.requestUpdate = null;
  }

  public autoscaleInfo(_start: Logical, _end: Logical): AutoscaleInfo | null {
    return null;
  }

  public abstract getRenderData(): unknown; // todo: make proper type
  public abstract getState(): unknown;
  public abstract getSettingsTabs(): SettingsTab[];
  public abstract isCreationPending(): boolean;
  public abstract setState(state: unknown): void;

  public abstract hitTest(x: number, y: number): PrimitiveHoveredItem | null;
  public abstract updateAllViews(): void;
  public abstract paneViews(): readonly IPrimitivePaneView[];
  public abstract priceAxisPaneViews(): readonly IPrimitivePaneView[];
  public abstract priceAxisViews(): readonly ISeriesPrimitiveAxisView[];
  public abstract timeAxisPaneViews(): readonly IPrimitivePaneView[];
  public abstract timeAxisViews(): readonly ISeriesPrimitiveAxisView[];

  protected render(): void {
    this.updateAllViews();
    this.requestUpdate?.();
  }

  protected hideCrosshair(): void {
    this.chart.applyOptions({
      crosshair: {
        mode: CrosshairMode.Hidden,
      },
    });
  }

  protected showCrosshair(): void {
    this.chart.applyOptions({
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });
  }

  protected getEventPoint(event: PointerEvent): Point {
    return getPointerPointFromEvent(this.container, event);
  }

  protected bindEvents(): void {
    if (this.isBound) {
      return;
    }

    this.isBound = true;

    this.container.addEventListener('dblclick', this.handleDoubleClick);
    this.container.addEventListener('pointerdown', this.handlePointerDown);
    this.container.addEventListener('contextmenu', this.handleContextMenu);

    window.addEventListener('pointermove', this.handlePointerMove);
    window.addEventListener('pointerup', this.handlePointerUp);
    window.addEventListener('pointercancel', this.handlePointerUp);
  }

  protected unbindEvents(): void {
    if (!this.isBound) {
      return;
    }

    this.isBound = false;

    this.container.removeEventListener('dblclick', this.handleDoubleClick);
    this.container.removeEventListener('pointerdown', this.handlePointerDown);
    this.container.removeEventListener('contextmenu', this.handleContextMenu);

    window.removeEventListener('pointermove', this.handlePointerMove);
    window.removeEventListener('pointerup', this.handlePointerUp);
    window.removeEventListener('pointercancel', this.handlePointerUp);
  }

  // todo: хочется общую реализацию для каждой кнопки
  protected handleContextMenu(event: MouseEvent): void {}
  protected handleDoubleClick(event: MouseEvent): void {}
  protected handlePointerDown(event: PointerEvent): void {}
  protected handlePointerMove(event: PointerEvent): void {}
  protected handlePointerUp(event: PointerEvent): void {}

  protected abstract getGeometry(): unknown; // todo: make proper type
  protected abstract getTimeAxisSegments(): AxisSegment[];
  protected abstract getPriceAxisSegments(): AxisSegment[];
  protected abstract getTimeAxisLabel(kind: string): AxisLabel | null;
  protected abstract getPriceAxisLabel(kind: string): AxisLabel | null;
}



import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';

import { Subscription } from 'rxjs';

import { DOMObject, DOMObjectParams } from '@core/DOMObject';
import { ISeriesDrawing } from '@core/Drawings/common';
import { DrawingSnapshotItem } from '@core/DrawingsManager';
import { Hotkeys, Keys } from '@core/Hotkeys';

import { DrawingsNames } from '@src/constants';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { SettingsTab, SettingsValues } from '@src/types/settings';

type IDrawing = DOMObject;

interface DrawingParams extends DOMObjectParams {
  drawingName: DrawingsNames;
  lwcChart: IChartApi;
  mainSeries: SeriesStrategies;
  onDelete: (id: string) => void;
  construct: (chart: IChartApi, series: ISeriesApi<SeriesType>) => ISeriesDrawing;
  hotkeys: Hotkeys;
  setCopyPasteBuffer: (copiedObject: DrawingSnapshotItem) => void;
  resetActiveTool: () => void;
}

export class Drawing extends DOMObject implements IDrawing {
  private lwcDrawing: ISeriesDrawing;
  private mainSeries: SeriesStrategies;
  private drawingName: DrawingsNames;
  private hotkeys: Hotkeys;

  private escapeUnregisterHash: string | null = null;
  private deleteUnregisterHash: string | null = null;
  private copyUnregisterHash: string | null = null;

  constructor({
    lwcChart,
    name,
    mainSeries,
    drawingName,
    id,
    onDelete,
    zIndex,
    moveUp,
    moveDown,
    construct,
    paneId,
    hotkeys,
    setCopyPasteBuffer,
    resetActiveTool,
  }: DrawingParams) {
    super({ id, name, zIndex, onDelete, moveUp, moveDown, paneId });

    this.hotkeys = hotkeys;
    this.lwcDrawing = construct(lwcChart, mainSeries);
    this.onDelete = onDelete;

    this.escapeUnregisterHash = hotkeys.register({
      keys: [Keys.escape],
      callback: () => {
        this.delete();
        resetActiveTool();
      },
    });

    this.lwcDrawing.subscribeIsSelected((isSelected) => {
      if (isSelected) {
        this.deleteUnregisterHash = hotkeys.register({
          keys: [Keys.delete],
          callback: () => {
            this.delete();
          },
        });

        this.copyUnregisterHash = hotkeys.register({
          keys: [Keys.control, Keys.c],
          callback: () => {
            if (!this.isCreationPending()) {
              const copiedObject = {
                id: this.id,
                drawingName: this.getDrawingName(),
                state: this.getState(),
                isLocked: this.isLocked(),
              };

              setCopyPasteBuffer(copiedObject);
            }
          },
        });
      } else {
        hotkeys.unregister({
          keys: [Keys.delete],
          hash: this.deleteUnregisterHash,
        });

        hotkeys.unregister({
          keys: [Keys.control, Keys.c],
          hash: this.copyUnregisterHash,
        });
      }
    });

    this.mainSeries = mainSeries;
    this.drawingName = drawingName;

    this.afterCreation(() => {
      hotkeys.unregister({
        keys: [Keys.escape],
        hash: this.escapeUnregisterHash,
      });
    });
  }

  public delete(): void {
    this.destroy();
    super.delete();
  }

  public getDrawingName(): DrawingsNames {
    return this.drawingName;
  }

  public getLwcDrawing(): ISeriesDrawing {
    return this.lwcDrawing;
  }

  public show(): void {
    this.lwcDrawing.show();
    super.show();
  }

  public hide(): void {
    this.lwcDrawing.hide();
    super.hide();
  }

  public rebind = (nextMainSeries: SeriesStrategies): void => {
    this.lwcDrawing.rebind(nextMainSeries);
    this.mainSeries = nextMainSeries;
  };

  public isCreationPending(): boolean {
    return this.lwcDrawing.isCreationPending();
  }

  public subscribeIsLocked(callback: (isLocked: boolean) => void): Subscription {
    return this.lwcDrawing.subscribeIsLocked(callback);
  }

  public isLocked(): boolean {
    return this.lwcDrawing.isLocked();
  }

  public isSelected(): boolean {
    return this.lwcDrawing.isSelected();
  }

  public setLocked(isLocked: boolean): void {
    this.lwcDrawing.setLocked(isLocked);
  }

  public async waitForCreation(): Promise<void> {
    return this.lwcDrawing.waitTillReady();
  }

  public shouldShowInObjectTree(): boolean {
    return this.lwcDrawing.shouldShowInObjectTree();
  }

  public getState(): unknown {
    return this.lwcDrawing.getState();
  }

  public setState(state: unknown): void {
    this.lwcDrawing.setState(state);
  }

  public getSettings(): SettingsValues {
    return this.lwcDrawing.getSettings();
  }

  public updateSettings(settings: SettingsValues): void {
    this.lwcDrawing.updateSettings(settings);
  }

  public getSettingsTabs(): SettingsTab[] {
    return this.lwcDrawing.getSettingsTabs();
  }

  public hasSettings(): boolean {
    return this.getSettingsTabs().some((tab) => tab.fields.length > 0);
  }

  public destroy(): void {
    this.hotkeys.unregister({
      keys: [Keys.delete],
      hash: this.deleteUnregisterHash,
    });

    this.hotkeys.unregister({
      keys: [Keys.escape],
      hash: this.escapeUnregisterHash,
    });

    this.hotkeys.unregister({
      keys: [Keys.control, Keys.c],
      hash: this.copyUnregisterHash,
    });

    this.mainSeries.detachPrimitive(this.lwcDrawing);
    this.lwcDrawing.destroy();
  }

  private async afterCreation(callback: () => void): Promise<void> {
    await this.lwcDrawing.waitTillReady();
    callback();
  }
}