@pipsend/charts 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * Pipsend Charts v2.2.0
3
+ * Pipsend Charts v2.3.0-dev+202609220108
4
4
  * Copyright (c) 2026 Pipsend
5
5
  * Licensed under MIT License
6
6
  * Built on TradingView Lightweight Charts™ (Apache 2.0)
@@ -14245,6 +14245,8 @@ function shapeSize(shape, originalSize) {
14245
14245
  return size(originalSize, 0.8);
14246
14246
  case 'square':
14247
14247
  return size(originalSize, 0.7);
14248
+ case 'tag':
14249
+ return size(originalSize, 1);
14248
14250
  }
14249
14251
  }
14250
14252
  function calculateShapeHeight(barSpacing) {
@@ -14354,6 +14356,85 @@ function hitTestSquare(centerX, centerY, size, x, y) {
14354
14356
  y >= top && y <= top + squareSize;
14355
14357
  }
14356
14358
 
14359
+ const TAIL_HALF_WIDTH_RATIO = 0.62;
14360
+ /**
14361
+ * Lays the pointer into the edge the path is already travelling along, so the
14362
+ * bubble and its tail are a single outline. Two subpaths would leave a stroked
14363
+ * seam where they meet.
14364
+ */
14365
+ // eslint-disable-next-line max-params
14366
+ function tailPath(ctx, xCenter, edgeY, tailHeight, halfWidth, up) {
14367
+ // A tip with a small radius reads as drawn rather than as an artefact of the
14368
+ // rasteriser, which a bare triangle does at these sizes.
14369
+ const tipRadius = Math.min(halfWidth / 3, tailHeight / 3);
14370
+ const sign = up ? -1 : 1;
14371
+ const tipY = edgeY + sign * tailHeight;
14372
+ const shoulderY = tipY - sign * tipRadius;
14373
+ // The top edge is travelled left to right, the bottom edge right to left.
14374
+ const entry = up ? -halfWidth : halfWidth;
14375
+ ctx.lineTo(xCenter + entry, edgeY);
14376
+ ctx.lineTo(xCenter + (up ? -tipRadius : tipRadius), shoulderY);
14377
+ ctx.quadraticCurveTo(xCenter, tipY, xCenter + (up ? tipRadius : -tipRadius), shoulderY);
14378
+ ctx.lineTo(xCenter - entry, edgeY);
14379
+ }
14380
+ /**
14381
+ * Draws a rounded speech-bubble tag with an optional pointer towards its anchor.
14382
+ */
14383
+ function drawTag(ctx, params) {
14384
+ const hpr = params._internal_horizontalPixelRatio;
14385
+ const vpr = params._internal_verticalPixelRatio;
14386
+ const left = Math.round((params._internal_x - params._internal_width / 2) * hpr);
14387
+ const right = Math.round((params._internal_x + params._internal_width / 2) * hpr);
14388
+ const top = Math.round((params._internal_centerY - params._internal_height / 2) * vpr);
14389
+ const bottom = Math.round((params._internal_centerY + params._internal_height / 2) * vpr);
14390
+ const xCenter = Math.round(params._internal_x * hpr);
14391
+ const radius = Math.min(params._internal_radius * hpr, (right - left) / 2, (bottom - top) / 2);
14392
+ const tailHeight = params._internal_tailHeight * vpr;
14393
+ const tailHalfWidth = Math.min(tailHeight * TAIL_HALF_WIDTH_RATIO, Math.max(0, (right - left) / 2 - radius));
14394
+ const hasTail = params._internal_tailDirection !== 'none' && tailHalfWidth > 0 && tailHeight > 0;
14395
+ ctx.save();
14396
+ ctx.beginPath();
14397
+ if (!hasTail) {
14398
+ drawRoundRect(ctx, left, top, right - left, bottom - top, [radius, radius, radius, radius]);
14399
+ }
14400
+ else {
14401
+ ctx.moveTo(left + radius, top);
14402
+ if (params._internal_tailDirection === 'up') {
14403
+ tailPath(ctx, xCenter, top, tailHeight, tailHalfWidth, true);
14404
+ }
14405
+ ctx.lineTo(right - radius, top);
14406
+ ctx.arcTo(right, top, right, top + radius, radius);
14407
+ ctx.lineTo(right, bottom - radius);
14408
+ ctx.arcTo(right, bottom, right - radius, bottom, radius);
14409
+ if (params._internal_tailDirection === 'down') {
14410
+ tailPath(ctx, xCenter, bottom, tailHeight, tailHalfWidth, false);
14411
+ }
14412
+ ctx.lineTo(left + radius, bottom);
14413
+ ctx.arcTo(left, bottom, left, bottom - radius, radius);
14414
+ ctx.lineTo(left, top + radius);
14415
+ ctx.arcTo(left, top, left + radius, top, radius);
14416
+ }
14417
+ ctx.closePath();
14418
+ ctx.fillStyle = params._internal_fillColor;
14419
+ ctx.fill();
14420
+ if (params._internal_borderColor !== undefined && params._internal_borderWidth > 0) {
14421
+ ctx.lineWidth = Math.max(1, Math.round(params._internal_borderWidth * hpr));
14422
+ ctx.strokeStyle = params._internal_borderColor;
14423
+ ctx.stroke();
14424
+ }
14425
+ ctx.restore();
14426
+ }
14427
+ function hitTestTag(centerX, centerY, tag, x, y) {
14428
+ if (tag._internal_width <= 0) {
14429
+ return false;
14430
+ }
14431
+ const halfHeight = tag._internal_height / 2;
14432
+ const top = tag._internal_tailDirection === 'up' ? centerY - halfHeight - tag._internal_tailHeight : centerY - halfHeight;
14433
+ const bottom = tag._internal_tailDirection === 'down' ? centerY + halfHeight + tag._internal_tailHeight : centerY + halfHeight;
14434
+ const left = centerX - tag._internal_width / 2;
14435
+ return x >= left && x <= left + tag._internal_width && y >= top && y <= bottom;
14436
+ }
14437
+
14357
14438
  function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
14358
14439
  ctx.save();
14359
14440
  ctx.scale(horizontalPixelRatio, verticalPixelRatio);
@@ -14370,9 +14451,11 @@ class SeriesMarkersRenderer {
14370
14451
  constructor() {
14371
14452
  this._private__data = null;
14372
14453
  this._private__textWidthCache = new TextWidthCache();
14454
+ this._private__tagTextWidthCache = new TextWidthCache();
14373
14455
  this._private__fontSize = -1;
14374
14456
  this._private__fontFamily = '';
14375
14457
  this._private__font = '';
14458
+ this._private__tagFont = '';
14376
14459
  this._private__zOrder = 'normal';
14377
14460
  }
14378
14461
  _internal_setData(data) {
@@ -14383,7 +14466,11 @@ class SeriesMarkersRenderer {
14383
14466
  this._private__fontSize = fontSize;
14384
14467
  this._private__fontFamily = fontFamily;
14385
14468
  this._private__font = makeFont(fontSize, fontFamily);
14469
+ // Tags carry a label rather than an annotation, so they get the heavier
14470
+ // weight — and their own cache, since widths differ per font.
14471
+ this._private__tagFont = makeFont(fontSize, fontFamily, '600');
14386
14472
  this._private__textWidthCache._internal_reset();
14473
+ this._private__tagTextWidthCache._internal_reset();
14387
14474
  }
14388
14475
  this._private__zOrder = zOrder;
14389
14476
  }
@@ -14423,14 +14510,21 @@ class SeriesMarkersRenderer {
14423
14510
  return;
14424
14511
  }
14425
14512
  ctx.textBaseline = 'middle';
14426
- ctx.font = this._private__font;
14427
14513
  for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
14428
14514
  const item = this._private__data._internal_items[index];
14515
+ const isTag = item._internal_tag !== undefined;
14516
+ ctx.font = isTag ? this._private__tagFont : this._private__font;
14429
14517
  if (item._internal_text !== undefined) {
14430
- item._internal_text._internal_width = this._private__textWidthCache._internal_measureText(ctx, item._internal_text._internal_content);
14518
+ const cache = isTag ? this._private__tagTextWidthCache : this._private__textWidthCache;
14519
+ item._internal_text._internal_width = cache._internal_measureText(ctx, item._internal_text._internal_content);
14431
14520
  item._internal_text._internal_height = this._private__fontSize;
14432
14521
  item._internal_text._internal_x = item._internal_x - item._internal_text._internal_width / 2;
14433
14522
  }
14523
+ if (item._internal_tag !== undefined) {
14524
+ const textWidth = item._internal_text !== undefined ? item._internal_text._internal_width : 0;
14525
+ item._internal_tag._internal_width = Math.max(textWidth + item._internal_tag._internal_paddingX * 2, item._internal_tag._internal_height);
14526
+ item._internal_tag._internal_left = item._internal_x - item._internal_tag._internal_width / 2;
14527
+ }
14434
14528
  drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
14435
14529
  }
14436
14530
  }
@@ -14445,12 +14539,40 @@ function bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRat
14445
14539
  };
14446
14540
  }
14447
14541
  function drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio) {
14542
+ if (item._internal_tag !== undefined) {
14543
+ drawTagItem(item, item._internal_tag, ctx, horizontalPixelRatio, verticalPixelRatio);
14544
+ return;
14545
+ }
14448
14546
  ctx.fillStyle = item._internal_color;
14449
14547
  if (item._internal_text !== undefined) {
14450
14548
  drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
14451
14549
  }
14452
14550
  drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
14453
14551
  }
14552
+ // eslint-disable-next-line max-params
14553
+ function drawTagItem(item, tag, ctx, horizontalPixelRatio, verticalPixelRatio) {
14554
+ if (item._internal_size === 0) {
14555
+ return;
14556
+ }
14557
+ drawTag(ctx, {
14558
+ _internal_x: item._internal_x,
14559
+ _internal_centerY: item._internal_y,
14560
+ _internal_width: tag._internal_width,
14561
+ _internal_height: tag._internal_height,
14562
+ _internal_tailHeight: tag._internal_tailHeight,
14563
+ _internal_tailDirection: tag._internal_tailDirection,
14564
+ _internal_radius: tag._internal_radius,
14565
+ _internal_fillColor: item._internal_color,
14566
+ _internal_borderColor: item._internal_borderColor,
14567
+ _internal_borderWidth: item._internal_borderWidth ?? 1,
14568
+ _internal_horizontalPixelRatio: horizontalPixelRatio,
14569
+ _internal_verticalPixelRatio: verticalPixelRatio,
14570
+ });
14571
+ if (item._internal_text !== undefined) {
14572
+ ctx.fillStyle = item._internal_textColor ?? '#FFFFFF';
14573
+ drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
14574
+ }
14575
+ }
14454
14576
  function drawShape(item, ctx, coordinates) {
14455
14577
  if (item._internal_size === 0) {
14456
14578
  return;
@@ -14468,10 +14590,16 @@ function drawShape(item, ctx, coordinates) {
14468
14590
  case 'square':
14469
14591
  drawSquare(ctx, coordinates, item._internal_size);
14470
14592
  return;
14593
+ case 'tag':
14594
+ // Handled by drawTagItem, which needs both pixel ratios and the text.
14595
+ return;
14471
14596
  }
14472
14597
  ensureNever(item._internal_shape);
14473
14598
  }
14474
14599
  function hitTestItem(item, x, y) {
14600
+ if (item._internal_tag !== undefined) {
14601
+ return item._internal_size !== 0 && hitTestTag(item._internal_x, item._internal_y, item._internal_tag, x, y);
14602
+ }
14475
14603
  if (item._internal_text !== undefined && hitTestText(item._internal_text._internal_x, item._internal_text._internal_y, item._internal_text._internal_width, item._internal_text._internal_height, x, y)) {
14476
14604
  return true;
14477
14605
  }
@@ -14490,6 +14618,8 @@ function hitTestShape(item, x, y) {
14490
14618
  return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
14491
14619
  case 'square':
14492
14620
  return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
14621
+ case 'tag':
14622
+ return false;
14493
14623
  }
14494
14624
  }
14495
14625
 
@@ -14522,15 +14652,71 @@ function getPrice(seriesData, marker, isInverted) {
14522
14652
  }
14523
14653
  return;
14524
14654
  }
