diff --git a/.size-limit.js b/.size-limit.js index 243d79f1e3..b9e58ab0d0 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,21 +4,21 @@ module.exports = [ { name: 'CJS', path: 'dist/lightweight-charts.production.cjs', - limit: '46.97 KB', + limit: '47.07 KB', }, { name: 'ESM', path: 'dist/lightweight-charts.production.mjs', - limit: '46.90 KB', + limit: '47.01 KB', }, { name: 'Standalone-ESM', path: 'dist/lightweight-charts.standalone.production.mjs', - limit: '48.59 KB', + limit: '48.70 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '48.64 KB', + limit: '48.74 KB', }, ]; diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts index 2729a00e0d..e90d11a269 100644 --- a/src/api/chart-api.ts +++ b/src/api/chart-api.ts @@ -119,6 +119,7 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { private readonly _seriesMapReversed: Map> = new Map(); private readonly _clickedDelegate: Delegate = new Delegate(); + private readonly _dblClickedDelegate: Delegate = new Delegate(); private readonly _crosshairMovedDelegate: Delegate = new Delegate(); private readonly _timeScaleApi: TimeScaleApi; @@ -138,6 +139,14 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { }, this ); + this._chartWidget.dblClicked().subscribe( + (paramSupplier: MouseEventParamsImplSupplier) => { + if (this._dblClickedDelegate.hasListeners()) { + this._dblClickedDelegate.fire(this._convertMouseParams(paramSupplier())); + } + }, + this + ); this._chartWidget.crosshairMoved().subscribe( (paramSupplier: MouseEventParamsImplSupplier) => { if (this._crosshairMovedDelegate.hasListeners()) { @@ -153,6 +162,7 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { public remove(): void { this._chartWidget.clicked().unsubscribeAll(this); + this._chartWidget.dblClicked().unsubscribeAll(this); this._chartWidget.crosshairMoved().unsubscribeAll(this); this._timeScaleApi.destroy(); @@ -162,6 +172,7 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { this._seriesMapReversed.clear(); this._clickedDelegate.destroy(); + this._dblClickedDelegate.destroy(); this._crosshairMovedDelegate.destroy(); this._dataLayer.destroy(); } @@ -252,6 +263,14 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { this._clickedDelegate.unsubscribe(handler); } + public subscribeDblClick(handler: MouseEventHandler): void { + this._dblClickedDelegate.subscribe(handler); + } + + public unsubscribeDblClick(handler: MouseEventHandler): void { + this._dblClickedDelegate.unsubscribe(handler); + } + public subscribeCrosshairMove(handler: MouseEventHandler): void { this._crosshairMovedDelegate.subscribe(handler); } diff --git a/src/api/ichart-api.ts b/src/api/ichart-api.ts index a16385c1cc..551cf88e47 100644 --- a/src/api/ichart-api.ts +++ b/src/api/ichart-api.ts @@ -222,6 +222,36 @@ export interface IChartApi { */ unsubscribeClick(handler: MouseEventHandler): void; + /** + * Subscribe to the chart double-click event. + * + * @param handler - Handler to be called on mouse double-click. + * @example + * ```js + * function myDblClickHandler(param) { + * if (!param.point) { + * return; + * } + * + * console.log(`Double Click at ${param.point.x}, ${param.point.y}. The time is ${param.time}.`); + * } + * + * chart.subscribeDblClick(myDblClickHandler); + * ``` + */ + subscribeDblClick(handler: MouseEventHandler): void; + + /** + * Unsubscribe a handler that was previously subscribed using {@link subscribeDblClick}. + * + * @param handler - Previously subscribed handler + * @example + * ```js + * chart.unsubscribeDblClick(myDblClickHandler); + * ``` + */ + unsubscribeDblClick(handler: MouseEventHandler): void; + /** * Subscribe to the crosshair move event. * diff --git a/src/gui/chart-widget.ts b/src/gui/chart-widget.ts index da800c45b5..5c78d5068a 100644 --- a/src/gui/chart-widget.ts +++ b/src/gui/chart-widget.ts @@ -59,6 +59,7 @@ export class ChartWidget implements IDestroyable { private _invalidateMask: InvalidateMask | null = null; private _drawPlanned: boolean = false; private _clicked: Delegate = new Delegate(); + private _dblClicked: Delegate = new Delegate(); private _crosshairMoved: Delegate = new Delegate(); private _onWheelBound: (event: WheelEvent) => void; private _observer: ResizeObserver | null = null; @@ -151,6 +152,7 @@ export class ChartWidget implements IDestroyable { for (const paneWidget of this._paneWidgets) { this._tableElement.removeChild(paneWidget.getElement()); paneWidget.clicked().unsubscribeAll(this); + paneWidget.dblClicked().unsubscribeAll(this); paneWidget.destroy(); } this._paneWidgets = []; @@ -168,6 +170,7 @@ export class ChartWidget implements IDestroyable { this._crosshairMoved.destroy(); this._clicked.destroy(); + this._dblClicked.destroy(); this._uninstallObserver(); } @@ -234,6 +237,10 @@ export class ChartWidget implements IDestroyable { return this._clicked; } + public dblClicked(): ISubscription { + return this._dblClicked; + } + public crosshairMoved(): ISubscription { return this._crosshairMoved; } @@ -684,6 +691,7 @@ export class ChartWidget implements IDestroyable { const paneWidget = ensureDefined(this._paneWidgets.pop()); this._tableElement.removeChild(paneWidget.getElement()); paneWidget.clicked().unsubscribeAll(this); + paneWidget.dblClicked().unsubscribeAll(this); paneWidget.destroy(); // const paneSeparator = this._paneSeparators.pop(); @@ -696,6 +704,7 @@ export class ChartWidget implements IDestroyable { for (let i = actualPaneWidgetsCount; i < targetPaneWidgetsCount; i++) { const paneWidget = new PaneWidget(this, panes[i]); paneWidget.clicked().subscribe(this._onPaneWidgetClicked.bind(this), this); + paneWidget.dblClicked().subscribe(this._onPaneWidgetDblClicked.bind(this), this); this._paneWidgets.push(paneWidget); @@ -777,6 +786,14 @@ export class ChartWidget implements IDestroyable { this._clicked.fire(() => this._getMouseEventParamsImpl(time, point, event)); } + private _onPaneWidgetDblClicked( + time: TimePointIndex | null, + point: Point | null, + event: TouchMouseEventData + ): void { + this._dblClicked.fire(() => this._getMouseEventParamsImpl(time, point, event)); + } + private _onPaneWidgetCrosshairMoved( time: TimePointIndex | null, point: Point | null, diff --git a/src/gui/pane-widget.ts b/src/gui/pane-widget.ts index 32820b6dd0..78a40cccdb 100644 --- a/src/gui/pane-widget.ts +++ b/src/gui/pane-widget.ts @@ -76,6 +76,7 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { private _startScrollingPos: StartScrollPosition | null = null; private _isScrolling: boolean = false; private _clicked: Delegate = new Delegate(); + private _dblClicked: Delegate = new Delegate(); private _prevPinchScale: number = 0; private _longTap: boolean = false; private _startTrackPoint: Point | null = null; @@ -266,6 +267,17 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { this._fireClickedDelegate(event); } + public mouseDoubleClickEvent(event: MouseEventHandlerMouseEvent | MouseEventHandlerTouchEvent): void { + if (this._state === null) { + return; + } + this._fireMouseClickDelegate(this._dblClicked, event); + } + + public doubleTapEvent(event: MouseEventHandlerTouchEvent): void { + this.mouseDoubleClickEvent(event); + } + public pressedMouseMoveEvent(event: MouseEventHandlerMouseEvent): void { this._onMouseEvent(); this._pressedMouseTouchMoveEvent(event); @@ -313,6 +325,10 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { return this._clicked; } + public dblClicked(): ISubscription { + return this._dblClicked; + } + public pinchStartEvent(): void { this._prevPinchScale = 1; this._model().stopTimeScaleAnimation(); @@ -501,10 +517,14 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { } private _fireClickedDelegate(event: MouseEventHandlerEventBase): void { + this._fireMouseClickDelegate(this._clicked, event); + } + + private _fireMouseClickDelegate(delegate: Delegate, event: MouseEventHandlerEventBase): void { const x = event.localX; const y = event.localY; - if (this._clicked.hasListeners()) { - this._clicked.fire(this._model().timeScale().coordinateToIndex(x), { x, y }, event); + if (delegate.hasListeners()) { + delegate.fire(this._model().timeScale().coordinateToIndex(x), { x, y }, event); } } diff --git a/tests/e2e/helpers/perform-interactions.ts b/tests/e2e/helpers/perform-interactions.ts index 195baa15f2..8776533c54 100644 --- a/tests/e2e/helpers/perform-interactions.ts +++ b/tests/e2e/helpers/perform-interactions.ts @@ -6,6 +6,7 @@ import { doVerticalDrag, } from './mouse-drag-actions'; import { doMouseScroll } from './mouse-scroll-actions'; +import { pageTimeout } from './page-timeout'; import { doLongTouch, doPinchZoomTouch, doSwipeTouch } from './touch-actions'; import { doZoomInZoomOut } from './zoom-action'; @@ -84,7 +85,9 @@ async function performAction( await target.click({ button: 'left' }); break; case 'doubleClick': - await target.click({ button: 'left', clickCount: 2 }); + await target.click({ button: 'left' }); + await pageTimeout(page, 200); + await target.click({ button: 'left' }); break; case 'outsideClick': { diff --git a/tests/e2e/interactions/test-cases/mouse/double-click-pane.js b/tests/e2e/interactions/test-cases/mouse/double-click-pane.js new file mode 100644 index 0000000000..2ccc7aaa59 --- /dev/null +++ b/tests/e2e/interactions/test-cases/mouse/double-click-pane.js @@ -0,0 +1,78 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function initialInteractionsToPerform() { + return [{ action: 'doubleClick', target: 'pane' }]; +} + +function finalInteractionsToPerform() { + return []; +} + +let chart; +let clickCount = 0; +let dblClickCount = 0; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addLineSeries(); + + const mainSeriesData = generateData(); + mainSeries.setData(mainSeriesData); + + chart.subscribeDblClick(mouseParams => { + if (!mouseParams) { + return; + } + dblClickCount += 1; + }); + chart.subscribeClick(mouseParams => { + if (!mouseParams) { + return; + } + clickCount += 1; + }); + + return new Promise(resolve => { + requestAnimationFrame(() => { + resolve(); + }); + }); +} + +function afterInitialInteractions() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterFinalInteractions() { + if (clickCount < 1) { + throw new Error('Expected click event handler to be evoked.'); + } + if (dblClickCount < 1) { + throw new Error('Expected double click event handler to be evoked.'); + } + if (clickCount > 1) { + throw new Error('Expected click event handler to be evoked only once.'); + } + if (dblClickCount > 1) { + throw new Error( + 'Expected double click event handler to be evoked only once.' + ); + } + + return Promise.resolve(); +}