tsichart-core 2.0.0 → 2.1.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.
package/dist/index.umd.js CHANGED
@@ -21599,8 +21599,6 @@
21599
21599
  return voronoi;
21600
21600
  }
21601
21601
 
21602
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
21603
-
21604
21602
  function getDefaultExportFromCjs (x) {
21605
21603
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
21606
21604
  }
@@ -29015,7 +29013,7 @@
29015
29013
  swimLaneLabelHeightPadding: 8,
29016
29014
  labelLeftPadding: 28
29017
29015
  };
29018
- const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', ']', '}', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
29016
+ const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '}', ']', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
29019
29017
  const NONNUMERICTOPMARGIN = 8;
29020
29018
  const LINECHARTTOPPADDING = 16;
29021
29019
  const GRIDCONTAINERCLASS = 'tsi-gridContainer';
@@ -30451,35 +30449,41 @@
30451
30449
  }
30452
30450
  }
30453
30451
 
30454
- const NUMERICSPLITBYHEIGHT = 44;
30455
- const NONNUMERICSPLITBYHEIGHT = 24;
30452
+ /**
30453
+ * Constants for Legend component layout and behavior
30454
+ */
30455
+ const LEGEND_CONSTANTS = {
30456
+ /** Height in pixels for each numeric split-by item (includes type selector dropdown) */
30457
+ NUMERIC_SPLITBY_HEIGHT: 44,
30458
+ /** Height in pixels for each non-numeric (categorical/events) split-by item */
30459
+ NON_NUMERIC_SPLITBY_HEIGHT: 24,
30460
+ /** Height in pixels for the series name label header */
30461
+ NAME_LABEL_HEIGHT: 24,
30462
+ /** Buffer distance in pixels from scroll edge before triggering "load more" */
30463
+ SCROLL_BUFFER: 40,
30464
+ /** Number of split-by items to load per batch when paginating */
30465
+ BATCH_SIZE: 20,
30466
+ /** Minimum height in pixels for aggregate container */
30467
+ MIN_AGGREGATE_HEIGHT: 201,
30468
+ /** Minimum width in pixels for each series label in compact mode */
30469
+ MIN_SERIES_WIDTH: 124,
30470
+ };
30456
30471
  class Legend extends Component {
30457
30472
  constructor(drawChart, renderTarget, legendWidth) {
30458
30473
  super(renderTarget);
30459
30474
  this.renderSplitBys = (aggKey, aggSelection, dataType, noSplitBys) => {
30460
- var splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
30461
- var firstSplitBy = this.chartComponentData.displayState[aggKey].splitBys[Object.keys(this.chartComponentData.displayState[aggKey].splitBys)[0]];
30462
- var firstSplitByType = firstSplitBy ? firstSplitBy.visibleType : null;
30463
- Object.keys(this.chartComponentData.displayState[aggKey].splitBys).reduce((isSame, curr) => {
30464
- return (firstSplitByType == this.chartComponentData.displayState[aggKey].splitBys[curr].visibleType) && isSame;
30465
- }, true);
30466
- let showMoreSplitBys = () => {
30467
- const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
30468
- this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
30469
- if (oldShownSplitBys != this.chartComponentData.displayState[aggKey].shownSplitBys) {
30470
- this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30471
- }
30472
- };
30475
+ const splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
30476
+ const showMoreSplitBys = () => this.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
30473
30477
  let splitByContainer = aggSelection.selectAll(".tsi-splitByContainer").data([aggKey]);
30474
- var splitByContainerEntered = splitByContainer.enter().append("div")
30478
+ const splitByContainerEntered = splitByContainer.enter().append("div")
30475
30479
  .merge(splitByContainer)
30476
30480
  .classed("tsi-splitByContainer", true);
30477
- var splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
30481
+ const splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
30478
30482
  .data(splitByLabelData.slice(0, this.chartComponentData.displayState[aggKey].shownSplitBys), function (d) {
30479
30483
  return d;
30480
30484
  });
30481
- let self = this;
30482
- var splitByLabelsEntered = splitByLabels
30485
+ const self = this;
30486
+ const splitByLabelsEntered = splitByLabels
30483
30487
  .enter()
30484
30488
  .append("div")
30485
30489
  .merge(splitByLabels)
@@ -30493,135 +30497,60 @@
30493
30497
  }
30494
30498
  })
30495
30499
  .on("click", function (event, splitBy) {
30496
- if (self.legendState == "compact") {
30497
- self.toggleSplitByVisible(aggKey, splitBy);
30498
- }
30499
- else {
30500
- self.toggleSticky(aggKey, splitBy);
30501
- }
30502
- self.drawChart();
30500
+ self.handleSplitByClick(aggKey, splitBy);
30503
30501
  })
30504
30502
  .on("mouseover", function (event, splitBy) {
30505
30503
  event.stopPropagation();
30506
- self.labelMouseover(aggKey, splitBy);
30504
+ self.handleSplitByMouseOver(aggKey, splitBy);
30507
30505
  })
30508
30506
  .on("mouseout", function (event) {
30509
30507
  event.stopPropagation();
30510
- self.svgSelection.selectAll(".tsi-valueElement")
30511
- .attr("stroke-opacity", 1)
30512
- .attr("fill-opacity", 1);
30513
- self.labelMouseout(self.svgSelection, aggKey);
30508
+ self.handleSplitByMouseOut(aggKey);
30514
30509
  })
30515
30510
  .attr("class", (splitBy, i) => {
30516
- let compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
30517
- let shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
30518
- return `tsi-splitByLabel tsi-splitByLabel ${compact} ${shown}`;
30511
+ const compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
30512
+ const shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
30513
+ return `tsi-splitByLabel ${compact} ${shown}`;
30519
30514
  })
30520
- .classed("stickied", (splitBy, i) => {
30521
- if (self.chartComponentData.stickiedKey != null) {
30522
- return aggKey == self.chartComponentData.stickiedKey.aggregateKey && splitBy == self.chartComponentData.stickiedKey.splitBy;
30523
- }
30524
- });
30525
- var colors = Utils.createSplitByColors(self.chartComponentData.displayState, aggKey, self.chartOptions.keepSplitByColor);
30515
+ .classed("stickied", (splitBy, i) => self.isStickied(aggKey, splitBy));
30516
+ // Use helper methods to render each split-by element
30526
30517
  splitByLabelsEntered.each(function (splitBy, j) {
30527
- let color = (self.chartComponentData.isFromHeatmap) ? self.chartComponentData.displayState[aggKey].color : colors[j];
30518
+ const selection = select(this);
30519
+ // Add color key (conditionally based on data type and legend state)
30528
30520
  if (dataType === DataTypes.Numeric || noSplitBys || self.legendState === 'compact') {
30529
- let colorKey = select(this).selectAll('.tsi-colorKey').data([color]);
30530
- let colorKeyEntered = colorKey.enter()
30531
- .append("div")
30532
- .attr("class", 'tsi-colorKey')
30533
- .merge(colorKey);
30534
- if (dataType === DataTypes.Numeric) {
30535
- colorKeyEntered.style('background-color', (d) => {
30536
- return d;
30537
- });
30538
- }
30539
- else {
30540
- self.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
30541
- }
30542
- select(this).classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && this.legendState !== 'compact');
30543
- colorKey.exit().remove();
30521
+ self.addColorKey(selection, aggKey, splitBy, dataType);
30522
+ selection.classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && self.legendState !== 'compact');
30544
30523
  }
30545
30524
  else {
30546
- select(this).selectAll('.tsi-colorKey').remove();
30547
- }
30548
- if (select(this).select('.tsi-eyeIcon').empty()) {
30549
- select(this).append("button")
30550
- .attr("class", "tsi-eyeIcon")
30551
- .attr('aria-label', () => {
30552
- let showOrHide = self.chartComponentData.displayState[aggKey].splitBys[splitBy].visible ? self.getString('hide series') : self.getString('show series');
30553
- return `${showOrHide} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`;
30554
- })
30555
- .attr('title', () => self.getString('Show/Hide values'))
30556
- .on("click", function (event) {
30557
- event.stopPropagation();
30558
- self.toggleSplitByVisible(aggKey, splitBy);
30559
- select(this)
30560
- .classed("shown", Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy));
30561
- self.drawChart();
30562
- });
30563
- }
30564
- if (select(this).select('.tsi-seriesName').empty()) {
30565
- let seriesName = select(this)
30566
- .append('div')
30567
- .attr('class', 'tsi-seriesName');
30568
- Utils.appendFormattedElementsFromString(seriesName, noSplitBys ? (self.chartComponentData.displayState[aggKey].name) : splitBy);
30525
+ selection.selectAll('.tsi-colorKey').remove();
30569
30526
  }
30527
+ // Add eye icon
30528
+ self.addEyeIcon(selection, aggKey, splitBy);
30529
+ // Add series name
30530
+ self.addSeriesName(selection, aggKey, splitBy);
30531
+ // Add series type selection for numeric data
30570
30532
  if (dataType === DataTypes.Numeric) {
30571
- if (select(this).select('.tsi-seriesTypeSelection').empty()) {
30572
- select(this).append("select")
30573
- .attr('aria-label', `${self.getString("Series type selection for")} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`)
30574
- .attr('class', 'tsi-seriesTypeSelection')
30575
- .on("change", function (data) {
30576
- var seriesType = select(this).property("value");
30577
- self.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
30578
- self.drawChart();
30579
- })
30580
- .on("click", (event) => {
30581
- event.stopPropagation();
30582
- });
30583
- }
30584
- select(this).select('.tsi-seriesTypeSelection')
30585
- .each(function (d) {
30586
- var typeLabels = select(this).selectAll('option')
30587
- .data(data => self.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map((type) => {
30588
- return {
30589
- type: type,
30590
- aggKey: aggKey,
30591
- splitBy: splitBy,
30592
- visibleMeasure: Utils.getAgVisibleMeasure(self.chartComponentData.displayState, aggKey, splitBy)
30593
- };
30594
- }));
30595
- typeLabels
30596
- .enter()
30597
- .append("option")
30598
- .attr("class", "seriesTypeLabel")
30599
- .merge(typeLabels)
30600
- .property("selected", (data) => {
30601
- return ((data.type == Utils.getAgVisibleMeasure(self.chartComponentData.displayState, data.aggKey, data.splitBy)) ?
30602
- " selected" : "");
30603
- })
30604
- .text((data) => data.type);
30605
- typeLabels.exit().remove();
30606
- });
30533
+ self.addSeriesTypeSelection(selection, aggKey, splitBy);
30607
30534
  }
30608
30535
  else {
30609
- select(this).selectAll('.tsi-seriesTypeSelection').remove();
30536
+ selection.selectAll('.tsi-seriesTypeSelection').remove();
30610
30537
  }
30611
30538
  });
30612
30539
  splitByLabels.exit().remove();
30613
- let shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
30540
+ // Show more button
30541
+ const shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
30614
30542
  splitByContainerEntered.selectAll('.tsi-legendShowMore').remove();
30615
30543
  if (this.legendState === 'shown' && shouldShowMore) {
30616
30544
  splitByContainerEntered.append('button')
30617
30545
  .text(this.getString('Show more'))
30618
30546
  .attr('class', 'tsi-legendShowMore')
30619
- .style('display', (this.legendState === 'shown' && shouldShowMore) ? 'block' : 'none')
30547
+ .style('display', 'block')
30620
30548
  .on('click', showMoreSplitBys);
30621
30549
  }
30550
+ // Scroll handler for infinite scrolling
30622
30551
  splitByContainerEntered.on("scroll", function () {
30623
30552
  if (self.chartOptions.legend === 'shown') {
30624
- if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
30553
+ if (this.scrollTop + this.clientHeight + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollHeight) {
30625
30554
  showMoreSplitBys();
30626
30555
  }
30627
30556
  }
@@ -30646,10 +30575,125 @@
30646
30575
  };
30647
30576
  this.drawChart = drawChart;
30648
30577
  this.legendWidth = legendWidth;
30649
- this.legendElement = select(renderTarget).insert("div", ":first-child")
30578
+ this.legendElement = select(renderTarget)
30579
+ .insert("div", ":first-child")
30650
30580
  .attr("class", "tsi-legend")
30651
- .style("left", "0px")
30652
- .style("width", (this.legendWidth) + "px"); // - 16 for the width of the padding
30581
+ .style("left", "0px");
30582
+ // Note: width is set conditionally in draw() based on legendState
30583
+ // to allow CSS to control width in compact mode
30584
+ }
30585
+ getHeightPerSplitBy(aggKey) {
30586
+ const dataType = this.chartComponentData.displayState[aggKey].dataType;
30587
+ return dataType === DataTypes.Numeric
30588
+ ? LEGEND_CONSTANTS.NUMERIC_SPLITBY_HEIGHT
30589
+ : LEGEND_CONSTANTS.NON_NUMERIC_SPLITBY_HEIGHT;
30590
+ }
30591
+ addColorKey(selection, aggKey, splitBy, dataType) {
30592
+ const colors = Utils.createSplitByColors(this.chartComponentData.displayState, aggKey, this.chartOptions.keepSplitByColor);
30593
+ const splitByKeys = Object.keys(this.chartComponentData.timeArrays[aggKey]);
30594
+ const splitByIndex = splitByKeys.indexOf(splitBy);
30595
+ const color = this.chartComponentData.isFromHeatmap
30596
+ ? this.chartComponentData.displayState[aggKey].color
30597
+ : colors[splitByIndex];
30598
+ const colorKey = selection.selectAll('.tsi-colorKey').data([color]);
30599
+ const colorKeyEntered = colorKey.enter()
30600
+ .append('div')
30601
+ .attr('class', 'tsi-colorKey')
30602
+ .merge(colorKey);
30603
+ if (dataType === DataTypes.Numeric) {
30604
+ colorKeyEntered.style('background-color', d => d);
30605
+ }
30606
+ else {
30607
+ this.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
30608
+ }
30609
+ colorKey.exit().remove();
30610
+ }
30611
+ addEyeIcon(selection, aggKey, splitBy) {
30612
+ if (selection.select('.tsi-eyeIcon').empty()) {
30613
+ selection.append('button')
30614
+ .attr('class', 'tsi-eyeIcon')
30615
+ .attr('aria-label', () => {
30616
+ const showOrHide = this.chartComponentData.displayState[aggKey].splitBys[splitBy].visible
30617
+ ? this.getString('hide series')
30618
+ : this.getString('show series');
30619
+ return `${showOrHide} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`;
30620
+ })
30621
+ .attr('title', () => this.getString('Show/Hide values'))
30622
+ .on('click', (event) => {
30623
+ event.stopPropagation();
30624
+ this.toggleSplitByVisible(aggKey, splitBy);
30625
+ this.drawChart();
30626
+ });
30627
+ }
30628
+ selection.select('.tsi-eyeIcon')
30629
+ .classed('shown', Utils.getAgVisible(this.chartComponentData.displayState, aggKey, splitBy));
30630
+ }
30631
+ addSeriesName(selection, aggKey, splitBy) {
30632
+ if (selection.select('.tsi-seriesName').empty()) {
30633
+ const seriesName = selection.append('div')
30634
+ .attr('class', 'tsi-seriesName');
30635
+ const noSplitBys = Object.keys(this.chartComponentData.timeArrays[aggKey]).length === 1
30636
+ && Object.keys(this.chartComponentData.timeArrays[aggKey])[0] === '';
30637
+ const displayText = noSplitBys
30638
+ ? this.chartComponentData.displayState[aggKey].name
30639
+ : splitBy;
30640
+ Utils.appendFormattedElementsFromString(seriesName, displayText);
30641
+ }
30642
+ }
30643
+ addSeriesTypeSelection(selection, aggKey, splitBy) {
30644
+ if (selection.select('.tsi-seriesTypeSelection').empty()) {
30645
+ selection.append('select')
30646
+ .attr('aria-label', `${this.getString('Series type selection for')} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`)
30647
+ .attr('class', 'tsi-seriesTypeSelection')
30648
+ .on('change', (event) => {
30649
+ const seriesType = select(event.target).property('value');
30650
+ this.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
30651
+ this.drawChart();
30652
+ })
30653
+ .on('click', (event) => {
30654
+ event.stopPropagation();
30655
+ });
30656
+ }
30657
+ selection.select('.tsi-seriesTypeSelection')
30658
+ .each((d, i, nodes) => {
30659
+ const typeLabels = select(nodes[i])
30660
+ .selectAll('option')
30661
+ .data(this.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map(type => ({
30662
+ type,
30663
+ aggKey,
30664
+ splitBy,
30665
+ visibleMeasure: Utils.getAgVisibleMeasure(this.chartComponentData.displayState, aggKey, splitBy)
30666
+ })));
30667
+ typeLabels.enter()
30668
+ .append('option')
30669
+ .attr('class', 'seriesTypeLabel')
30670
+ .merge(typeLabels)
30671
+ .property('selected', (data) => data.type === Utils.getAgVisibleMeasure(this.chartComponentData.displayState, data.aggKey, data.splitBy))
30672
+ .text((data) => data.type);
30673
+ typeLabels.exit().remove();
30674
+ });
30675
+ }
30676
+ handleSplitByClick(aggKey, splitBy) {
30677
+ if (this.legendState === 'compact') {
30678
+ this.toggleSplitByVisible(aggKey, splitBy);
30679
+ }
30680
+ else {
30681
+ this.toggleSticky(aggKey, splitBy);
30682
+ }
30683
+ this.drawChart();
30684
+ }
30685
+ handleSplitByMouseOver(aggKey, splitBy) {
30686
+ this.labelMouseover(aggKey, splitBy);
30687
+ }
30688
+ handleSplitByMouseOut(aggKey) {
30689
+ this.svgSelection.selectAll(".tsi-valueElement")
30690
+ .attr("stroke-opacity", 1)
30691
+ .attr("fill-opacity", 1);
30692
+ this.labelMouseout(this.svgSelection, aggKey);
30693
+ }
30694
+ isStickied(aggKey, splitBy) {
30695
+ const stickied = this.chartComponentData.stickiedKey;
30696
+ return stickied?.aggregateKey === aggKey && stickied?.splitBy === splitBy;
30653
30697
  }