14655
+ /**
14656
+ * Lays out a `tag` marker: the bubble carries the text, so it is measured from
14657
+ * the font rather than from the bar spacing, and its pointer is aimed back at
14658
+ * the anchor price.
14659
+ */
14660
+ // eslint-disable-next-line max-params
14661
+ function fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier) {
14662
+ const height = Math.round((textHeight + 5 /* Constants.TagPaddingY */ * 2) * sizeMultiplier);
14663
+ const tailHeight = Math.round(7 /* Constants.TagTailHeight */ * sizeMultiplier);
14664
+ const ignoreOffset = isPriceMarker(marker.position);
14665
+ const halfHeight = height / 2;
14666
+ let tailDirection = 'none';
14667
+ let centerY = anchorY;
14668
+ switch (marker.position) {
14669
+ case 'aboveBar':
14670
+ case 'atPriceTop': {
14671
+ const offset = ignoreOffset ? 0 : offsets._internal_aboveBar;
14672
+ tailDirection = 'down';
14673
+ centerY = (anchorY - offset - tailHeight - halfHeight);
14674
+ if (!ignoreOffset) {
14675
+ offsets._internal_aboveBar += height + tailHeight + shapeMargin;
14676
+ }
14677
+ break;
14678
+ }
14679
+ case 'belowBar':
14680
+ case 'atPriceBottom': {
14681
+ const offset = ignoreOffset ? 0 : offsets._internal_belowBar;
14682
+ tailDirection = 'up';
14683
+ centerY = (anchorY + offset + tailHeight + halfHeight);
14684
+ if (!ignoreOffset) {
14685
+ offsets._internal_belowBar += height + tailHeight + shapeMargin;
14686
+ }
14687
+ break;
14688
+ }
14689
+ }
14690
+ const tag = {
14691
+ _internal_height: height,
14692
+ _internal_tailHeight: tailHeight,
14693
+ _internal_tailDirection: tailDirection,
14694
+ _internal_radius: marker.borderRadius ?? 6 /* Constants.TagRadius */,
14695
+ _internal_paddingX: 9 /* Constants.TagPaddingX */ * sizeMultiplier,
14696
+ _internal_left: 0,
14697
+ _internal_width: 0,
14698
+ };
14699
+ rendererItem._internal_size = height;
14700
+ rendererItem._internal_y = centerY;
14701
+ rendererItem._internal_tag = tag;
14702
+ if (rendererItem._internal_text !== undefined) {
14703
+ rendererItem._internal_text._internal_y = centerY;
14704
+ }
14705
+ }
14525
14706
  // eslint-disable-next-line max-params, complexity
14526
- function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart) {
14527
- const price = getPrice(seriesData, marker, series.priceScale().options().invertScale);
14707
+ function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart, invertScale) {
14708
+ const price = getPrice(seriesData, marker, invertScale);
14528
14709
  if (price === undefined) {
14529
14710
  return;
14530
14711
  }
14531
14712
  const ignoreOffset = isPriceMarker(marker.position);
14532
14713
  const timeScale = chart.timeScale();
14533
14714
  const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1;
14715
+ if (marker.shape === 'tag') {
14716
+ const anchorY = ensureNotNull(series.priceToCoordinate(price));
14717
+ fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier);
14718
+ return;
14719
+ }
14534
14720
  const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier;
14535
14721
  const halfSize = shapeSize / 2;
14536
14722
  rendererItem._internal_size = shapeSize;
@@ -14637,6 +14823,10 @@ class SeriesMarkersPaneView {
14637
14823
  _internal_externalId: marker.id,
14638
14824
  _internal_internalId: marker._internal_internalId,
14639
14825
  _internal_text: undefined,
14826
+ _internal_textColor: marker.textColor,
14827
+ _internal_borderColor: marker.borderColor,
14828
+ _internal_borderWidth: marker.borderWidth,
14829
+ _internal_tag: undefined,
14640
14830
  }));
14641
14831
  this._private__dataInvalidated = false;
14642
14832
  }
@@ -14647,13 +14837,22 @@ class SeriesMarkersPaneView {
14647
14837
  return;
14648
14838
  }
14649
14839
  const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from), Math.ceil(visibleBars.to));
14650
- const firstValue = this._private__series.data()[0];
14840
+ const firstValue = this._private__series.dataByIndex(0, 1 /* MismatchDirection.NearestRight */);
14651
14841
  if (firstValue === null) {
14652
14842
  return;
14653
14843
  }
14654
14844
  if (this._private__data._internal_items.length === 0) {
14655
14845
  return;
14656
14846
  }
14847
+ // This runs inside the hit-test and paint loops, where a throw costs the
14848
+ // rest of the frame.
14849
+ let invertScale = false;
14850
+ try {
14851
+ invertScale = this._private__series.priceScale().options().invertScale;
14852
+ }
14853
+ catch (error) {
14854
+ invertScale = false;
14855
+ }
14657
14856
  let prevTimeIndex = NaN;
14658
14857
  const shapeMargin$1 = shapeMargin(timeScale.options().barSpacing);
14659
14858
  const offsets = {
@@ -14684,7 +14883,7 @@ class SeriesMarkersPaneView {
14684
14883
  if (dataAt === null) {
14685
14884
  continue;
14686
14885
  }
14687
- fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart);
14886
+ fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart, invertScale);
14688
14887
  }
14689
14888
  this._private__invalidated = false;
14690
14889
  }
@@ -14842,6 +15041,10 @@ class SeriesMarkersPrimitive {
14842
15041
  text: marker.text,
14843
15042
  size: marker.size,
14844
15043
  price: marker.price,
15044
+ textColor: marker.textColor,
15045
+ borderColor: marker.borderColor,
15046
+ borderWidth: marker.borderWidth,
15047
+ borderRadius: marker.borderRadius,
14845
15048
  _internal_originalTime: marker.time,
14846
15049
  };
