Загрузка данных
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 945fc82c5ad51d762dfc1982f0a583419f6ec892..dc4ba10ed6cb94be88752e319cccbd4f8da4830d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- Добавлены тултипы для элементов управления в шапке графика
- Добавлен параметр `showSnapshotButton?: boolean` для управления отображением кнопки «Снимок графика»
- Добавлен VWAP индикатор
+- Добавлена механика ctrl+c/ctrl+v для дровингов
# 0.2.1
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index dc72e4e6bddef066e1e1907753944e365166c2b6..d80060268388852b2acd22de541901861ea4354a 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -15,7 +15,6 @@ interface DrawingParams extends DOMObjectParams {
lwcChart: IChartApi;
mainSeries: SeriesStrategies;
onDelete: (id: string) => void;
- onCopy: () => void;
construct: (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => ISeriesDrawing;
selected$: Observable<boolean>;
isSelected: () => boolean;
@@ -35,7 +34,6 @@ export class Drawing extends DOMObject {
private subscriptions = new Subscription();
private unregisterDeleteHotkey = () => {};
- private unregisterCopyHotkey = () => {};
constructor({
lwcChart,
@@ -44,7 +42,6 @@ export class Drawing extends DOMObject {
drawingName,
id,
onDelete,
- onCopy,
zIndex,
moveUp,
moveDown,
@@ -103,15 +100,6 @@ export class Drawing extends DOMObject {
this.delete();
},
});
-
- this.unregisterCopyHotkey = this.hotkeys.register({
- keys: [Keys.mod, Keys.c],
- callback: () => {
- if (!this.isCreationPending()) {
- onCopy();
- }
- },
- });
}),
);
}
@@ -219,9 +207,7 @@ export class Drawing extends DOMObject {
private unregisterSelectedDrawingHotkeys(): void {
this.unregisterDeleteHotkey();
- this.unregisterCopyHotkey();
this.unregisterDeleteHotkey = () => {};
- this.unregisterCopyHotkey = () => {};
}
}
diff --git a/src/core/Drawings/axisLine/axisLine.ts b/src/core/Drawings/axisLine/axisLine.ts
index 9da496e9abda866ef0c076e722b784b3433974e3..8bce3938d26a20a7ddfa9640033e2fc3a72cf043 100644
--- a/src/core/Drawings/axisLine/axisLine.ts
+++ b/src/core/Drawings/axisLine/axisLine.ts
@@ -36,6 +36,19 @@ interface AxisLineParams extends BaseDrawingParams {
direction: AxisLineDirection;
}
+export function isAxisLineState(state: unknown): state is AxisLineState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'time' in state &&
+ 'price' in state &&
+ ((typeof state.time === 'number' && state.price === null) ||
+ (state.time === null && typeof state.price === 'number'))
+ );
+}
+
interface AxisLineState {
hidden: boolean;
mode: AxisLineMode;
diff --git a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
index a288a14cad591c7118567b6e2718e3ac59e7ea6b..ccc8e3b691698e1783ad044b1fc162651b56d9e1 100644
--- a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
+++ b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
@@ -29,6 +29,23 @@ type PriceLabelKind = 'top' | 'bottom';
type FibonacciRetracementParams = BaseDrawingParams;
+export function isFibonacciRetracementState(state: unknown): state is FibonacciRetracementState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startTime' in state &&
+ typeof state.startTime === 'number' &&
+ 'endTime' in state &&
+ typeof state.endTime === 'number' &&
+ 'startPrice' in state &&
+ typeof state.startPrice === 'number' &&
+ 'endPrice' in state &&
+ typeof state.endPrice === 'number'
+ );
+}
+
interface FibonacciRetracementState {
hidden: boolean;
mode: FibonacciRetracementMode;
diff --git a/src/core/Drawings/line/lineDrawing.ts b/src/core/Drawings/line/lineDrawing.ts
index 128255b28d9bd901e225f6d8c796e10de919980b..b21f8814cb7baf4267f114b63f04da81cd8b5e4e 100644
--- a/src/core/Drawings/line/lineDrawing.ts
+++ b/src/core/Drawings/line/lineDrawing.ts
@@ -5,6 +5,8 @@ import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase, TwoPointGeometry
import { type ChartOptionsModel, Defaults, LineMarker, type SettingsTab } from '@src/types';
import { formatDate, formatPrice } from '@src/utils';
+import { isAnchor } from '../utils';
+
import { LineDrawingPaneView } from './paneView';
import { createDefaultSettings, getLineDrawingSettingsTabs } from './settings';
@@ -24,6 +26,14 @@ interface LineDrawingParams extends BaseDrawingParams {
defaultMarkers?: Partial<LineDrawingMarkers>;
}
+export function isLineDrawingState(state: unknown): state is LineDrawingState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return 'startAnchor' in state && isAnchor(state.startAnchor) && 'endAnchor' in state && isAnchor(state.endAnchor);
+}
+
interface LineDrawingState {
hidden: boolean;
mode: LineDrawingMode;
diff --git a/src/core/Drawings/ray/ray.ts b/src/core/Drawings/ray/ray.ts
index 46a152f0867d181894d1d1210a1698eb5ccf7261..a90c04a7ddd3d251334b15d6dfae716d30bc429f 100644
--- a/src/core/Drawings/ray/ray.ts
+++ b/src/core/Drawings/ray/ray.ts
@@ -1,6 +1,6 @@
import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase } from '@src/core/Drawings/base';
import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
@@ -23,6 +23,19 @@ type PriceLabelKind = 'start' | 'direction';
type RayParams = BaseDrawingParams;
+export function isRayState(state: unknown): state is RayState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startAnchor' in state &&
+ isAnchor(state.startAnchor) &&
+ 'directionAnchor' in state &&
+ isAnchor(state.directionAnchor)
+ );
+}
+
interface RayState {
hidden: boolean;
mode: RayMode;
diff --git a/src/core/Drawings/regressionTrend/regressionTrend.ts b/src/core/Drawings/regressionTrend/regressionTrend.ts
index 6ed24bfd367a9c32664674a6d64490e755e6eb9a..b9ad932850c9c26eeb097eb13ffb7b83a6b45774 100644
--- a/src/core/Drawings/regressionTrend/regressionTrend.ts
+++ b/src/core/Drawings/regressionTrend/regressionTrend.ts
@@ -25,6 +25,19 @@ type PriceLabelKind = 'start' | 'end';
type RegressionTrendParams = BaseDrawingParams;
+export function isRegressionTrendState(state: unknown): state is RegressionTrendState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startTime' in state &&
+ typeof state.startTime === 'number' &&
+ 'endTime' in state &&
+ typeof state.endTime === 'number'
+ );
+}
+
interface RegressionTrendState {
hidden: boolean;
mode: RegressionTrendMode;
diff --git a/src/core/Drawings/sliderPosition/sliderPosition.ts b/src/core/Drawings/sliderPosition/sliderPosition.ts
index c9abaa9ac3918de4726334d0c6b7d8ef2983537b..1fa5d8d7bbb73335e7c4a9e9dccbd55a5bf7b3c4 100644
--- a/src/core/Drawings/sliderPosition/sliderPosition.ts
+++ b/src/core/Drawings/sliderPosition/sliderPosition.ts
@@ -51,6 +51,25 @@ interface SliderPositionParams extends BaseDrawingParams {
resetTriggers?: Observable<unknown>[];
}
+export function isSliderPositionState(state: unknown): state is SliderPositionState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return (
+ 'startTime' in state &&
+ typeof state.startTime === 'number' &&
+ 'endTime' in state &&
+ typeof state.endTime === 'number' &&
+ 'entryPrice' in state &&
+ typeof state.entryPrice === 'number' &&
+ 'stopPrice' in state &&
+ typeof state.stopPrice === 'number' &&
+ 'targetPrice' in state &&
+ typeof state.targetPrice === 'number'
+ );
+}
+
interface SliderPositionState {
hidden: boolean;
mode: SliderMode;
diff --git a/src/core/Drawings/text/text.ts b/src/core/Drawings/text/text.ts
index 03c59c46ef8dfb2eb95b2ab6be10cfd3ddff0b65..9489acb4130a608630839b4dc7c9560896bd54ee 100644
--- a/src/core/Drawings/text/text.ts
+++ b/src/core/Drawings/text/text.ts
@@ -9,7 +9,7 @@ import {
getYCoordinateFromPrice,
isPointInBounds,
} from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
+import { isAnchor, updateViews } from '@core/Drawings/utils';
import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
import { Defaults, SettingsTab } from '@src/types';
@@ -23,6 +23,14 @@ import type { ChartOptionsModel } from '@src/types';
type TextMode = 'idle' | 'dragging' | 'ready';
+export function isTextState(state: unknown): state is TextState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return 'point' in state && isAnchor(state.point);
+}
+
export interface TextState {
hidden: boolean;
mode: TextMode;
diff --git a/src/core/Drawings/traectory/traectory.ts b/src/core/Drawings/traectory/traectory.ts
index 8d2fb230572b8d8ba90ae0309c7ca6a990f4c88d..aad51bf06c25bc85543f7e8f7e287e24f00a8512 100644
--- a/src/core/Drawings/traectory/traectory.ts
+++ b/src/core/Drawings/traectory/traectory.ts
@@ -2,7 +2,7 @@ import { PrimitiveHoveredItem } from 'lightweight-charts';
import { clamp } from 'lodash-es';
import { clampPointToContainer, getContainerSize, isNearPoint } from '@core/Drawings/helpers';
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
@@ -18,6 +18,14 @@ type TraectoryMode = 'idle' | 'drawing' | 'ready' | 'dragging-point' | 'dragging
type TraectoryHandleId = `${number}`;
type TraectoryParams = BaseDrawingParams;
+export function isTraectoryState(state: unknown): state is TraectoryState {
+ if (typeof state !== 'object' || state === null) {
+ return false;
+ }
+
+ return 'points' in state && Array.isArray(state.points) && state.points.every((point) => isAnchor(point));
+}
+
export interface TraectoryState {
hidden: boolean;
mode: TraectoryMode;
diff --git a/src/core/Drawings/utils.ts b/src/core/Drawings/utils.ts
index a00fdf76fd8222319e2578fd891a5a0bf8baf12b..92cbcc6a3b8f89503446106dcd7173e2351a9b68 100644
--- a/src/core/Drawings/utils.ts
+++ b/src/core/Drawings/utils.ts
@@ -1,4 +1,4 @@
-import type { Point, UpdatableView } from './types';
+import type { Anchor, Point, UpdatableView } from './types';
export function updateViews(views: readonly UpdatableView[]): void {
for (const view of views) {
@@ -66,3 +66,14 @@ export function getDistanceToSegment(point: Point, startPoint: Point, endPoint:
return Math.hypot(point.x - projectionX, point.y - projectionY);
}
+
+export function isAnchor(value: unknown): value is Anchor {
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ 'time' in value &&
+ typeof value.time === 'number' &&
+ 'price' in value &&
+ typeof value.price === 'number'
+ );
+}
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 5b1f486c6e87eaff0dd87fbceb90637769eb5ccb..24384f31b243688193cf346c8e12b09a49d57cb9 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -13,6 +13,7 @@ import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool, DOMObjectSnapshot } from '@src/types';
+import { COPPIED_DRWAING_ID } from './DrawingsManagerCollection';
import { Pane } from './Pane';
import type { DrawingInteraction } from '@src/core/Drawings/base';
@@ -69,7 +70,6 @@ export class DrawingsManager {
private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
private pane: Pane;
- private copyPasteBuffer: DrawingSnapshotItem | null = null;
private setActiveTool: (name: ActiveDrawingTool) => void;
private getActiveTool: () => ActiveDrawingTool;
private getIsEndlessMode: () => boolean;
@@ -120,36 +120,14 @@ export class DrawingsManager {
}),
);
+ // todo: перенести в panе
window.addEventListener('pointerup', this.handlePointerUp);
window.addEventListener('pointercancel', this.handlePointerUp);
this.container.addEventListener('click', this.handleClick);
this.container.addEventListener('pointerdown', this.handlePointerDown);
this.container.addEventListener('dblclick', this.handleDoubleClick);
this.container.addEventListener('contextmenu', this.handleContextMenu);
- // todo: implement ctrl+v
- // hotkeys.register({
- // keys: [Keys.control, Keys.v],
- // callback: () => {
- // const bufferWithAppliedPosition = {
- // ...this.copyPasteBuffer,
- // state: {
- // ...this.copyPasteBuffer?.state,
- // startAnchor: {
- // price: 73.36210252637723,
- // time: 1783679170
- // }
- // }
- // }
- //
- // this.setSnapshot([
- // ...this.getSnapshot(),
- // bufferWithAppliedPosition
- // ])
- // // hotkeys.unregister({ // todo: unregister all else ctrl+c's
- // // keys: [Keys.control, Keys.c]
- // // })
- // }
- // })
+ this.container.addEventListener('contextmenu', this.handleContextMenu);
}
// TODO: handlePointerDown конфликтует с DrawingsManagerCollection.handlePaneClick
@@ -240,6 +218,7 @@ export class DrawingsManager {
};
private handleClick = (event: MouseEvent): void => {
+ // todo: move method to pane
if (!this.isEventInPane(event)) {
return;
}
@@ -248,6 +227,8 @@ export class DrawingsManager {
pendingDrawing?.getSeriesDrawing().click(event);
+ this.pane.setSelected();
+
this.DOM.refreshEntities();
};
@@ -561,11 +542,6 @@ export class DrawingsManager {
drawingName: name,
name: drawingLabelById()[name],
onDelete: this.removeDrawing,
- onCopy: () => {
- if (createdDrawing) {
- this.copyPasteBuffer = this.createDrawingSnapshot(createdDrawing);
- }
- },
zIndex: entityZIndex,
moveDown,
moveUp,
@@ -634,13 +610,20 @@ export class DrawingsManager {
drawings.push(
this.createDrawing({
name: item.drawingName,
- options: {
- id: item.id,
- state: cloneDeep(item.state),
- isLocked: item.isLocked,
- zIndex: item.zIndex,
- shouldUpdateDrawingsList: false,
- },
+ options:
+ item.id === COPPIED_DRWAING_ID
+ ? {
+ state: cloneDeep(item.state),
+ isLocked: item.isLocked,
+ shouldUpdateDrawingsList: false,
+ }
+ : {
+ id: item.id,
+ state: cloneDeep(item.state),
+ isLocked: item.isLocked,
+ zIndex: item.zIndex,
+ shouldUpdateDrawingsList: false,
+ },
}),
);
@@ -698,6 +681,16 @@ export class DrawingsManager {
this.removeDrawing(drawing.id);
}
+ public getSelectedDrawingSnapshot(): DrawingSnapshotItem | null {
+ const drawing = this.selectedDrawing$.value;
+
+ if (!drawing) {
+ return null;
+ }
+
+ return this.createDrawingSnapshot(drawing);
+ }
+
public toggleSelectedDrawingLock(): void {
const drawing = this.selectedDrawing$.value;
@@ -768,7 +761,6 @@ export class DrawingsManager {
this.drawings$.value.forEach((drawing) => drawing.destroy());
this.selectedDrawingSnapshot = null;
- this.copyPasteBuffer = null;
this.subscriptions.unsubscribe();
this.drawings$.complete();
diff --git a/src/core/DrawingsManagerCollection.ts b/src/core/DrawingsManagerCollection.ts
index 06359f4739f3b061fc75e939bb071ed0629aa35c..992999f1e553540f17cae07bb192de4912d1fee8 100644
--- a/src/core/DrawingsManagerCollection.ts
+++ b/src/core/DrawingsManagerCollection.ts
@@ -1,21 +1,43 @@
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { DrawingsNames } from '@src/constants';
import { DRAWING_KEYBOARD_SHORTCUTS, findDrawingPointerShortcut } from '@src/core/Drawings/shortcuts';
import { ActiveDrawingTool, SettingsValues } from '@src/types';
+import { ChartMouseEvents } from './ChartMouseEvents';
import { Drawing } from './Drawings';
-import { DrawingsManager } from './DrawingsManager';
+import { isAxisLineState } from './Drawings/axisLine/axisLine';
+import { isFibonacciRetracementState } from './Drawings/fibonacciRetracement/fibonacciRetracement';
+import { getPriceFromYCoordinate, getTimeFromXCoordinate } from './Drawings/helpers';
+import { isLineDrawingState } from './Drawings/line/lineDrawing';
+import { isRayState } from './Drawings/ray/ray';
+import { isRegressionTrendState } from './Drawings/regressionTrend/regressionTrend';
+import { isSliderPositionState } from './Drawings/sliderPosition/sliderPosition';
+import { isTextState } from './Drawings/text/text';
+import { isTraectoryState } from './Drawings/traectory/traectory';
+import { SeriesApi } from './Drawings/types';
+import { DrawingsManager, DrawingSnapshotItem } from './DrawingsManager';
import { Hotkeys, Keys } from './Hotkeys';
import type { Pane } from './Pane';
+import type { IChartApi, MouseEventParams, Point, Time } from 'lightweight-charts';
-import type { MouseEventParams } from 'lightweight-charts';
+interface DrawingsManagerCollectionParams {
+ hotkeys: Hotkeys;
+ selectedPane: Observable<Pane | null>;
+ subscribeChartEvent: ChartMouseEvents['subscribe'];
+ lwcChart: IChartApi;
+}
+
+export const COPPIED_DRWAING_ID = 'coppied';
-// todo: нужно дописывать класс)
export class DrawingsManagerCollection {
private managersMap: Map<number, DrawingsManager> = new Map();
private hotkeys: Hotkeys;
+ private selectedPane: Pane | null = null;
+ private copyPasteBuffer: DrawingSnapshotItem | null = null;
+ private cursorPos = new BehaviorSubject<null | Point>(null);
+ private copyPasteCoords: Point | null = null;
private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
private endlessMode$ = new BehaviorSubject(false);
@@ -23,8 +45,13 @@ export class DrawingsManagerCollection {
private isAwaitingDrawingStart = false;
private unregisterHotkeys: (() => void)[] = [];
- constructor({ hotkeys }: { hotkeys: Hotkeys }) {
+ private subscriptions = new Subscription();
+
+ constructor({ hotkeys, selectedPane, subscribeChartEvent, lwcChart }: DrawingsManagerCollectionParams) {
this.hotkeys = hotkeys;
+ selectedPane.subscribe((next) => {
+ this.selectedPane = next;
+ });
for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
this.unregisterHotkeys.push(
@@ -37,6 +64,88 @@ export class DrawingsManagerCollection {
);
}
+ this.subscriptions.add(
+ subscribeChartEvent('crosshairMove', (param) => {
+ this.cursorPos.next(param.point ?? null);
+ }),
+ );
+
+ this.unregisterHotkeys.push(
+ this.hotkeys.register({
+ keys: [Keys.mod, Keys.c],
+ callback: () => {
+ const pane = this.selectedPane;
+ if (!pane) {
+ return;
+ }
+
+ const drawingManager = pane.getDrawingManager();
+
+ const yCoord = this.cursorPos.value?.y;
+ const xCoord = this.cursorPos.value?.x;
+
+ if (!yCoord || !xCoord) {
+ this.copyPasteBuffer = null;
+ return;
+ }
+
+ this.copyPasteBuffer = drawingManager.getSelectedDrawingSnapshot();
+ this.copyPasteCoords = { x: xCoord, y: yCoord };
+ },
+ }),
+ );
+
+ this.unregisterHotkeys.push(
+ this.hotkeys.register({
+ keys: [Keys.mod, Keys.v],
+ callback: () => {
+ const pane = this.selectedPane;
+ if (!pane) {
+ return;
+ }
+
+ const drawingManager = pane.getDrawingManager();
+
+ const drawingToPaste = this.copyPasteBuffer;
+
+ if (!drawingToPaste) {
+ return;
+ }
+
+ const { state } = drawingToPaste;
+
+ const yCoord = this.cursorPos.value?.y;
+ const xCoord = this.cursorPos.value?.x;
+
+ const prevYCoord = this.copyPasteCoords?.y;
+ const prevXCoord = this.copyPasteCoords?.x;
+
+ if (!yCoord || !xCoord || !prevYCoord || !prevXCoord) {
+ return;
+ }
+
+ const shift = {
+ priceDiff:
+ <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, yCoord) -
+ <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, prevYCoord),
+ timeDiff:
+ <number>getTimeFromXCoordinate(lwcChart, xCoord) - <number>getTimeFromXCoordinate(lwcChart, prevXCoord),
+ };
+
+ const shiftedState = shiftDrawingStateCoordsTo(state, shift);
+
+ const bufferWithAppliedPosition: DrawingSnapshotItem = {
+ ...drawingToPaste,
+ id: COPPIED_DRWAING_ID,
+ zIndex: drawingToPaste.zIndex,
+ state: shiftedState,
+ };
+
+ drawingManager.setSnapshot([...drawingManager.getSnapshot(), bufferWithAppliedPosition]);
+ },
+ }),
+ );
+
this.unregisterHotkeys.push(
this.hotkeys.register({
keys: [Keys.escape],
@@ -135,6 +244,10 @@ export class DrawingsManagerCollection {
Array.from(this.managersMap.values())[0].openSelectedDrawingSettings();
};
+ public copySelectedDrawing = (): void => {
+ Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
+ };
+
public deleteSelectedDrawing = (): void => {
Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
};
@@ -156,3 +269,129 @@ export class DrawingsManagerCollection {
}
}
}
+
+function shiftDrawingStateCoordsTo(state: unknown, shift: { timeDiff: number; priceDiff: number }) {
+ if (isLineDrawingState(state)) {
+ // LineDrawing, ParallelChannelState, VolumeProfileState
+ const startAnchor = {
+ time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.startAnchor?.price as number) + shift.priceDiff,
+ };
+ const endAnchor = {
+ time: ((state.endAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.endAnchor?.price as number) + shift.priceDiff,
+ };
+ return {
+ ...state,
+ startAnchor,
+ endAnchor,
+ };
+ }
+ if (isRayState(state)) {
+ // Ray
+ const startAnchor = {
+ time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.startAnchor?.price as number) + shift.priceDiff,
+ };
+
+ const directionAnchor = {
+ time: ((state.directionAnchor?.time as number) + shift.timeDiff) as Time,
+ price: (state.directionAnchor?.price as number) + shift.priceDiff,
+ };
+
+ return {
+ ...state,
+ startAnchor,
+ directionAnchor,
+ };
+ }
+
+ if (isSliderPositionState(state)) {
+ // SliderPositionState
+ const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+ const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+ const entryPrice = (state.entryPrice as number) + shift.priceDiff;
+ const stopPrice = (state.stopPrice as number) + shift.priceDiff;
+ const targetPrice = (state.targetPrice as number) + shift.priceDiff;
+
+ return {
+ ...state,
+ startTime,
+ endTime,
+ entryPrice,
+ stopPrice,
+ targetPrice,
+ };
+ }
+ if (isFibonacciRetracementState(state)) {
+ // FibonacciRetracement, DiapsonState, RectangleState
+ const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+ const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+ const startPrice = (state.startPrice as number) + shift.priceDiff;
+ const endPrice = (state.endPrice as number) + shift.priceDiff;
+
+ return {
+ ...state,
+ startTime,
+ endTime,
+ startPrice,
+ endPrice,
+ };
+ }
+ if (isRegressionTrendState(state)) {
+ // RegressionTrend
+ const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+ const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+
+ return {
+ ...state,
+ startTime,
+ endTime,
+ };
+ }
+
+ if (isAxisLineState(state)) {
+ // AxisLine
+ const time = ((state.time as number) + shift.timeDiff) as Time;
+ const price = (state.price as number) + shift.priceDiff;
+
+ return {
+ ...state,
+ time,
+ price,
+ };
+ }
+
+ if (isTextState(state)) {
+ // TextState
+ const point = {
+ time: ((state.point?.time as number) + shift.timeDiff) as Time,
+ price: (state.point?.price as number) + shift.priceDiff,
+ };
+
+ return {
+ ...state,
+ point,
+ };
+ }
+
+ if (isTraectoryState(state)) {
+ // TraectoryState
+
+ const points = state.points.map((point) => {
+ const anchor = {
+ time: ((point?.time as number) + shift.timeDiff) as Time,
+ price: (point?.price as number) + shift.priceDiff,
+ };
+
+ return anchor;
+ });
+
+ return {
+ ...state,
+ points,
+ };
+ }
+
+ return state;
+}
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 0484870b14f536ffdf4d930fc32eccb47c1a5545..df0333525801649e77acf2a3b51262d15fe39d37 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -58,6 +58,7 @@ export interface PaneParams {
getActiveTool: () => ActiveDrawingTool;
getIsEndlessMode: () => boolean;
continueDrawing: (name: DrawingsNames) => void;
+ deselectAllPanes: () => void;
}
// todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
@@ -94,6 +95,8 @@ export class Pane implements ISerializable<PaneSnapshot> {
private readonly onPriceScaleStateChange: () => void;
private readonly subscriptions = new Subscription();
private paneContainerSyncFrameId: number | null = null;
+ private isSelected = new BehaviorSubject<boolean>(false);
+ private deselectAllPanes: () => void;
constructor({
lwcChart,
@@ -120,6 +123,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
getActiveTool,
getIsEndlessMode,
continueDrawing,
+ deselectAllPanes,
}: PaneParams) {
this.onDelete = onDelete;
this.onPriceScaleStateChange = onPriceScaleStateChange;
@@ -129,6 +133,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
this.subscribeChartEvent = subscribeChartEvent;
this.isMain = isMainPane;
this.id = id;
+ this.deselectAllPanes = deselectAllPanes;
if (isMainPane) {
this.lwcPane = this.lwcChart.panes()[MAIN_PANE_INDEX];
@@ -216,8 +221,25 @@ export class Pane implements ISerializable<PaneSnapshot> {
this.legendContainer.style.display = hasRuler ? 'none' : '';
}),
);
+
+ this.subscriptions.add(
+ this.drawingsManager.entities().subscribe((drawings) => {
+ const hasRuler = drawings.some((drawing) => drawing.getDrawingName() === DrawingsNames.ruler);
+
+ this.legendContainer.style.display = hasRuler ? 'none' : '';
+ }),
+ );
}
+ public setSelected = () => {
+ this.deselectAllPanes();
+ this.isSelected.next(true);
+ };
+
+ public deselected = () => {
+ this.isSelected.next(false);
+ };
+
public getLocalMainSeries = () => {
return this.localMainSeries.value;
};
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index a2e59cbc7ffd88a421c57287c24a23be18694fa3..91c4eee9c7e37596dd510917be79d9d9a398f6a5 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -1,7 +1,7 @@
-import { type Observable, Subscription } from 'rxjs';
+import { BehaviorSubject, type Observable, Subscription } from 'rxjs';
import { DataSource } from '@core/DataSource';
-import { DrawingsManagerSnapshot } from '@core/DrawingsManager';
+import { DrawingsManagerSnapshot, DrawingSnapshotItem } from '@core/DrawingsManager';
import { DrawingsManagerCollection } from '@core/DrawingsManagerCollection';
import { Pane, PaneParams } from '@core/Pane';
import { PriceAxisLabels } from '@core/PriceAxisLabels';
@@ -29,7 +29,8 @@ type CollectionDependencies =
| 'setActiveTool'
| 'getActiveTool'
| 'getIsEndlessMode'
- | 'continueDrawing';
+ | 'continueDrawing'
+ | 'deselectAllPanes';
interface PaneManagerStartParams {
compareEntities$: Observable<Indicator[]>;
@@ -48,6 +49,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
private readonly lwcChart: IChartApi;
private readonly panesMap = new Map<number, Pane>();
+ private selectedPane = new BehaviorSubject<Pane | null>(null);
private mainPane: Pane;
private nextPaneId: number;
private priceAxisLabels: PriceAxisLabels | null = null;
@@ -57,6 +59,13 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
private readonly subscriptions = new Subscription();
constructor({ panesSnapshot, ...sharedPaneParams }: PaneManagerParams) {
+ this.drawingsManagerCollection = new DrawingsManagerCollection({
+ hotkeys: sharedPaneParams.hotkeys,
+ selectedPane: this.selectedPane.asObservable(),
+ subscribeChartEvent: sharedPaneParams.subscribeChartEvent,
+ lwcChart: sharedPaneParams.lwcChart,
+ });
+
this.sharedPaneParams = {
...sharedPaneParams,
};
@@ -65,10 +74,6 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
const mainPaneSnapshot = panesSnapshot.find((paneSnapshot) => paneSnapshot.isMain);
const mainPaneId = mainPaneSnapshot?.id ?? 0;
- this.drawingsManagerCollection = new DrawingsManagerCollection({
- hotkeys: sharedPaneParams.hotkeys,
- });
-
this.mainPane = new Pane({
...this.sharedPaneParams,
...this.getCollectionDependencies(),
@@ -229,19 +234,25 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
this.panesMap.clear();
}
+ private deselectAllPanes(): void {
+ this.panesMap.forEach((pane) => {
+ pane.deselected();
+ });
+ }
+
private initClickListener(): void {
const handler = (param: MouseEventParams) => {
if (param.paneIndex === undefined) {
return;
}
- const clickedPane = this.getPaneByIndex(param.paneIndex);
+ this.selectedPane.next(this.getPaneByIndex(param.paneIndex) ?? null);
- if (!clickedPane) {
+ if (!this.selectedPane.value) {
return;
}
- this.drawingsManagerCollection.handlePaneClick(clickedPane, param);
+ this.drawingsManagerCollection.handlePaneClick(this.selectedPane.value, param);
};
this.lwcChart.subscribeClick(handler);
@@ -291,6 +302,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
getActiveTool: () => this.drawingsManagerCollection.getActiveToolValue(),
getIsEndlessMode: () => this.drawingsManagerCollection.getIsEndlessMode(),
continueDrawing: (name: DrawingsNames) => this.drawingsManagerCollection.activateDrawingTool(name),
+ deselectAllPanes: () => this.deselectAllPanes(),
};
}
diff --git a/src/core/__tests__/Pane.test.ts b/src/core/__tests__/Pane.test.ts
index d425fb524776ca1157d81e679d9f23f978d1ffd2..61755d7ae33dd47bab015a96d81e5c09b15b7980 100644
--- a/src/core/__tests__/Pane.test.ts
+++ b/src/core/__tests__/Pane.test.ts
@@ -160,6 +160,7 @@ describe('Pane', () => {
getActiveTool: jest.fn(),
getIsEndlessMode: jest.fn(),
continueDrawing: jest.fn(),
+ deselectAllPanes: jest.fn(),
});
}