30654
30698
  labelMouseoutWrapper(labelMouseout, svgSelection, event) {
30655
30699
  return (svgSelection, aggKey) => {
@@ -30691,14 +30735,11 @@
30691
30735
  return d == aggKey;
30692
30736
  }).node();
30693
30737
  var prospectiveScrollTop = Math.max((indexOfSplitBy - 1) * this.getHeightPerSplitBy(aggKey), 0);
30694
- if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - 40) || splitByNode.scrollTop > prospectiveScrollTop) {
30738
+ if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - LEGEND_CONSTANTS.SCROLL_BUFFER) || splitByNode.scrollTop > prospectiveScrollTop) {
30695
30739
  splitByNode.scrollTop = prospectiveScrollTop;
30696
30740
  }
30697
30741
  }
30698
30742
  }
30699
- getHeightPerSplitBy(aggKey) {
30700
- return (this.chartComponentData.displayState[aggKey].dataType === DataTypes.Numeric ? NUMERICSPLITBYHEIGHT : NONNUMERICSPLITBYHEIGHT);
30701
- }
30702
30743
  createGradient(gradientKey, svg, values) {
30703
30744
  let gradient = svg.append('defs').append('linearGradient')
30704
30745
  .attr('id', gradientKey).attr('x1', '0%').attr('x2', '0%').attr('y1', '0%').attr('y2', '100%');
@@ -30717,10 +30758,6 @@
30717
30758
  .attr("stop-opacity", 1);
30718
30759
  });
30719
30760
  }
30720
- isNonNumeric(aggKey) {
30721
- let dataType = this.chartComponentData.displayState[aggKey].dataType;
30722
- return (dataType === DataTypes.Categorical || dataType === DataTypes.Events);
30723
- }
30724
30761
  createNonNumericColorKey(dataType, colorKey, aggKey) {
30725
30762
  if (dataType === DataTypes.Categorical) {
30726
30763
  this.createCategoricalColorKey(colorKey, aggKey);
@@ -30776,6 +30813,13 @@
30776
30813
  rect.attr('fill', "url(#" + gradientKey + ")");
30777
30814
  }
30778
30815
  }
30816
+ handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys) {
30817
+ const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
30818
+ this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + LEGEND_CONSTANTS.BATCH_SIZE, splitByLabelData.length);
30819
+ if (oldShownSplitBys !== this.chartComponentData.displayState[aggKey].shownSplitBys) {
30820
+ this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30821
+ }
30822
+ }
30779
30823
  draw(legendState, chartComponentData, labelMouseover, svgSelection, options, labelMouseoutAction = null, stickySeriesAction = null, event) {
30780
30824
  this.chartOptions.setOptions(options);
30781
30825
  this.chartComponentData = chartComponentData;
@@ -30790,6 +30834,13 @@
30790
30834
  legend.style('visibility', this.legendState != 'hidden')
30791
30835
  .classed('compact', this.legendState == 'compact')
30792
30836
  .classed('hidden', this.legendState == 'hidden');
30837
+ // Set width conditionally - let CSS handle compact mode width
30838
+ if (this.legendState !== 'compact') {
30839
+ legend.style('width', `${this.legendWidth}px`);
30840
+ }
30841
+ else {
30842
+ legend.style('width', null); // Remove inline width style in compact mode
30843
+ }
30793
30844
  let seriesNames = Object.keys(this.chartComponentData.displayState);
30794
30845
  var seriesLabels = legend.selectAll(".tsi-seriesLabel")
30795
30846
  .data(seriesNames, d => d);
@@ -30800,7 +30851,7 @@
30800
30851
  return "tsi-seriesLabel " + (this.chartComponentData.displayState[d]["visible"] ? " shown" : "");
30801
30852
  })
30802
30853
  .style("min-width", () => {
30803
- return Math.min(124, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
30854
+ return Math.min(LEGEND_CONSTANTS.MIN_SERIES_WIDTH, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
30804
30855
  })
30805
30856
  .style("border-color", function (d, i) {
30806
30857
  if (select(this).classed("shown"))
@@ -30808,9 +30859,8 @@
30808
30859
  return "lightgray";
30809
30860
  });
30810
30861
  var self = this;
30811
- const heightPerNameLabel = 25;
30812
30862
  const usableLegendHeight = legend.node().clientHeight;
30813
- var prospectiveAggregateHeight = Math.ceil(Math.max(201, (usableLegendHeight / seriesLabelsEntered.size())));
30863
+ var prospectiveAggregateHeight = Math.ceil(Math.max(LEGEND_CONSTANTS.MIN_AGGREGATE_HEIGHT, (usableLegendHeight / seriesLabelsEntered.size())));
30814
30864
  var contentHeight = 0;
30815
30865
  seriesLabelsEntered.each(function (aggKey, i) {
30816
30866
  let heightPerSplitBy = self.getHeightPerSplitBy(aggKey);
@@ -30866,12 +30916,12 @@
30866
30916
  seriesNameLabel.exit().remove();
30867
30917
  var splitByContainerHeight;
30868
30918
  if (splitByLabelData.length > (prospectiveAggregateHeight / heightPerSplitBy)) {
30869
- splitByContainerHeight = prospectiveAggregateHeight - heightPerNameLabel;
30870
- contentHeight += splitByContainerHeight + heightPerNameLabel;
30919
+ splitByContainerHeight = prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30920
+ contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30871
30921
  }
30872
30922
  else if (splitByLabelData.length > 1 || (splitByLabelData.length === 1 && splitByLabelData[0] !== "")) {
30873
- splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + heightPerNameLabel;
30874
- contentHeight += splitByContainerHeight + heightPerNameLabel;
30923
+ splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30924
+ contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30875
30925
  }
30876
30926
  else {
30877
30927
  splitByContainerHeight = heightPerSplitBy;
@@ -30884,43 +30934,28 @@
30884
30934
  select(this).style("height", "unset");
30885
30935
  }
30886
30936
  var splitByContainer = select(this).selectAll(".tsi-splitByContainer").data([aggKey]);
30887
- var splitByContainerEntered = splitByContainer.enter().append("div")
30937
+ splitByContainer.enter().append("div")
30888
30938
  .merge(splitByContainer)
30889
30939
  .classed("tsi-splitByContainer", true);
30890
30940
  let aggSelection = select(this);
30891
30941
  self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30892
- splitByContainerEntered.on("scroll", function () {
30893
- if (self.chartOptions.legend == "shown") {
30894
- if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
30895
- const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
30896
- self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
30897
- if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
30898
- self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30899
- }
30900
- }
30901
- }
30902
- });
30942
+ // Compact mode horizontal scroll handler
30903
30943
  select(this).on('scroll', function () {
30904
30944
  if (self.chartOptions.legend == "compact") {
30905
- if (this.scrollLeft + this.clientWidth + 40 > this.scrollWidth) {
30906
- const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
30907
- self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
30908
- if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
30909
- this.renderSplitBys(dataType);
30910
- }
30945
+ if (this.scrollLeft + this.clientWidth + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollWidth) {
30946
+ self.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
30911
30947
  }
30912
30948
  }
30913
30949
  });
30914
30950
  splitByContainer.exit().remove();
30915
30951
  });