14847
15050
  if (marker.position === 'atPriceTop' ||
@@ -19143,6 +19346,7 @@ class TradingLineController {
19143
19346
  constructor(chart, element) {
19144
19347
  this._private__lines = new Map();
19145
19348
  this._private__hoveredId = null;
19349
+ this._private__hoveredPart = null;
19146
19350
  this._private__dragging = null;
19147
19351
  this._private__pointerId = null;
19148
19352
  this._private__pressOrigin = null;
@@ -19200,8 +19404,12 @@ class TradingLineController {
19200
19404
  if (this._private__dragging !== null) {
19201
19405
  return;
19202
19406
  }
19203
- const hovered = this._private__lineFromHit(param.hoveredObjectId);
19407
+ const hit = this._private__splitHit(param.hoveredObjectId);
19408
+ const hovered = hit === null ? null : this._private__lines.get(hit._internal_id) ?? null;
19204
19409
  this._private__setHovered(hovered ? hovered.id() : null);
19410
+ // The part is updated after the id, and separately from it: moving from a
19411
+ // label onto its close button keeps the same line hovered.
19412
+ this._private__setHoveredPart(hovered, hovered === null ? null : hit?._internal_part ?? null);
19205
19413
  this._private__applyCursor();
19206
19414
  }
19207
19415
  _private__onPointerDown(event) {
@@ -19305,24 +19513,43 @@ class TradingLineController {
19305
19513
  if (line === null) {
19306
19514
  return null;
19307
19515
  }
19308
- // While the pointer is over the panes the engine gives pane coordinates.
19309
- // Once it leaves them — during a captured drag — fall back to the pane
19310
- // element, which still excludes the price axis.
19516
+ // The pointer event is the gesture's own position, so it is the source of
19517
+ // truth. The crosshair is only a mirror of it: it stops updating as soon
19518
+ // as the pointer leaves the pane canvas — over the price axis, past the
19519
+ // chart's edge — and a drag driven from it freezes there while the user
19520
+ // is still moving.
19521
+ const pane = this._private__chart.panes()[line.paneIndex()];
19522
+ const paneElement = pane ? pane.getHTMLElement() : null;
19523
+ if (paneElement) {
19524
+ return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19525
+ }
19311
19526
  if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
19312
19527
  return line.priceAt(this._private__point._internal_y);
19313
19528
  }
19314
- const pane = this._private__chart.panes()[line.paneIndex()];
19315
- const paneElement = pane ? pane.getHTMLElement() : null;
19316
- if (!paneElement) {
19529
+ return null;
19530
+ }
19531
+ /**
19532
+ * Hit ids are either a bare line id or `id#part`, which is how a line with
19533
+ * several targets reports which one was hit.
19534
+ */
19535
+ _private__splitHit(externalId) {
19536
+ if (typeof externalId !== 'string' || externalId.length === 0) {
19317
19537
  return null;
19318
19538
  }
19319
- return line.priceAt(event.clientY - paneElement.getBoundingClientRect().top);
19539
+ const separator = externalId.indexOf('#');
19540
+ if (separator === -1) {
19541
+ return { _internal_id: externalId, _internal_part: null };
19542
+ }
19543
+ return { _internal_id: externalId.slice(0, separator), _internal_part: externalId.slice(separator + 1) };
19320
19544
  }
19321
- _private__lineFromHit(externalId) {
19322
- if (typeof externalId !== 'string') {
19323
- return null;
19545
+ _private__setHoveredPart(line, part) {
19546
+ if (this._private__hoveredPart === part) {
19547
+ return;
19548
+ }
19549
+ this._private__hoveredPart = part;
19550
+ if (line && line.setHoveredPart) {
19551
+ line.setHoveredPart(part);
19324
19552
  }
19325
- return this._private__lines.get(externalId) ?? null;
19326
19553
  }
19327
19554
  _private__setHovered(id) {
19328
19555
  if (this._private__hoveredId === id) {
@@ -19332,6 +19559,7 @@ class TradingLineController {
19332
19559
  this._private__lines.get(this._private__hoveredId)?.setHovered(false);
19333
19560
  }
19334
19561
  this._private__hoveredId = id;
19562
+ this._private__hoveredPart = null;
19335
19563
  if (id !== null) {
19336
19564
  this._private__lines.get(id)?.setHovered(true);
19337
19565
  }
@@ -19855,6 +20083,702 @@ function createInteractiveLineManager(chart, series) {
19855
20083
  return new InteractiveLineManager(chart, series);
19856
20084
  }
19857
20085
 
20086
+ const positionLineDefaultColors = {
20087
+ entry: '#2962FF',
20088
+ takeProfit: '#26A17B',
20089
+ stopLoss: '#D9A23B',
20090
+ };
20091
+ const positionLineThemeDefaults = {
20092
+ badgeTextColor: '#FFFFFF',
20093
+ borderRadius: 4,
20094
+ borderWidth: 1,
20095
+ paddingX: 8,
20096
+ paddingY: 6,
20097
+ backgroundOpacity: 1,
20098
+ };
20099
+ const positionLineLevels = ['takeProfit', 'entry', 'stopLoss'];
20100
+
20101
+ class PositionLineRenderer {
20102
+ constructor() {
20103
+ this._private__data = null;
20104
+ this._private__layouts = [];
20105
+ this._private__connectorX = 0;
20106
+ this._private__font = '';
20107
+ this._private__colorParser = new ColorParser([]);
20108
+ this._private__textWidthCache = new TextWidthCache(50);
20109
+ }
20110
+ _internal_setData(data) {
20111
+ this._private__data = data;
20112
+ }
20113
+ _internal_hitTest(x, y) {
20114
+ // The close button sits on top of the label, which sits on top of the
20115
+ // line, so the narrowest target is tested first.
20116
+ for (const layout of this._private__layouts) {
20117
+ if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
20118
+ Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20119
+ return { _internal_level: layout._internal_level, _internal_part: 'close' };
20120
+ }
20121
+ }
20122
+ for (const layout of this._private__layouts) {
20123
+ if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_y) <= layout._internal_height / 2) {
20124
+ return { _internal_level: layout._internal_level, _internal_part: 'body' };
20125
+ }
20126
+ }
20127
+ for (const layout of this._private__layouts) {
20128
+ if (Math.abs(y - layout._internal_y) <= 8 /* Constants.LineHitTolerance */) {
20129
+ return { _internal_level: layout._internal_level, _internal_part: 'line' };
20130
+ }
20131
+ }
20132
+ return null;
20133
+ }
20134
+ draw(target) {
20135
+ target.useBitmapCoordinateSpace((scope) => this._private__drawImpl(scope));
20136
+ }
20137
+ _private__drawImpl(scope) {
20138
+ const data = this._private__data;
20139
+ this._private__layouts = [];
20140
+ if (data === null || data._internal_levels.length === 0) {
20141
+ return;
20142
+ }
20143
+ const { context: ctx, mediaSize } = scope;
20144
+ ctx.save();
20145
+ if (this._private__font !== data._internal_font) {
20146
+ this._private__font = data._internal_font;
20147
+ this._private__textWidthCache._internal_reset();
20148
+ }
20149
+ ctx.font = data._internal_font;
20150
+ ctx.textBaseline = 'middle';
20151
+ this._private__connectorX = Math.max(0, mediaSize.width - data._internal_rightOffset);
20152
+ const lineEnd = data._internal_connectorVisible ? this._private__connectorX : mediaSize.width;
20153
+ for (const level of data._internal_levels) {
20154
+ this._private__layouts.push(this._private__drawLevel(scope, data, level, lineEnd));
20155
+ }
20156
+ if (data._internal_connectorVisible && data._internal_levels.length > 1) {
20157
+ this._private__drawConnector(scope, data);
20158
+ }
20159
+ ctx.restore();
20160
+ }
20161
+ // eslint-disable-next-line max-params
20162
+ _private__drawLevel(scope, data, level, lineEnd) {
20163
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20164
+ const height = data._internal_fontSize + data._internal_paddingY * 2;
20165
+ const badgeWidth = level._internal_badge.length === 0
20166
+ ? 0
20167
+ : Math.max(this._private__textWidthCache._internal_measureText(ctx, level._internal_badge) + data._internal_paddingX * 2, height * 0.85 /* Constants.BadgeMinWidthRatio */);
20168
+ const labelWidth = this._private__textWidthCache._internal_measureText(ctx, level._internal_text) + data._internal_paddingX * 2;
20169
+ const closeWidth = level._internal_closable ? height : 0;
20170
+ const pillWidth = badgeWidth + labelWidth + closeWidth;
20171
+ const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
20172
+ const pillLeft = pillRight - pillWidth;
20173
+ const top = level._internal_y - height / 2;
20174
+ this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
20175
+ this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
20176
+ let cellLeft = pillLeft;
20177
+ if (badgeWidth > 0) {
20178
+ this._private__drawBadge(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: badgeWidth, _internal_height: height });
20179
+ cellLeft += badgeWidth;
20180
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20181
+ }
20182
+ ctx.save();
20183
+ ctx.scale(hpr, vpr);
20184
+ ctx.textAlign = 'center';
20185
+ ctx.fillStyle = data._internal_textColor ?? level._internal_color;
20186
+ ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, level._internal_y);
20187
+ ctx.restore();
20188
+ cellLeft += labelWidth;
20189
+ if (level._internal_closable) {
20190
+ this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
20191
+ this._private__drawCloseButton(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: closeWidth, _internal_height: height });
20192
+ }
20193
+ return {
20194
+ _internal_level: level._internal_level,
20195
+ _internal_y: level._internal_y,
20196
+ _internal_height: height,
20197
+ _internal_pillLeft: pillLeft,
20198
+ _internal_pillRight: pillRight,
20199
+ _internal_closeLeft: cellLeft,
20200
+ _internal_closeRight: cellLeft + closeWidth,
20201
+ _internal_closable: level._internal_closable,
20202
+ };
20203
+ }
20204
+ // eslint-disable-next-line max-params
20205
+ _private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd) {
20206
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20207
+ const lineWidth = Math.max(1, Math.round(level._internal_lineWidth * vpr));
20208
+ const y = Math.round(level._internal_y * vpr) - Math.floor(lineWidth / 2);
20209
+ ctx.save();
20210
+ ctx.strokeStyle = level._internal_color;
20211
+ ctx.lineWidth = lineWidth;
20212
+ setLineStyle(ctx, level._internal_lineStyle);
20213
+ ctx.beginPath();
20214
+ ctx.moveTo(0, y);
20215
+ ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
20216
+ if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
20217
+ ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
20218
+ ctx.lineTo(Math.round(lineEnd * hpr), y);
20219
+ }
20220
+ ctx.stroke();
20221
+ ctx.restore();
20222
+ }
20223
+ // eslint-disable-next-line max-params
20224
+ _private__drawPill(scope, data, level, box) {
20225
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20226
+ const hovered = data._internal_hoveredLevel === level._internal_level;
20227
+ const borderWidth = Math.max(1, Math.round((hovered ? data._internal_borderWidth + 1 : data._internal_borderWidth) * hpr));
20228
+ ctx.save();
20229
+ ctx.setLineDash([]);
20230
+ bitmapRoundRect(ctx, box, data._internal_borderRadius, hpr, vpr);
20231
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20232
+ ctx.fill();
20233
+ ctx.lineWidth = borderWidth;
20234
+ ctx.strokeStyle = level._internal_color;
20235
+ ctx.stroke();
20236
+ ctx.restore();
20237
+ }
20238
+ // eslint-disable-next-line max-params
20239
+ _private__drawBadge(scope, data, level, box) {
20240
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20241
+ if (level._internal_badgeFilled) {
20242
+ // The badge shares the label's left corners, so its fill has to stop
20243
+ // short of the border instead of squaring it off.
20244
+ const inset = data._internal_borderWidth;
20245
+ ctx.save();
20246
+ bitmapRoundRect(ctx, { _internal_left: box._internal_left + inset, _internal_top: box._internal_top + inset, _internal_width: box._internal_width - inset, _internal_height: box._internal_height - inset * 2 }, Math.max(0, data._internal_borderRadius - inset), hpr, vpr, 'left');
20247
+ ctx.fillStyle = level._internal_color;
20248
+ ctx.fill();
20249
+ ctx.restore();
20250
+ }
20251
+ ctx.save();
20252
+ ctx.scale(hpr, vpr);
20253
+ ctx.textAlign = 'center';
20254
+ ctx.fillStyle = level._internal_badgeFilled ? data._internal_badgeTextColor : level._internal_color;
20255
+ ctx.fillText(level._internal_badge, box._internal_left + box._internal_width / 2, box._internal_top + box._internal_height / 2);
20256
+ ctx.restore();
20257
+ }
20258
+ // eslint-disable-next-line max-params
20259
+ _private__drawSeparator(ctx, color, x, top, height, hpr, vpr) {
20260
+ const width = Math.max(1, Math.floor(hpr));
20261
+ ctx.save();
20262
+ ctx.setLineDash([]);
20263
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(color, 0.4 /* Constants.SeparatorAlpha */);
20264
+ ctx.fillRect(Math.round(x * hpr), Math.round(top * vpr), width, Math.round(height * vpr));
20265
+ ctx.restore();
20266
+ }
20267
+ // eslint-disable-next-line max-params
20268
+ _private__drawCloseButton(scope, data, level, box) {
20269
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20270
+ const hovered = data._internal_hoveredLevel === level._internal_level && data._internal_hoveredPart === 'close';
20271
+ if (hovered) {
20272
+ ctx.save();
20273
+ const inset = data._internal_borderWidth;
20274
+ bitmapRoundRect(ctx, { _internal_left: box._internal_left, _internal_top: box._internal_top + inset, _internal_width: box._internal_width - inset, _internal_height: box._internal_height - inset * 2 }, Math.max(0, data._internal_borderRadius - inset), hpr, vpr, 'right');
20275
+ ctx.fillStyle = this._private__colorParser._internal_applyAlpha(level._internal_color, 0.25);
20276
+ ctx.fill();
20277
+ ctx.restore();
20278
+ }
20279
+ const centerX = (box._internal_left + box._internal_width / 2) * hpr;
20280
+ const centerY = (box._internal_top + box._internal_height / 2) * vpr;
20281
+ const arm = (7 /* Constants.CloseGlyphSize */ / 2) * hpr;
20282
+ ctx.save();
20283
+ ctx.setLineDash([]);
20284
+ ctx.lineCap = 'round';
20285
+ ctx.lineWidth = Math.max(1, Math.round(1.4 * hpr));
20286
+ ctx.strokeStyle = level._internal_color;
20287
+ ctx.beginPath();
20288
+ ctx.moveTo(centerX - arm, centerY - arm);
20289
+ ctx.lineTo(centerX + arm, centerY + arm);
20290
+ ctx.moveTo(centerX + arm, centerY - arm);
20291
+ ctx.lineTo(centerX - arm, centerY + arm);
20292
+ ctx.stroke();
20293
+ ctx.restore();
20294
+ }
20295
+ _private__drawConnector(scope, data) {
20296
+ const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
20297
+ const entry = data._internal_levels.find((level) => level._internal_level === 'entry') ?? data._internal_levels[0];
20298
+ const ys = data._internal_levels.map((level) => level._internal_y);
20299
+ const x = Math.round(this._private__connectorX * hpr);
20300
+ ctx.save();
20301
+ ctx.setLineDash([]);
20302
+ ctx.strokeStyle = entry._internal_color;
20303
+ ctx.lineWidth = Math.max(1, Math.floor(hpr));
20304
+ ctx.beginPath();
20305
+ ctx.moveTo(x, Math.round(Math.min(...ys) * vpr));
20306
+ ctx.lineTo(x, Math.round(Math.max(...ys) * vpr));
20307
+ ctx.stroke();
20308
+ for (const level of data._internal_levels) {
20309
+ ctx.beginPath();
20310
+ ctx.arc(x, Math.round(level._internal_y * vpr), 4.5 /* Constants.HandleRadius */ * hpr, 0, 2 * Math.PI);
20311
+ ctx.fillStyle = level._internal_level === 'entry'
20312
+ ? entry._internal_color
20313
+ : this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
20314
+ ctx.fill();
20315
+ ctx.lineWidth = Math.max(1, Math.round(1.5 * hpr));
20316
+ ctx.strokeStyle = level._internal_color;
20317
+ ctx.stroke();
20318
+ }
20319
+ ctx.restore();
20320
+ }
20321
+ }
20322
+ // eslint-disable-next-line max-params
20323
+ function bitmapRoundRect(ctx, box, radius, hpr, vpr, side = 'all') {
20324
+ const left = Math.round(box._internal_left * hpr);
20325
+ const top = Math.round(box._internal_top * vpr);
20326
+ const width = Math.round(box._internal_width * hpr);
20327
+ const height = Math.round(box._internal_height * vpr);
20328
+ const r = Math.min(radius * hpr, width / 2, height / 2);
20329
+ const radii = side === 'left'
20330
+ ? [r, 0, 0, r]
20331
+ : side === 'right'
20332
+ ? [0, r, r, 0]
20333
+ : [r, r, r, r];
20334
+ drawRoundRect(ctx, left, top, width, height, radii);
20335
+ }
20336
+
20337
+ class PositionLinePaneView {
20338
+ constructor() {
20339
+ this._private__renderer = new PositionLineRenderer();
20340
+ this._private__data = null;
20341
+ }
20342
+ _internal_update(data) {
20343
+ this._private__data = data;
20344
+ }
20345
+ renderer() {
20346
+ if (this._private__data === null) {
20347
+ return null;
20348
+ }
20349
+ this._private__renderer._internal_setData(this._private__data);
20350
+ return this._private__renderer;
20351
+ }
20352
+ zOrder() {
20353
+ return 'top';
20354
+ }
20355
+ _internal_hitTest(x, y) {
20356
+ return this._private__data === null ? null : this._private__renderer._internal_hitTest(x, y);
20357
+ }
20358
+ }
20359
+
20360
+ class PositionLineAxisView {
20361
+ constructor(source) {
20362
+ this._private__source = source;
20363
+ }
20364
+ coordinate() {
20365
+ // A hidden label is pushed far off the axis so it cannot reserve a gap
20366
+ // between the labels that are shown.
20367
+ return this._private__label()?._internal_coordinate ?? -1e3;
20368
+ }
20369
+ text() {
20370
+ return this._private__label()?._internal_text ?? '';
20371
+ }
20372
+ textColor() {
20373
+ return this._private__label()?._internal_textColor ?? '#FFFFFF';
20374
+ }
20375
+ backColor() {
20376
+ return this._private__label()?._internal_color ?? '#000000';
20377
+ }
20378
+ visible() {
20379
+ return this._private__label() !== null;
20380
+ }
20381
+ _private__label() {
20382
+ return this._private__source();
20383
+ }
20384
+ }
20385
+
20386
+ let nextPositionId = 0;
20387
+ const DEFAULT_RIGHT_OFFSET = 56;
20388
+ /**
20389
+ * A group of trading levels — entry, take profit and stop loss — drawn as one
20390
+ * unit: a line per level, a label carrying size and P&L, and a connector that
20391
+ * ties the three together.
20392
+ *
20393
+ * Input is not handled here. Every trading line on a chart shares a single
20394
+ * TradingLineController, which is what guarantees that one gesture moves one
20395
+ * level, and that a position line and a plain draggable price line on the same
20396
+ * chart do not fight over the pointer.
20397
+ */
20398
+ class PositionLine {
20399
+ constructor(chart, series, options) {
20400
+ this._private__paneView = new PositionLinePaneView();
20401
+ this._private__colorParser = new ColorParser([]);
20402
+ this._private__attached = false;
20403
+ this._private__hoveredLevel = null;
20404
+ this._private__hoveredPart = null;
20405
+ this._private__dragLevel = null;
20406
+ this._private__priceBeforeDrag = null;
20407
+ this._private__chart = chart;
20408
+ this._private__series = series;
20409
+ this._private__id = `pipsend-position-line-${nextPositionId++}`;
20410
+ this._private__options = options;
20411
+ this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
20412
+ this._private__paneViews = [this._private__paneView];
20413
+ this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
20414
+ this._private__series.attachPrimitive(this);
20415
+ acquireController(this._private__chart)?._internal_register(this);
20416
+ }
20417
+ // ---- ISeriesPrimitive ----
20418
+ attached(param) {
20419
+ this._private__requestUpdate = param.requestUpdate;
20420
+ this._private__attached = true;
20421
+ this.requestUpdate();
20422
+ }
20423
+ detached() {
20424
+ this._private__attached = false;
20425
+ this._private__requestUpdate = undefined;
20426
+ }
20427
+ paneViews() {
20428
+ return this._private__paneViews;
20429
+ }
20430
+ priceAxisViews() {
20431
+ return this._private__axisViews;
20432
+ }
20433
+ updateAllViews() {
20434
+ this._private__paneView._internal_update(this._private__buildRendererData());
20435
+ }
20436
+ hitTest(x, y) {
20437
+ const hit = this._private__paneView._internal_hitTest(x, y);
20438
+ if (hit === null) {
20439
+ return null;
20440
+ }
20441
+ const level = this._private__levelOptions(hit._internal_level);
20442
+ if (level === null) {
20443
+ return null;
20444
+ }
20445
+ return {
20446
+ externalId: `${this._private__id}#${hit._internal_level}:${hit._internal_part}`,
20447
+ zOrder: 'top',
20448
+ cursorStyle: hit._internal_part === 'close'
20449
+ ? 'pointer'
20450
+ : (level.draggable ?? true) ? 'ns-resize' : undefined,
20451
+ };
20452
+ }
20453
+ // ---- ControllableLine ----
20454
+ id() {
20455
+ return this._private__id;
20456
+ }
20457
+ paneIndex() {
20458
+ try {
20459
+ return this._private__series.getPane().paneIndex();
20460
+ }
20461
+ catch (error) {
20462
+ // The engine asserts the series belongs to a pane, and that lookup can
20463
+ // come back empty while panes are being rebuilt. The main pane is the
20464
+ // right guess and keeps the gesture alive instead of throwing mid-drag.
20465
+ return 0;
20466
+ }
20467
+ }
20468
+ setHoveredPart(part) {
20469
+ const [level, name] = part === null ? [null, null] : part.split(':');
20470
+ const nextLevel = level ?? null;
20471
+ const nextPart = name ?? null;
20472
+ if (this._private__hoveredLevel === nextLevel && this._private__hoveredPart === nextPart) {
20473
+ return;
20474
+ }
20475
+ this._private__hoveredLevel = nextLevel;
20476
+ this._private__hoveredPart = nextPart;
20477
+ this.requestUpdate();
20478
+ }
20479
+ setHovered(hovered) {
20480
+ if (!hovered) {
20481
+ this.setHoveredPart(null);
20482
+ }
20483
+ }
20484
+ isDraggable() {
20485
+ if (this._private__hoveredLevel === null || this._private__hoveredPart === 'close') {
20486
+ return false;
20487
+ }
20488
+ return this._private__levelOptions(this._private__hoveredLevel)?.draggable ?? true;
20489
+ }
20490
+ isClickable() {
20491
+ if (this._private__hoveredPart === 'close') {
20492
+ return true;
20493
+ }
20494
+ return this._private__options.onClick !== undefined;
20495
+ }
20496
+ priceAt(paneY) {
20497
+ const price = this._private__series.coordinateToPrice(paneY);
20498
+ return price === null ? null : price;
20499
+ }
20500
+ beginDrag() {
20501
+ this._private__dragLevel = this._private__hoveredLevel;
20502
+ if (this._private__dragLevel === null) {
20503
+ return;
20504
+ }
20505
+ const price = this._private__levelOptions(this._private__dragLevel)?.price ?? null;
20506
+ this._private__priceBeforeDrag = price;
20507
+ if (price !== null && this._private__options.onDragStart) {
20508
+ this._private__options.onDragStart(this._private__dragLevel, price);
20509
+ }
20510
+ }
20511
+ dragTo(price) {
20512
+ if (this._private__dragLevel === null) {
20513
+ return;
20514
+ }
20515
+ this._private__applyPrice(this._private__dragLevel, price);
20516
+ if (this._private__options.onDragMove) {
20517
+ this._private__options.onDragMove(this._private__dragLevel, price);
20518
+ }
20519
+ }
20520
+ commitDrag() {
20521
+ const level = this._private__dragLevel;
20522
+ this._private__dragLevel = null;
20523
+ this._private__priceBeforeDrag = null;
20524
+ if (level === null) {
20525
+ return;
20526
+ }
20527
+ const price = this._private__levelOptions(level)?.price;
20528
+ if (price !== undefined && this._private__options.onDragEnd) {
20529
+ this._private__options.onDragEnd(level, price);
20530
+ }
20531
+ }
20532
+ cancelDrag() {
20533
+ const level = this._private__dragLevel;
20534
+ const original = this._private__priceBeforeDrag;
20535
+ this._private__dragLevel = null;
20536
+ this._private__priceBeforeDrag = null;
20537
+ if (level === null) {
20538
+ return;
20539
+ }
20540
+ // Reported even when the price did not move, so a caller that pauses work
20541
+ // on `onDragStart` always gets the matching end of the gesture.
20542
+ const restored = original !== null && original !== this._private__levelOptions(level)?.price;
20543
+ if (restored) {
20544
+ this._private__applyPrice(level, original);
20545
+ if (this._private__options.onDragMove) {
20546
+ this._private__options.onDragMove(level, original);
20547
+ }
20548
+ }
20549
+ if (this._private__options.onDragCancel) {
20550
+ this._private__options.onDragCancel(level, this._private__levelOptions(level)?.price ?? 0);
20551
+ }
20552
+ }
20553
+ click() {
20554
+ const level = this._private__hoveredLevel;
20555
+ if (level === null) {
20556
+ return;
20557
+ }
20558
+ if (this._private__hoveredPart === 'close') {
20559
+ if (this._private__options.onClose) {
20560
+ this._private__options.onClose(level);
20561
+ }
20562
+ return;
20563
+ }
20564
+ const price = this._private__levelOptions(level)?.price;
20565
+ if (price !== undefined && this._private__options.onClick) {
20566
+ this._private__options.onClick(level, price);
20567
+ }
20568
+ }
20569
+ // ---- public API ----
20570
+ /**
20571
+ * Updates the group. Any subset of options can be passed; the levels given
20572
+ * are merged into the current ones, and a level set to `undefined` is
20573
+ * removed.
20574
+ */
20575
+ applyOptions(options) {
20576
+ // A level under an active gesture owns its price. Callers feeding the
20577
+ // group from a socket re-send the old price several times a second, and
20578
+ // letting that through would pull the level out from under the cursor.
20579
+ const dragged = this._private__dragLevel;
20580
+ const draggedPrice = dragged === null ? undefined : this._private__levelOptions(dragged)?.price;
20581
+ const merged = { ...this._private__options, ...options };
20582
+ merged.entry = { ...this._private__options.entry, ...options.entry };
20583
+ merged.takeProfit = mergeLevel(this._private__options.takeProfit, options, 'takeProfit');
20584
+ merged.stopLoss = mergeLevel(this._private__options.stopLoss, options, 'stopLoss');
20585
+ this._private__options = merged;
20586
+ if (dragged !== null && draggedPrice !== undefined) {
20587
+ const level = this._private__levelOptions(dragged);
20588
+ if (level !== null) {
20589
+ level.price = draggedPrice;
20590
+ }
20591
+ }
20592
+ if (options.theme) {
20593
+ // Merged onto what is already applied, so two partial theme updates do
20594
+ // not undo each other.
20595
+ this._private__theme = { ...this._private__theme, ...options.theme };
20596
+ }
20597
+ this.requestUpdate();
20598
+ }
20599
+ /**
20600
+ * Moves one level to a new price, recomputing its text when the level has a
20601
+ * `formatText`.
20602
+ *
20603
+ * Ignored for a level the user is currently dragging — the gesture owns the
20604
+ * price until it reports back through
20605
+ * {@link PositionLineOptions.onDragEnd} or
20606
+ * {@link PositionLineOptions.onDragCancel}.
20607
+ */
20608
+ setPrice(level, price) {
20609
+ if (this._private__dragLevel === level) {
20610
+ return;
20611
+ }
20612
+ this._private__applyPrice(level, price);
20613
+ }
20614
+ /**
20615
+ * Replaces one level's label text. Use this for text that changes on its own
20616
+ * — a ticking P&L on a fixed entry — rather than with the price.
20617
+ */
20618
+ setText(level, text) {
20619
+ const options = this._private__levelOptions(level);
20620
+ if (options === null) {
20621
+ return;
20622
+ }
20623
+ options.text = text;
20624
+ this.requestUpdate();
20625
+ }
20626
+ /**
20627
+ * The current price of each level that is present.
20628
+ */
20629
+ prices() {
20630
+ const result = {};
20631
+ for (const level of positionLineLevels) {
20632
+ const options = this._private__levelOptions(level);
20633
+ if (options !== null) {
20634
+ result[level] = options.price;
20635
+ }
20636
+ }
20637
+ return result;
20638
+ }
20639
+ options() {
20640
+ return this._private__options;
20641
+ }
20642
+ /**
20643
+ * Removes the group from the chart and releases its share of the input
20644
+ * controller.
20645
+ */
20646
+ remove() {
20647
+ releaseController(this._private__chart, this);
20648
+ this._private__series.detachPrimitive(this);
20649
+ }
20650
+ requestUpdate() {
20651
+ if (this._private__requestUpdate) {
20652
+ this._private__requestUpdate();
20653
+ }
20654
+ }
20655
+ /** Reads back the label the price axis should draw for a level. */
20656
+ _private__axisLabel(level) {
20657
+ const options = this._private__levelOptions(level);
20658
+ if (options === null || !isFinite(options.price) ||
20659
+ this._private__options.visible === false || options.axisLabelVisible === false) {
20660
+ return null;
20661
+ }
20662
+ const y = this._private__series.priceToCoordinate(options.price);
20663
+ if (y === null) {
20664
+ return null;
20665
+ }
20666
+ const color = options.color ?? positionLineDefaultColors[level];
20667
+ return {
20668
+ _internal_coordinate: y,
20669
+ _internal_text: this._private__series.priceFormatter().format(options.price),
20670
+ _internal_color: color,
20671
+ _internal_textColor: this._private__colorParser._internal_generateContrastColors(color)._internal_foreground,
20672
+ };
20673
+ }
20674
+ _private__levelOptions(level) {
20675
+ const options = level === 'entry' ? this._private__options.entry : this._private__options[level];
20676
+ return options ?? null;
20677
+ }
20678
+ _private__applyPrice(level, price) {
20679
+ const options = this._private__levelOptions(level);
20680
+ if (options === null) {
20681
+ return;
20682
+ }
20683
+ options.price = price;
20684
+ if (options.formatText) {
20685
+ options.text = options.formatText(price);
20686
+ }
20687
+ this.requestUpdate();
20688
+ }
20689
+ _private__buildRendererData() {
20690
+ if (!this._private__attached || this._private__options.visible === false) {
20691
+ return null;
20692
+ }
20693
+ const levels = [];
20694
+ for (const level of positionLineLevels) {
20695
+ const options = this._private__levelOptions(level);
20696
+ // A level added through a patch can be missing its price until the
20697
+ // caller supplies one; drawing it would put it at NaN.
20698
+ if (options === null || !isFinite(options.price)) {
20699
+ continue;
20700
+ }
20701
+ const y = this._private__series.priceToCoordinate(options.price);
20702
+ if (y === null) {
20703
+ continue;
20704
+ }
20705
+ const isEntry = level === 'entry';
20706
+ levels.push({
20707
+ _internal_level: level,
20708
+ _internal_y: y,
20709
+ _internal_color: options.color ?? positionLineDefaultColors[level],
20710
+ _internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
20711
+ _internal_badge: options.badge ?? '',
20712
+ _internal_badgeFilled: options.badgeFilled ?? isEntry,
20713
+ _internal_lineWidth: options.lineWidth ?? 1,
20714
+ _internal_lineStyle: options.lineStyle ?? (isEntry ? 0 /* LineStyle.Solid */ : 2 /* LineStyle.Dashed */),
20715
+ _internal_closable: options.closable ?? true,
20716
+ _internal_draggable: options.draggable ?? true,
20717
+ });
20718
+ }
20719
+ if (levels.length === 0) {
20720
+ return null;
20721
+ }
20722
+ const layout = this._private__chart.options()['layout'];
20723
+ const fontSize = this._private__theme.fontSize ?? layout.fontSize;
20724
+ return {
20725
+ _internal_levels: levels,
20726
+ _internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
20727
+ _internal_textColor: this._private__theme.textColor,
20728
+ _internal_badgeTextColor: this._private__theme.badgeTextColor,
20729
+ _internal_borderRadius: this._private__theme.borderRadius,
20730
+ _internal_borderWidth: this._private__theme.borderWidth,
20731
+ _internal_paddingX: this._private__theme.paddingX,
20732
+ _internal_paddingY: this._private__theme.paddingY,
20733
+ _internal_backgroundOpacity: this._private__theme.backgroundOpacity,
20734
+ _internal_font: makeFont(fontSize, this._private__theme.fontFamily ?? layout.fontFamily),
20735
+ _internal_fontSize: fontSize,
20736
+ _internal_connectorVisible: this._private__options.connectorVisible !== false && levels.length > 1,
20737
+ _internal_rightOffset: this._private__options.rightOffset ?? DEFAULT_RIGHT_OFFSET,
20738
+ _internal_hoveredLevel: this._private__hoveredLevel,
20739
+ _internal_hoveredPart: this._private__hoveredPart,
20740
+ };
20741
+ }
20742
+ }
20743
+ function mergeLevel(current, options, key) {
20744
+ if (!(key in options)) {
20745
+ return current;
20746
+ }
20747
+ const next = options[key];
20748
+ return next === undefined ? undefined : { ...current, ...next };
20749
+ }
20750
+ /**
20751
+ * Reads the chart's own background so the label sits on a solid surface. The
20752
+ * parameter is typed as the public {@link Background}: the production build
20753
+ * renames the members of structural types it does not see in the public API,
20754
+ * and an anonymous `{ color: string }` here resolved to nothing at runtime.
20755
+ */
20756
+ function resolveBackgroundColor(background) {
20757
+ return background.type === "solid" /* ColorType.Solid */ ? background.color : background.topColor;
20758
+ }
20759
+ /**
20760
+ * Creates a position line group: entry, take profit and stop loss drawn as one
20761
+ * interactive unit.
20762
+ *
20763
+ * @param series - The series the levels are priced against.
20764
+ * @param chart - The chart the series belongs to.
20765
+ * @param options - The levels and their style.
20766
+ *
20767
+ * @example
20768
+ * ```js
20769
+ * const position = createPositionLine(candleSeries, chart, {
20770
+ * entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
20771
+ * takeProfit: { price: 4349.58, badge: '1', text: '+ 42.79 USD' },
20772
+ * stopLoss: { price: 4266.06, badge: '1', text: '- 40.73 USD' },
20773
+ * onClose: (level) => console.log('close', level),
20774
+ * onDragEnd: (level, price) => console.log(level, 'moved to', price),
20775
+ * });
20776
+ * ```
20777
+ */
20778
+ function createPositionLine(series, chart, options) {
20779
+ return new PositionLine(chart, series, options);
20780
+ }
20781
+
19858
20782
  /** Hit parts that drag the whole object rather than a single handle. */