30916
30952
  if (this.chartOptions.legend == 'shown') {
30917
- legend.node().clientHeight;
30918
30953
  //minSplitBysForFlexGrow: the minimum number of split bys for flex-grow to be triggered
30919
30954
  if (contentHeight < usableLegendHeight) {
30920
30955
  this.legendElement.classed("tsi-flexLegend", true);
30921
30956
  seriesLabelsEntered.each(function (d) {
30922
30957
  let heightPerSplitBy = self.getHeightPerSplitBy(d);
30923
- var minSplitByForFlexGrow = (prospectiveAggregateHeight - heightPerNameLabel) / heightPerSplitBy;
30958
+ var minSplitByForFlexGrow = (prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT) / heightPerSplitBy;
30924
30959
  var splitBysCount = Object.keys(self.chartComponentData.displayState[String(select(this).data()[0])].splitBys).length;
30925
30960
  if (splitBysCount > minSplitByForFlexGrow) {
30926
30961
  select(this).style("flex-grow", 1);
@@ -30933,6 +30968,12 @@
30933
30968
  }
30934
30969
  seriesLabels.exit().remove();
30935
30970
  }
30971
+ destroy() {
30972
+ this.legendElement.remove();
30973
+ // Note: Virtual list cleanup will be added when virtual scrolling is implemented
30974
+ // this.virtualLists.forEach(list => list.destroy());
30975
+ // this.virtualLists.clear();
30976
+ }
30936
30977
  }
30937
30978
 
30938
30979
  class ChartComponentData {
@@ -31579,9 +31620,9 @@
31579
31620
  // maintainable code, while at the same time manually optimizing for tiny minified file size,
31580
31621
  // browser compatibility without additional requirements
31581
31622
  // and very few assumptions about the user's page layout.
31582
- var global$1 = typeof window !== 'undefined' ? window : null;
31583
- var ssr = global$1 === null;
31584
- var document$1 = !ssr ? global$1.document : undefined;
31623
+ var global = typeof window !== 'undefined' ? window : null;
31624
+ var ssr = global === null;
31625
+ var document$1 = !ssr ? global.document : undefined;
31585
31626
 
31586
31627
  // Save a couple long function names that are used frequently.
31587
31628
  // This optimization saves around 400 bytes.
@@ -32047,11 +32088,11 @@
32047
32088
  self.dragging = false;
32048
32089
 
32049
32090
  // Remove the stored event listeners. This is why we store them.
32050
- global$1[removeEventListener]('mouseup', self.stop);
32051
- global$1[removeEventListener]('touchend', self.stop);
32052
- global$1[removeEventListener]('touchcancel', self.stop);
32053
- global$1[removeEventListener]('mousemove', self.move);
32054
- global$1[removeEventListener]('touchmove', self.move);
32091
+ global[removeEventListener]('mouseup', self.stop);
32092
+ global[removeEventListener]('touchend', self.stop);
32093
+ global[removeEventListener]('touchcancel', self.stop);
32094
+ global[removeEventListener]('mousemove', self.move);
32095
+ global[removeEventListener]('touchmove', self.move);
32055
32096
 
32056
32097
  // Clear bound function references
32057
32098
  self.stop = null;
@@ -32108,11 +32149,11 @@
32108
32149
  self.stop = stopDragging.bind(self);
32109
32150
 
32110
32151
  // All the binding. `window` gets the stop events in case we drag out of the elements.
32111
- global$1[addEventListener]('mouseup', self.stop);
32112
- global$1[addEventListener]('touchend', self.stop);
32113
- global$1[addEventListener]('touchcancel', self.stop);
32114
- global$1[addEventListener]('mousemove', self.move);
32115
- global$1[addEventListener]('touchmove', self.move);
32152
+ global[addEventListener]('mouseup', self.stop);
32153
+ global[addEventListener]('touchend', self.stop);
32154
+ global[addEventListener]('touchcancel', self.stop);
32155
+ global[addEventListener]('mousemove', self.move);
32156
+ global[addEventListener]('touchmove', self.move);
32116
32157
 
32117
32158
  // Disable selection. Disable!
32118
32159
  a[addEventListener]('selectstart', NOOP);
@@ -36749,6 +36790,8 @@
36749
36790
  .append("text")
36750
36791
  .attr("class", (d) => `tsi-swimLaneLabel-${lane} tsi-swimLaneLabel ${onClickPresentAndValid(d) ? 'tsi-boldOnHover' : ''}`)
36751
36792
  .attr("role", "heading")
36793
+ .attr("aria-roledescription", this.getString("Swimlane label"))
36794
+ .attr("aria-label", d => d.label)
36752
36795
  .attr("aria-level", "3")
36753
36796
  .merge(label)
36754
36797
  .style("text-anchor", "middle")
@@ -37290,1220 +37333,27 @@
37290
37333
  var momentExports = requireMoment();
37291
37334
  var moment = /*@__PURE__*/getDefaultExportFromCjs(momentExports);
37292
37335
 
37293
- var pikaday$1 = {exports: {}};
37294
-
37295
- /*!
37296
- * Pikaday
37297
- *
37298
- * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday
37299
- */
37300
- var pikaday = pikaday$1.exports;
37301
-
37302
- var hasRequiredPikaday;
37303
-
37304
- function requirePikaday () {
37305
- if (hasRequiredPikaday) return pikaday$1.exports;
37306
- hasRequiredPikaday = 1;
37307
- (function (module, exports) {
37308
- (function (root, factory) {
37309
-
37310
- var moment;
37311
- {
37312
- // CommonJS module
37313
- // Load moment.js as an optional dependency
37314
- try { moment = requireMoment(); } catch (e) { moment = (typeof window !== 'undefined' && window.moment) || undefined; }
37315
- module.exports = factory(moment);
37316
- }
37317
- }(typeof self !== 'undefined' ? self :
37318
- typeof window !== 'undefined' ? window :
37319
- typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
37320
- pikaday, function (moment) {
37321
-
37322
- /**
37323
- * feature detection and helper functions
37324
- */
37325
- var hasMoment = typeof moment === 'function' || (moment && typeof moment.version === 'string'),
37326
-
37327
- hasEventListeners = !!window.addEventListener,
37328
-
37329
- document = window.document,
37330
-
37331
- sto = window.setTimeout,
37332
-
37333
- addEvent = function (el, e, callback, capture) {
37334
- if (hasEventListeners) {
37335
- el.addEventListener(e, callback, !!capture);
37336
- } else {
37337
- el.attachEvent('on' + e, callback);
37338
- }
37339
- },
37340
-
37341
- removeEvent = function (el, e, callback, capture) {
37342
- if (hasEventListeners) {
37343
- el.removeEventListener(e, callback, !!capture);
37344
- } else {
37345
- el.detachEvent('on' + e, callback);
37346
- }
37347
- },
37348
-
37349
- trim = function (str) {
37350
- return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
37351
- },
37352
-
37353
- hasClass = function (el, cn) {
37354
- return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1;
37355
- },
37356
-
37357
- addClass = function (el, cn) {
37358
- if (!hasClass(el, cn)) {
37359
- el.className = (el.className === '') ? cn : el.className + ' ' + cn;
37360
- }
37361
- },
37362
-
37363
- removeClass = function (el, cn) {
37364
- el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' '));
37365
- },
37366
-
37367
- isArray = function (obj) {
37368
- return (/Array/).test(Object.prototype.toString.call(obj));
37369
- },
37370
-
37371
- isDate = function (obj) {
37372
- return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime());
37373
- },
37374
-
37375
- isWeekend = function (date) {
37376
- var day = date.getDay();
37377
- return day === 0 || day === 6;
37378
- },
37379
-
37380
- isLeapYear = function (year) {
37381
- // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
37382
- return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
37383
- },
37384
-
37385
- getDaysInMonth = function (year, month) {
37386
- return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
37387
- },
37388
-
37389
- setToStartOfDay = function (date) {
37390
- if (isDate(date)) date.setHours(0, 0, 0, 0);
37391
- },
37392
-
37393
- compareDates = function (a, b) {
37394
- // weak date comparison (use setToStartOfDay(date) to ensure correct result)
37395
- return a.getTime() === b.getTime();
37396
- },
37397
-
37398
- extend = function (to, from, overwrite) {
37399
- var prop, hasProp;
37400
- for (prop in from) {
37401
- hasProp = to[prop] !== undefined;
37402
- if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) {
37403
- if (isDate(from[prop])) {
37404
- if (overwrite) {
37405
- to[prop] = new Date(from[prop].getTime());
37406
- }
37407
- }
37408
- else if (isArray(from[prop])) {
37409
- if (overwrite) {
37410
- to[prop] = from[prop].slice(0);
37411
- }
37412
- } else {
37413
- to[prop] = extend({}, from[prop], overwrite);
37414
- }
37415
- } else if (overwrite || !hasProp) {
37416
- to[prop] = from[prop];
37417
- }
37418
- }
37419
- return to;
37420
- },
37421
-
37422
- fireEvent = function (el, eventName, data) {
37423
- var ev;
37424
-
37425
- if (document.createEvent) {
37426
- ev = document.createEvent('HTMLEvents');
37427
- ev.initEvent(eventName, true, false);
37428
- ev = extend(ev, data);
37429
- el.dispatchEvent(ev);
37430
- } else if (document.createEventObject) {
37431
- ev = document.createEventObject();
37432
- ev = extend(ev, data);
37433
- el.fireEvent('on' + eventName, ev);
37434
- }
37435
- },
37436
-
37437
- adjustCalendar = function (calendar) {
37438
- if (calendar.month < 0) {
37439
- calendar.year -= Math.ceil(Math.abs(calendar.month) / 12);
37440
- calendar.month += 12;
37441
- }
37442
- if (calendar.month > 11) {
37443
- calendar.year += Math.floor(Math.abs(calendar.month) / 12);
37444
- calendar.month -= 12;
37445
- }
37446
- return calendar;
37447
- },
37448
-
37449
- /**
37450
- * defaults and localisation
37451
- */
37452
- defaults = {
37453
-
37454
- // bind the picker to a form field
37455
- field: null,
37456
-
37457
- // automatically show/hide the picker on `field` focus (default `true` if `field` is set)
37458
- bound: undefined,
37459
-
37460
- // position of the datepicker, relative to the field (default to bottom & left)
37461
- // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position)
37462
- position: 'bottom left',
37463
-
37464
- // automatically fit in the viewport even if it means repositioning from the position option
37465
- reposition: true,
37466
-
37467
- // the default output format for `.toString()` and `field` value
37468
- format: 'YYYY-MM-DD',
37469
-
37470
- // the toString function which gets passed a current date object and format
37471
- // and returns a string
37472
- toString: null,
37473
-
37474
- // used to create date object from current input string
37475
- parse: null,
37476
-
37477
- // the initial date to view when first opened
37478
- defaultDate: null,
37479
-
37480
- // make the `defaultDate` the initial selected value
37481
- setDefaultDate: false,
37482
-
37483
- // first day of week (0: Sunday, 1: Monday etc)
37484
- firstDay: 0,
37485
-
37486
- // the default flag for moment's strict date parsing
37487
- formatStrict: false,
37488
-
37489
- // the minimum/earliest date that can be selected
37490
- minDate: null,
37491
- // the maximum/latest date that can be selected
37492
- maxDate: null,
37493
-
37494
- // number of years either side, or array of upper/lower range
37495
- yearRange: 10,
37496
-
37497
- // show week numbers at head of row
37498
- showWeekNumber: false,
37499
-
37500
- // Week picker mode
37501
- pickWholeWeek: false,
37502
-
37503
- // used internally (don't config outside)
37504
- minYear: 0,
37505
- maxYear: 9999,
37506
- minMonth: undefined,
37507
- maxMonth: undefined,
37508
-
37509
- startRange: null,
37510
- endRange: null,
37511
-
37512
- isRTL: false,
37513
-
37514
- // Additional text to append to the year in the calendar title
37515
- yearSuffix: '',
37516
-
37517
- // Render the month after year in the calendar title
37518
- showMonthAfterYear: false,
37519
-
37520
- // Render days of the calendar grid that fall in the next or previous month
37521
- showDaysInNextAndPreviousMonths: false,
37522
-
37523
- // Allows user to select days that fall in the next or previous month
37524
- enableSelectionDaysInNextAndPreviousMonths: false,
37525
-
37526
- // how many months are visible
37527
- numberOfMonths: 1,
37528
-
37529
- // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`)
37530
- // only used for the first display or when a selected date is not visible
37531
- mainCalendar: 'left',
37532
-
37533
- // Specify a DOM element to render the calendar in
37534
- container: undefined,
37535
-
37536
- // Blur field when date is selected
37537
- blurFieldOnSelect: true,
37538
-
37539
- // internationalization
37540
- i18n: {
37541
- previousMonth: 'Previous Month',
37542
- nextMonth: 'Next Month',
37543
- months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
37544
- weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
37545
- weekdaysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
37546
- },
37547
-
37548
- // Theme Classname
37549
- theme: null,
37550
-
37551
- // events array
37552
- events: [],
37553
-
37554
- // callback function
37555
- onSelect: null,
37556
- onOpen: null,
37557
- onClose: null,
37558
- onDraw: null,
37559
-
37560
- // Enable keyboard input
37561
- keyboardInput: true
37562
- },
37563
-
37564
-
37565
- /**
37566
- * templating functions to abstract HTML rendering
37567
- */
37568
- renderDayName = function (opts, day, abbr) {
37569
- day += opts.firstDay;
37570
- while (day >= 7) {
37571
- day -= 7;
37572
- }
37573
- return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
37574
- },
37575
-
37576
- renderDay = function (opts) {
37577
- var arr = [];
37578
- var ariaSelected = 'false';
37579
- if (opts.isEmpty) {
37580
- if (opts.showDaysInNextAndPreviousMonths) {
37581
- arr.push('is-outside-current-month');
37582
-
37583
- if (!opts.enableSelectionDaysInNextAndPreviousMonths) {
37584
- arr.push('is-selection-disabled');
37585
- }
37586
-
37587
- } else {
37588
- return '<td class="is-empty"></td>';
37589
- }
37590
- }
37591
- if (opts.isDisabled) {
37592
- arr.push('is-disabled');
37593
- }
37594
- if (opts.isToday) {
37595
- arr.push('is-today');
37596
- }
37597
- if (opts.isSelected) {
37598
- arr.push('is-selected');
37599
- ariaSelected = 'true';
37600
- }
37601
- if (opts.hasEvent) {
37602
- arr.push('has-event');
37603
- }
37604
- if (opts.isInRange) {
37605
- arr.push('is-inrange');
37606
- }
37607
- if (opts.isStartRange) {
37608
- arr.push('is-startrange');
37609
- }
37610
- if (opts.isEndRange) {
37611
- arr.push('is-endrange');
37612
- }
37613
- return '<td data-day="' + opts.day + '" class="' + arr.join(' ') + '" aria-selected="' + ariaSelected + '">' +
37614
- '<button tabIndex="-1" class="pika-button pika-day" type="button" ' +
37615
- 'data-pika-year="' + opts.year + '" data-pika-month="' + opts.month + '" data-pika-day="' + opts.day + '">' +
37616
- opts.day +
37617
- '</button>' +
37618
- '</td>';
37619
- },
37620
-
37621
- renderWeek = function (d, m, y) {
37622
- // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified.
37623
- var onejan = new Date(y, 0, 1),
37624
- weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay() + 1) / 7);
37625
- return '<td class="pika-week">' + weekNum + '</td>';
37626
- },
37627
-
37628
- renderRow = function (days, isRTL, pickWholeWeek, isRowSelected) {
37629
- return '<tr class="pika-row' + (pickWholeWeek ? ' pick-whole-week' : '') + (isRowSelected ? ' is-selected' : '') + '">' + (isRTL ? days.reverse() : days).join('') + '</tr>';
37630
- },
37631
-
37632
- renderBody = function (rows) {
37633
- return '<tbody>' + rows.join('') + '</tbody>';
37634
- },
37635
-
37636
- renderHead = function (opts) {
37637
- var i, arr = [];
37638
- if (opts.showWeekNumber) {
37639
- arr.push('<th></th>');
37640
- }
37641
- for (i = 0; i < 7; i++) {
37642
- arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
37643
- }
37644
- return '<thead><tr>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</tr></thead>';
37645
- },
37646
-
37647
- renderTitle = function (instance, c, year, month, refYear, randId) {
37648
- var i, j, arr,
37649
- opts = instance._o,
37650
- isMinYear = year === opts.minYear,
37651
- isMaxYear = year === opts.maxYear,
37652
- html = '<div id="' + randId + '" class="pika-title">',
37653
- monthHtml,
37654
- yearHtml,
37655
- prev = true,
37656
- next = true;
37657
-
37658
- for (arr = [], i = 0; i < 12; i++) {
37659
- arr.push('<option value="' + (year === refYear ? i - c : 12 + i - c) + '"' +
37660
- (i === month ? ' selected="selected"' : '') +
37661
- ((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled="disabled"' : '') + '>' +
37662
- opts.i18n.months[i] + '</option>');
37663
- }
37664
-
37665
- monthHtml = '<div class="pika-label">' + opts.i18n.months[month] + '<select aria-label="select month" class="pika-select pika-select-month" tabindex="-1">' + arr.join('') + '</select></div>';
37666
-
37667
- if (isArray(opts.yearRange)) {
37668
- i = opts.yearRange[0];
37669
- j = opts.yearRange[1] + 1;
37670
- } else {
37671
- i = year - opts.yearRange;
37672
- j = 1 + year + opts.yearRange;
37673
- }
37674
-
37675
- for (arr = []; i < j && i <= opts.maxYear; i++) {
37676
- if (i >= opts.minYear) {
37677
- arr.push('<option value="' + i + '"' + (i === year ? ' selected="selected"' : '') + '>' + (i) + '</option>');
37678
- }
37679
- }
37680
- yearHtml = '<div class="pika-label">' + year + opts.yearSuffix + '<select aria-label="select year" class="pika-select pika-select-year" tabindex="-1">' + arr.join('') + '</select></div>';
37681
-
37682
- if (opts.showMonthAfterYear) {
37683
- html += yearHtml + monthHtml;
37684
- } else {
37685
- html += monthHtml + yearHtml;
37686
- }
37687
-
37688
- if (isMinYear && (month === 0 || opts.minMonth >= month)) {
37689
- prev = false;
37690
- }
37691
-
37692
- if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
37693
- next = false;
37694
- }
37695
-
37696
- if (c === 0) {
37697
- html += '<button tabIndex="-1" class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
37698
- }
37699
- if (c === (instance._o.numberOfMonths - 1)) {
37700
- html += '<button tabIndex="-1" class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
37701
- }
37702
-
37703
- return html += '</div>';
37704
- },
37705
-
37706
- renderTable = function (opts, data, randId) {
37707
- return '<table cellpadding="0" cellspacing="0" class="pika-table" role="grid" aria-labelledby="' + randId + '">' + renderHead(opts) + renderBody(data) + '</table>';
37708
- },
37709
-
37710
-
37711
- /**
37712
- * Pikaday constructor
37713
- */
37714
- Pikaday = function (options) {
37715
- var self = this,
37716
- opts = self.config(options);
37717
-
37718
- self._onMouseDown = function (e) {
37719
- if (!self._v) {
37720
- return;
37721
- }
37722
- e = e || window.event;
37723
- var target = e.target || e.srcElement;
37724
- if (!target) {
37725
- return;
37726
- }
37727
-
37728
- if (!hasClass(target, 'is-disabled')) {
37729
- if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) {
37730
- self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day')));
37731
- if (opts.bound) {
37732
- sto(function () {
37733
- self.hide();
37734
- if (opts.blurFieldOnSelect && opts.field) {
37735
- opts.field.blur();
37736
- }
37737
- }, 100);
37738
- }
37739
- }
37740
- else if (hasClass(target, 'pika-prev')) {
37741
- self.prevMonth();
37742
- }
37743
- else if (hasClass(target, 'pika-next')) {
37744
- self.nextMonth();
37745
- }
37746
- }
37747
- if (!hasClass(target, 'pika-select')) {
37748
- // if this is touch event prevent mouse events emulation
37749
- if (e.preventDefault) {
37750
- e.preventDefault();
37751
- } else {
37752
- e.returnValue = false;
37753
- return false;
37754
- }
37755
- } else {
37756
- self._c = true;
37757
- }
37758
- };
37759
-
37760
- self._onChange = function (e) {
37761
- e = e || window.event;
37762
- var target = e.target || e.srcElement;
37763
- if (!target) {
37764
- return;
37765
- }
37766
- if (hasClass(target, 'pika-select-month')) {
37767
- self.gotoMonth(target.value);
37768
- }
37769
- else if (hasClass(target, 'pika-select-year')) {
37770
- self.gotoYear(target.value);
37771
- }
37772
- };
37773
-
37774
- self._onKeyChange = function (e) {
37775
- e = e || window.event;
37776
- // ignore if event comes from input box
37777
- if (self.isVisible() && e.target && e.target.type !== 'text') {
37778
-
37779
- switch (e.keyCode) {
37780
- case 13:
37781
- case 27:
37782
- if (opts.field) {
37783
- opts.field.blur();
37784
- }
37785
- break;
37786
- case 37:
37787
- e.preventDefault();
37788
- self.adjustDate('subtract', 1);
37789
- break;
37790
- case 38:
37791
- self.adjustDate('subtract', 7);
37792
- break;
37793
- case 39:
37794
- self.adjustDate('add', 1);
37795
- break;
37796
- case 40:
37797
- self.adjustDate('add', 7);
37798
- break;
37799
- }
37800
- }
37801
- };
37802
-
37803
- self._onInputChange = function (e) {
37804
- var date;
37805
-
37806
- if (e.firedBy === self) {
37807
- return;
37808
- }
37809
- if (opts.parse) {
37810
- date = opts.parse(opts.field.value, opts.format);
37811
- } else if (hasMoment) {
37812
- date = moment(opts.field.value, opts.format, opts.formatStrict);
37813
- date = (date && date.isValid()) ? date.toDate() : null;
37814
- }
37815
- else {
37816
- date = new Date(Date.parse(opts.field.value));
37817
- }
37818
- // if (isDate(date)) {
37819
- // self.setDate(date);
37820
- // }
37821
- // if (!self._v) {
37822
- // self.show();
37823
- // }
37824
- };
37825
-
37826
- self._onInputFocus = function () {
37827
- self.show();
37828
- };
37829
-
37830
- self._onInputClick = function () {
37831
- self.show();
37832
- };
37833
-
37834
- self._onInputBlur = function () {
37835
- // IE allows pika div to gain focus; catch blur the input field
37836
- var pEl = document.activeElement;
37837
- do {
37838
- if (hasClass(pEl, 'pika-single')) {
37839
- return;
37840
- }
37841
- }
37842
- while ((pEl = pEl.parentNode));
37843
-
37844
- if (!self._c) {
37845
- self._b = sto(function () {
37846
- self.hide();
37847
- }, 50);
37848
- }
37849
- self._c = false;
37850
- };
37851
-
37852
- self._onClick = function (e) {
37853
- e = e || window.event;
37854
- var target = e.target || e.srcElement,
37855
- pEl = target;
37856
- if (!target) {
37857
- return;
37858
- }
37859
- if (!hasEventListeners && hasClass(target, 'pika-select')) {
37860
- if (!target.onchange) {
37861
- target.setAttribute('onchange', 'return;');
37862
- addEvent(target, 'change', self._onChange);
37863
- }
37864
- }
37865
- do {
37866
- if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) {
37867
- return;
37868
- }
37869
- }
37870
- while ((pEl = pEl.parentNode));
37871
- if (self._v && target !== opts.trigger && pEl !== opts.trigger) {
37872
- self.hide();
37873
- }
37874
- };
37875
-
37876
- self.el = document.createElement('div');
37877
- self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : '');
37878
-
37879
- addEvent(self.el, 'mousedown', self._onMouseDown, true);
37880
- addEvent(self.el, 'touchend', self._onMouseDown, true);
37881
- addEvent(self.el, 'change', self._onChange);
37882
-
37883
- if (opts.keyboardInput) {
37884
- addEvent(document, 'keydown', self._onKeyChange);
37885
- }
37886
-
37887
- if (opts.field) {
37888
- if (opts.container) {
37889
- opts.container.appendChild(self.el);
37890
- } else if (opts.bound) {
37891
- document.body.appendChild(self.el);
37892
- } else {
37893
- opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
37894
- }
37895
- addEvent(opts.field, 'change', self._onInputChange);
37896
-
37897
- if (!opts.defaultDate) {
37898
- if (hasMoment && opts.field.value) {
37899
- opts.defaultDate = moment(opts.field.value, opts.format).toDate();
37900
- } else {
37901
- opts.defaultDate = new Date(Date.parse(opts.field.value));
37902
- }
37903
- opts.setDefaultDate = true;
37904
- }
37905
- }
37906
-
37907
- var defDate = opts.defaultDate;
37908
-
37909
- if (isDate(defDate)) {
37910
- if (opts.setDefaultDate) {
37911
- self.setDate(defDate, true);
37912
- } else {
37913
- self.gotoDate(defDate);
37914
- }
37915
- } else {
37916
- self.gotoDate(new Date());
37917
- }
37918
-
37919
- if (opts.bound) {
37920
- this.hide();
37921
- self.el.className += ' is-bound';
37922
- addEvent(opts.trigger, 'click', self._onInputClick);
37923
- addEvent(opts.trigger, 'focus', self._onInputFocus);
37924
- addEvent(opts.trigger, 'blur', self._onInputBlur);
37925
- } else {
37926
- this.show();
37927
- }
37928
- };
37929
-
37930
-
37931
- /**
37932
- * public Pikaday API
37933
- */
37934
- Pikaday.prototype = {
37935
-
37936
-
37937
- /**
37938
- * configure functionality
37939
- */
37940
- config: function (options) {
37941
- if (!this._o) {
37942
- this._o = extend({}, defaults, true);
37943
- }
37944
-
37945
- var opts = extend(this._o, options, true);
37946
-
37947
- opts.isRTL = !!opts.isRTL;
37948
-
37949
- opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
37950
-
37951
- opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null;
37952
-
37953
- opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
37954
-
37955
- opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field;
37956
-
37957
- opts.disableWeekends = !!opts.disableWeekends;
37958
-
37959
- opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null;
37960
-
37961
- var nom = parseInt(opts.numberOfMonths, 10) || 1;
37962
- opts.numberOfMonths = nom > 4 ? 4 : nom;
37963
-
37964
- if (!isDate(opts.minDate)) {
37965
- opts.minDate = false;
37966
- }
37967
- if (!isDate(opts.maxDate)) {
37968
- opts.maxDate = false;
37969
- }
37970
- if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
37971
- opts.maxDate = opts.minDate = false;
37972
- }
37973
- if (opts.minDate) {
37974
- this.setMinDate(opts.minDate);
37975
- }
37976
- if (opts.maxDate) {
37977
- this.setMaxDate(opts.maxDate);
37978
- }
37979
-
37980
- if (isArray(opts.yearRange)) {
37981
- var fallback = new Date().getFullYear() - 10;
37982
- opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
37983
- opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
37984
- } else {
37985
- opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
37986
- if (opts.yearRange > 100) {
37987
- opts.yearRange = 100;
37988
- }
37989
- }
37990
-
37991
- return opts;
37992
- },
37993
-
37994
- /**
37995
- * return a formatted string of the current selection (using Moment.js if available)
37996
- */
37997
- toString: function (format) {
37998
- format = format || this._o.format;
37999
- if (!isDate(this._d)) {
38000
- return '';
38001
- }
38002
- if (this._o.toString) {
38003
- return this._o.toString(this._d, format);
38004
- }
38005
- if (hasMoment) {
38006
- return moment(this._d).format(format);
38007
- }
38008
- return this._d.toDateString();
38009
- },
38010
-
38011
- /**
38012
- * return a Moment.js object of the current selection (if available)
38013
- */
38014
- getMoment: function () {
38015
- return hasMoment ? moment(this._d) : null;
38016
- },
38017
-
38018
- /**
38019
- * set the current selection from a Moment.js object (if available)
38020
- */
38021
- setMoment: function (date, preventOnSelect) {
38022
- if (hasMoment && moment.isMoment(date)) {
38023
- this.setDate(date.toDate(), preventOnSelect);
38024
- }
38025
- },
38026
-
38027
- /**
38028
- * return a Date object of the current selection
38029
- */
38030
- getDate: function () {
38031
- return isDate(this._d) ? new Date(this._d.getTime()) : null;
38032
- },
38033
-
38034
- /**
38035
- * set the current selection
38036
- */
38037
- setDate: function (date, preventOnSelect) {
38038
- if (!date) {
38039
- this._d = null;
38040
-
38041
- if (this._o.field) {
38042
- this._o.field.value = '';
38043
- fireEvent(this._o.field, 'change', { firedBy: this });
38044
- }
38045
-
38046
- return this.draw();
38047
- }
38048
- if (typeof date === 'string') {
38049
- date = new Date(Date.parse(date));
38050
- }
38051
- if (!isDate(date)) {
38052
- return;
38053
- }
38054
-
38055
- var min = this._o.minDate,
38056
- max = this._o.maxDate;
38057
-
38058
- if (isDate(min) && date < min) {
38059
- date = min;
38060
- } else if (isDate(max) && date > max) {
38061
- date = max;
38062
- }
38063
-
38064
- this._d = new Date(date.getTime());
38065
- setToStartOfDay(this._d);
38066
- this.gotoDate(this._d);
38067
-
38068
- if (this._o.field) {
38069
- this._o.field.value = this.toString();
38070
- fireEvent(this._o.field, 'change', { firedBy: this });
38071
- }
38072
- if (!preventOnSelect && typeof this._o.onSelect === 'function') {
38073
- this._o.onSelect.call(this, this.getDate());
38074
- }
38075
- },
38076
-
38077
- /**
38078
- * change view to a specific date
38079
- */
38080
- gotoDate: function (date) {
38081
- var newCalendar = true;
38082
-
38083
- if (!isDate(date)) {
38084
- return;
38085
- }
38086
-
38087
- if (this.calendars) {
38088
- var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1),
38089
- lastVisibleDate = new Date(this.calendars[this.calendars.length - 1].year, this.calendars[this.calendars.length - 1].month, 1),
38090
- visibleDate = date.getTime();
38091
- // get the end of the month
38092
- lastVisibleDate.setMonth(lastVisibleDate.getMonth() + 1);
38093
- lastVisibleDate.setDate(lastVisibleDate.getDate() - 1);
38094
- newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate);
38095
- }
38096
-
38097
- if (newCalendar) {
38098
- this.calendars = [{
38099
- month: date.getMonth(),
38100
- year: date.getFullYear()
38101
- }];
38102
- if (this._o.mainCalendar === 'right') {
38103
- this.calendars[0].month += 1 - this._o.numberOfMonths;
38104
- }
38105
- }
38106
-
38107
- this.adjustCalendars();
38108
- },
38109
-
38110
- adjustDate: function (sign, days) {
38111
-
38112
- var day = this.getDate() || new Date();
38113
- var difference = parseInt(days) * 24 * 60 * 60 * 1000;
38114
-
38115
- var newDay;
38116
-
38117
- if (sign === 'add') {
38118
- newDay = new Date(day.valueOf() + difference);
38119
- } else if (sign === 'subtract') {
38120
- newDay = new Date(day.valueOf() - difference);
38121
- }
38122
-
38123
- this.setDate(newDay);
38124
- },
38125
-
38126
- adjustCalendars: function () {
38127
- this.calendars[0] = adjustCalendar(this.calendars[0]);
38128
- for (var c = 1; c < this._o.numberOfMonths; c++) {
38129
- this.calendars[c] = adjustCalendar({
38130
- month: this.calendars[0].month + c,
38131
- year: this.calendars[0].year
38132
- });
38133
- }
38134
- this.draw();
38135
- },
38136
-
38137
- gotoToday: function () {
38138
- this.gotoDate(new Date());
38139
- },
38140
-
38141
- /**
38142
- * change view to a specific month (zero-index, e.g. 0: January)
38143
- */
38144
- gotoMonth: function (month) {
38145
- if (!isNaN(month)) {
38146
- this.calendars[0].month = parseInt(month, 10);
38147
- this.adjustCalendars();
38148
- }
38149
- },
38150
-
38151
- nextMonth: function () {
38152
- this.calendars[0].month++;
38153
- this.adjustCalendars();
38154
- },
38155
-
38156
- prevMonth: function () {
38157
- this.calendars[0].month--;
38158
- this.adjustCalendars();
38159
- },
38160
-
38161
- /**
38162
- * change view to a specific full year (e.g. "2012")
38163
- */
38164
- gotoYear: function (year) {
38165
- if (!isNaN(year)) {
38166
- this.calendars[0].year = parseInt(year, 10);
38167
- this.adjustCalendars();
38168
- }
38169
- },
38170
-
38171
- /**
38172
- * change the minDate
38173
- */
38174
- setMinDate: function (value) {
38175
- if (value instanceof Date) {
38176
- setToStartOfDay(value);
38177
- this._o.minDate = value;
38178
- this._o.minYear = value.getFullYear();
38179
- this._o.minMonth = value.getMonth();
38180
- } else {
38181
- this._o.minDate = defaults.minDate;
38182
- this._o.minYear = defaults.minYear;
38183
- this._o.minMonth = defaults.minMonth;
38184
- this._o.startRange = defaults.startRange;
38185
- }
38186
-
38187
- this.draw();
38188
- },
38189
-
38190
- /**
38191
- * change the maxDate
38192
- */
38193
- setMaxDate: function (value) {
38194
- if (value instanceof Date) {
38195
- setToStartOfDay(value);
38196
- this._o.maxDate = value;
38197
- this._o.maxYear = value.getFullYear();
38198
- this._o.maxMonth = value.getMonth();
38199
- } else {
38200
- this._o.maxDate = defaults.maxDate;
38201
- this._o.maxYear = defaults.maxYear;
38202
- this._o.maxMonth = defaults.maxMonth;
38203
- this._o.endRange = defaults.endRange;
38204
- }
38205
-
38206
- this.draw();
38207
- },
38208
-
38209
- setStartRange: function (value) {
38210
- this._o.startRange = value;
38211
- },
38212
-
38213
- setEndRange: function (value) {
38214
- this._o.endRange = value;
38215
- },
38216
-
38217
- /**
38218
- * refresh the HTML
38219
- */
38220
- draw: function (force) {
38221
- if (!this._v && !force) {
38222
- return;
38223
- }
38224
- var opts = this._o,
38225
- minYear = opts.minYear,
38226
- maxYear = opts.maxYear,
38227
- minMonth = opts.minMonth,
38228
- maxMonth = opts.maxMonth,
38229
- html = '',
38230
- randId;
38231
-
38232
- if (this._y <= minYear) {
38233
- this._y = minYear;
38234
- if (!isNaN(minMonth) && this._m < minMonth) {
38235
- this._m = minMonth;
38236
- }
38237
- }
38238
- if (this._y >= maxYear) {
38239
- this._y = maxYear;
38240
- if (!isNaN(maxMonth) && this._m > maxMonth) {
38241
- this._m = maxMonth;
38242
- }
38243
- }
38244
-
38245
- for (var c = 0; c < opts.numberOfMonths; c++) {
38246
- randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2);
38247
- html += '<div class="pika-lendar">' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId) + '</div>';
38248
- }
38249
-
38250
- this.el.innerHTML = html;
38251
-
38252
- if (opts.bound) {
38253
- if (opts.field.type !== 'hidden') {
38254
- sto(function () {
38255
- opts.trigger.focus();
38256
- }, 1);
38257
- }
38258
- }
38259
-
38260
- if (typeof this._o.onDraw === 'function') {
38261
- this._o.onDraw(this);
38262
- }
38263
-
38264
- if (opts.bound) {
38265
- // let the screen reader user know to use arrow keys
38266
- opts.field.setAttribute('aria-label', 'Use the arrow keys to pick a date');
38267
- }
38268
- },
38269
-
38270
- adjustPosition: function () {
38271
- var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect;
38272
-
38273
- if (this._o.container) return;
38274
-
38275
- this.el.style.position = 'absolute';
38276
-
38277
- field = this._o.trigger;
38278
- pEl = field;
38279
- width = this.el.offsetWidth;
38280
- height = this.el.offsetHeight;
38281
- viewportWidth = window.innerWidth || document.documentElement.clientWidth;
38282
- viewportHeight = window.innerHeight || document.documentElement.clientHeight;
38283
- scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
38284
-
38285
- if (typeof field.getBoundingClientRect === 'function') {
38286
- clientRect = field.getBoundingClientRect();
38287
- left = clientRect.left + window.pageXOffset;
38288
- top = clientRect.bottom + window.pageYOffset;
38289
- } else {
38290
- left = pEl.offsetLeft;
38291
- top = pEl.offsetTop + pEl.offsetHeight;
38292
- while ((pEl = pEl.offsetParent)) {
38293
- left += pEl.offsetLeft;
38294
- top += pEl.offsetTop;
38295
- }
38296
- }
38297
-
38298
- // default position is bottom & left
38299
- if ((this._o.reposition && left + width > viewportWidth) ||
38300
- (
38301
- this._o.position.indexOf('right') > -1 &&
38302
- left - width + field.offsetWidth > 0
38303
- )
38304
- ) {
38305
- left = left - width + field.offsetWidth;
38306
- }
38307
- if ((this._o.reposition && top + height > viewportHeight + scrollTop) ||
38308
- (
38309
- this._o.position.indexOf('top') > -1 &&
38310
- top - height - field.offsetHeight > 0
38311
- )
38312
- ) {
38313
- top = top - height - field.offsetHeight;
38314
- }
38315
-
38316
- this.el.style.left = left + 'px';
38317
- this.el.style.top = top + 'px';
38318
- },
38319
-
38320
- /**
38321
- * render HTML for a particular month
38322
- */
38323
- render: function (year, month, randId) {
38324
- var opts = this._o,
38325
- now = new Date(),
38326
- days = getDaysInMonth(year, month),
38327
- before = new Date(year, month, 1).getDay(),
38328
- data = [],
38329
- row = [];
38330
- setToStartOfDay(now);
38331
- if (opts.firstDay > 0) {
38332
- before -= opts.firstDay;
38333
- if (before < 0) {
38334
- before += 7;
38335
- }
38336
- }
38337
- var previousMonth = month === 0 ? 11 : month - 1,
38338
- nextMonth = month === 11 ? 0 : month + 1,
38339
- yearOfPreviousMonth = month === 0 ? year - 1 : year,
38340
- yearOfNextMonth = month === 11 ? year + 1 : year,
38341
- daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth);
38342
- var cells = days + before,
38343
- after = cells;
38344
- while (after > 7) {
38345
- after -= 7;
38346
- }
38347
- cells += 7 - after;
38348
- var isWeekSelected = false;
38349
- for (var i = 0, r = 0; i < cells; i++) {
38350
- var day = new Date(year, month, 1 + (i - before)),
38351
- isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
38352
- isToday = compareDates(day, now),
38353
- hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false,
38354
- isEmpty = i < before || i >= (days + before),
38355
- dayNumber = 1 + (i - before),
38356
- monthNumber = month,
38357
- yearNumber = year,
38358
- isStartRange = opts.startRange && compareDates(opts.startRange, day),
38359
- isEndRange = opts.endRange && compareDates(opts.endRange, day),
38360
- isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange,
38361
- isDisabled = (opts.minDate && day < opts.minDate) ||
38362
- (opts.maxDate && day > opts.maxDate) ||
38363
- (opts.disableWeekends && isWeekend(day)) ||
38364
- (opts.disableDayFn && opts.disableDayFn(day));
38365
-
38366
- if (isEmpty) {
38367
- if (i < before) {
38368
- dayNumber = daysInPreviousMonth + dayNumber;
38369
- monthNumber = previousMonth;
38370
- yearNumber = yearOfPreviousMonth;
38371
- } else {
38372
- dayNumber = dayNumber - days;
38373
- monthNumber = nextMonth;
38374
- yearNumber = yearOfNextMonth;
38375
- }
38376
- }
38377
-
38378
- var dayConfig = {
38379
- day: dayNumber,
38380
- month: monthNumber,
38381
- year: yearNumber,
38382
- hasEvent: hasEvent,
38383
- isSelected: isSelected,
38384
- isToday: isToday,
38385
- isDisabled: isDisabled,
38386
- isEmpty: isEmpty,
38387
- isStartRange: isStartRange,
38388
- isEndRange: isEndRange,
38389
- isInRange: isInRange,
38390
- showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths,
38391
- enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths
38392
- };
38393
-
38394
- if (opts.pickWholeWeek && isSelected) {
38395
- isWeekSelected = true;
38396
- }
38397
-
38398
- row.push(renderDay(dayConfig));
38399
-
38400
- if (++r === 7) {
38401
- if (opts.showWeekNumber) {
38402
- row.unshift(renderWeek(i - before, month, year));
38403
- }
38404
- data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected));
38405
- row = [];
38406
- r = 0;
38407
- isWeekSelected = false;
38408
- }
38409
- }
38410
- return renderTable(opts, data, randId);
38411
- },
38412
-
38413
- isVisible: function () {
38414
- return this._v;
38415
- },
38416
-
38417
- show: function () {
38418
- if (!this.isVisible()) {
38419
- this._v = true;
38420
- this.draw();
38421
- removeClass(this.el, 'is-hidden');
38422
- if (this._o.bound) {
38423
- addEvent(document, 'click', this._onClick);
38424
- this.adjustPosition();
38425
- }
38426
- if (typeof this._o.onOpen === 'function') {
38427
- this._o.onOpen.call(this);
38428
- }
38429
- }
38430
- },
38431
-
38432
- hide: function () {
38433
- var v = this._v;
38434
- if (v !== false) {
38435
- if (this._o.bound) {
38436
- removeEvent(document, 'click', this._onClick);
38437
- }
38438
- this.el.style.position = 'static'; // reset
38439
- this.el.style.left = 'auto';
38440
- this.el.style.top = 'auto';
38441
- addClass(this.el, 'is-hidden');
38442
- this._v = false;
38443
- if (v !== undefined && typeof this._o.onClose === 'function') {
38444
- this._o.onClose.call(this);
38445
- }
38446
- }
38447
- },
38448
-
38449
- /**
38450
- * GAME OVER
38451
- */
38452
- destroy: function () {
38453
- var opts = this._o;
38454
-
38455
- this.hide();
38456
- removeEvent(this.el, 'mousedown', this._onMouseDown, true);
38457
- removeEvent(this.el, 'touchend', this._onMouseDown, true);
38458
- removeEvent(this.el, 'change', this._onChange);
38459
- if (opts.keyboardInput) {
38460
- removeEvent(document, 'keydown', this._onKeyChange);
38461
- }
38462
- if (opts.field) {
38463
- removeEvent(opts.field, 'change', this._onInputChange);
38464
- if (opts.bound) {
38465
- removeEvent(opts.trigger, 'click', this._onInputClick);
38466
- removeEvent(opts.trigger, 'focus', this._onInputFocus);
38467
- removeEvent(opts.trigger, 'blur', this._onInputBlur);
38468
- }
38469
- }
38470
- if (this.el.parentNode) {
38471
- this.el.parentNode.removeChild(this.el);
38472
- }
38473
- }
38474
-
38475
- };
38476
-
38477
- return Pikaday;
38478
- }));
38479
- } (pikaday$1));
38480
- return pikaday$1.exports;
38481
- }
38482
-
38483
- var pikadayExports = /*@__PURE__*/ requirePikaday();
38484
- var Pikaday = /*@__PURE__*/getDefaultExportFromCjs(pikadayExports);
38485
-
38486
- // Ensure moment is available globally for Pikaday
38487
- if (typeof window !== 'undefined') {
38488
- window.moment = moment;
38489
- }
38490
- // Export a function to safely create Pikaday instances
38491
- function createPikaday(options) {
38492
- if (typeof window === 'undefined') {
38493
- console.warn('Pikaday requires a browser environment');
38494
- return null;
38495
- }
38496
- const Pikaday = window.Pikaday;
38497
- if (!Pikaday) {
38498
- console.error('Pikaday not available. Make sure pikaday.js is loaded.');
38499
- return null;
38500
- }
38501
- if (!moment || !window.moment) {
38502
- console.error('Moment.js not available. Pikaday requires moment.js.');
38503
- return null;
38504
- }
38505
- return new Pikaday(options);
38506
- }
37336
+ // Ensure moment is available globally for Pikaday
37337
+ if (typeof window !== 'undefined') {
37338
+ window.moment = moment;
37339
+ }
37340
+ // Export a function to safely create Pikaday instances
37341
+ function createPikaday(options) {
37342
+ if (typeof window === 'undefined') {
37343
+ console.warn('Pikaday requires a browser environment');
37344
+ return null;
37345
+ }
37346
+ const Pikaday = window.Pikaday;
37347
+ if (!Pikaday) {
37348
+ console.error('Pikaday not available. Make sure pikaday.js is loaded.');
37349
+ return null;
37350
+ }
37351
+ if (!moment || !window.moment) {
37352
+ console.error('Moment.js not available. Pikaday requires moment.js.');
37353
+ return null;
37354
+ }
37355
+ return new Pikaday(options);
37356
+ }
38507
37357
 
38508
37358
  class DateTimePicker extends ChartComponent {
38509
37359
  constructor(renderTarget) {
@@ -39077,7 +37927,30 @@
39077
37927
  this.pickerIsVisible = false;
39078
37928
  }
39079
37929
  buttonDateTimeFormat(millis) {
39080
- return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
37930
+ const date = new Date(millis);
37931
+ const locale = this.chartOptions.dateLocale || 'en-US';
37932
+ const is24Hour = this.chartOptions.is24HourTime !== false;
37933
+ const formatOptions = {
37934
+ year: 'numeric',
37935
+ month: '2-digit',
37936
+ day: '2-digit',
37937
+ hour: '2-digit',
37938
+ minute: '2-digit',
37939
+ second: '2-digit',
37940
+ hour12: !is24Hour
37941
+ };
37942
+ try {
37943
+ if (this.chartOptions.offset && this.chartOptions.offset !== 'Local') {
37944
+ formatOptions.timeZone = this.getTimezoneFromOffset(this.chartOptions.offset);
37945
+ }
37946
+ const baseFormat = date.toLocaleString(locale, formatOptions);
37947
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
37948
+ return `${baseFormat}.${milliseconds}`;
37949
+ }
37950
+ catch (error) {
37951
+ console.warn(`Failed to format date for locale ${locale}:`, error);
37952
+ return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
37953
+ }
39081
37954
  }
39082
37955
  render(chartOptions, minMillis, maxMillis, onSet = null) {
39083
37956
  this.chartOptions.setOptions(chartOptions);
@@ -39097,11 +37970,22 @@
39097
37970
  }
39098
37971
  super.themify(select(this.renderTarget), this.chartOptions.theme);
39099
37972
  }