19859
20783
  const WHOLE_OBJECT_PARTS = new Set([
19860
20784
  'line',
@@ -20012,6 +20936,9 @@ class DrawingToolBase {
20012
20936
  get type() {
20013
20937
  return this._type;
20014
20938
  }
20939
+ isComplete() {
20940
+ return true;
20941
+ }
20015
20942
  /**
20016
20943
  * Lets a tool hand the base its own options object, once, in the constructor.
20017
20944
  *
@@ -20339,6 +21266,7 @@ class TrendLineTool extends DrawingToolBase {
20339
21266
  this._private__dragStartPoint2 = null;
20340
21267
  // Interactive mode
20341
21268
  this._private__isComplete = false;
21269
+ this._private__interactive = null;
20342
21270
  this._private__previewPoint = null;
20343
21271
  this._private__trendOptions = {
20344
21272
  lineColor: '#2962ff',
@@ -20386,7 +21314,7 @@ class TrendLineTool extends DrawingToolBase {
20386
21314
  this._private__isComplete = true;
20387
21315
  }
20388
21316
  });
20389
- // Start the interactive drawing mode
21317
+ this._private__interactive = manager;
20390
21318
  manager._internal_startDrawing();
20391
21319
  }
20392
21320
  _private__createPrimitive() {
@@ -20716,20 +21644,20 @@ class TrendLineTool extends DrawingToolBase {
20716
21644
  _update() {
20717
21645
  this._private__requestUpdate();
20718
21646
  }
21647
+ isComplete() {
21648
+ return this._private__isComplete;
21649
+ }
20719
21650
  remove() {
21651
+ if (this._private__interactive) {
21652
+ this._private__interactive._internal_destroy();
21653
+ this._private__interactive = null;
21654
+ }
20720
21655
  if (this._private__primitive) {
20721
21656
  // Detach from series
20722
21657
  this._series.detachPrimitive(this._private__primitive);
20723
21658
  // Clean up references
20724
21659
  this._private__primitive = null;
20725
21660
  this._private__paneView = null;
20726
- // Force chart update by triggering a resize
20727
- const chartElement = this._chart.chartElement?.();
20728
- if (chartElement) {
20729
- const currentWidth = chartElement.clientWidth;
20730
- const currentHeight = chartElement.clientHeight;
20731
- this._chart.resize(currentWidth, currentHeight);
20732
- }
20733
21661
  }
20734
21662
  if (this._private__trendOptions.onRemove) {
20735
21663
  this._private__trendOptions.onRemove();
@@ -21002,12 +21930,6 @@ class HorizontalLineTool extends DrawingToolBase {
21002
21930
  this._series.detachPrimitive(this._private__primitive);
21003
21931
  this._private__primitive = null;
21004
21932
  this._private__paneView = null;
21005
- const chartElement = this._chart.chartElement?.();
21006
- if (chartElement) {
21007
- const currentWidth = chartElement.clientWidth;
21008
- const currentHeight = chartElement.clientHeight;
21009
- this._chart.resize(currentWidth, currentHeight);
21010
- }
21011
21933
  }
21012
21934
  if (this._private__hLineOptions.onRemove) {
21013
21935
  this._private__hLineOptions.onRemove();
@@ -21623,6 +22545,9 @@ class PolygonTool extends DrawingToolBase {
21623
22545
  points() {
21624
22546
  return this._private__points;
21625
22547
  }
22548
+ isComplete() {
22549
+ return this._private__isComplete;
22550
+ }
21626
22551
  remove() {
21627
22552
  this._private__stopDrawing();
21628
22553
  if (this._private__primitive) {
@@ -22093,6 +23018,9 @@ class ParallelChannelTool extends DrawingToolBase {
22093
23018
  isHovered() {
22094
23019
  return this._private__isHovered;
22095
23020
  }
23021
+ isComplete() {
23022
+ return this._private__isComplete;
23023
+ }
22096
23024
  remove() {
22097
23025
  this._private__stopDrawing();
22098
23026
  if (this._private__primitive) {
@@ -22557,6 +23485,9 @@ class PitchforkTool extends DrawingToolBase {
22557
23485
  isHovered() {
22558
23486
  return this._private__isHovered;
22559
23487
  }
23488
+ isComplete() {
23489
+ return this._private__isComplete;
23490
+ }
22560
23491
  remove() {
22561
23492
  this._private__stopDrawing();
22562
23493
  if (this._private__primitive) {
@@ -22837,12 +23768,6 @@ class VerticalLineTool extends DrawingToolBase {
22837
23768
  this._series.detachPrimitive(this._private__primitive);
22838
23769
  this._private__primitive = null;
22839
23770
  this._private__paneView = null;
22840
- const chartElement = this._chart.chartElement?.();
22841
- if (chartElement) {
22842
- const currentWidth = chartElement.clientWidth;
22843
- const currentHeight = chartElement.clientHeight;
22844
- this._chart.resize(currentWidth, currentHeight);
22845
- }
22846
23771
  }
22847
23772
  if (this._private__vLineOptions.onRemove) {
22848
23773
  this._private__vLineOptions.onRemove();
@@ -23487,17 +24412,14 @@ class FibonacciTool extends DrawingToolBase {
23487
24412
  }
23488
24413
  return false;
23489
24414
  }
24415
+ isComplete() {
24416
+ return this._private__isComplete;
24417
+ }
23490
24418
  remove() {
23491
24419
  if (this._private__primitive) {
23492
24420
  this._series.detachPrimitive(this._private__primitive);
23493
24421
  this._private__primitive = null;
23494
24422
  this._private__paneView = null;
23495
- const chartElement = this._chart.chartElement?.();
23496
- if (chartElement) {
23497
- const currentWidth = chartElement.clientWidth;
23498
- const currentHeight = chartElement.clientHeight;
23499
- this._chart.resize(currentWidth, currentHeight);
23500
- }
23501
24423
  }
23502
24424
  if (this._private__drawingManager) {
23503
24425
  this._private__drawingManager._internal_destroy();
@@ -23513,12 +24435,6 @@ class FibonacciTool extends DrawingToolBase {
23513
24435
  this._series.detachPrimitive(this._private__primitive);
23514
24436
  this._private__primitive = null;
23515
24437
  this._private__paneView = null;
23516
- const chartElement = this._chart.chartElement?.();
23517
- if (chartElement) {
23518
- const currentWidth = chartElement.clientWidth;
23519
- const currentHeight = chartElement.clientHeight;
23520
- this._chart.resize(currentWidth, currentHeight);
23521
- }
23522
24438
  }
23523
24439
  if (this._private__fibOptions.onRemove) {
23524
24440
  this._private__fibOptions.onRemove();
@@ -23946,12 +24862,6 @@ class FibonacciExtensionTool extends DrawingToolBase {
23946
24862
  this._series.detachPrimitive(this._private__primitive);
23947
24863
  this._private__primitive = null;
23948
24864
  this._private__paneView = null;
23949
- const chartElement = this._chart.chartElement?.();
23950
- if (chartElement) {
23951
- const currentWidth = chartElement.clientWidth;
23952
- const currentHeight = chartElement.clientHeight;
23953
- this._chart.resize(currentWidth, currentHeight);
23954
- }
23955
24865
  }
23956
24866
  }
23957
24867
  }
@@ -24377,6 +25287,9 @@ class FibonacciFanTool extends DrawingToolBase {
24377
25287
  _update() {
24378
25288
  this._private__requestUpdate();
24379
25289
  }
25290
+ isComplete() {
25291
+ return !this._private__isDrawing;
25292
+ }
24380
25293
  remove() {
24381
25294
  if (this._private__clickHandler) {
24382
25295
  this._chart.unsubscribeClick(this._private__clickHandler);
@@ -24390,12 +25303,6 @@ class FibonacciFanTool extends DrawingToolBase {
24390
25303
  this._series.detachPrimitive(this._private__primitive);
24391
25304
  this._private__primitive = null;
24392
25305
  this._private__paneView = null;
24393
- const chartElement = this._chart.chartElement?.();
24394
- if (chartElement) {
24395
- const currentWidth = chartElement.clientWidth;
24396
- const currentHeight = chartElement.clientHeight;
24397
- this._chart.resize(currentWidth, currentHeight);
24398
- }
24399
25306
  }
24400
25307
  if (this._private__fanOptions.onRemove) {
24401
25308
  this._private__fanOptions.onRemove();
@@ -24920,6 +25827,9 @@ class RectangleTool extends DrawingToolBase {
24920
25827
  _update() {
24921
25828
  this._private__requestUpdate();
24922
25829
  }
25830
+ isComplete() {
25831
+ return !this._private__isDrawing;
25832
+ }
24923
25833
  remove() {
24924
25834
  if (this._private__clickHandler) {
24925
25835
  this._chart.unsubscribeClick(this._private__clickHandler);
@@ -24933,12 +25843,6 @@ class RectangleTool extends DrawingToolBase {
24933
25843
  this._series.detachPrimitive(this._private__primitive);
24934
25844
  this._private__primitive = null;
24935
25845
  this._private__paneView = null;
24936
- const chartElement = this._chart.chartElement?.();
24937
- if (chartElement) {
24938
- const currentWidth = chartElement.clientWidth;
24939
- const currentHeight = chartElement.clientHeight;
24940
- this._chart.resize(currentWidth, currentHeight);
24941
- }
24942
25846
  }
24943
25847
  if (this._private__rectOptions.onRemove) {
24944
25848
  this._private__rectOptions.onRemove();
@@ -25383,6 +26287,9 @@ class CircleTool extends DrawingToolBase {
25383
26287
  _update() {
25384
26288
  this._private__requestUpdate();
25385
26289
  }
26290
+ isComplete() {
26291
+ return !this._private__isDrawing;
26292
+ }
25386
26293
  remove() {
25387
26294
  if (this._private__clickHandler) {
25388
26295
  this._chart.unsubscribeClick(this._private__clickHandler);
@@ -25396,12 +26303,6 @@ class CircleTool extends DrawingToolBase {
25396
26303
  this._series.detachPrimitive(this._private__primitive);
25397
26304
  this._private__primitive = null;
25398
26305
  this._private__paneView = null;
25399
- const chartElement = this._chart.chartElement?.();
25400
- if (chartElement) {
25401
- const currentWidth = chartElement.clientWidth;
25402
- const currentHeight = chartElement.clientHeight;
25403
- this._chart.resize(currentWidth, currentHeight);
25404
- }
25405
26306
  }
25406
26307
  if (this._private__circleOptions.onRemove) {
25407
26308
  this._private__circleOptions.onRemove();
@@ -25925,6 +26826,9 @@ class ArrowTool extends DrawingToolBase {
25925
26826
  _update() {
25926
26827
  this._private__requestUpdate();
25927
26828
  }
26829
+ isComplete() {
26830
+ return !this._private__isDrawing;
26831
+ }
25928
26832
  remove() {
25929
26833
  // Clean up click handler
25930
26834
  if (this._private__clickHandler) {
@@ -25948,12 +26852,6 @@ class ArrowTool extends DrawingToolBase {
25948
26852
  this._series.detachPrimitive(this._private__primitive);
25949
26853
  this._private__primitive = null;
25950
26854
  this._private__paneView = null;
25951
- const chartElement = this._chart.chartElement?.();
25952
- if (chartElement) {
25953
- const currentWidth = chartElement.clientWidth;
25954
- const currentHeight = chartElement.clientHeight;
25955
- this._chart.resize(currentWidth, currentHeight);
25956
- }
25957
26855
  }
25958
26856
  if (this._private__arrowOptions.onRemove) {
25959
26857
  this._private__arrowOptions.onRemove();
@@ -26491,7 +27389,7 @@ class TextTool extends DrawingToolBase {
26491
27389
  }
26492
27390
  _private__setupClickOutsideHandler() {
26493
27391
  // Set up click handler to finish editing when clicking outside
26494
- this._private__clickOutsideHandler = this._chart.subscribeClick((param) => {
27392
+ const handler = (param) => {
26495
27393
  if (!this._private__isEditing)
26496
27394
  return;
26497
27395
  // Get click coordinates
@@ -26507,7 +27405,9 @@ class TextTool extends DrawingToolBase {
26507
27405
  this.remove();
26508
27406
  }
26509
27407
  }
26510
- });
27408
+ };
27409
+ this._private__clickOutsideHandler = handler;
27410
+ this._chart.subscribeClick(handler);
26511
27411
  }
26512
27412
  _private__createPrimitive() {
26513
27413
  this._private__paneView = this._private__createPaneView();
@@ -26812,12 +27712,6 @@ class TextTool extends DrawingToolBase {
26812
27712
  this._series.detachPrimitive(this._private__primitive);
26813
27713
  this._private__primitive = null;
26814
27714
  this._private__paneView = null;
26815
- const chartElement = this._chart.chartElement?.();
26816
- if (chartElement) {
26817
- const currentWidth = chartElement.clientWidth;
26818
- const currentHeight = chartElement.clientHeight;
26819
- this._chart.resize(currentWidth, currentHeight);
26820
- }
26821
27715
  }
26822
27716
  if (this._private__textOptions.onRemove) {
26823
27717
  this._private__textOptions.onRemove();
@@ -27400,12 +28294,6 @@ class RulerTool extends DrawingToolBase {
27400
28294
  this._series.detachPrimitive(this._private__primitive);
27401
28295
  this._private__primitive = null;
27402
28296
  this._private__paneView = null;
27403
- const chartElement = this._chart.chartElement?.();
27404
- if (chartElement) {
27405
- const currentWidth = chartElement.clientWidth;
27406
- const currentHeight = chartElement.clientHeight;
27407
- this._chart.resize(currentWidth, currentHeight);
27408
- }
27409
28297
  }
27410
28298
  if (this._private__drawingManager) {
27411
28299
  this._private__drawingManager._internal_destroy();
@@ -28261,12 +29149,6 @@ class PositionTool extends DrawingToolBase {
28261
29149
  this._series.detachPrimitive(this._private__primitive);
28262
29150
  this._private__primitive = null;
28263
29151
  this._private__paneView = null;
28264
- const chartElement = this._chart.chartElement?.();
28265
- if (chartElement) {
28266
- const currentWidth = chartElement.clientWidth;
28267
- const currentHeight = chartElement.clientHeight;
28268
- this._chart.resize(currentWidth, currentHeight);
28269
- }
28270
29152
  }
28271
29153
  if (this._private__positionOptions.onRemove) {
28272
29154
  this._private__positionOptions.onRemove();
@@ -28609,12 +29491,6 @@ class GanttTool extends DrawingToolBase {
28609
29491
  this._series.detachPrimitive(this._private__primitive);
28610
29492
  this._private__primitive = null;
28611
29493
  this._private__paneView = null;
28612
- const chartElement = this._chart.chartElement?.();
28613
- if (chartElement) {
28614
- const currentWidth = chartElement.clientWidth;
28615
- const currentHeight = chartElement.clientHeight;
28616
- this._chart.resize(currentWidth, currentHeight);
28617
- }
28618
29494
  }
28619
29495
  if (this._private__ganttOptions.onRemove) {
28620
29496
  this._private__ganttOptions.onRemove();
@@ -28625,6 +29501,8 @@ function createGanttTool(context, options) {
28625
29501
  return new GanttTool(context, options);
28626
29502
  }
28627
29503
 
29504
+ /** Undo steps kept, each a full snapshot of every drawing. */
29505
+ const UNDO_HISTORY_LIMIT = 100;
28628
29506
  /**
28629
29507
  * Central manager for all drawing tools - TradingView style
28630
29508
  * Handles multiple tools, selection, deletion, and keyboard shortcuts
@@ -28647,6 +29525,8 @@ class DrawingToolsManager {
28647
29525
  this._private__pointerDownHandler = null;
28648
29526
  this._private__pointerUpHandler = null;
28649
29527
  this._private__pointerCancelHandler = null;
29528
+ this._private__lostCaptureHandler = null;
29529
+ this._private__windowPointerUpHandler = null;
28650
29530
  this._private__pointerId = null;
28651
29531
  /** A press that grabbed a tool already selected it; its click must not undo that. */
28652
29532
  this._private__gestureHandled = false;
@@ -28656,6 +29536,9 @@ class DrawingToolsManager {
28656
29536
  this._private__selectionChanged = new Delegate();
28657
29537
  this._private__isDragging = false;
28658
29538
  this._private__draggedToolId = null;
29539
+ this._private__pendingToolIds = new Set();
29540
+ this._private__completionCheck = null;
29541
+ this._private__drawingCompleted = new Delegate();
28659
29542
  this._private__chart = chart;
28660
29543
  this._private__series = series;
28661
29544
  this._private__options = {
@@ -28694,9 +29577,63 @@ class DrawingToolsManager {
28694
29577
  addTool(tool, id) {
28695
29578
  const toolId = id ?? tool.id;
28696
29579
  this._private__tools.set(toolId, tool);
29580
+ if (!tool.isComplete()) {
29581
+ this._private__pendingToolIds.add(toolId);
29582
+ }
28697
29583
  this.recordChange();
28698
29584
  return toolId;
28699
29585
  }
29586
+ hasPendingDrawing() {
29587
+ return this._private__pendingToolIds.size > 0;
29588
+ }
29589
+ /** Removes every tool still waiting for points; returns how many were dropped. */
29590
+ cancelPendingDrawings() {
29591
+ if (this._private__pendingToolIds.size === 0) {
29592
+ return 0;
29593
+ }
29594
+ let cancelled = 0;
29595
+ for (const id of Array.from(this._private__pendingToolIds)) {
29596
+ const tool = this._private__tools.get(id);
29597
+ if (tool) {
29598
+ if (this._private__selectedToolId === id) {
29599
+ this._private__applySelection(null);
29600
+ }
29601
+ tool.remove();
29602
+ this._private__tools.delete(id);
29603
+ cancelled++;
29604
+ }
29605
+ }
29606
+ this._private__pendingToolIds.clear();
29607
+ if (cancelled > 0) {
29608
+ this.recordChange();
29609
+ }
29610
+ return cancelled;
29611
+ }
29612
+ subscribeDrawingCompleted(handler) {
29613
+ this._private__drawingCompleted._internal_subscribe(handler);
29614
+ }
29615
+ unsubscribeDrawingCompleted(handler) {
29616
+ this._private__drawingCompleted._internal_unsubscribe(handler);
29617
+ }
29618
+ _private__scheduleCompletionCheck() {
29619
+ if (this._private__pendingToolIds.size === 0 || this._private__completionCheck !== null) {
29620
+ return;
29621
+ }
29622
+ this._private__completionCheck = setTimeout(() => {
29623
+ this._private__completionCheck = null;
29624
+ for (const id of Array.from(this._private__pendingToolIds)) {
29625
+ const tool = this._private__tools.get(id);
29626
+ if (!tool) {
29627
+ this._private__pendingToolIds.delete(id);
29628
+ continue;
29629
+ }
29630
+ if (tool.isComplete()) {
29631
+ this._private__pendingToolIds.delete(id);
29632
+ this._private__drawingCompleted._internal_fire(id);
29633
+ }
29634
+ }
29635
+ }, 0);
29636
+ }
28700
29637
  /**
28701
29638
  * Remove a tool by ID
28702
29639
  */
@@ -28708,6 +29645,7 @@ class DrawingToolsManager {
28708
29645
  }
28709
29646
  tool.remove();
28710
29647
  this._private__tools.delete(id);
29648
+ this._private__pendingToolIds.delete(id);
28711
29649
  this.recordChange();
28712
29650
  return true;
28713
29651
  }
@@ -28754,6 +29692,9 @@ class DrawingToolsManager {
28754
29692
  return;
28755
29693
  }
28756
29694
  this._private__past.push(this._private__committed);
29695
+ if (this._private__past.length > UNDO_HISTORY_LIMIT) {
29696
+ this._private__past.shift();
29697
+ }
28757
29698
  this._private__future = [];
28758
29699
  this._private__committed = this.serializeAll();
28759
29700
  }
@@ -28907,6 +29848,7 @@ class DrawingToolsManager {
28907
29848
  this._private__applySelection(null);
28908
29849
  this._private__tools.forEach((tool) => tool.remove());
28909
29850
  this._private__tools.clear();
29851
+ this._private__pendingToolIds.clear();
28910
29852
  this.recordChange();
28911
29853
  }
28912
29854
  /**
@@ -29127,8 +30069,17 @@ class DrawingToolsManager {
29127
30069
  * Destroy the manager and cleanup
29128
30070
  */
29129
30071
  destroy() {
30072
+ if (this._private__completionCheck !== null) {
30073
+ clearTimeout(this._private__completionCheck);
30074
+ this._private__completionCheck = null;
30075
+ }
30076
+ this._private__drawingCompleted._internal_destroy();
30077
+ this._private__selectionChanged._internal_destroy();
29130
30078
  this.clearAll();
29131
30079
  this._private__detachEventHandlers();
30080
+ this._private__past = [];
30081
+ this._private__future = [];
30082
+ this._private__committed = [];
29132
30083
  }
29133
30084
  _private__attachEventHandlers() {
29134
30085
  // Click handler for selection
@@ -29146,11 +30097,17 @@ class DrawingToolsManager {
29146
30097
  const chartElement = this._private__chart.chartElement();
29147
30098
  if (chartElement) {
29148
30099
  this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
29149
- this._private__pointerUpHandler = (event) => this._private__handlePointerUp();
29150
- this._private__pointerCancelHandler = (event) => this._private__handlePointerUp();
30100
+ this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
30101
+ this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
30102
+ this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
29151
30103
  chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
29152
30104
  chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
29153
30105
  chartElement.addEventListener('pointercancel', this._private__pointerCancelHandler);
30106
+ chartElement.addEventListener('lostpointercapture', this._private__lostCaptureHandler);
30107
+ }
30108
+ if (isRunningOnClientSide) {
30109
+ this._private__windowPointerUpHandler = (event) => this._private__handlePointerUp(event);
30110
+ window.addEventListener('pointerup', this._private__windowPointerUpHandler);
29154
30111
  }
29155
30112
  // Keyboard handler for shortcuts
29156
30113
  if (this._private__options.enableKeyboardShortcuts) {
@@ -29183,6 +30140,14 @@ class DrawingToolsManager {
29183
30140
  chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
29184
30141
  this._private__pointerCancelHandler = null;
29185
30142
  }
30143
+ if (this._private__lostCaptureHandler) {
30144
+ chartElement.removeEventListener('lostpointercapture', this._private__lostCaptureHandler);
30145
+ this._private__lostCaptureHandler = null;
30146
+ }
30147
+ }
30148
+ if (this._private__windowPointerUpHandler) {
30149
+ window.removeEventListener('pointerup', this._private__windowPointerUpHandler);
30150
+ this._private__windowPointerUpHandler = null;
29186
30151
  }
29187
30152
  if (this._private__keyboardHandler) {
29188
30153
  document.removeEventListener('keydown', this._private__keyboardHandler);
@@ -29190,6 +30155,7 @@ class DrawingToolsManager {
29190
30155
  }
29191
30156
  }
29192
30157
  _private__handleClick(param) {
30158
+ this._private__scheduleCompletionCheck();
29193
30159
  if (!param.point) {
29194
30160
  return;
29195
30161
  }
@@ -29269,6 +30235,9 @@ class DrawingToolsManager {
29269
30235
  return null;
29270
30236
  }
29271
30237
  _private__handlePointerDown(event) {
30238
+ if (this._private__isDragging) {
30239
+ return;
30240
+ }
29272
30241
  // Cleared for every press, then raised only if this one grabs a drawing
29273
30242
  setInputClaim(this._private__chart, false);
29274
30243
  const chartElement = this._private__chart.chartElement();
@@ -29304,28 +30273,51 @@ class DrawingToolsManager {
29304
30273
  this._private__gestureHandled = true;
29305
30274
  this._private__draggedToolId = toolId;
29306
30275
  this._private__pointerId = event.pointerId;
29307
- chartElement.setPointerCapture(event.pointerId);
30276
+ try {
30277
+ chartElement.setPointerCapture(event.pointerId);
30278
+ }
30279
+ catch (error) {
30280
+ this._private__pointerId = null;
30281
+ }
29308
30282
  tool.startDrag(endpoint, anchor);
29309
30283
  // Panning is stopped by disabling it for the gesture, not by preventing
29310
30284
  // the event: preventDefault on pointerdown suppresses the compatibility
29311
30285
  // mouse events, and those are what drive the crosshair the drag reads.
29312
30286
  this._private__disableChartInteractions();
29313
30287
  }
29314
- _private__handlePointerUp() {
30288
+ _private__handlePointerUp(event) {
29315
30289
  if (!this._private__isDragging || this._private__draggedToolId === null) {
29316
30290
  return;
29317
30291
  }
29318
- const tool = this._private__tools.get(this._private__draggedToolId);
29319
- if (tool) {
29320
- tool.stopDrag();
30292
+ if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
30293
+ return;
29321
30294
  }
29322
- this._private__releasePointer();
29323
- this._private__enableChartInteractions();
30295
+ const draggedToolId = this._private__draggedToolId;
30296
+ // Cleared first: anything below can throw, and a half-finished release
30297
+ // would leave the manager dragging forever.
29324
30298
  this._private__isDragging = false;
29325
30299
  this._private__draggedToolId = null;
30300
+ try {
30301
+ const tool = this._private__tools.get(draggedToolId);
30302
+ if (tool) {
30303
+ tool.stopDrag();
30304
+ }
30305
+ }
30306
+ finally {
30307
+ this._private__releasePointer();
30308
+ this._private__enableChartInteractions();
30309
+ }
29326
30310
  // A finished drag is one step, not one per frame
29327
30311
  this.recordChange();
29328
30312
  }
30313
+ /** Ends the drag when capture is lost without a pointerup ever arriving. */
30314
+ _private__handleLostPointerCapture(event) {
30315
+ if (this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
30316
+ return;
30317
+ }
30318
+ this._private__pointerId = null;
30319
+ this._private__handlePointerUp();
30320
+ }
29329
30321
  /**
29330
30322
  * Pane element of a tool's series. The engine asserts the series has a pane,
29331
30323
  * so the lookup is guarded.
@@ -29372,10 +30364,15 @@ class DrawingToolsManager {
29372
30364
  this._private__pointerId = null;
29373
30365
  }
29374
30366
  _private__disableChartInteractions() {
30367
+ if (this._private__interactionsBeforeDrag !== null) {
30368
+ return;
30369
+ }
29375
30370
  const options = this._private__chart.options();
30371
+ // `options()` returns the live object and `applyOptions` merges into it
30372
+ // in place, so a snapshot by reference restores all-false.
29376
30373
  this._private__interactionsBeforeDrag = {
29377
- _internal_handleScroll: options.handleScroll,
29378
- _internal_handleScale: options.handleScale,
30374
+ _internal_handleScroll: clone(options.handleScroll),
30375
+ _internal_handleScale: clone(options.handleScale),
29379
30376
  };
29380
30377
  this._private__chart.applyOptions({
29381
30378
  handleScroll: false,
@@ -29396,6 +30393,14 @@ class DrawingToolsManager {
29396
30393
  });
29397
30394
  }
29398
30395
  _private__handleKeyboard(event) {
30396
+ const target = event.target;
30397
+ const typing = !!target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
30398
+ if (typing) {
30399
+ return;
30400
+ }
30401
+ if (event.key === 'Enter') {
30402
+ this._private__scheduleCompletionCheck();
30403
+ }
29399
30404
  // Delete or Backspace to remove selected tool
29400
30405
  if (event.key === 'Delete' || event.key === 'Backspace') {
29401
30406
  if (this.deleteSelected()) {
@@ -29416,8 +30421,8 @@ class DrawingToolsManager {
29416
30421
  }
29417
30422
  return;
29418
30423
  }
29419
- // Escape to deselect
29420
30424
  if (event.key === 'Escape') {
30425
+ this.cancelPendingDrawings();
29421
30426
  this._private__handlePointerUp();
29422
30427
  this._private__deselectAll();
29423
30428
  this.stopDrawing();
@@ -29453,7 +30458,7 @@ const customSeriesDefaultOptions = {
29453
30458
  * Returns the current version as a string. For example `'1.0.0'`.
29454
30459
  */
29455
30460
  function version() {
29456
- return "2.2.0";
30461
+ return "2.3.0-dev+202609220108";
29457
30462
  }
29458
30463
 
29459
- export { areaSeries as AreaSeries, ArrowTool, barSeries as BarSeries, baselineSeries as BaselineSeries, BrushTool, candlestickSeries as CandlestickSeries, CircleTool, ColorType, CrossLineTool, CrosshairMode, DEFAULT_SESSIONS, DraggablePriceLine, DrawingToolsManager, EXTENDED_FIBONACCI_RATIOS, FibonacciCalculator, FibonacciExtensionTool, FibonacciFanTool, FibonacciTool, GanttTool, histogramSeries as HistogramSeries, HorizontalLineTool, InteractiveLineManager, LastPriceAnimationMode, lineSeries as LineSeries, LineStyle, LineType, MismatchDirection, ParallelChannelTool, PitchforkTool, PolygonTool, PositionCalculator, PositionMarkersManager, PositionTool, PriceLineSource, PriceScaleMode, RectangleTool, RulerCalculator, RulerTool, STANDARD_FIBONACCI_RATIOS, TextTool, TickMarkType, TrackingModeExitMode, TradingSessionsIndicator, TrendLineTool, VerticalLineTool, VolumeProfileIndicator, applyADX, applyATR, applyBollingerBands, applyCCI, applyDonchianChannels, applyEMA, applyHeikinAshi, applyIchimoku, applyKeltnerChannels, applyMACD, applyMFI, applyOBV, applyParabolicSAR, applyPivotPoints, applyRSI, applySMA, applyStochastic, applySuperTrend, applyVWAP, applyVolume, applyWMA, applyWilliamsR, calculateHeikinAshi, calculatePipConfig, coordinateFromTime, createArrowTool, createBrushTool, createChart, createChartEx, createCircleTool, createCrossLineTool, createFibonacciExtensionTool, createFibonacciFanTool, createFibonacciTool, createGanttTool, createHorizontalLineTool, createImageWatermark, createInteractiveLineManager, createOptionsChart, createParallelChannelTool, createPitchforkTool, createPolygonTool, createPositionMarkers, createPositionTool, createRectangleTool, createRulerTool, createSeriesMarkers, createTextTool, createTextWatermark, createTradingLine, createTradingSessionsIndicator, createTrendLineTool, createUpDownMarkers, createVerticalLineTool, createVolumeProfileIndicator, createYieldCurveChart, customSeriesDefaultOptions, defaultHorzScaleBehavior, getPipConfig, hideSeriesPaint, isBusinessDay, isUTCTimestamp, setSeriesVolumes, setVolumeProfileData, timeFromCoordinate, version };
30464
+ export { areaSeries as AreaSeries, ArrowTool, barSeries as BarSeries, baselineSeries as BaselineSeries, BrushTool, candlestickSeries as CandlestickSeries, CircleTool, ColorType, CrossLineTool, CrosshairMode, DEFAULT_SESSIONS, DraggablePriceLine, DrawingToolsManager, EXTENDED_FIBONACCI_RATIOS, FibonacciCalculator, FibonacciExtensionTool, FibonacciFanTool, FibonacciTool, GanttTool, histogramSeries as HistogramSeries, HorizontalLineTool, InteractiveLineManager, LastPriceAnimationMode, lineSeries as LineSeries, LineStyle, LineType, MismatchDirection, ParallelChannelTool, PitchforkTool, PolygonTool, PositionCalculator, PositionLine, PositionMarkersManager, PositionTool, PriceLineSource, PriceScaleMode, RectangleTool, RulerCalculator, RulerTool, STANDARD_FIBONACCI_RATIOS, TextTool, TickMarkType, TrackingModeExitMode, TradingSessionsIndicator, TrendLineTool, VerticalLineTool, VolumeProfileIndicator, applyADX, applyATR, applyBollingerBands, applyCCI, applyDonchianChannels, applyEMA, applyHeikinAshi, applyIchimoku, applyKeltnerChannels, applyMACD, applyMFI, applyOBV, applyParabolicSAR, applyPivotPoints, applyRSI, applySMA, applyStochastic, applySuperTrend, applyVWAP, applyVolume, applyWMA, applyWilliamsR, calculateHeikinAshi, calculatePipConfig, coordinateFromTime, createArrowTool, createBrushTool, createChart, createChartEx, createCircleTool, createCrossLineTool, createFibonacciExtensionTool, createFibonacciFanTool, createFibonacciTool, createGanttTool, createHorizontalLineTool, createImageWatermark, createInteractiveLineManager, createOptionsChart, createParallelChannelTool, createPitchforkTool, createPolygonTool, createPositionLine, createPositionMarkers, createPositionTool, createRectangleTool, createRulerTool, createSeriesMarkers, createTextTool, createTextWatermark, createTradingLine, createTradingSessionsIndicator, createTrendLineTool, createUpDownMarkers, createVerticalLineTool, createVolumeProfileIndicator, createYieldCurveChart, customSeriesDefaultOptions, defaultHorzScaleBehavior, getPipConfig, hideSeriesPaint, isBusinessDay, isUTCTimestamp, setSeriesVolumes, setVolumeProfileData, timeFromCoordinate, version };