37973
+ getTimezoneFromOffset(offset) {
37974
+ const timezoneMap = {
37975
+ 'UTC': 'UTC',
37976
+ 'EST': 'America/New_York',
37977
+ 'PST': 'America/Los_Angeles',
37978
+ 'CST': 'America/Chicago',
37979
+ 'MST': 'America/Denver'
37980
+ };
37981
+ return timezoneMap[offset] || 'UTC';
37982
+ }
39100
37983
  }
39101
37984
 
39102
37985
  class DateTimeButtonRange extends DateTimeButton {
39103
37986
  constructor(renderTarget) {
39104
37987
  super(renderTarget);
37988
+ this.clickOutsideHandler = null;
39105
37989
  }
39106
37990
  setButtonText(fromMillis, toMillis, isRelative, quickTime) {
39107
37991
  let fromString = this.buttonDateTimeFormat(fromMillis);
@@ -39121,10 +38005,38 @@
39121
38005
  onClose() {
39122
38006
  this.dateTimePickerContainer.style("display", "none");
39123
38007
  this.dateTimeButton.node().focus();
38008
+ this.removeClickOutsideHandler();
38009
+ }
38010
+ removeClickOutsideHandler() {
38011
+ if (this.clickOutsideHandler) {
38012
+ document.removeEventListener('click', this.clickOutsideHandler);
38013
+ this.clickOutsideHandler = null;
38014
+ }
38015
+ }
38016
+ setupClickOutsideHandler() {
38017
+ // Remove any existing handler first
38018
+ this.removeClickOutsideHandler();
38019
+ // Add handler after a small delay to prevent the opening click from immediately closing the picker
38020
+ setTimeout(() => {
38021
+ this.clickOutsideHandler = (event) => {
38022
+ const pickerElement = this.dateTimePickerContainer.node();
38023
+ const buttonElement = this.dateTimeButton.node();
38024
+ const target = event.target;
38025
+ // Check if click is outside both the picker and the button
38026
+ if (pickerElement && buttonElement &&
38027
+ !pickerElement.contains(target) &&
38028
+ !buttonElement.contains(target)) {
38029
+ this.onClose();
38030
+ }
38031
+ };
38032
+ document.addEventListener('click', this.clickOutsideHandler);
38033
+ }, 0);
39124
38034
  }
39125
38035
  render(chartOptions = {}, minMillis, maxMillis, fromMillis = null, toMillis = null, onSet = null, onCancel = null) {
39126
38036
  super.render(chartOptions, minMillis, maxMillis, onSet);
39127
- select(this.renderTarget).classed('tsi-dateTimeContainerRange', true);
38037
+ let container = select(this.renderTarget);
38038
+ container.classed('tsi-dateTimeContainerRange', true);
38039
+ container.style('position', 'relative');
39128
38040
  this.fromMillis = fromMillis;
39129
38041
  this.toMillis = toMillis;
39130
38042
  this.onCancel = onCancel ? onCancel : () => { };
@@ -39150,6 +38062,7 @@
39150
38062
  this.onClose();
39151
38063
  this.onCancel();
39152
38064
  });
38065
+ this.setupClickOutsideHandler();
39153
38066
  }
39154
38067
  });
39155
38068
  }
@@ -43455,7 +42368,7 @@
43455
42368
  super(renderTarget);
43456
42369
  this.chartOptions = new ChartOptions(); // TODO handle onkeyup and oninput in chart options
43457
42370
  }
43458
- render(environmentFqdn, getToken, chartOptions) {
42371
+ render(chartOptions) {
43459
42372
  this.chartOptions.setOptions(chartOptions);
43460
42373
  let targetElement = select(this.renderTarget);
43461
42374
  targetElement.html("");
@@ -43809,20 +42722,102 @@
43809
42722
  }
43810
42723
  }
43811
42724
 
42725
+ // Centralized renderer for the hierarchy tree. Keeps a stable D3 data-join and
42726
+ // updates existing DOM nodes instead of fully recreating them on each render.
42727
+ class TreeRenderer {
42728
+ static render(owner, data, target) {
42729
+ // Ensure an <ul> exists for this target (one list per level)
42730
+ let list = target.select('ul');
42731
+ if (list.empty()) {
42732
+ list = target.append('ul').attr('role', target === owner.hierarchyElem ? 'tree' : 'group');
42733
+ }
42734
+ const entries = Object.keys(data).map(k => ({ key: k, item: data[k] }));
42735
+ const liSelection = list.selectAll('li').data(entries, (d) => d && d.key);
42736
+ liSelection.exit().remove();
42737
+ const liEnter = liSelection.enter().append('li')
42738
+ .attr('role', 'none')
42739
+ .classed('tsi-leaf', (d) => !!d.item.isLeaf);
42740
+ const liMerged = liEnter.merge(liSelection);
42741
+ const setSize = entries.length;
42742
+ liMerged.each((d, i, nodes) => {
42743
+ const entry = d;
42744
+ const li = select(nodes[i]);
42745
+ if (owner.selectedIds && owner.selectedIds.includes(entry.item.id)) {
42746
+ li.classed('tsi-selected', true);
42747
+ }
42748
+ else {
42749
+ li.classed('tsi-selected', false);
42750
+ }
42751
+ // determine instance vs hierarchy node by presence of isLeaf flag
42752
+ const isInstance = !!entry.item.isLeaf;
42753
+ const nodeNameToCheckIfExists = isInstance ? owner.instanceNodeString(entry.item) : entry.key;
42754
+ const displayName = (entry.item && (entry.item.displayName || nodeNameToCheckIfExists)) || nodeNameToCheckIfExists;
42755
+ li.attr('data-display-name', displayName);
42756
+ let itemElem = li.select('.tsi-hierarchyItem');
42757
+ if (itemElem.empty()) {
42758
+ const newListElem = owner.createHierarchyItemElem(entry.item, entry.key);
42759
+ li.node().appendChild(newListElem.node());
42760
+ itemElem = li.select('.tsi-hierarchyItem');
42761
+ }
42762
+ itemElem.attr('aria-label', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
42763
+ itemElem.attr('title', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
42764
+ itemElem.attr('aria-expanded', String(entry.item.isExpanded));
42765
+ // accessibility: set treeitem level and position in set
42766
+ const ariaLevel = String(((entry.item && typeof entry.item.level === 'number') ? entry.item.level : 0) + 1);
42767
+ itemElem.attr('aria-level', ariaLevel);
42768
+ itemElem.attr('aria-posinset', String(i + 1));
42769
+ itemElem.attr('aria-setsize', String(setSize));
42770
+ if (!isInstance) {
42771
+ itemElem.select('.tsi-caret-icon').attr('style', `left: ${(entry.item.level) * 18 + 20}px`);
42772
+ itemElem.select('.tsi-name').text(entry.key);
42773
+ itemElem.select('.tsi-instanceCount').text(entry.item.cumulativeInstanceCount);
42774
+ }
42775
+ else {
42776
+ const nameSpan = itemElem.select('.tsi-name');
42777
+ nameSpan.html('');
42778
+ Utils.appendFormattedElementsFromString(nameSpan, owner.instanceNodeStringToDisplay(entry.item));
42779
+ }
42780
+ entry.item.node = li;
42781
+ if (entry.item.children) {
42782
+ entry.item.isExpanded = true;
42783
+ li.classed('tsi-expanded', true);
42784
+ // recurse using TreeRenderer to keep rendering logic centralized
42785
+ TreeRenderer.render(owner, entry.item.children, li);
42786
+ }
42787
+ else {
42788
+ li.classed('tsi-expanded', false);
42789
+ li.selectAll('ul').remove();
42790
+ }
42791
+ });
42792
+ }
42793
+ }
42794
+
43812
42795
  class HierarchyNavigation extends Component {
43813
42796
  constructor(renderTarget) {
43814
42797
  super(renderTarget);
43815
42798
  this.path = [];
42799
+ // debounce + request cancellation fields
42800
+ this.debounceTimer = null;
42801
+ this.debounceDelay = 250; // ms
42802
+ this.requestCounter = 0; // increments for each outgoing request
42803
+ this.latestRequestId = 0; // id of the most recent request
43816
42804
  //selectedIds
43817
42805
  this.selectedIds = [];
43818
42806
  this.searchEnabled = true;
43819
- this.renderSearchResult = (r, payload, target) => {
42807
+ this.autocompleteEnabled = true; // Enable/disable autocomplete suggestions
42808
+ // Search mode state
42809
+ this.isSearchMode = false;
42810
+ // Paths that should be auto-expanded (Set of path strings like "Factory North/Building A")
42811
+ this.pathsToAutoExpand = new Set();
42812
+ this.renderSearchResult = async (r, payload, target) => {
43820
42813
  const hierarchyData = r.hierarchyNodes?.hits?.length
43821
42814
  ? this.fillDataRecursively(r.hierarchyNodes, payload, payload)
43822
42815
  : {};
43823
42816
  const instancesData = r.instances?.hits?.length
43824
42817
  ? r.instances.hits.reduce((acc, i) => {
43825
- acc[this.instanceNodeIdentifier(i)] = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
42818
+ const inst = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
42819
+ inst.displayName = this.instanceNodeStringToDisplay(i) || '';
42820
+ acc[this.instanceNodeIdentifier(i)] = inst;
43826
42821
  return acc;
43827
42822
  }, {})
43828
42823
  : {};
@@ -43833,7 +42828,17 @@
43833
42828
  }
43834
42829
  hitCountElem.text(r.hierarchyNodes.hitCount);
43835
42830
  }
43836
- this.renderTree({ ...hierarchyData, ...instancesData }, target);
42831
+ const merged = { ...hierarchyData, ...instancesData };
42832
+ this.renderTree(merged, target);
42833
+ // Auto-expand nodes that should be expanded and load their children
42834
+ for (const key in hierarchyData) {
42835
+ const node = hierarchyData[key];
42836
+ if (node.isExpanded && !node.children) {
42837
+ // This node should be expanded but doesn't have children loaded yet
42838
+ // We need to trigger expansion after the node is rendered
42839
+ await this.autoExpandNode(node);
42840
+ }
42841
+ }
43837
42842
  };
43838
42843
  this.hierarchyNodeIdentifier = (hName) => {
43839
42844
  return hName ? hName : '(' + this.getString("Empty") + ')';
@@ -43855,12 +42860,20 @@
43855
42860
  const targetElement = select(this.renderTarget).text('');
43856
42861
  this.hierarchyNavWrapper = this.createHierarchyNavWrapper(targetElement);
43857
42862
  this.selectedIds = preselectedIds;
42863
+ // Allow disabling autocomplete via options
42864
+ if (hierarchyNavOptions.autocompleteEnabled !== undefined) {
42865
+ this.autocompleteEnabled = hierarchyNavOptions.autocompleteEnabled;
42866
+ }
42867
+ // Pre-compute paths that need to be auto-expanded for preselected instances
42868
+ if (preselectedIds && preselectedIds.length > 0) {
42869
+ await this.computePathsToAutoExpand(preselectedIds);
42870
+ }
43858
42871
  //render search wrapper
43859
- //this.renderSearchBox()
42872
+ this.renderSearchBox();
43860
42873
  super.themify(this.hierarchyNavWrapper, this.chartOptions.theme);
43861
42874
  const results = this.createResultsWrapper(this.hierarchyNavWrapper);
43862
42875
  this.hierarchyElem = this.createHierarchyElem(results);
43863
- this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
42876
+ await this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
43864
42877
  }
43865
42878
  createHierarchyNavWrapper(targetElement) {
43866
42879
  return targetElement.append('div').attr('class', 'tsi-hierarchy-nav-wrapper');
@@ -43868,8 +42881,129 @@
43868
42881
  createResultsWrapper(hierarchyNavWrapper) {
43869
42882
  return hierarchyNavWrapper.append('div').classed('tsi-hierarchy-or-list-wrapper', true);
43870
42883
  }
42884
+ // create hierarchy container and attach keyboard handler
43871
42885
  createHierarchyElem(results) {
43872
- return results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
42886
+ const sel = results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
42887
+ // attach keydown listener for keyboard navigation (delegated)
42888
+ // use native event to preserve focus handling
42889
+ const node = sel.node();
42890
+ if (node) {
42891
+ node.addEventListener('keydown', (ev) => this.onKeyDown(ev));
42892
+ }
42893
+ return sel;
42894
+ }
42895
+ // Keyboard navigation handlers and helpers
42896
+ onKeyDown(ev) {
42897
+ const key = ev.key;
42898
+ const active = document.activeElement;
42899
+ const container = this.hierarchyElem?.node();
42900
+ if (!container)
42901
+ return;
42902
+ const isInside = active && container.contains(active);
42903
+ if (!isInside && (key === 'ArrowDown' || key === 'ArrowUp')) {
42904
+ // focus first visible item on navigation keys
42905
+ const visible = this.getVisibleItemElems();
42906
+ if (visible.length) {
42907
+ this.focusItem(visible[0]);
42908
+ ev.preventDefault();
42909
+ }
42910
+ return;
42911
+ }
42912
+ if (!active)
42913
+ return;
42914
+ const current = active.classList && active.classList.contains('tsi-hierarchyItem') ? active : active.closest('.tsi-hierarchyItem');
42915
+ if (!current)
42916
+ return;
42917
+ switch (key) {
42918
+ case 'ArrowDown':
42919
+ this.focusNext(current);
42920
+ ev.preventDefault();
42921
+ break;
42922
+ case 'ArrowUp':
42923
+ this.focusPrev(current);
42924
+ ev.preventDefault();
42925
+ break;
42926
+ case 'ArrowRight':
42927
+ this.handleArrowRight(current);
42928
+ ev.preventDefault();
42929
+ break;
42930
+ case 'ArrowLeft':
42931
+ this.handleArrowLeft(current);
42932
+ ev.preventDefault();
42933
+ break;
42934
+ case 'Enter':
42935
+ case ' ':
42936
+ // activate (toggle expand or select)
42937
+ current.click();
42938
+ ev.preventDefault();
42939
+ break;
42940
+ }
42941
+ }
42942
+ getVisibleItemElems() {
42943
+ if (!this.hierarchyElem)
42944
+ return [];
42945
+ const root = this.hierarchyElem.node();
42946
+ if (!root)
42947
+ return [];
42948
+ const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
42949
+ return items.filter(i => i.offsetParent !== null && getComputedStyle(i).display !== 'none');
42950
+ }
42951
+ focusItem(elem) {
42952
+ if (!this.hierarchyElem)
42953
+ return;
42954
+ const root = this.hierarchyElem.node();
42955
+ if (!root)
42956
+ return;
42957
+ const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
42958
+ items.forEach(i => i.setAttribute('tabindex', '-1'));
42959
+ elem.setAttribute('tabindex', '0');
42960
+ elem.focus();
42961
+ }
42962
+ focusNext(current) {
42963
+ const visible = this.getVisibleItemElems();
42964
+ const idx = visible.indexOf(current);
42965
+ if (idx >= 0 && idx < visible.length - 1) {
42966
+ this.focusItem(visible[idx + 1]);
42967
+ }
42968
+ }
42969
+ focusPrev(current) {
42970
+ const visible = this.getVisibleItemElems();
42971
+ const idx = visible.indexOf(current);
42972
+ if (idx > 0) {
42973
+ this.focusItem(visible[idx - 1]);
42974
+ }
42975
+ }
42976
+ handleArrowRight(current) {
42977
+ const caret = current.querySelector('.tsi-caret-icon');
42978
+ const expanded = current.getAttribute('aria-expanded') === 'true';
42979
+ if (caret && !expanded) {
42980
+ // expand
42981
+ current.click();
42982
+ return;
42983
+ }
42984
+ // if already expanded, move to first child
42985
+ if (caret && expanded) {
42986
+ const li = current.closest('li');
42987
+ const childLi = li?.querySelector('ul > li');
42988
+ const childItem = childLi?.querySelector('.tsi-hierarchyItem');
42989
+ if (childItem)
42990
+ this.focusItem(childItem);
42991
+ }
42992
+ }
42993
+ handleArrowLeft(current) {
42994
+ const caret = current.querySelector('.tsi-caret-icon');
42995
+ const expanded = current.getAttribute('aria-expanded') === 'true';
42996
+ if (caret && expanded) {
42997
+ // collapse
42998
+ current.click();
42999
+ return;
43000
+ }
43001
+ // move focus to parent
43002
+ const li = current.closest('li');
43003
+ const parentLi = li?.parentElement?.closest('li');
43004
+ const parentItem = parentLi?.querySelector('.tsi-hierarchyItem');
43005
+ if (parentItem)
43006
+ this.focusItem(parentItem);
43873
43007
  }
43874
43008
  // prepares the parameters for search request
43875
43009
  requestPayload(hierarchy = null) {
@@ -43878,32 +43012,7 @@
43878
43012
  }
43879
43013
  // renders tree for both 'Navigate' and 'Filter' mode (with Hierarchy View option selected), locInTarget refers to the 'show more' element -either hierarchy or instance- within the target
43880
43014
  renderTree(data, target) {
43881
- let list = target.append('ul').attr("role", target === this.hierarchyElem ? "tree" : "group");
43882
- Object.keys(data).forEach(el => {
43883
- let nodeNameToCheckIfExists = data[el] instanceof InstanceNode ? this.instanceNodeString(data[el]) : el;
43884
- let li;
43885
- if (list.selectAll(".tsi-name").nodes().find(e => e.innerText === nodeNameToCheckIfExists)) {
43886
- li = null;
43887
- }
43888
- else {
43889
- li = list.append('li').classed('tsi-leaf', data[el].isLeaf);
43890
- //if the node is already selected, we want to highlight it
43891
- if (this.selectedIds && this.selectedIds.includes(data[el].id)) {
43892
- li.classed('tsi-selected', true);
43893
- }
43894
- }
43895
- if (!li)
43896
- return;
43897
- li.attr("role", "none");
43898
- let newListElem = this.createHierarchyItemElem(data[el], el);
43899
- li.node().appendChild(newListElem.node());
43900
- data[el].node = li;
43901
- if (data[el].children) {
43902
- data[el].isExpanded = true;
43903
- data[el].node.classed('tsi-expanded', true);
43904
- this.renderTree(data[el].children, data[el].node);
43905
- }
43906
- });
43015
+ TreeRenderer.render(this, data, target);
43907
43016
  }
43908
43017
  renderSearchBox() {
43909
43018
  this.searchWrapperElem = this.hierarchyNavWrapper.append('div').classed('tsi-hierarchy-search', true);
@@ -43912,40 +43021,140 @@
43912
43021
  let input = inputWrapper
43913
43022
  .append("input")
43914
43023
  .attr("class", "tsi-searchInput")
43915
- .attr("aria-label", this.getString("Search Time Series Instances"))
43916
- .attr("aria-describedby", "tsi-search-desc")
43024
+ .attr("aria-label", this.getString("Search"))
43025
+ .attr("aria-describedby", "tsi-hierarchy-search-desc")
43917
43026
  .attr("role", "combobox")
43918
43027
  .attr("aria-owns", "tsi-search-results")
43919
43028
  .attr("aria-expanded", "false")
43920
43029
  .attr("aria-haspopup", "listbox")
43921
- .attr("placeholder", this.getString("Search Time Series Instances") + "...");
43030
+ .attr("placeholder", this.getString("Search") + "...");
43031
+ // Add ARIA description for screen readers
43032
+ inputWrapper
43033
+ .append("span")
43034
+ .attr("id", "tsi-hierarchy-search-desc")
43035
+ .style("display", "none")
43036
+ .text(this.getString("Search suggestion instruction") || "Use arrow keys to navigate suggestions");
43037
+ // Add live region for search results info
43038
+ inputWrapper
43039
+ .append("div")
43040
+ .attr("class", "tsi-search-results-info")
43041
+ .attr("aria-live", "assertive");
43042
+ // Add clear button
43043
+ let clear = inputWrapper
43044
+ .append("div")
43045
+ .attr("class", "tsi-clear")
43046
+ .attr("tabindex", "0")
43047
+ .attr("role", "button")
43048
+ .attr("aria-label", "Clear Search")
43049
+ .on("click keydown", function (event) {
43050
+ if (Utils.isKeyDownAndNotEnter(event)) {
43051
+ return;
43052
+ }
43053
+ input.node().value = "";
43054
+ self.exitSearchMode();
43055
+ self.ap.close();
43056
+ select(this).classed("tsi-shown", false);
43057
+ });
43058
+ // Initialize Awesomplete for autocomplete (only if enabled)
43059
+ let Awesomplete = window.Awesomplete;
43060
+ if (this.autocompleteEnabled && Awesomplete) {
43061
+ this.ap = new Awesomplete(input.node(), {
43062
+ minChars: 1,
43063
+ maxItems: 10,
43064
+ autoFirst: true
43065
+ });
43066
+ }
43067
+ else {
43068
+ // Create a dummy object if autocomplete is disabled
43069
+ this.ap = {
43070
+ list: [],
43071
+ close: () => { },
43072
+ evaluate: () => { }
43073
+ };
43074
+ }
43922
43075
  let self = this;
43076
+ let noSuggest = false;
43077
+ let justAwesompleted = false;
43078
+ // Handle autocomplete selection (only if enabled)
43079
+ if (this.autocompleteEnabled) {
43080
+ input.node().addEventListener("awesomplete-selectcomplete", (event) => {
43081
+ noSuggest = true;
43082
+ const selectedValue = event.text.value;
43083
+ // Trigger search with selected value
43084
+ self.performDeepSearch(selectedValue);
43085
+ justAwesompleted = true;
43086
+ });
43087
+ }
43923
43088
  input.on("keydown", (event) => {
43089
+ // Handle ESC key to clear the search box
43090
+ if (event.key === 'Escape') {
43091
+ const inputElement = event.target;
43092
+ inputElement.value = '';
43093
+ // Trigger input event to clear search results
43094
+ self.exitSearchMode();
43095
+ self.ap.close();
43096
+ clear.classed("tsi-shown", false);
43097
+ return;
43098
+ }
43924
43099
  this.chartOptions.onKeydown(event, this.ap);
43925
43100
  });
43926
- var searchText;
43101
+ input.node().addEventListener("keyup", function (event) {
43102
+ if (justAwesompleted) {
43103
+ justAwesompleted = false;
43104
+ return;
43105
+ }
43106
+ let key = event.which || event.keyCode;
43107
+ if (key === 13) {
43108
+ noSuggest = true;
43109
+ }
43110
+ });
43111
+ // Debounced input handler to reduce work while typing
43927
43112
  input.on("input", function (event) {
43928
- searchText = event.target.value;
43929
- if (searchText.length === 0) {
43930
- //clear the tree
43931
- self.hierarchyElem.selectAll('ul').remove();
43932
- self.pathSearchAndRenderResult({ search: { payload: self.requestPayload() }, render: { target: self.hierarchyElem } });
43113
+ const val = event.target.value;
43114
+ // always clear existing timer
43115
+ if (self.debounceTimer) {
43116
+ clearTimeout(self.debounceTimer);
43117
+ self.debounceTimer = null;
43118
+ }
43119
+ // Show/hide clear button
43120
+ clear.classed("tsi-shown", val.length > 0);
43121
+ if (!val || val.length === 0) {
43122
+ // Exit search mode and restore navigation view
43123
+ self.exitSearchMode();
43124
+ self.ap.close();
43125
+ return;
43126
+ }
43127
+ // Populate autocomplete suggestions with instance leaves (only if enabled)
43128
+ if (self.autocompleteEnabled && !noSuggest && val.length >= 1) {
43129
+ self.fetchAutocompleteSuggestions(val);
43933
43130
  }
43934
43131
  else {
43935
- //filter the tree
43936
- self.filterTree(searchText);
43132
+ self.ap.close();
43937
43133
  }
43134
+ // Use deep search for comprehensive results
43135
+ self.debounceTimer = setTimeout(() => {
43136
+ self.performDeepSearch(val);
43137
+ }, self.debounceDelay);
43138
+ noSuggest = false;
43938
43139
  });
43939
43140
  }
43940
43141
  async pathSearchAndRenderResult({ search: { payload, bubbleUpReject = false }, render: { target, locInTarget = null } }) {
43142
+ const requestId = ++this.requestCounter;
43143
+ this.latestRequestId = requestId;
43941
43144
  try {
43942
43145
  const result = await this.searchFunction(payload);
43146
+ if (requestId !== this.latestRequestId) {
43147
+ return;
43148
+ }
43943
43149
  if (result.error) {
43944
43150
  throw result.error;
43945
43151
  }
43946
- this.renderSearchResult(result, payload, target);
43152
+ await this.renderSearchResult(result, payload, target);
43947
43153
  }
43948
43154
  catch (err) {
43155
+ if (requestId !== this.latestRequestId) {
43156
+ return;
43157
+ }
43949
43158
  this.chartOptions.onError("Error in hierarchy navigation", "Failed to complete search", err instanceof XMLHttpRequest ? err : null);
43950
43159
  if (bubbleUpReject) {
43951
43160
  throw err;
@@ -43953,11 +43162,18 @@
43953
43162
  }
43954
43163
  }
43955
43164
  filterTree(searchText) {
43956
- let tree = this.hierarchyElem.selectAll('ul').nodes()[0];
43957
- let list = tree.querySelectorAll('li');
43165
+ const nodes = this.hierarchyElem.selectAll('ul').nodes();
43166
+ if (!nodes || !nodes.length)
43167
+ return;
43168
+ const tree = nodes[0];
43169
+ if (!tree)
43170
+ return;
43171
+ const list = tree.querySelectorAll('li');
43172
+ const needle = searchText.toLowerCase();
43958
43173
  list.forEach((li) => {
43959
- let name = li.querySelector('.tsi-name').innerText;
43960
- if (name.toLowerCase().includes(searchText.toLowerCase())) {
43174
+ const attrName = li.getAttribute('data-display-name');
43175
+ let name = attrName && attrName.length ? attrName : (li.querySelector('.tsi-name')?.textContent || '');
43176
+ if (name.toLowerCase().includes(needle)) {
43961
43177
  li.style.display = 'block';
43962
43178
  }
43963
43179
  else {
@@ -43965,11 +43181,300 @@
43965
43181
  }
43966
43182
  });
43967
43183
  }
43184
+ // Fetch autocomplete suggestions for instances (leaves)
43185
+ async fetchAutocompleteSuggestions(searchText) {
43186
+ if (!searchText || searchText.length < 1) {
43187
+ this.ap.list = [];
43188
+ return;
43189
+ }
43190
+ try {
43191
+ // Call server search to get instance suggestions
43192
+ const payload = {
43193
+ path: this.path,
43194
+ searchTerm: searchText,
43195
+ recursive: true,
43196
+ includeInstances: true,
43197
+ // Limit results for autocomplete
43198
+ maxResults: 10
43199
+ };
43200
+ const results = await this.searchFunction(payload);
43201
+ if (results.error) {
43202
+ this.ap.list = [];
43203
+ return;
43204
+ }
43205
+ // Extract instance names for autocomplete suggestions
43206
+ const suggestions = [];
43207
+ if (results.instances?.hits) {
43208
+ results.instances.hits.forEach((i) => {
43209
+ const displayName = this.instanceNodeStringToDisplay(i);
43210
+ const pathStr = i.hierarchyPath && i.hierarchyPath.length > 0
43211
+ ? i.hierarchyPath.join(' > ') + ' > '
43212
+ : '';
43213
+ suggestions.push({
43214
+ label: pathStr + displayName,
43215
+ value: displayName
43216
+ });
43217
+ });
43218
+ }
43219
+ // Update Awesomplete list
43220
+ this.ap.list = suggestions;
43221
+ }
43222
+ catch (err) {
43223
+ // Silently fail for autocomplete - don't interrupt user experience
43224
+ this.ap.list = [];
43225
+ }
43226
+ }
43227
+ // Perform deep search across entire hierarchy using server-side search
43228
+ async performDeepSearch(searchText) {
43229
+ if (!searchText || searchText.length < 2) {
43230
+ this.exitSearchMode();
43231
+ return;
43232
+ }
43233
+ this.isSearchMode = true;
43234
+ const requestId = ++this.requestCounter;
43235
+ this.latestRequestId = requestId;
43236
+ try {
43237
+ // Call server search with recursive flag
43238
+ const payload = {
43239
+ path: this.path,
43240
+ searchTerm: searchText,
43241
+ recursive: true, // Search entire subtree
43242
+ includeInstances: true
43243
+ };
43244
+ const results = await this.searchFunction(payload);
43245
+ if (requestId !== this.latestRequestId)
43246
+ return; // Stale request
43247
+ if (results.error) {
43248
+ throw results.error;
43249
+ }
43250
+ // Render search results in flat list view
43251
+ this.renderSearchResults(results, searchText);
43252
+ }
43253
+ catch (err) {
43254
+ if (requestId !== this.latestRequestId)
43255
+ return;
43256
+ this.chartOptions.onError("Search failed", "Unable to search hierarchy", err instanceof XMLHttpRequest ? err : null);
43257
+ }
43258
+ }
43259
+ // Render search results with breadcrumb paths
43260
+ renderSearchResults(results, searchText) {
43261
+ this.hierarchyElem.selectAll('*').remove();
43262
+ const flatResults = [];
43263
+ // Flatten hierarchy results with full paths
43264
+ if (results.hierarchyNodes?.hits) {
43265
+ results.hierarchyNodes.hits.forEach((h) => {
43266
+ flatResults.push({
43267
+ type: 'hierarchy',
43268
+ name: h.name,
43269
+ path: h.path || [],
43270
+ id: h.id,
43271
+ cumulativeInstanceCount: h.cumulativeInstanceCount,
43272
+ highlightedName: this.highlightMatch(h.name, searchText),
43273
+ node: h
43274
+ });
43275
+ });
43276
+ }
43277
+ // Flatten instance results with full paths
43278
+ if (results.instances?.hits) {
43279
+ results.instances.hits.forEach((i) => {
43280
+ const displayName = this.instanceNodeStringToDisplay(i);
43281
+ flatResults.push({
43282
+ type: 'instance',
43283
+ name: i.name,
43284
+ path: i.hierarchyPath || [],
43285
+ id: i.id,
43286
+ timeSeriesId: i.timeSeriesId,
43287
+ description: i.description,
43288
+ highlightedName: this.highlightMatch(displayName, searchText),
43289
+ node: i
43290
+ });
43291
+ });
43292
+ }
43293
+ // Render flat list with breadcrumbs
43294
+ const searchList = this.hierarchyElem
43295
+ .append('div')
43296
+ .classed('tsi-search-results', true);
43297
+ if (flatResults.length === 0) {
43298
+ searchList.append('div')
43299
+ .classed('tsi-noResults', true)
43300
+ .text(this.getString('No results'));
43301
+ return;
43302
+ }
43303
+ searchList.append('div')
43304
+ .classed('tsi-search-results-header', true)
43305
+ .html(`<strong>${flatResults.length}</strong> ${this.getString('results found') || 'results found'}`);
43306
+ const resultItems = searchList.selectAll('.tsi-search-result-item')
43307
+ .data(flatResults)
43308
+ .enter()
43309
+ .append('div')
43310
+ .classed('tsi-search-result-item', true)
43311
+ .attr('tabindex', '0')
43312
+ .attr('role', 'option')
43313
+ .attr('aria-label', (d) => {
43314
+ const pathStr = d.path.length > 0 ? d.path.join(' > ') + ' > ' : '';
43315
+ return pathStr + d.name;
43316
+ });
43317
+ const self = this;
43318
+ resultItems.each(function (d) {
43319
+ const item = select(this);
43320
+ // Breadcrumb path
43321
+ if (d.path.length > 0) {
43322
+ item.append('div')
43323
+ .classed('tsi-search-breadcrumb', true)
43324
+ .text(d.path.join(' > '));
43325
+ }
43326
+ // Highlighted name
43327
+ item.append('div')
43328
+ .classed('tsi-search-result-name', true)
43329
+ .html(d.highlightedName);
43330
+ // Instance description or count
43331
+ if (d.type === 'instance' && d.description) {
43332
+ item.append('div')
43333
+ .classed('tsi-search-result-description', true)
43334
+ .text(d.description);
43335
+ }
43336
+ else if (d.type === 'hierarchy') {
43337
+ item.append('div')
43338
+ .classed('tsi-search-result-count', true)
43339
+ .text(`${d.cumulativeInstanceCount || 0} instances`);
43340
+ }
43341
+ });
43342
+ // Click handlers
43343
+ resultItems.on('click keydown', function (event, d) {
43344
+ if (Utils.isKeyDownAndNotEnter(event))
43345
+ return;
43346
+ if (d.type === 'instance') {
43347
+ // Handle instance selection
43348
+ if (self.chartOptions.onInstanceClick) {
43349
+ const inst = new InstanceNode(d.timeSeriesId, d.name, d.path.length, d.id, d.description);
43350
+ // Update selection state
43351
+ if (self.selectedIds && self.selectedIds.includes(d.id)) {
43352
+ self.selectedIds = self.selectedIds.filter(id => id !== d.id);
43353
+ select(this).classed('tsi-selected', false);
43354
+ }
43355
+ else {
43356
+ self.selectedIds.push(d.id);
43357
+ select(this).classed('tsi-selected', true);
43358
+ }
43359
+ self.chartOptions.onInstanceClick(inst);
43360
+ }
43361
+ }
43362
+ else {
43363
+ // Navigate to hierarchy node - exit search and expand to that path
43364
+ self.navigateToPath(d.path);
43365
+ }
43366
+ });
43367
+ // Apply selection state to already-selected instances
43368
+ resultItems.each(function (d) {
43369
+ if (d.type === 'instance' && self.selectedIds && self.selectedIds.includes(d.id)) {
43370
+ select(this).classed('tsi-selected', true);
43371
+ }
43372
+ });
43373
+ }
43374
+ // Exit search mode and restore tree
43375
+ exitSearchMode() {
43376
+ this.isSearchMode = false;
43377
+ this.hierarchyElem.selectAll('*').remove();
43378
+ this.pathSearchAndRenderResult({
43379
+ search: { payload: this.requestPayload() },
43380
+ render: { target: this.hierarchyElem }
43381
+ });
43382
+ }
43383
+ // Navigate to a specific path in the hierarchy
43384
+ async navigateToPath(targetPath) {
43385
+ this.exitSearchMode();
43386
+ // For now, just exit search mode and return to root
43387
+ // In a more advanced implementation, this would progressively
43388
+ // expand nodes along the path to reveal the target
43389
+ // This would require waiting for each level to load before expanding the next
43390
+ }
43391
+ // Pre-compute which paths need to be auto-expanded for preselected instances
43392
+ async computePathsToAutoExpand(instanceIds) {
43393
+ if (!instanceIds || instanceIds.length === 0) {
43394
+ return;
43395
+ }
43396
+ // console.log('[HierarchyNavigation] Computing paths to auto-expand for:', instanceIds);
43397
+ try {
43398
+ this.pathsToAutoExpand.clear();
43399
+ for (const instanceId of instanceIds) {
43400
+ // Search for this specific instance
43401
+ const result = await this.searchFunction({
43402
+ path: this.path,
43403
+ searchTerm: instanceId,
43404
+ recursive: true,
43405
+ includeInstances: true
43406
+ });
43407
+ if (result?.instances?.hits) {
43408
+ for (const instance of result.instances.hits) {
43409
+ // Match by ID
43410
+ if (instance.id === instanceId ||
43411
+ (instance.id && instance.id.includes(instanceId))) {
43412
+ if (instance.hierarchyPath && instance.hierarchyPath.length > 0) {
43413
+ // Add all parent paths that need to be expanded
43414
+ const hierarchyPath = instance.hierarchyPath;
43415
+ for (let i = 1; i <= hierarchyPath.length; i++) {
43416
+ const pathArray = hierarchyPath.slice(0, i);
43417
+ const pathKey = pathArray.join('/');
43418
+ this.pathsToAutoExpand.add(pathKey);
43419
+ }
43420
+ }
43421
+ }
43422
+ }
43423
+ }
43424
+ }
43425
+ // console.log('[HierarchyNavigation] Paths to auto-expand:', Array.from(this.pathsToAutoExpand));
43426
+ }
43427
+ catch (err) {
43428
+ console.warn('Failed to compute paths to auto-expand:', err);
43429
+ }
43430
+ }
43431
+ // Check if a path should be auto-expanded
43432
+ shouldAutoExpand(pathArray) {
43433
+ if (this.pathsToAutoExpand.size === 0) {
43434
+ return false;
43435
+ }
43436
+ const pathKey = pathArray.join('/');
43437
+ return this.pathsToAutoExpand.has(pathKey);
43438
+ }
43439
+ // Auto-expand a node by triggering its expand function
43440
+ async autoExpandNode(node) {
43441
+ if (!node || !node.expand || !node.node) {
43442
+ return;
43443
+ }
43444
+ try {
43445
+ // Wait for the DOM node to be available
43446
+ await new Promise(resolve => setTimeout(resolve, 10));
43447
+ // Mark as expanded visually
43448
+ node.node.classed('tsi-expanded', true);
43449
+ // Call the expand function to load children
43450
+ await node.expand();
43451
+ // console.log(`[HierarchyNavigation] Auto-expanded node: ${node.path.join('/')}`);
43452
+ }
43453
+ catch (err) {
43454
+ console.warn(`Failed to auto-expand node ${node.path.join('/')}:`, err);
43455
+ }
43456
+ }
43457
+ // Highlight search term in text
43458
+ highlightMatch(text, searchTerm) {
43459
+ if (!text)
43460
+ return '';
43461
+ const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43462
+ const regex = new RegExp(`(${escapedTerm})`, 'gi');
43463
+ return text.replace(regex, '<mark>$1</mark>');
43464
+ }
43968
43465
  // creates in-depth data object using the server response for hierarchyNodes to show in the tree all expanded, considering UntilChildren
43969
43466
  fillDataRecursively(hierarchyNodes, payload, payloadForContinuation = null) {
43970
43467
  let data = {};
43971
43468
  hierarchyNodes.hits.forEach((h) => {
43972
43469
  let hierarchy = new HierarchyNode(h.name, payload.path, payload.path.length - this.path.length, h.cumulativeInstanceCount, h.id);
43470
+ // cache display name on node for client-side filtering
43471
+ hierarchy.displayName = h.name || '';
43472
+ // Check if this path should be auto-expanded
43473
+ const shouldExpand = this.shouldAutoExpand(hierarchy.path);
43474
+ if (shouldExpand) {
43475
+ hierarchy.isExpanded = true;
43476
+ //console.log(`[HierarchyNavigation] Auto-expanding node: ${hierarchy.path.join('/')}`);
43477
+ }
43973
43478
  hierarchy.expand = () => {
43974
43479
  hierarchy.isExpanded = true;
43975
43480
  hierarchy.node.classed('tsi-expanded', true);
@@ -43993,7 +43498,7 @@
43993
43498
  .attr('style', `padding-left: ${hORi.isLeaf ? hORi.level * 18 + 20 : (hORi.level + 1) * 18 + 20}px`)
43994
43499
  .attr('tabindex', 0)
43995
43500
  //.attr('arialabel', isHierarchyNode ? key : Utils.getTimeSeriesIdString(hORi))
43996
- .attr('arialabel', isHierarchyNode ? key : self.getAriaLabel(hORi))
43501
+ .attr('aria-label', isHierarchyNode ? key : self.getAriaLabel(hORi))
43997
43502
  .attr('title', isHierarchyNode ? key : self.getAriaLabel(hORi))
43998
43503
  .attr("role", "treeitem").attr('aria-expanded', hORi.isExpanded)
43999
43504
  .on('click keydown', async function (event) {
@@ -44045,6 +43550,8 @@
44045
43550
  return hORi.description || hORi.name || hORi.id || Utils.getTimeSeriesIdString(hORi);
44046
43551
  }
44047
43552
  }
43553
+ // TreeRenderer has been moved to its own module: ./TreeRenderer
43554
+ // The rendering logic was extracted to reduce file size and improve testability.
44048
43555
  class HierarchyNode {
44049
43556
  constructor(name, parentPath, level, cumulativeInstanceCount = null, id = null) {
44050
43557
  this.name = name;
@@ -44289,6 +43796,7 @@
44289
43796
  class DateTimeButtonSingle extends DateTimeButton {
44290
43797
  constructor(renderTarget) {
44291
43798
  super(renderTarget);
43799
+ this.clickOutsideHandler = null;
44292
43800
  this.sDTPOnSet = (millis = null) => {
44293
43801
  if (millis !== null) {
44294
43802
  this.dateTimeButton.text(this.buttonDateTimeFormat(millis));
@@ -44301,6 +43809,32 @@
44301
43809
  closeSDTP() {
44302
43810
  this.dateTimePickerContainer.style("display", "none");
44303
43811
  this.dateTimeButton.node().focus();
43812
+ this.removeClickOutsideHandler();
43813
+ }
43814
+ removeClickOutsideHandler() {
43815
+ if (this.clickOutsideHandler) {
43816
+ document.removeEventListener('click', this.clickOutsideHandler);
43817
+ this.clickOutsideHandler = null;
43818
+ }
43819
+ }
43820
+ setupClickOutsideHandler() {
43821
+ // Remove any existing handler first
43822
+ this.removeClickOutsideHandler();
43823
+ // Add handler after a small delay to prevent the opening click from immediately closing the picker
43824
+ setTimeout(() => {
43825
+ this.clickOutsideHandler = (event) => {
43826
+ const pickerElement = this.dateTimePickerContainer.node();
43827
+ const buttonElement = this.dateTimeButton.node();
43828
+ const target = event.target;
43829
+ // Check if click is outside both the picker and the button
43830
+ if (pickerElement && buttonElement &&
43831
+ !pickerElement.contains(target) &&
43832
+ !buttonElement.contains(target)) {
43833
+ this.closeSDTP();
43834
+ }
43835
+ };
43836
+ document.addEventListener('click', this.clickOutsideHandler);
43837
+ }, 0);
44304
43838
  }
44305
43839
  render(chartOptions = {}, minMillis, maxMillis, selectedMillis = null, onSet = null) {
44306
43840
  super.render(chartOptions, minMillis, maxMillis, onSet);
@@ -44310,12 +43844,11 @@
44310
43844
  if (!this.dateTimePicker) {
44311
43845
  this.dateTimePicker = new SingleDateTimePicker(this.dateTimePickerContainer.node());
44312
43846
  }
44313
- let targetElement = select(this.renderTarget);
44314
- (targetElement.select(".tsi-dateTimePickerContainer")).selectAll("*");
44315
43847
  this.dateTimeButton.on("click", () => {
44316
43848
  this.chartOptions.dTPIsModal = true;
44317
43849
  this.dateTimePickerContainer.style("display", "block");
44318
43850
  this.dateTimePicker.render(this.chartOptions, this.minMillis, this.maxMillis, this.selectedMillis, this.sDTPOnSet);
43851
+ this.setupClickOutsideHandler();
44319
43852
  });
44320
43853
  }
44321
43854
  }
@@ -44613,8 +44146,16 @@
44613
44146
  class PlaybackControls extends Component {
44614
44147
  constructor(renderTarget, initialTimeStamp = null) {
44615
44148
  super(renderTarget);
44616
- this.handleRadius = 7;
44617
- this.minimumPlaybackInterval = 1000; // 1 second
44149
+ this.playbackInterval = null;
44150
+ this.playButton = null;
44151
+ this.handleElement = null;
44152
+ this.controlsContainer = null;
44153
+ this.track = null;
44154
+ this.selectTimeStampCallback = null;
44155
+ this.wasPlayingWhenDragStarted = false;
44156
+ this.rafId = null;
44157
+ this.handleRadius = PlaybackControls.CONSTANTS.HANDLE_RADIUS;
44158
+ this.minimumPlaybackInterval = PlaybackControls.CONSTANTS.MINIMUM_PLAYBACK_INTERVAL_MS;
44618
44159
  this.playbackInterval = null;
44619
44160
  this.selectedTimeStamp = initialTimeStamp;
44620
44161
  }
@@ -44622,6 +44163,21 @@
44622
44163
  return this.selectedTimeStamp;
44623
44164
  }
44624
44165
  render(start, end, onSelectTimeStamp, options, playbackSettings) {
44166
+ // Validate inputs
44167
+ if (!(start instanceof Date) || !(end instanceof Date)) {
44168
+ throw new TypeError('start and end must be Date objects');
44169
+ }
44170
+ if (start >= end) {
44171
+ throw new RangeError('start must be before end');
44172
+ }
44173
+ if (!onSelectTimeStamp || typeof onSelectTimeStamp !== 'function') {
44174
+ throw new TypeError('onSelectTimeStamp must be a function');
44175
+ }
44176
+ // Clean up any pending animation frames before re-rendering
44177
+ if (this.rafId !== null) {
44178
+ cancelAnimationFrame(this.rafId);
44179
+ this.rafId = null;
44180
+ }
44625
44181
  this.end = end;
44626
44182
  this.selectTimeStampCallback = onSelectTimeStamp;
44627
44183
  this.chartOptions.setOptions(options);
@@ -44683,6 +44239,9 @@
44683
44239
  this.playButton = this.controlsContainer.append('button')
44684
44240
  .classed('tsi-play-button', this.playbackInterval === null)
44685
44241
  .classed('tsi-pause-button', this.playbackInterval !== null)
44242
+ // Accessibility attributes
44243
+ .attr('aria-label', 'Play/Pause playback')
44244
+ .attr('title', 'Play/Pause playback')
44686
44245
  .on('click', () => {
44687
44246
  if (this.playbackInterval === null) {
44688
44247
  this.play();
@@ -44734,6 +44293,27 @@
44734
44293
  this.updateSelection(handlePosition, this.selectedTimeStamp);
44735
44294
  this.selectTimeStampCallback(this.selectedTimeStamp);
44736
44295
  }
44296
+ /**
44297
+ * Cleanup resources to prevent memory leaks
44298
+ */
44299
+ destroy() {
44300
+ this.pause();
44301
+ // Cancel any pending animation frames
44302
+ if (this.rafId !== null) {
44303
+ cancelAnimationFrame(this.rafId);
44304
+ this.rafId = null;
44305
+ }
44306
+ // Remove event listeners
44307
+ if (this.controlsContainer) {
44308
+ this.controlsContainer.selectAll('*').on('.', null);
44309
+ }
44310
+ // Clear DOM references
44311
+ this.playButton = null;
44312
+ this.handleElement = null;
44313
+ this.controlsContainer = null;
44314
+ this.track = null;
44315
+ this.selectTimeStampCallback = null;
44316
+ }
44737
44317
  clamp(number, min, max) {
44738
44318
  let clamped = Math.max(number, min);
44739
44319
  return Math.min(clamped, max);
@@ -44742,9 +44322,17 @@
44742
44322
  this.wasPlayingWhenDragStarted = this.wasPlayingWhenDragStarted ||
44743
44323
  (this.playbackInterval !== null);
44744
44324
  this.pause();
44745
- let handlePosition = this.clamp(positionX, 0, this.trackWidth);
44746
- this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
44747
- this.updateSelection(handlePosition, this.selectedTimeStamp);
44325
+ // Use requestAnimationFrame to batch DOM updates for better performance
44326
+ // Cancel any pending animation frame to prevent stacking updates
44327
+ if (this.rafId !== null) {
44328
+ cancelAnimationFrame(this.rafId);
44329
+ }
44330
+ this.rafId = requestAnimationFrame(() => {
44331
+ const handlePosition = this.clamp(positionX, 0, this.trackWidth);
44332
+ this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
44333
+ this.updateSelection(handlePosition, this.selectedTimeStamp);
44334
+ this.rafId = null;
44335
+ });
44748
44336
  }
44749
44337
  onDragEnd() {
44750
44338
  this.selectTimeStampCallback(this.selectedTimeStamp);
@@ -44767,6 +44355,12 @@
44767
44355
  .text(this.timeFormatter(timeStamp));
44768
44356
  }
44769
44357
  }
44358
+ PlaybackControls.CONSTANTS = {
44359
+ HANDLE_RADIUS: 7,
44360
+ MINIMUM_PLAYBACK_INTERVAL_MS: 1000,
44361
+ HANDLE_PADDING: 8,
44362
+ AXIS_OFFSET: 6,
44363
+ };
44770
44364
  class TimeAxis extends TemporalXAxisComponent {
44771
44365
  constructor(renderTarget) {
44772
44366
  super(renderTarget);