tsichart-core 2.0.0-beta.7 → 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.js CHANGED
@@ -132,7 +132,7 @@ const swimlaneLabelConstants = {
132
132
  swimLaneLabelHeightPadding: 8,
133
133
  labelLeftPadding: 28
134
134
  };
135
- const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', ']', '}', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
135
+ const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '}', ']', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
136
136
  const NONNUMERICTOPMARGIN = 8;
137
137
  const LINECHARTTOPPADDING = 16;
138
138
  const GRIDCONTAINERCLASS = 'tsi-gridContainer';
@@ -576,50 +576,89 @@ class Utils {
576
576
  }
577
577
  return hclColor.toString();
578
578
  }
579
+ /**
580
+ * Creates an array of colors for split-by series
581
+ * @param displayState - The current display state
582
+ * @param aggKey - The aggregate key
583
+ * @param ignoreIsOnlyAgg - Whether to ignore the "only aggregate" optimization
584
+ * @returns Array of color strings for each split-by
585
+ */
579
586
  static createSplitByColors(displayState, aggKey, ignoreIsOnlyAgg = false) {
580
- if (Object.keys(displayState[aggKey]["splitBys"]).length == 1)
587
+ const splitBys = displayState[aggKey]?.splitBys;
588
+ if (!splitBys) {
589
+ return [];
590
+ }
591
+ const splitByCount = Object.keys(splitBys).length;
592
+ // Early return for single split-by
593
+ if (splitByCount === 1) {
581
594
  return [displayState[aggKey].color];
582
- var isOnlyAgg = Object.keys(displayState).reduce((accum, currAgg) => {
583
- if (currAgg == aggKey)
584
- return accum;
585
- if (displayState[currAgg]["visible"] == false)
586
- return accum && true;
587
- return false;
588
- }, true);
589
- if (isOnlyAgg && !ignoreIsOnlyAgg) {
590
- return this.generateColors(Object.keys(displayState[aggKey]["splitBys"]).length);
591
595
  }
592
- var aggColor = displayState[aggKey].color;
593
- var interpolateColor = d3__namespace.scaleLinear().domain([0, Object.keys(displayState[aggKey]["splitBys"]).length])
594
- .range([d3__namespace.hcl(aggColor).darker().l, d3__namespace.hcl(aggColor).brighter().l]);
595
- var colors = [];
596
- for (var i = 0; i < Object.keys(displayState[aggKey]["splitBys"]).length; i++) {
597
- const newColor = d3__namespace.hcl(aggColor);
596
+ // Create cache key for memoization
597
+ const cacheKey = `${aggKey}_${splitByCount}_${displayState[aggKey].color}_${ignoreIsOnlyAgg}`;
598
+ if (this.splitByColorCache.has(cacheKey)) {
599
+ return this.splitByColorCache.get(cacheKey);
600
+ }
601
+ const isOnlyVisibleAgg = !ignoreIsOnlyAgg && this.isOnlyVisibleAggregate(displayState, aggKey);
602
+ let colors;
603
+ if (isOnlyVisibleAgg) {
604
+ // Generate distinct colors when this is the only visible aggregate
605
+ colors = this.generateColors(splitByCount);
606
+ }
607
+ else {
608
+ // Generate color variations based on aggregate color
609
+ colors = this.generateSplitByColorVariations(displayState[aggKey].color, splitByCount);
610
+ }
611
+ // Cache the result
612
+ this.splitByColorCache.set(cacheKey, colors);
613
+ // Limit cache size to prevent memory leaks
614
+ if (this.splitByColorCache.size > 100) {
615
+ const firstKey = this.splitByColorCache.keys().next().value;
616
+ this.splitByColorCache.delete(firstKey);
617
+ }
618
+ return colors;
619
+ }
620
+ /**
621
+ * Helper method to check if an aggregate is the only visible one
622
+ */
623
+ static isOnlyVisibleAggregate(displayState, aggKey) {
624
+ for (const currAgg in displayState) {
625
+ if (currAgg !== aggKey && displayState[currAgg]?.visible !== false) {
626
+ return false;
627
+ }
628
+ }
629
+ return true;
630
+ }
631
+ /**
632
+ * Helper method to generate color variations for split-bys
633
+ */
634
+ static generateSplitByColorVariations(baseColor, count) {
635
+ const baseHcl = d3__namespace.hcl(baseColor);
636
+ const interpolateColor = d3__namespace.scaleLinear()
637
+ .domain([0, count])
638
+ .range([baseHcl.darker().l, baseHcl.brighter().l]);
639
+ const colors = new Array(count);
640
+ for (let i = 0; i < count; i++) {
641
+ const newColor = d3__namespace.hcl(baseColor);
598
642
  newColor.l = interpolateColor(i);
599
- colors.push(newColor.formatHex());
643
+ colors[i] = newColor.formatHex();
600
644
  }
601
645
  return colors;
602
646
  }
647
+ /**
648
+ * Clears the split-by color cache (useful when display state changes significantly)
649
+ */
650
+ static clearSplitByColorCache() {
651
+ this.splitByColorCache.clear();
652
+ }
603
653
  static colorSplitBy(displayState, splitByIndex, aggKey, ignoreIsOnlyAgg = false) {
604
- if (Object.keys(displayState[aggKey]["splitBys"]).length == 1)
605
- return displayState[aggKey].color;
606
- var isOnlyAgg = Object.keys(displayState).reduce((accum, currAgg) => {
607
- if (currAgg == aggKey)
608
- return accum;
609
- if (displayState[currAgg]["visible"] == false)
610
- return accum && true;
611
- return false;
612
- }, true);
613
- if (isOnlyAgg && !ignoreIsOnlyAgg) {
614
- var splitByColors = this.generateColors(Object.keys(displayState[aggKey]["splitBys"]).length);
615
- return splitByColors[splitByIndex];
654
+ const colors = this.createSplitByColors(displayState, aggKey, ignoreIsOnlyAgg);
655
+ if (typeof splitByIndex === 'number' &&
656
+ Number.isInteger(splitByIndex) &&
657
+ splitByIndex >= 0 &&
658
+ splitByIndex < colors.length) {
659
+ return colors[splitByIndex];
616
660
  }
617
- var aggColor = displayState[aggKey].color;
618
- var interpolateColor = d3__namespace.scaleLinear().domain([0, Object.keys(displayState[aggKey]["splitBys"]).length])
619
- .range([d3__namespace.hcl(aggColor).darker().l, d3__namespace.hcl(aggColor).brighter().l]);
620
- const newColor = d3__namespace.hcl(aggColor);
621
- newColor.l = interpolateColor(splitByIndex);
622
- return newColor.formatHex();
661
+ return displayState[aggKey]?.color || '#000000';
623
662
  }
624
663
  static getTheme(theme) {
625
664
  return theme ? 'tsi-' + theme : 'tsi-dark';
@@ -1043,6 +1082,7 @@ class Utils {
1043
1082
  }
1044
1083
  }
1045
1084
  Utils.guidForNullTSID = Utils.guid();
1085
+ Utils.splitByColorCache = new Map();
1046
1086
  Utils.equalToEventTarget = (function (current, event) {
1047
1087
  return (current == event.target);
1048
1088
  });
@@ -1528,35 +1568,41 @@ class Component {
1528
1568
  }
1529
1569
  }
1530
1570
 
1531
- const NUMERICSPLITBYHEIGHT = 44;
1532
- const NONNUMERICSPLITBYHEIGHT = 24;
1571
+ /**
1572
+ * Constants for Legend component layout and behavior
1573
+ */
1574
+ const LEGEND_CONSTANTS = {
1575
+ /** Height in pixels for each numeric split-by item (includes type selector dropdown) */
1576
+ NUMERIC_SPLITBY_HEIGHT: 44,
1577
+ /** Height in pixels for each non-numeric (categorical/events) split-by item */
1578
+ NON_NUMERIC_SPLITBY_HEIGHT: 24,
1579
+ /** Height in pixels for the series name label header */
1580
+ NAME_LABEL_HEIGHT: 24,
1581
+ /** Buffer distance in pixels from scroll edge before triggering "load more" */
1582
+ SCROLL_BUFFER: 40,
1583
+ /** Number of split-by items to load per batch when paginating */
1584
+ BATCH_SIZE: 20,
1585
+ /** Minimum height in pixels for aggregate container */
1586
+ MIN_AGGREGATE_HEIGHT: 201,
1587
+ /** Minimum width in pixels for each series label in compact mode */
1588
+ MIN_SERIES_WIDTH: 124,
1589
+ };
1533
1590
  class Legend extends Component {
1534
1591
  constructor(drawChart, renderTarget, legendWidth) {
1535
1592
  super(renderTarget);
1536
1593
  this.renderSplitBys = (aggKey, aggSelection, dataType, noSplitBys) => {
1537
- var splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
1538
- var firstSplitBy = this.chartComponentData.displayState[aggKey].splitBys[Object.keys(this.chartComponentData.displayState[aggKey].splitBys)[0]];
1539
- var firstSplitByType = firstSplitBy ? firstSplitBy.visibleType : null;
1540
- Object.keys(this.chartComponentData.displayState[aggKey].splitBys).reduce((isSame, curr) => {
1541
- return (firstSplitByType == this.chartComponentData.displayState[aggKey].splitBys[curr].visibleType) && isSame;
1542
- }, true);
1543
- let showMoreSplitBys = () => {
1544
- const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
1545
- this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
1546
- if (oldShownSplitBys != this.chartComponentData.displayState[aggKey].shownSplitBys) {
1547
- this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
1548
- }
1549
- };
1594
+ const splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
1595
+ const showMoreSplitBys = () => this.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
1550
1596
  let splitByContainer = aggSelection.selectAll(".tsi-splitByContainer").data([aggKey]);
1551
- var splitByContainerEntered = splitByContainer.enter().append("div")
1597
+ const splitByContainerEntered = splitByContainer.enter().append("div")
1552
1598
  .merge(splitByContainer)
1553
1599
  .classed("tsi-splitByContainer", true);
1554
- var splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
1600
+ const splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
1555
1601
  .data(splitByLabelData.slice(0, this.chartComponentData.displayState[aggKey].shownSplitBys), function (d) {
1556
1602
  return d;
1557
1603
  });
1558
- let self = this;
1559
- var splitByLabelsEntered = splitByLabels
1604
+ const self = this;
1605
+ const splitByLabelsEntered = splitByLabels
1560
1606
  .enter()
1561
1607
  .append("div")
1562
1608
  .merge(splitByLabels)
@@ -1570,135 +1616,60 @@ class Legend extends Component {
1570
1616
  }
1571
1617
  })
1572
1618
  .on("click", function (event, splitBy) {
1573
- if (self.legendState == "compact") {
1574
- self.toggleSplitByVisible(aggKey, splitBy);
1575
- }
1576
- else {
1577
- self.toggleSticky(aggKey, splitBy);
1578
- }
1579
- self.drawChart();
1619
+ self.handleSplitByClick(aggKey, splitBy);
1580
1620
  })
1581
1621
  .on("mouseover", function (event, splitBy) {
1582
1622
  event.stopPropagation();
1583
- self.labelMouseover(aggKey, splitBy);
1623
+ self.handleSplitByMouseOver(aggKey, splitBy);
1584
1624
  })
1585
1625
  .on("mouseout", function (event) {
1586
1626
  event.stopPropagation();
1587
- self.svgSelection.selectAll(".tsi-valueElement")
1588
- .attr("stroke-opacity", 1)
1589
- .attr("fill-opacity", 1);
1590
- self.labelMouseout(self.svgSelection, aggKey);
1627
+ self.handleSplitByMouseOut(aggKey);
1591
1628
  })
1592
1629
  .attr("class", (splitBy, i) => {
1593
- let compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
1594
- let shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
1595
- return `tsi-splitByLabel tsi-splitByLabel ${compact} ${shown}`;
1630
+ const compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
1631
+ const shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
1632
+ return `tsi-splitByLabel ${compact} ${shown}`;
1596
1633
  })
1597
- .classed("stickied", (splitBy, i) => {
1598
- if (self.chartComponentData.stickiedKey != null) {
1599
- return aggKey == self.chartComponentData.stickiedKey.aggregateKey && splitBy == self.chartComponentData.stickiedKey.splitBy;
1600
- }
1601
- });
1602
- var colors = Utils.createSplitByColors(self.chartComponentData.displayState, aggKey, self.chartOptions.keepSplitByColor);
1634
+ .classed("stickied", (splitBy, i) => self.isStickied(aggKey, splitBy));
1635
+ // Use helper methods to render each split-by element
1603
1636
  splitByLabelsEntered.each(function (splitBy, j) {
1604
- let color = (self.chartComponentData.isFromHeatmap) ? self.chartComponentData.displayState[aggKey].color : colors[j];
1637
+ const selection = d3__namespace.select(this);
1638
+ // Add color key (conditionally based on data type and legend state)
1605
1639
  if (dataType === DataTypes.Numeric || noSplitBys || self.legendState === 'compact') {
1606
- let colorKey = d3__namespace.select(this).selectAll('.tsi-colorKey').data([color]);
1607
- let colorKeyEntered = colorKey.enter()
1608
- .append("div")
1609
- .attr("class", 'tsi-colorKey')
1610
- .merge(colorKey);
1611
- if (dataType === DataTypes.Numeric) {
1612
- colorKeyEntered.style('background-color', (d) => {
1613
- return d;
1614
- });
1615
- }
1616
- else {
1617
- self.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
1618
- }
1619
- d3__namespace.select(this).classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && this.legendState !== 'compact');
1620
- colorKey.exit().remove();
1640
+ self.addColorKey(selection, aggKey, splitBy, dataType);
1641
+ selection.classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && self.legendState !== 'compact');
1621
1642
  }
1622
1643
  else {
1623
- d3__namespace.select(this).selectAll('.tsi-colorKey').remove();
1624
- }
1625
- if (d3__namespace.select(this).select('.tsi-eyeIcon').empty()) {
1626
- d3__namespace.select(this).append("button")
1627
- .attr("class", "tsi-eyeIcon")
1628
- .attr('aria-label', () => {
1629
- let showOrHide = self.chartComponentData.displayState[aggKey].splitBys[splitBy].visible ? self.getString('hide series') : self.getString('show series');
1630
- return `${showOrHide} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`;
1631
- })
1632
- .attr('title', () => self.getString('Show/Hide values'))
1633
- .on("click", function (event) {
1634
- event.stopPropagation();
1635
- self.toggleSplitByVisible(aggKey, splitBy);
1636
- d3__namespace.select(this)
1637
- .classed("shown", Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy));
1638
- self.drawChart();
1639
- });
1640
- }
1641
- if (d3__namespace.select(this).select('.tsi-seriesName').empty()) {
1642
- let seriesName = d3__namespace.select(this)
1643
- .append('div')
1644
- .attr('class', 'tsi-seriesName');
1645
- Utils.appendFormattedElementsFromString(seriesName, noSplitBys ? (self.chartComponentData.displayState[aggKey].name) : splitBy);
1644
+ selection.selectAll('.tsi-colorKey').remove();
1646
1645
  }
1646
+ // Add eye icon
1647
+ self.addEyeIcon(selection, aggKey, splitBy);
1648
+ // Add series name
1649
+ self.addSeriesName(selection, aggKey, splitBy);
1650
+ // Add series type selection for numeric data
1647
1651
  if (dataType === DataTypes.Numeric) {
1648
- if (d3__namespace.select(this).select('.tsi-seriesTypeSelection').empty()) {
1649
- d3__namespace.select(this).append("select")
1650
- .attr('aria-label', `${self.getString("Series type selection for")} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`)
1651
- .attr('class', 'tsi-seriesTypeSelection')
1652
- .on("change", function (data) {
1653
- var seriesType = d3__namespace.select(this).property("value");
1654
- self.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
1655
- self.drawChart();
1656
- })
1657
- .on("click", (event) => {
1658
- event.stopPropagation();
1659
- });
1660
- }
1661
- d3__namespace.select(this).select('.tsi-seriesTypeSelection')
1662
- .each(function (d) {
1663
- var typeLabels = d3__namespace.select(this).selectAll('option')
1664
- .data(data => self.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map((type) => {
1665
- return {
1666
- type: type,
1667
- aggKey: aggKey,
1668
- splitBy: splitBy,
1669
- visibleMeasure: Utils.getAgVisibleMeasure(self.chartComponentData.displayState, aggKey, splitBy)
1670
- };
1671
- }));
1672
- typeLabels
1673
- .enter()
1674
- .append("option")
1675
- .attr("class", "seriesTypeLabel")
1676
- .merge(typeLabels)
1677
- .property("selected", (data) => {
1678
- return ((data.type == Utils.getAgVisibleMeasure(self.chartComponentData.displayState, data.aggKey, data.splitBy)) ?
1679
- " selected" : "");
1680
- })
1681
- .text((data) => data.type);
1682
- typeLabels.exit().remove();
1683
- });
1652
+ self.addSeriesTypeSelection(selection, aggKey, splitBy);
1684
1653
  }
1685
1654
  else {
1686
- d3__namespace.select(this).selectAll('.tsi-seriesTypeSelection').remove();
1655
+ selection.selectAll('.tsi-seriesTypeSelection').remove();
1687
1656
  }
1688
1657
  });
1689
1658
  splitByLabels.exit().remove();
1690
- let shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
1659
+ // Show more button
1660
+ const shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
1691
1661
  splitByContainerEntered.selectAll('.tsi-legendShowMore').remove();
1692
1662
  if (this.legendState === 'shown' && shouldShowMore) {
1693
1663
  splitByContainerEntered.append('button')
1694
1664
  .text(this.getString('Show more'))
1695
1665
  .attr('class', 'tsi-legendShowMore')
1696
- .style('display', (this.legendState === 'shown' && shouldShowMore) ? 'block' : 'none')
1666
+ .style('display', 'block')
1697
1667
  .on('click', showMoreSplitBys);
1698
1668
  }
1669
+ // Scroll handler for infinite scrolling
1699
1670
  splitByContainerEntered.on("scroll", function () {
1700
1671
  if (self.chartOptions.legend === 'shown') {
1701
- if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
1672
+ if (this.scrollTop + this.clientHeight + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollHeight) {
1702
1673
  showMoreSplitBys();
1703
1674
  }
1704
1675
  }
@@ -1723,10 +1694,125 @@ class Legend extends Component {
1723
1694
  };
1724
1695
  this.drawChart = drawChart;
1725
1696
  this.legendWidth = legendWidth;
1726
- this.legendElement = d3__namespace.select(renderTarget).insert("div", ":first-child")
1697
+ this.legendElement = d3__namespace.select(renderTarget)
1698
+ .insert("div", ":first-child")
1727
1699
  .attr("class", "tsi-legend")
1728
- .style("left", "0px")
1729
- .style("width", (this.legendWidth) + "px"); // - 16 for the width of the padding
1700
+ .style("left", "0px");
1701
+ // Note: width is set conditionally in draw() based on legendState
1702
+ // to allow CSS to control width in compact mode
1703
+ }
1704
+ getHeightPerSplitBy(aggKey) {
1705
+ const dataType = this.chartComponentData.displayState[aggKey].dataType;
1706
+ return dataType === DataTypes.Numeric
1707
+ ? LEGEND_CONSTANTS.NUMERIC_SPLITBY_HEIGHT
1708
+ : LEGEND_CONSTANTS.NON_NUMERIC_SPLITBY_HEIGHT;
1709
+ }
1710
+ addColorKey(selection, aggKey, splitBy, dataType) {
1711
+ const colors = Utils.createSplitByColors(this.chartComponentData.displayState, aggKey, this.chartOptions.keepSplitByColor);
1712
+ const splitByKeys = Object.keys(this.chartComponentData.timeArrays[aggKey]);
1713
+ const splitByIndex = splitByKeys.indexOf(splitBy);
1714
+ const color = this.chartComponentData.isFromHeatmap
1715
+ ? this.chartComponentData.displayState[aggKey].color
1716
+ : colors[splitByIndex];
1717
+ const colorKey = selection.selectAll('.tsi-colorKey').data([color]);
1718
+ const colorKeyEntered = colorKey.enter()
1719
+ .append('div')
1720
+ .attr('class', 'tsi-colorKey')
1721
+ .merge(colorKey);
1722
+ if (dataType === DataTypes.Numeric) {
1723
+ colorKeyEntered.style('background-color', d => d);
1724
+ }
1725
+ else {
1726
+ this.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
1727
+ }
1728
+ colorKey.exit().remove();
1729
+ }
1730
+ addEyeIcon(selection, aggKey, splitBy) {
1731
+ if (selection.select('.tsi-eyeIcon').empty()) {
1732
+ selection.append('button')
1733
+ .attr('class', 'tsi-eyeIcon')
1734
+ .attr('aria-label', () => {
1735
+ const showOrHide = this.chartComponentData.displayState[aggKey].splitBys[splitBy].visible
1736
+ ? this.getString('hide series')
1737
+ : this.getString('show series');
1738
+ return `${showOrHide} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`;
1739
+ })
1740
+ .attr('title', () => this.getString('Show/Hide values'))
1741
+ .on('click', (event) => {
1742
+ event.stopPropagation();
1743
+ this.toggleSplitByVisible(aggKey, splitBy);
1744
+ this.drawChart();
1745
+ });
1746
+ }
1747
+ selection.select('.tsi-eyeIcon')
1748
+ .classed('shown', Utils.getAgVisible(this.chartComponentData.displayState, aggKey, splitBy));
1749
+ }
1750
+ addSeriesName(selection, aggKey, splitBy) {
1751
+ if (selection.select('.tsi-seriesName').empty()) {
1752
+ const seriesName = selection.append('div')
1753
+ .attr('class', 'tsi-seriesName');
1754
+ const noSplitBys = Object.keys(this.chartComponentData.timeArrays[aggKey]).length === 1
1755
+ && Object.keys(this.chartComponentData.timeArrays[aggKey])[0] === '';
1756
+ const displayText = noSplitBys
1757
+ ? this.chartComponentData.displayState[aggKey].name
1758
+ : splitBy;
1759
+ Utils.appendFormattedElementsFromString(seriesName, displayText);
1760
+ }
1761
+ }
1762
+ addSeriesTypeSelection(selection, aggKey, splitBy) {
1763
+ if (selection.select('.tsi-seriesTypeSelection').empty()) {
1764
+ selection.append('select')
1765
+ .attr('aria-label', `${this.getString('Series type selection for')} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`)
1766
+ .attr('class', 'tsi-seriesTypeSelection')
1767
+ .on('change', (event) => {
1768
+ const seriesType = d3__namespace.select(event.target).property('value');
1769
+ this.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
1770
+ this.drawChart();
1771
+ })
1772
+ .on('click', (event) => {
1773
+ event.stopPropagation();
1774
+ });
1775
+ }
1776
+ selection.select('.tsi-seriesTypeSelection')
1777
+ .each((d, i, nodes) => {
1778
+ const typeLabels = d3__namespace.select(nodes[i])
1779
+ .selectAll('option')
1780
+ .data(this.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map(type => ({
1781
+ type,
1782
+ aggKey,
1783
+ splitBy,
1784
+ visibleMeasure: Utils.getAgVisibleMeasure(this.chartComponentData.displayState, aggKey, splitBy)
1785
+ })));
1786
+ typeLabels.enter()
1787
+ .append('option')
1788
+ .attr('class', 'seriesTypeLabel')
1789
+ .merge(typeLabels)
1790
+ .property('selected', (data) => data.type === Utils.getAgVisibleMeasure(this.chartComponentData.displayState, data.aggKey, data.splitBy))
1791
+ .text((data) => data.type);
1792
+ typeLabels.exit().remove();
1793
+ });
1794
+ }
1795
+ handleSplitByClick(aggKey, splitBy) {
1796
+ if (this.legendState === 'compact') {
1797
+ this.toggleSplitByVisible(aggKey, splitBy);
1798
+ }
1799
+ else {
1800
+ this.toggleSticky(aggKey, splitBy);
1801
+ }
1802
+ this.drawChart();
1803
+ }
1804
+ handleSplitByMouseOver(aggKey, splitBy) {
1805
+ this.labelMouseover(aggKey, splitBy);
1806
+ }
1807
+ handleSplitByMouseOut(aggKey) {
1808
+ this.svgSelection.selectAll(".tsi-valueElement")
1809
+ .attr("stroke-opacity", 1)
1810
+ .attr("fill-opacity", 1);
1811
+ this.labelMouseout(this.svgSelection, aggKey);
1812
+ }
1813
+ isStickied(aggKey, splitBy) {
1814
+ const stickied = this.chartComponentData.stickiedKey;
1815
+ return stickied?.aggregateKey === aggKey && stickied?.splitBy === splitBy;
1730
1816
  }
1731
1817
  labelMouseoutWrapper(labelMouseout, svgSelection, event) {
1732
1818
  return (svgSelection, aggKey) => {
@@ -1768,14 +1854,11 @@ class Legend extends Component {
1768
1854
  return d == aggKey;
1769
1855
  }).node();
1770
1856
  var prospectiveScrollTop = Math.max((indexOfSplitBy - 1) * this.getHeightPerSplitBy(aggKey), 0);
1771
- if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - 40) || splitByNode.scrollTop > prospectiveScrollTop) {
1857
+ if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - LEGEND_CONSTANTS.SCROLL_BUFFER) || splitByNode.scrollTop > prospectiveScrollTop) {
1772
1858
  splitByNode.scrollTop = prospectiveScrollTop;
1773
1859
  }
1774
1860
  }
1775
1861
  }
1776
- getHeightPerSplitBy(aggKey) {
1777
- return (this.chartComponentData.displayState[aggKey].dataType === DataTypes.Numeric ? NUMERICSPLITBYHEIGHT : NONNUMERICSPLITBYHEIGHT);
1778
- }
1779
1862
  createGradient(gradientKey, svg, values) {
1780
1863
  let gradient = svg.append('defs').append('linearGradient')
1781
1864
  .attr('id', gradientKey).attr('x1', '0%').attr('x2', '0%').attr('y1', '0%').attr('y2', '100%');
@@ -1794,10 +1877,6 @@ class Legend extends Component {
1794
1877
  .attr("stop-opacity", 1);
1795
1878
  });
1796
1879
  }
1797
- isNonNumeric(aggKey) {
1798
- let dataType = this.chartComponentData.displayState[aggKey].dataType;
1799
- return (dataType === DataTypes.Categorical || dataType === DataTypes.Events);
1800
- }
1801
1880
  createNonNumericColorKey(dataType, colorKey, aggKey) {
1802
1881
  if (dataType === DataTypes.Categorical) {
1803
1882
  this.createCategoricalColorKey(colorKey, aggKey);
@@ -1853,6 +1932,13 @@ class Legend extends Component {
1853
1932
  rect.attr('fill', "url(#" + gradientKey + ")");
1854
1933
  }
1855
1934
  }
1935
+ handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys) {
1936
+ const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
1937
+ this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + LEGEND_CONSTANTS.BATCH_SIZE, splitByLabelData.length);
1938
+ if (oldShownSplitBys !== this.chartComponentData.displayState[aggKey].shownSplitBys) {
1939
+ this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
1940
+ }
1941
+ }
1856
1942
  draw(legendState, chartComponentData, labelMouseover, svgSelection, options, labelMouseoutAction = null, stickySeriesAction = null, event) {
1857
1943
  this.chartOptions.setOptions(options);
1858
1944
  this.chartComponentData = chartComponentData;
@@ -1867,6 +1953,13 @@ class Legend extends Component {
1867
1953
  legend.style('visibility', this.legendState != 'hidden')
1868
1954
  .classed('compact', this.legendState == 'compact')
1869
1955
  .classed('hidden', this.legendState == 'hidden');
1956
+ // Set width conditionally - let CSS handle compact mode width
1957
+ if (this.legendState !== 'compact') {
1958
+ legend.style('width', `${this.legendWidth}px`);
1959
+ }
1960
+ else {
1961
+ legend.style('width', null); // Remove inline width style in compact mode
1962
+ }
1870
1963
  let seriesNames = Object.keys(this.chartComponentData.displayState);
1871
1964
  var seriesLabels = legend.selectAll(".tsi-seriesLabel")
1872
1965
  .data(seriesNames, d => d);
@@ -1877,7 +1970,7 @@ class Legend extends Component {
1877
1970
  return "tsi-seriesLabel " + (this.chartComponentData.displayState[d]["visible"] ? " shown" : "");
1878
1971
  })
1879
1972
  .style("min-width", () => {
1880
- return Math.min(124, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
1973
+ return Math.min(LEGEND_CONSTANTS.MIN_SERIES_WIDTH, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
1881
1974
  })
1882
1975
  .style("border-color", function (d, i) {
1883
1976
  if (d3__namespace.select(this).classed("shown"))
@@ -1885,9 +1978,8 @@ class Legend extends Component {
1885
1978
  return "lightgray";
1886
1979
  });
1887
1980
  var self = this;
1888
- const heightPerNameLabel = 25;
1889
1981
  const usableLegendHeight = legend.node().clientHeight;
1890
- var prospectiveAggregateHeight = Math.ceil(Math.max(201, (usableLegendHeight / seriesLabelsEntered.size())));
1982
+ var prospectiveAggregateHeight = Math.ceil(Math.max(LEGEND_CONSTANTS.MIN_AGGREGATE_HEIGHT, (usableLegendHeight / seriesLabelsEntered.size())));
1891
1983
  var contentHeight = 0;
1892
1984
  seriesLabelsEntered.each(function (aggKey, i) {
1893
1985
  let heightPerSplitBy = self.getHeightPerSplitBy(aggKey);
@@ -1943,12 +2035,12 @@ class Legend extends Component {
1943
2035
  seriesNameLabel.exit().remove();
1944
2036
  var splitByContainerHeight;
1945
2037
  if (splitByLabelData.length > (prospectiveAggregateHeight / heightPerSplitBy)) {
1946
- splitByContainerHeight = prospectiveAggregateHeight - heightPerNameLabel;
1947
- contentHeight += splitByContainerHeight + heightPerNameLabel;
2038
+ splitByContainerHeight = prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
2039
+ contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
1948
2040
  }
1949
2041
  else if (splitByLabelData.length > 1 || (splitByLabelData.length === 1 && splitByLabelData[0] !== "")) {
1950
- splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + heightPerNameLabel;
1951
- contentHeight += splitByContainerHeight + heightPerNameLabel;
2042
+ splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
2043
+ contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
1952
2044
  }
1953
2045
  else {
1954
2046
  splitByContainerHeight = heightPerSplitBy;
@@ -1961,43 +2053,28 @@ class Legend extends Component {
1961
2053
  d3__namespace.select(this).style("height", "unset");
1962
2054
  }
1963
2055
  var splitByContainer = d3__namespace.select(this).selectAll(".tsi-splitByContainer").data([aggKey]);
1964
- var splitByContainerEntered = splitByContainer.enter().append("div")
2056
+ splitByContainer.enter().append("div")
1965
2057
  .merge(splitByContainer)
1966
2058
  .classed("tsi-splitByContainer", true);
1967
2059
  let aggSelection = d3__namespace.select(this);
1968
2060
  self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
1969
- splitByContainerEntered.on("scroll", function () {
1970
- if (self.chartOptions.legend == "shown") {
1971
- if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
1972
- const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
1973
- self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
1974
- if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
1975
- self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
1976
- }
1977
- }
1978
- }
1979
- });
2061
+ // Compact mode horizontal scroll handler
1980
2062
  d3__namespace.select(this).on('scroll', function () {
1981
2063
  if (self.chartOptions.legend == "compact") {
1982
- if (this.scrollLeft + this.clientWidth + 40 > this.scrollWidth) {
1983
- const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
1984
- self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
1985
- if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
1986
- this.renderSplitBys(dataType);
1987
- }
2064
+ if (this.scrollLeft + this.clientWidth + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollWidth) {
2065
+ self.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
1988
2066
  }
1989
2067
  }
1990
2068
  });
1991
2069
  splitByContainer.exit().remove();
1992
2070
  });
1993
2071
  if (this.chartOptions.legend == 'shown') {
1994
- legend.node().clientHeight;
1995
2072
  //minSplitBysForFlexGrow: the minimum number of split bys for flex-grow to be triggered
1996
2073
  if (contentHeight < usableLegendHeight) {
1997
2074
  this.legendElement.classed("tsi-flexLegend", true);
1998
2075
  seriesLabelsEntered.each(function (d) {
1999
2076
  let heightPerSplitBy = self.getHeightPerSplitBy(d);
2000
- var minSplitByForFlexGrow = (prospectiveAggregateHeight - heightPerNameLabel) / heightPerSplitBy;
2077
+ var minSplitByForFlexGrow = (prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT) / heightPerSplitBy;
2001
2078
  var splitBysCount = Object.keys(self.chartComponentData.displayState[String(d3__namespace.select(this).data()[0])].splitBys).length;
2002
2079
  if (splitBysCount > minSplitByForFlexGrow) {
2003
2080
  d3__namespace.select(this).style("flex-grow", 1);
@@ -2010,6 +2087,12 @@ class Legend extends Component {
2010
2087
  }
2011
2088
  seriesLabels.exit().remove();
2012
2089
  }
2090
+ destroy() {
2091
+ this.legendElement.remove();
2092
+ // Note: Virtual list cleanup will be added when virtual scrolling is implemented
2093
+ // this.virtualLists.forEach(list => list.destroy());
2094
+ // this.virtualLists.clear();
2095
+ }
2013
2096
  }
2014
2097
 
2015
2098
  class ChartComponentData {
@@ -3461,6 +3544,8 @@ class ContextMenu extends Component {
3461
3544
  this.drawChart = drawChart;
3462
3545
  this.contextMenuElement = d3__namespace.select(renderTarget).insert("div", ":first-child")
3463
3546
  .attr("class", "tsi-contextMenu")
3547
+ .attr("aria-label", "Context Menu")
3548
+ .attr("role", "menu")
3464
3549
  .style("left", "0px")
3465
3550
  .style("top", "0px");
3466
3551
  }
@@ -3549,6 +3634,7 @@ class ContextMenu extends Component {
3549
3634
  var actionElementsEntered = actionElements.enter()
3550
3635
  .append("div")
3551
3636
  .attr("class", `tsi-actionElement`)
3637
+ .attr("role", "menuitem")
3552
3638
  .classed('tsi-hasSubMenu', d => d.isNested)
3553
3639
  .merge(actionElements)
3554
3640
  .text(d => d.name)
@@ -3675,6 +3761,7 @@ class Tooltip extends Component {
3675
3761
  }).data([theme]);
3676
3762
  this.tooltipDiv = tooltip.enter().append('div')
3677
3763
  .attr('class', 'tsi-tooltip')
3764
+ .attr('role', 'tooltip')
3678
3765
  .merge(tooltip)
3679
3766
  .each(function (d) {
3680
3767
  d3__namespace.select(this).selectAll("*").remove();
@@ -6280,6 +6367,10 @@ class LineChart extends TemporalXAxisComponent {
6280
6367
  label.enter()
6281
6368
  .append("text")
6282
6369
  .attr("class", (d) => `tsi-swimLaneLabel-${lane} tsi-swimLaneLabel ${onClickPresentAndValid(d) ? 'tsi-boldOnHover' : ''}`)
6370
+ .attr("role", "heading")
6371
+ .attr("aria-roledescription", this.getString("Swimlane label"))
6372
+ .attr("aria-label", d => d.label)
6373
+ .attr("aria-level", "3")
6283
6374
  .merge(label)
6284
6375
  .style("text-anchor", "middle")
6285
6376
  .attr("transform", d => `translate(${(-this.horizontalLabelOffset + swimlaneLabelConstants.labelLeftPadding)},${(d.offset + d.height / 2)}) rotate(-90)`)
@@ -6304,13 +6395,12 @@ class LineChart extends TemporalXAxisComponent {
6304
6395
  });
6305
6396
  }
6306
6397
  render(data, options, aggregateExpressionOptions) {
6307
- console.log('LineChart render called a');
6308
6398
  super.render(data, options, aggregateExpressionOptions);
6309
6399
  this.originalSwimLanes = this.aggregateExpressionOptions.map((aEO) => {
6310
6400
  return aEO.swimLane;
6311
6401
  });
6312
6402
  this.originalSwimLaneOptions = options.swimLaneOptions;
6313
- this.hasBrush = options && (options.brushMoveAction || options.brushMoveEndAction || options.brushContextMenuActions);
6403
+ this.hasBrush = !!(options && (options.brushMoveAction || options.brushMoveEndAction || options.brushContextMenuActions));
6314
6404
  this.chartOptions.setOptions(options);
6315
6405
  this.chartMargins.right = this.chartOptions.labelSeriesWithMarker ? (SERIESLABELWIDTH + 8) : LINECHARTCHARTMARGINS.right;
6316
6406
  this.width = this.getWidth();
@@ -6359,6 +6449,7 @@ class LineChart extends TemporalXAxisComponent {
6359
6449
  .attr("type", "button")
6360
6450
  .on("click", function () {
6361
6451
  self.overwriteSwimLanes();
6452
+ // cast to any to avoid TS incompatibility when spreading chartOptions instance into ILineChartOptions
6362
6453
  self.render(self.data, { ...self.chartOptions, yAxisState: self.nextStackedState() }, self.aggregateExpressionOptions);
6363
6454
  d3__namespace.select(this).attr("aria-label", () => self.getString("set axis state to") + ' ' + self.nextStackedState());
6364
6455
  setTimeout(() => d3__namespace.select(this).node().focus(), 200);
@@ -6385,6 +6476,7 @@ class LineChart extends TemporalXAxisComponent {
6385
6476
  this.svgSelection = this.targetElement.append("svg")
6386
6477
  .attr("class", "tsi-lineChartSVG tsi-chartSVG")
6387
6478
  .attr('title', this.getString('Line chart'))
6479
+ .attr("role", "img")
6388
6480
  .attr("height", this.height);
6389
6481
  var g = this.svgSelection.append("g")
6390
6482
  .classed("svgGroup", true)
@@ -6770,1257 +6862,6 @@ class LineChart extends TemporalXAxisComponent {
6770
6862
  }
6771
6863
  }
6772
6864
 
6773
- function getDefaultExportFromCjs (x) {
6774
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
6775
- }
6776
-
6777
- var pikaday$1 = {exports: {}};
6778
-
6779
- /*!
6780
- * Pikaday
6781
- *
6782
- * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday
6783
- */
6784
- var pikaday = pikaday$1.exports;
6785
-
6786
- var hasRequiredPikaday;
6787
-
6788
- function requirePikaday () {
6789
- if (hasRequiredPikaday) return pikaday$1.exports;
6790
- hasRequiredPikaday = 1;
6791
- (function (module, exports) {
6792
- (function (root, factory)
6793
- {
6794
-
6795
- var moment;
6796
- {
6797
- // CommonJS module
6798
- // Load moment.js as an optional dependency
6799
- try { moment = require('moment'); } catch (e) {}
6800
- module.exports = factory(moment);
6801
- }
6802
- }(pikaday, function (moment)
6803
- {
6804
-
6805
- /**
6806
- * feature detection and helper functions
6807
- */
6808
- var hasMoment = typeof moment === 'function',
6809
-
6810
- hasEventListeners = !!window.addEventListener,
6811
-
6812
- document = window.document,
6813
-
6814
- sto = window.setTimeout,
6815
-
6816
- addEvent = function(el, e, callback, capture)
6817
- {
6818
- if (hasEventListeners) {
6819
- el.addEventListener(e, callback, !!capture);
6820
- } else {
6821
- el.attachEvent('on' + e, callback);
6822
- }
6823
- },
6824
-
6825
- removeEvent = function(el, e, callback, capture)
6826
- {
6827
- if (hasEventListeners) {
6828
- el.removeEventListener(e, callback, !!capture);
6829
- } else {
6830
- el.detachEvent('on' + e, callback);
6831
- }
6832
- },
6833
-
6834
- trim = function(str)
6835
- {
6836
- return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,'');
6837
- },
6838
-
6839
- hasClass = function(el, cn)
6840
- {
6841
- return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1;
6842
- },
6843
-
6844
- addClass = function(el, cn)
6845
- {
6846
- if (!hasClass(el, cn)) {
6847
- el.className = (el.className === '') ? cn : el.className + ' ' + cn;
6848
- }
6849
- },
6850
-
6851
- removeClass = function(el, cn)
6852
- {
6853
- el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' '));
6854
- },
6855
-
6856
- isArray = function(obj)
6857
- {
6858
- return (/Array/).test(Object.prototype.toString.call(obj));
6859
- },
6860
-
6861
- isDate = function(obj)
6862
- {
6863
- return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime());
6864
- },
6865
-
6866
- isWeekend = function(date)
6867
- {
6868
- var day = date.getDay();
6869
- return day === 0 || day === 6;
6870
- },
6871
-
6872
- isLeapYear = function(year)
6873
- {
6874
- // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
6875
- return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
6876
- },
6877
-
6878
- getDaysInMonth = function(year, month)
6879
- {
6880
- return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
6881
- },
6882
-
6883
- setToStartOfDay = function(date)
6884
- {
6885
- if (isDate(date)) date.setHours(0,0,0,0);
6886
- },
6887
-
6888
- compareDates = function(a,b)
6889
- {
6890
- // weak date comparison (use setToStartOfDay(date) to ensure correct result)
6891
- return a.getTime() === b.getTime();
6892
- },
6893
-
6894
- extend = function(to, from, overwrite)
6895
- {
6896
- var prop, hasProp;
6897
- for (prop in from) {
6898
- hasProp = to[prop] !== undefined;
6899
- if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) {
6900
- if (isDate(from[prop])) {
6901
- if (overwrite) {
6902
- to[prop] = new Date(from[prop].getTime());
6903
- }
6904
- }
6905
- else if (isArray(from[prop])) {
6906
- if (overwrite) {
6907
- to[prop] = from[prop].slice(0);
6908
- }
6909
- } else {
6910
- to[prop] = extend({}, from[prop], overwrite);
6911
- }
6912
- } else if (overwrite || !hasProp) {
6913
- to[prop] = from[prop];
6914
- }
6915
- }
6916
- return to;
6917
- },
6918
-
6919
- fireEvent = function(el, eventName, data)
6920
- {
6921
- var ev;
6922
-
6923
- if (document.createEvent) {
6924
- ev = document.createEvent('HTMLEvents');
6925
- ev.initEvent(eventName, true, false);
6926
- ev = extend(ev, data);
6927
- el.dispatchEvent(ev);
6928
- } else if (document.createEventObject) {
6929
- ev = document.createEventObject();
6930
- ev = extend(ev, data);
6931
- el.fireEvent('on' + eventName, ev);
6932
- }
6933
- },
6934
-
6935
- adjustCalendar = function(calendar) {
6936
- if (calendar.month < 0) {
6937
- calendar.year -= Math.ceil(Math.abs(calendar.month)/12);
6938
- calendar.month += 12;
6939
- }
6940
- if (calendar.month > 11) {
6941
- calendar.year += Math.floor(Math.abs(calendar.month)/12);
6942
- calendar.month -= 12;
6943
- }
6944
- return calendar;
6945
- },
6946
-
6947
- /**
6948
- * defaults and localisation
6949
- */
6950
- defaults = {
6951
-
6952
- // bind the picker to a form field
6953
- field: null,
6954
-
6955
- // automatically show/hide the picker on `field` focus (default `true` if `field` is set)
6956
- bound: undefined,
6957
-
6958
- // position of the datepicker, relative to the field (default to bottom & left)
6959
- // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position)
6960
- position: 'bottom left',
6961
-
6962
- // automatically fit in the viewport even if it means repositioning from the position option
6963
- reposition: true,
6964
-
6965
- // the default output format for `.toString()` and `field` value
6966
- format: 'YYYY-MM-DD',
6967
-
6968
- // the toString function which gets passed a current date object and format
6969
- // and returns a string
6970
- toString: null,
6971
-
6972
- // used to create date object from current input string
6973
- parse: null,
6974
-
6975
- // the initial date to view when first opened
6976
- defaultDate: null,
6977
-
6978
- // make the `defaultDate` the initial selected value
6979
- setDefaultDate: false,
6980
-
6981
- // first day of week (0: Sunday, 1: Monday etc)
6982
- firstDay: 0,
6983
-
6984
- // the default flag for moment's strict date parsing
6985
- formatStrict: false,
6986
-
6987
- // the minimum/earliest date that can be selected
6988
- minDate: null,
6989
- // the maximum/latest date that can be selected
6990
- maxDate: null,
6991
-
6992
- // number of years either side, or array of upper/lower range
6993
- yearRange: 10,
6994
-
6995
- // show week numbers at head of row
6996
- showWeekNumber: false,
6997
-
6998
- // Week picker mode
6999
- pickWholeWeek: false,
7000
-
7001
- // used internally (don't config outside)
7002
- minYear: 0,
7003
- maxYear: 9999,
7004
- minMonth: undefined,
7005
- maxMonth: undefined,
7006
-
7007
- startRange: null,
7008
- endRange: null,
7009
-
7010
- isRTL: false,
7011
-
7012
- // Additional text to append to the year in the calendar title
7013
- yearSuffix: '',
7014
-
7015
- // Render the month after year in the calendar title
7016
- showMonthAfterYear: false,
7017
-
7018
- // Render days of the calendar grid that fall in the next or previous month
7019
- showDaysInNextAndPreviousMonths: false,
7020
-
7021
- // Allows user to select days that fall in the next or previous month
7022
- enableSelectionDaysInNextAndPreviousMonths: false,
7023
-
7024
- // how many months are visible
7025
- numberOfMonths: 1,
7026
-
7027
- // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`)
7028
- // only used for the first display or when a selected date is not visible
7029
- mainCalendar: 'left',
7030
-
7031
- // Specify a DOM element to render the calendar in
7032
- container: undefined,
7033
-
7034
- // Blur field when date is selected
7035
- blurFieldOnSelect : true,
7036
-
7037
- // internationalization
7038
- i18n: {
7039
- previousMonth : 'Previous Month',
7040
- nextMonth : 'Next Month',
7041
- months : ['January','February','March','April','May','June','July','August','September','October','November','December'],
7042
- weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
7043
- weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
7044
- },
7045
-
7046
- // Theme Classname
7047
- theme: null,
7048
-
7049
- // events array
7050
- events: [],
7051
-
7052
- // callback function
7053
- onSelect: null,
7054
- onOpen: null,
7055
- onClose: null,
7056
- onDraw: null,
7057
-
7058
- // Enable keyboard input
7059
- keyboardInput: true
7060
- },
7061
-
7062
-
7063
- /**
7064
- * templating functions to abstract HTML rendering
7065
- */
7066
- renderDayName = function(opts, day, abbr)
7067
- {
7068
- day += opts.firstDay;
7069
- while (day >= 7) {
7070
- day -= 7;
7071
- }
7072
- return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
7073
- },
7074
-
7075
- renderDay = function(opts)
7076
- {
7077
- var arr = [];
7078
- var ariaSelected = 'false';
7079
- if (opts.isEmpty) {
7080
- if (opts.showDaysInNextAndPreviousMonths) {
7081
- arr.push('is-outside-current-month');
7082
-
7083
- if(!opts.enableSelectionDaysInNextAndPreviousMonths) {
7084
- arr.push('is-selection-disabled');
7085
- }
7086
-
7087
- } else {
7088
- return '<td class="is-empty"></td>';
7089
- }
7090
- }
7091
- if (opts.isDisabled) {
7092
- arr.push('is-disabled');
7093
- }
7094
- if (opts.isToday) {
7095
- arr.push('is-today');
7096
- }
7097
- if (opts.isSelected) {
7098
- arr.push('is-selected');
7099
- ariaSelected = 'true';
7100
- }
7101
- if (opts.hasEvent) {
7102
- arr.push('has-event');
7103
- }
7104
- if (opts.isInRange) {
7105
- arr.push('is-inrange');
7106
- }
7107
- if (opts.isStartRange) {
7108
- arr.push('is-startrange');
7109
- }
7110
- if (opts.isEndRange) {
7111
- arr.push('is-endrange');
7112
- }
7113
- return '<td data-day="' + opts.day + '" class="' + arr.join(' ') + '" aria-selected="' + ariaSelected + '">' +
7114
- '<button tabIndex="-1" class="pika-button pika-day" type="button" ' +
7115
- 'data-pika-year="' + opts.year + '" data-pika-month="' + opts.month + '" data-pika-day="' + opts.day + '">' +
7116
- opts.day +
7117
- '</button>' +
7118
- '</td>';
7119
- },
7120
-
7121
- renderWeek = function (d, m, y) {
7122
- // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified.
7123
- var onejan = new Date(y, 0, 1),
7124
- weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7);
7125
- return '<td class="pika-week">' + weekNum + '</td>';
7126
- },
7127
-
7128
- renderRow = function(days, isRTL, pickWholeWeek, isRowSelected)
7129
- {
7130
- return '<tr class="pika-row' + (pickWholeWeek ? ' pick-whole-week' : '') + (isRowSelected ? ' is-selected' : '') + '">' + (isRTL ? days.reverse() : days).join('') + '</tr>';
7131
- },
7132
-
7133
- renderBody = function(rows)
7134
- {
7135
- return '<tbody>' + rows.join('') + '</tbody>';
7136
- },
7137
-
7138
- renderHead = function(opts)
7139
- {
7140
- var i, arr = [];
7141
- if (opts.showWeekNumber) {
7142
- arr.push('<th></th>');
7143
- }
7144
- for (i = 0; i < 7; i++) {
7145
- arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
7146
- }
7147
- return '<thead><tr>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</tr></thead>';
7148
- },
7149
-
7150
- renderTitle = function(instance, c, year, month, refYear, randId)
7151
- {
7152
- var i, j, arr,
7153
- opts = instance._o,
7154
- isMinYear = year === opts.minYear,
7155
- isMaxYear = year === opts.maxYear,
7156
- html = '<div id="' + randId + '" class="pika-title">',
7157
- monthHtml,
7158
- yearHtml,
7159
- prev = true,
7160
- next = true;
7161
-
7162
- for (arr = [], i = 0; i < 12; i++) {
7163
- arr.push('<option value="' + (year === refYear ? i - c : 12 + i - c) + '"' +
7164
- (i === month ? ' selected="selected"': '') +
7165
- ((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled="disabled"' : '') + '>' +
7166
- opts.i18n.months[i] + '</option>');
7167
- }
7168
-
7169
- 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>';
7170
-
7171
- if (isArray(opts.yearRange)) {
7172
- i = opts.yearRange[0];
7173
- j = opts.yearRange[1] + 1;
7174
- } else {
7175
- i = year - opts.yearRange;
7176
- j = 1 + year + opts.yearRange;
7177
- }
7178
-
7179
- for (arr = []; i < j && i <= opts.maxYear; i++) {
7180
- if (i >= opts.minYear) {
7181
- arr.push('<option value="' + i + '"' + (i === year ? ' selected="selected"': '') + '>' + (i) + '</option>');
7182
- }
7183
- }
7184
- 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>';
7185
-
7186
- if (opts.showMonthAfterYear) {
7187
- html += yearHtml + monthHtml;
7188
- } else {
7189
- html += monthHtml + yearHtml;
7190
- }
7191
-
7192
- if (isMinYear && (month === 0 || opts.minMonth >= month)) {
7193
- prev = false;
7194
- }
7195
-
7196
- if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
7197
- next = false;
7198
- }
7199
-
7200
- if (c === 0) {
7201
- html += '<button tabIndex="-1" class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
7202
- }
7203
- if (c === (instance._o.numberOfMonths - 1) ) {
7204
- html += '<button tabIndex="-1" class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
7205
- }
7206
-
7207
- return html += '</div>';
7208
- },
7209
-
7210
- renderTable = function(opts, data, randId)
7211
- {
7212
- return '<table cellpadding="0" cellspacing="0" class="pika-table" role="grid" aria-labelledby="' + randId + '">' + renderHead(opts) + renderBody(data) + '</table>';
7213
- },
7214
-
7215
-
7216
- /**
7217
- * Pikaday constructor
7218
- */
7219
- Pikaday = function(options)
7220
- {
7221
- var self = this,
7222
- opts = self.config(options);
7223
-
7224
- self._onMouseDown = function(e)
7225
- {
7226
- if (!self._v) {
7227
- return;
7228
- }
7229
- e = e || window.event;
7230
- var target = e.target || e.srcElement;
7231
- if (!target) {
7232
- return;
7233
- }
7234
-
7235
- if (!hasClass(target, 'is-disabled')) {
7236
- if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) {
7237
- self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day')));
7238
- if (opts.bound) {
7239
- sto(function() {
7240
- self.hide();
7241
- if (opts.blurFieldOnSelect && opts.field) {
7242
- opts.field.blur();
7243
- }
7244
- }, 100);
7245
- }
7246
- }
7247
- else if (hasClass(target, 'pika-prev')) {
7248
- self.prevMonth();
7249
- }
7250
- else if (hasClass(target, 'pika-next')) {
7251
- self.nextMonth();
7252
- }
7253
- }
7254
- if (!hasClass(target, 'pika-select')) {
7255
- // if this is touch event prevent mouse events emulation
7256
- if (e.preventDefault) {
7257
- e.preventDefault();
7258
- } else {
7259
- e.returnValue = false;
7260
- return false;
7261
- }
7262
- } else {
7263
- self._c = true;
7264
- }
7265
- };
7266
-
7267
- self._onChange = function(e)
7268
- {
7269
- e = e || window.event;
7270
- var target = e.target || e.srcElement;
7271
- if (!target) {
7272
- return;
7273
- }
7274
- if (hasClass(target, 'pika-select-month')) {
7275
- self.gotoMonth(target.value);
7276
- }
7277
- else if (hasClass(target, 'pika-select-year')) {
7278
- self.gotoYear(target.value);
7279
- }
7280
- };
7281
-
7282
- self._onKeyChange = function(e)
7283
- {
7284
- e = e || window.event;
7285
- // ignore if event comes from input box
7286
- if (self.isVisible() && e.target && e.target.type !== 'text') {
7287
-
7288
- switch(e.keyCode){
7289
- case 13:
7290
- case 27:
7291
- if (opts.field) {
7292
- opts.field.blur();
7293
- }
7294
- break;
7295
- case 37:
7296
- e.preventDefault();
7297
- self.adjustDate('subtract', 1);
7298
- break;
7299
- case 38:
7300
- self.adjustDate('subtract', 7);
7301
- break;
7302
- case 39:
7303
- self.adjustDate('add', 1);
7304
- break;
7305
- case 40:
7306
- self.adjustDate('add', 7);
7307
- break;
7308
- }
7309
- }
7310
- };
7311
-
7312
- self._onInputChange = function(e)
7313
- {
7314
- var date;
7315
-
7316
- if (e.firedBy === self) {
7317
- return;
7318
- }
7319
- if (opts.parse) {
7320
- date = opts.parse(opts.field.value, opts.format);
7321
- } else if (hasMoment) {
7322
- date = moment(opts.field.value, opts.format, opts.formatStrict);
7323
- date = (date && date.isValid()) ? date.toDate() : null;
7324
- }
7325
- else {
7326
- date = new Date(Date.parse(opts.field.value));
7327
- }
7328
- // if (isDate(date)) {
7329
- // self.setDate(date);
7330
- // }
7331
- // if (!self._v) {
7332
- // self.show();
7333
- // }
7334
- };
7335
-
7336
- self._onInputFocus = function()
7337
- {
7338
- self.show();
7339
- };
7340
-
7341
- self._onInputClick = function()
7342
- {
7343
- self.show();
7344
- };
7345
-
7346
- self._onInputBlur = function()
7347
- {
7348
- // IE allows pika div to gain focus; catch blur the input field
7349
- var pEl = document.activeElement;
7350
- do {
7351
- if (hasClass(pEl, 'pika-single')) {
7352
- return;
7353
- }
7354
- }
7355
- while ((pEl = pEl.parentNode));
7356
-
7357
- if (!self._c) {
7358
- self._b = sto(function() {
7359
- self.hide();
7360
- }, 50);
7361
- }
7362
- self._c = false;
7363
- };
7364
-
7365
- self._onClick = function(e)
7366
- {
7367
- e = e || window.event;
7368
- var target = e.target || e.srcElement,
7369
- pEl = target;
7370
- if (!target) {
7371
- return;
7372
- }
7373
- if (!hasEventListeners && hasClass(target, 'pika-select')) {
7374
- if (!target.onchange) {
7375
- target.setAttribute('onchange', 'return;');
7376
- addEvent(target, 'change', self._onChange);
7377
- }
7378
- }
7379
- do {
7380
- if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) {
7381
- return;
7382
- }
7383
- }
7384
- while ((pEl = pEl.parentNode));
7385
- if (self._v && target !== opts.trigger && pEl !== opts.trigger) {
7386
- self.hide();
7387
- }
7388
- };
7389
-
7390
- self.el = document.createElement('div');
7391
- self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : '');
7392
-
7393
- addEvent(self.el, 'mousedown', self._onMouseDown, true);
7394
- addEvent(self.el, 'touchend', self._onMouseDown, true);
7395
- addEvent(self.el, 'change', self._onChange);
7396
-
7397
- if (opts.keyboardInput) {
7398
- addEvent(document, 'keydown', self._onKeyChange);
7399
- }
7400
-
7401
- if (opts.field) {
7402
- if (opts.container) {
7403
- opts.container.appendChild(self.el);
7404
- } else if (opts.bound) {
7405
- document.body.appendChild(self.el);
7406
- } else {
7407
- opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
7408
- }
7409
- addEvent(opts.field, 'change', self._onInputChange);
7410
-
7411
- if (!opts.defaultDate) {
7412
- if (hasMoment && opts.field.value) {
7413
- opts.defaultDate = moment(opts.field.value, opts.format).toDate();
7414
- } else {
7415
- opts.defaultDate = new Date(Date.parse(opts.field.value));
7416
- }
7417
- opts.setDefaultDate = true;
7418
- }
7419
- }
7420
-
7421
- var defDate = opts.defaultDate;
7422
-
7423
- if (isDate(defDate)) {
7424
- if (opts.setDefaultDate) {
7425
- self.setDate(defDate, true);
7426
- } else {
7427
- self.gotoDate(defDate);
7428
- }
7429
- } else {
7430
- self.gotoDate(new Date());
7431
- }
7432
-
7433
- if (opts.bound) {
7434
- this.hide();
7435
- self.el.className += ' is-bound';
7436
- addEvent(opts.trigger, 'click', self._onInputClick);
7437
- addEvent(opts.trigger, 'focus', self._onInputFocus);
7438
- addEvent(opts.trigger, 'blur', self._onInputBlur);
7439
- } else {
7440
- this.show();
7441
- }
7442
- };
7443
-
7444
-
7445
- /**
7446
- * public Pikaday API
7447
- */
7448
- Pikaday.prototype = {
7449
-
7450
-
7451
- /**
7452
- * configure functionality
7453
- */
7454
- config: function(options)
7455
- {
7456
- if (!this._o) {
7457
- this._o = extend({}, defaults, true);
7458
- }
7459
-
7460
- var opts = extend(this._o, options, true);
7461
-
7462
- opts.isRTL = !!opts.isRTL;
7463
-
7464
- opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
7465
-
7466
- opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null;
7467
-
7468
- opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
7469
-
7470
- opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field;
7471
-
7472
- opts.disableWeekends = !!opts.disableWeekends;
7473
-
7474
- opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null;
7475
-
7476
- var nom = parseInt(opts.numberOfMonths, 10) || 1;
7477
- opts.numberOfMonths = nom > 4 ? 4 : nom;
7478
-
7479
- if (!isDate(opts.minDate)) {
7480
- opts.minDate = false;
7481
- }
7482
- if (!isDate(opts.maxDate)) {
7483
- opts.maxDate = false;
7484
- }
7485
- if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
7486
- opts.maxDate = opts.minDate = false;
7487
- }
7488
- if (opts.minDate) {
7489
- this.setMinDate(opts.minDate);
7490
- }
7491
- if (opts.maxDate) {
7492
- this.setMaxDate(opts.maxDate);
7493
- }
7494
-
7495
- if (isArray(opts.yearRange)) {
7496
- var fallback = new Date().getFullYear() - 10;
7497
- opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
7498
- opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
7499
- } else {
7500
- opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
7501
- if (opts.yearRange > 100) {
7502
- opts.yearRange = 100;
7503
- }
7504
- }
7505
-
7506
- return opts;
7507
- },
7508
-
7509
- /**
7510
- * return a formatted string of the current selection (using Moment.js if available)
7511
- */
7512
- toString: function(format)
7513
- {
7514
- format = format || this._o.format;
7515
- if (!isDate(this._d)) {
7516
- return '';
7517
- }
7518
- if (this._o.toString) {
7519
- return this._o.toString(this._d, format);
7520
- }
7521
- if (hasMoment) {
7522
- return moment(this._d).format(format);
7523
- }
7524
- return this._d.toDateString();
7525
- },
7526
-
7527
- /**
7528
- * return a Moment.js object of the current selection (if available)
7529
- */
7530
- getMoment: function()
7531
- {
7532
- return hasMoment ? moment(this._d) : null;
7533
- },
7534
-
7535
- /**
7536
- * set the current selection from a Moment.js object (if available)
7537
- */
7538
- setMoment: function(date, preventOnSelect)
7539
- {
7540
- if (hasMoment && moment.isMoment(date)) {
7541
- this.setDate(date.toDate(), preventOnSelect);
7542
- }
7543
- },
7544
-
7545
- /**
7546
- * return a Date object of the current selection
7547
- */
7548
- getDate: function()
7549
- {
7550
- return isDate(this._d) ? new Date(this._d.getTime()) : null;
7551
- },
7552
-
7553
- /**
7554
- * set the current selection
7555
- */
7556
- setDate: function(date, preventOnSelect)
7557
- {
7558
- if (!date) {
7559
- this._d = null;
7560
-
7561
- if (this._o.field) {
7562
- this._o.field.value = '';
7563
- fireEvent(this._o.field, 'change', { firedBy: this });
7564
- }
7565
-
7566
- return this.draw();
7567
- }
7568
- if (typeof date === 'string') {
7569
- date = new Date(Date.parse(date));
7570
- }
7571
- if (!isDate(date)) {
7572
- return;
7573
- }
7574
-
7575
- var min = this._o.minDate,
7576
- max = this._o.maxDate;
7577
-
7578
- if (isDate(min) && date < min) {
7579
- date = min;
7580
- } else if (isDate(max) && date > max) {
7581
- date = max;
7582
- }
7583
-
7584
- this._d = new Date(date.getTime());
7585
- setToStartOfDay(this._d);
7586
- this.gotoDate(this._d);
7587
-
7588
- if (this._o.field) {
7589
- this._o.field.value = this.toString();
7590
- fireEvent(this._o.field, 'change', { firedBy: this });
7591
- }
7592
- if (!preventOnSelect && typeof this._o.onSelect === 'function') {
7593
- this._o.onSelect.call(this, this.getDate());
7594
- }
7595
- },
7596
-
7597
- /**
7598
- * change view to a specific date
7599
- */
7600
- gotoDate: function(date)
7601
- {
7602
- var newCalendar = true;
7603
-
7604
- if (!isDate(date)) {
7605
- return;
7606
- }
7607
-
7608
- if (this.calendars) {
7609
- var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1),
7610
- lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1),
7611
- visibleDate = date.getTime();
7612
- // get the end of the month
7613
- lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1);
7614
- lastVisibleDate.setDate(lastVisibleDate.getDate()-1);
7615
- newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate);
7616
- }
7617
-
7618
- if (newCalendar) {
7619
- this.calendars = [{
7620
- month: date.getMonth(),
7621
- year: date.getFullYear()
7622
- }];
7623
- if (this._o.mainCalendar === 'right') {
7624
- this.calendars[0].month += 1 - this._o.numberOfMonths;
7625
- }
7626
- }
7627
-
7628
- this.adjustCalendars();
7629
- },
7630
-
7631
- adjustDate: function(sign, days) {
7632
-
7633
- var day = this.getDate() || new Date();
7634
- var difference = parseInt(days)*24*60*60*1000;
7635
-
7636
- var newDay;
7637
-
7638
- if (sign === 'add') {
7639
- newDay = new Date(day.valueOf() + difference);
7640
- } else if (sign === 'subtract') {
7641
- newDay = new Date(day.valueOf() - difference);
7642
- }
7643
-
7644
- this.setDate(newDay);
7645
- },
7646
-
7647
- adjustCalendars: function() {
7648
- this.calendars[0] = adjustCalendar(this.calendars[0]);
7649
- for (var c = 1; c < this._o.numberOfMonths; c++) {
7650
- this.calendars[c] = adjustCalendar({
7651
- month: this.calendars[0].month + c,
7652
- year: this.calendars[0].year
7653
- });
7654
- }
7655
- this.draw();
7656
- },
7657
-
7658
- gotoToday: function()
7659
- {
7660
- this.gotoDate(new Date());
7661
- },
7662
-
7663
- /**
7664
- * change view to a specific month (zero-index, e.g. 0: January)
7665
- */
7666
- gotoMonth: function(month)
7667
- {
7668
- if (!isNaN(month)) {
7669
- this.calendars[0].month = parseInt(month, 10);
7670
- this.adjustCalendars();
7671
- }
7672
- },
7673
-
7674
- nextMonth: function()
7675
- {
7676
- this.calendars[0].month++;
7677
- this.adjustCalendars();
7678
- },
7679
-
7680
- prevMonth: function()
7681
- {
7682
- this.calendars[0].month--;
7683
- this.adjustCalendars();
7684
- },
7685
-
7686
- /**
7687
- * change view to a specific full year (e.g. "2012")
7688
- */
7689
- gotoYear: function(year)
7690
- {
7691
- if (!isNaN(year)) {
7692
- this.calendars[0].year = parseInt(year, 10);
7693
- this.adjustCalendars();
7694
- }
7695
- },
7696
-
7697
- /**
7698
- * change the minDate
7699
- */
7700
- setMinDate: function(value)
7701
- {
7702
- if(value instanceof Date) {
7703
- setToStartOfDay(value);
7704
- this._o.minDate = value;
7705
- this._o.minYear = value.getFullYear();
7706
- this._o.minMonth = value.getMonth();
7707
- } else {
7708
- this._o.minDate = defaults.minDate;
7709
- this._o.minYear = defaults.minYear;
7710
- this._o.minMonth = defaults.minMonth;
7711
- this._o.startRange = defaults.startRange;
7712
- }
7713
-
7714
- this.draw();
7715
- },
7716
-
7717
- /**
7718
- * change the maxDate
7719
- */
7720
- setMaxDate: function(value)
7721
- {
7722
- if(value instanceof Date) {
7723
- setToStartOfDay(value);
7724
- this._o.maxDate = value;
7725
- this._o.maxYear = value.getFullYear();
7726
- this._o.maxMonth = value.getMonth();
7727
- } else {
7728
- this._o.maxDate = defaults.maxDate;
7729
- this._o.maxYear = defaults.maxYear;
7730
- this._o.maxMonth = defaults.maxMonth;
7731
- this._o.endRange = defaults.endRange;
7732
- }
7733
-
7734
- this.draw();
7735
- },
7736
-
7737
- setStartRange: function(value)
7738
- {
7739
- this._o.startRange = value;
7740
- },
7741
-
7742
- setEndRange: function(value)
7743
- {
7744
- this._o.endRange = value;
7745
- },
7746
-
7747
- /**
7748
- * refresh the HTML
7749
- */
7750
- draw: function(force)
7751
- {
7752
- if (!this._v && !force) {
7753
- return;
7754
- }
7755
- var opts = this._o,
7756
- minYear = opts.minYear,
7757
- maxYear = opts.maxYear,
7758
- minMonth = opts.minMonth,
7759
- maxMonth = opts.maxMonth,
7760
- html = '',
7761
- randId;
7762
-
7763
- if (this._y <= minYear) {
7764
- this._y = minYear;
7765
- if (!isNaN(minMonth) && this._m < minMonth) {
7766
- this._m = minMonth;
7767
- }
7768
- }
7769
- if (this._y >= maxYear) {
7770
- this._y = maxYear;
7771
- if (!isNaN(maxMonth) && this._m > maxMonth) {
7772
- this._m = maxMonth;
7773
- }
7774
- }
7775
-
7776
- for (var c = 0; c < opts.numberOfMonths; c++) {
7777
- randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2);
7778
- 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>';
7779
- }
7780
-
7781
- this.el.innerHTML = html;
7782
-
7783
- if (opts.bound) {
7784
- if(opts.field.type !== 'hidden') {
7785
- sto(function() {
7786
- opts.trigger.focus();
7787
- }, 1);
7788
- }
7789
- }
7790
-
7791
- if (typeof this._o.onDraw === 'function') {
7792
- this._o.onDraw(this);
7793
- }
7794
-
7795
- if (opts.bound) {
7796
- // let the screen reader user know to use arrow keys
7797
- opts.field.setAttribute('aria-label', 'Use the arrow keys to pick a date');
7798
- }
7799
- },
7800
-
7801
- adjustPosition: function()
7802
- {
7803
- var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect;
7804
-
7805
- if (this._o.container) return;
7806
-
7807
- this.el.style.position = 'absolute';
7808
-
7809
- field = this._o.trigger;
7810
- pEl = field;
7811
- width = this.el.offsetWidth;
7812
- height = this.el.offsetHeight;
7813
- viewportWidth = window.innerWidth || document.documentElement.clientWidth;
7814
- viewportHeight = window.innerHeight || document.documentElement.clientHeight;
7815
- scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
7816
-
7817
- if (typeof field.getBoundingClientRect === 'function') {
7818
- clientRect = field.getBoundingClientRect();
7819
- left = clientRect.left + window.pageXOffset;
7820
- top = clientRect.bottom + window.pageYOffset;
7821
- } else {
7822
- left = pEl.offsetLeft;
7823
- top = pEl.offsetTop + pEl.offsetHeight;
7824
- while((pEl = pEl.offsetParent)) {
7825
- left += pEl.offsetLeft;
7826
- top += pEl.offsetTop;
7827
- }
7828
- }
7829
-
7830
- // default position is bottom & left
7831
- if ((this._o.reposition && left + width > viewportWidth) ||
7832
- (
7833
- this._o.position.indexOf('right') > -1 &&
7834
- left - width + field.offsetWidth > 0
7835
- )
7836
- ) {
7837
- left = left - width + field.offsetWidth;
7838
- }
7839
- if ((this._o.reposition && top + height > viewportHeight + scrollTop) ||
7840
- (
7841
- this._o.position.indexOf('top') > -1 &&
7842
- top - height - field.offsetHeight > 0
7843
- )
7844
- ) {
7845
- top = top - height - field.offsetHeight;
7846
- }
7847
-
7848
- this.el.style.left = left + 'px';
7849
- this.el.style.top = top + 'px';
7850
- },
7851
-
7852
- /**
7853
- * render HTML for a particular month
7854
- */
7855
- render: function(year, month, randId)
7856
- {
7857
- var opts = this._o,
7858
- now = new Date(),
7859
- days = getDaysInMonth(year, month),
7860
- before = new Date(year, month, 1).getDay(),
7861
- data = [],
7862
- row = [];
7863
- setToStartOfDay(now);
7864
- if (opts.firstDay > 0) {
7865
- before -= opts.firstDay;
7866
- if (before < 0) {
7867
- before += 7;
7868
- }
7869
- }
7870
- var previousMonth = month === 0 ? 11 : month - 1,
7871
- nextMonth = month === 11 ? 0 : month + 1,
7872
- yearOfPreviousMonth = month === 0 ? year - 1 : year,
7873
- yearOfNextMonth = month === 11 ? year + 1 : year,
7874
- daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth);
7875
- var cells = days + before,
7876
- after = cells;
7877
- while(after > 7) {
7878
- after -= 7;
7879
- }
7880
- cells += 7 - after;
7881
- var isWeekSelected = false;
7882
- for (var i = 0, r = 0; i < cells; i++)
7883
- {
7884
- var day = new Date(year, month, 1 + (i - before)),
7885
- isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
7886
- isToday = compareDates(day, now),
7887
- hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false,
7888
- isEmpty = i < before || i >= (days + before),
7889
- dayNumber = 1 + (i - before),
7890
- monthNumber = month,
7891
- yearNumber = year,
7892
- isStartRange = opts.startRange && compareDates(opts.startRange, day),
7893
- isEndRange = opts.endRange && compareDates(opts.endRange, day),
7894
- isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange,
7895
- isDisabled = (opts.minDate && day < opts.minDate) ||
7896
- (opts.maxDate && day > opts.maxDate) ||
7897
- (opts.disableWeekends && isWeekend(day)) ||
7898
- (opts.disableDayFn && opts.disableDayFn(day));
7899
-
7900
- if (isEmpty) {
7901
- if (i < before) {
7902
- dayNumber = daysInPreviousMonth + dayNumber;
7903
- monthNumber = previousMonth;
7904
- yearNumber = yearOfPreviousMonth;
7905
- } else {
7906
- dayNumber = dayNumber - days;
7907
- monthNumber = nextMonth;
7908
- yearNumber = yearOfNextMonth;
7909
- }
7910
- }
7911
-
7912
- var dayConfig = {
7913
- day: dayNumber,
7914
- month: monthNumber,
7915
- year: yearNumber,
7916
- hasEvent: hasEvent,
7917
- isSelected: isSelected,
7918
- isToday: isToday,
7919
- isDisabled: isDisabled,
7920
- isEmpty: isEmpty,
7921
- isStartRange: isStartRange,
7922
- isEndRange: isEndRange,
7923
- isInRange: isInRange,
7924
- showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths,
7925
- enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths
7926
- };
7927
-
7928
- if (opts.pickWholeWeek && isSelected) {
7929
- isWeekSelected = true;
7930
- }
7931
-
7932
- row.push(renderDay(dayConfig));
7933
-
7934
- if (++r === 7) {
7935
- if (opts.showWeekNumber) {
7936
- row.unshift(renderWeek(i - before, month, year));
7937
- }
7938
- data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected));
7939
- row = [];
7940
- r = 0;
7941
- isWeekSelected = false;
7942
- }
7943
- }
7944
- return renderTable(opts, data, randId);
7945
- },
7946
-
7947
- isVisible: function()
7948
- {
7949
- return this._v;
7950
- },
7951
-
7952
- show: function()
7953
- {
7954
- if (!this.isVisible()) {
7955
- this._v = true;
7956
- this.draw();
7957
- removeClass(this.el, 'is-hidden');
7958
- if (this._o.bound) {
7959
- addEvent(document, 'click', this._onClick);
7960
- this.adjustPosition();
7961
- }
7962
- if (typeof this._o.onOpen === 'function') {
7963
- this._o.onOpen.call(this);
7964
- }
7965
- }
7966
- },
7967
-
7968
- hide: function()
7969
- {
7970
- var v = this._v;
7971
- if (v !== false) {
7972
- if (this._o.bound) {
7973
- removeEvent(document, 'click', this._onClick);
7974
- }
7975
- this.el.style.position = 'static'; // reset
7976
- this.el.style.left = 'auto';
7977
- this.el.style.top = 'auto';
7978
- addClass(this.el, 'is-hidden');
7979
- this._v = false;
7980
- if (v !== undefined && typeof this._o.onClose === 'function') {
7981
- this._o.onClose.call(this);
7982
- }
7983
- }
7984
- },
7985
-
7986
- /**
7987
- * GAME OVER
7988
- */
7989
- destroy: function()
7990
- {
7991
- var opts = this._o;
7992
-
7993
- this.hide();
7994
- removeEvent(this.el, 'mousedown', this._onMouseDown, true);
7995
- removeEvent(this.el, 'touchend', this._onMouseDown, true);
7996
- removeEvent(this.el, 'change', this._onChange);
7997
- if (opts.keyboardInput) {
7998
- removeEvent(document, 'keydown', this._onKeyChange);
7999
- }
8000
- if (opts.field) {
8001
- removeEvent(opts.field, 'change', this._onInputChange);
8002
- if (opts.bound) {
8003
- removeEvent(opts.trigger, 'click', this._onInputClick);
8004
- removeEvent(opts.trigger, 'focus', this._onInputFocus);
8005
- removeEvent(opts.trigger, 'blur', this._onInputBlur);
8006
- }
8007
- }
8008
- if (this.el.parentNode) {
8009
- this.el.parentNode.removeChild(this.el);
8010
- }
8011
- }
8012
-
8013
- };
8014
-
8015
- return Pikaday;
8016
- }));
8017
- } (pikaday$1));
8018
- return pikaday$1.exports;
8019
- }
8020
-
8021
- var pikadayExports = /*@__PURE__*/ requirePikaday();
8022
- var Pikaday = /*@__PURE__*/getDefaultExportFromCjs(pikadayExports);
8023
-
8024
6865
  class TimezonePicker extends ChartComponent {
8025
6866
  constructor(renderTarget) {
8026
6867
  super(renderTarget);
@@ -8067,6 +6908,28 @@ class TimezonePicker extends ChartComponent {
8067
6908
  }
8068
6909
  }
8069
6910
 
6911
+ // Ensure moment is available globally for Pikaday
6912
+ if (typeof window !== 'undefined') {
6913
+ window.moment = moment$1;
6914
+ }
6915
+ // Export a function to safely create Pikaday instances
6916
+ function createPikaday(options) {
6917
+ if (typeof window === 'undefined') {
6918
+ console.warn('Pikaday requires a browser environment');
6919
+ return null;
6920
+ }
6921
+ const Pikaday = window.Pikaday;
6922
+ if (!Pikaday) {
6923
+ console.error('Pikaday not available. Make sure pikaday.js is loaded.');
6924
+ return null;
6925
+ }
6926
+ if (!moment$1 || !window.moment) {
6927
+ console.error('Moment.js not available. Pikaday requires moment.js.');
6928
+ return null;
6929
+ }
6930
+ return new Pikaday(options);
6931
+ }
6932
+
8070
6933
  class DateTimePicker extends ChartComponent {
8071
6934
  constructor(renderTarget) {
8072
6935
  super(renderTarget);
@@ -8337,8 +7200,8 @@ class DateTimePicker extends ChartComponent {
8337
7200
  weekdays: moment$1.localeData().weekdays(),
8338
7201
  weekdaysShort: moment$1.localeData().weekdaysMin()
8339
7202
  };
8340
- //@ts-ignore
8341
- this.calendarPicker = new Pikaday({
7203
+ // Use the safe Pikaday wrapper
7204
+ this.calendarPicker = createPikaday({
8342
7205
  bound: false,
8343
7206
  container: this.calendar.node(),
8344
7207
  field: this.calendar.node(),
@@ -8374,6 +7237,11 @@ class DateTimePicker extends ChartComponent {
8374
7237
  maxDate: this.convertToCalendarDate(this.maxMillis),
8375
7238
  defaultDate: Utils.adjustDateFromTimezoneOffset(new Date(this.fromMillis))
8376
7239
  });
7240
+ // Check if Pikaday was created successfully
7241
+ if (!this.calendarPicker) {
7242
+ console.error('Failed to create Pikaday calendar. Check moment.js availability.');
7243
+ return;
7244
+ }
8377
7245
  }
8378
7246
  setSelectedQuickTimes() {
8379
7247
  let isSelected = d => {
@@ -8634,7 +7502,30 @@ class DateTimeButton extends ChartComponent {
8634
7502
  this.pickerIsVisible = false;
8635
7503
  }
8636
7504
  buttonDateTimeFormat(millis) {
8637
- return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
7505
+ const date = new Date(millis);
7506
+ const locale = this.chartOptions.dateLocale || 'en-US';
7507
+ const is24Hour = this.chartOptions.is24HourTime !== false;
7508
+ const formatOptions = {
7509
+ year: 'numeric',
7510
+ month: '2-digit',
7511
+ day: '2-digit',
7512
+ hour: '2-digit',
7513
+ minute: '2-digit',
7514
+ second: '2-digit',
7515
+ hour12: !is24Hour
7516
+ };
7517
+ try {
7518
+ if (this.chartOptions.offset && this.chartOptions.offset !== 'Local') {
7519
+ formatOptions.timeZone = this.getTimezoneFromOffset(this.chartOptions.offset);
7520
+ }
7521
+ const baseFormat = date.toLocaleString(locale, formatOptions);
7522
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
7523
+ return `${baseFormat}.${milliseconds}`;
7524
+ }
7525
+ catch (error) {
7526
+ console.warn(`Failed to format date for locale ${locale}:`, error);
7527
+ return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
7528
+ }
8638
7529
  }
8639
7530
  render(chartOptions, minMillis, maxMillis, onSet = null) {
8640
7531
  this.chartOptions.setOptions(chartOptions);
@@ -8654,11 +7545,22 @@ class DateTimeButton extends ChartComponent {
8654
7545
  }
8655
7546
  super.themify(d3__namespace.select(this.renderTarget), this.chartOptions.theme);
8656
7547
  }
7548
+ getTimezoneFromOffset(offset) {
7549
+ const timezoneMap = {
7550
+ 'UTC': 'UTC',
7551
+ 'EST': 'America/New_York',
7552
+ 'PST': 'America/Los_Angeles',
7553
+ 'CST': 'America/Chicago',
7554
+ 'MST': 'America/Denver'
7555
+ };
7556
+ return timezoneMap[offset] || 'UTC';
7557
+ }
8657
7558
  }
8658
7559
 
8659
7560
  class DateTimeButtonRange extends DateTimeButton {
8660
7561
  constructor(renderTarget) {
8661
7562
  super(renderTarget);
7563
+ this.clickOutsideHandler = null;
8662
7564
  }
8663
7565
  setButtonText(fromMillis, toMillis, isRelative, quickTime) {
8664
7566
  let fromString = this.buttonDateTimeFormat(fromMillis);
@@ -8678,10 +7580,38 @@ class DateTimeButtonRange extends DateTimeButton {
8678
7580
  onClose() {
8679
7581
  this.dateTimePickerContainer.style("display", "none");
8680
7582
  this.dateTimeButton.node().focus();
7583
+ this.removeClickOutsideHandler();
7584
+ }
7585
+ removeClickOutsideHandler() {
7586
+ if (this.clickOutsideHandler) {
7587
+ document.removeEventListener('click', this.clickOutsideHandler);
7588
+ this.clickOutsideHandler = null;
7589
+ }
7590
+ }
7591
+ setupClickOutsideHandler() {
7592
+ // Remove any existing handler first
7593
+ this.removeClickOutsideHandler();
7594
+ // Add handler after a small delay to prevent the opening click from immediately closing the picker
7595
+ setTimeout(() => {
7596
+ this.clickOutsideHandler = (event) => {
7597
+ const pickerElement = this.dateTimePickerContainer.node();
7598
+ const buttonElement = this.dateTimeButton.node();
7599
+ const target = event.target;
7600
+ // Check if click is outside both the picker and the button
7601
+ if (pickerElement && buttonElement &&
7602
+ !pickerElement.contains(target) &&
7603
+ !buttonElement.contains(target)) {
7604
+ this.onClose();
7605
+ }
7606
+ };
7607
+ document.addEventListener('click', this.clickOutsideHandler);
7608
+ }, 0);
8681
7609
  }
8682
7610
  render(chartOptions = {}, minMillis, maxMillis, fromMillis = null, toMillis = null, onSet = null, onCancel = null) {
8683
7611
  super.render(chartOptions, minMillis, maxMillis, onSet);
8684
- d3__namespace.select(this.renderTarget).classed('tsi-dateTimeContainerRange', true);
7612
+ let container = d3__namespace.select(this.renderTarget);
7613
+ container.classed('tsi-dateTimeContainerRange', true);
7614
+ container.style('position', 'relative');
8685
7615
  this.fromMillis = fromMillis;
8686
7616
  this.toMillis = toMillis;
8687
7617
  this.onCancel = onCancel ? onCancel : () => { };
@@ -8707,6 +7637,7 @@ class DateTimeButtonRange extends DateTimeButton {
8707
7637
  this.onClose();
8708
7638
  this.onCancel();
8709
7639
  });
7640
+ this.setupClickOutsideHandler();
8710
7641
  }
8711
7642
  });
8712
7643
  }
@@ -8782,6 +7713,7 @@ class AvailabilityChart extends ChartComponent {
8782
7713
  }
8783
7714
  //transformation of buckets created by the UX client to buckets for the availabilityChart
8784
7715
  createDisplayBuckets(fromMillis, toMillis) {
7716
+ //TODO: "" key is confusing, should be "count" or something similar
8785
7717
  var keysInRange = Object.keys(this.transformedAvailability[0].availabilityCount[""]).reduce((inRangeObj, timestamp, i, timestamps) => {
8786
7718
  var currTSMillis = (new Date(timestamp)).valueOf();
8787
7719
  var nextTSMillis = currTSMillis + this.bucketSize;
@@ -8834,6 +7766,7 @@ class AvailabilityChart extends ChartComponent {
8834
7766
  this.bucketSize = null;
8835
7767
  }
8836
7768
  }
7769
+ //TODO: should have proper types for parameters
8837
7770
  render(transformedAvailability, chartOptions, rawAvailability = {}) {
8838
7771
  this.setChartOptions(chartOptions);
8839
7772
  this.rawAvailability = rawAvailability;
@@ -12419,7 +11352,7 @@ class ModelAutocomplete extends Component {
12419
11352
  super(renderTarget);
12420
11353
  this.chartOptions = new ChartOptions(); // TODO handle onkeyup and oninput in chart options
12421
11354
  }
12422
- render(environmentFqdn, getToken, chartOptions) {
11355
+ render(chartOptions) {
12423
11356
  this.chartOptions.setOptions(chartOptions);
12424
11357
  let targetElement = d3__namespace.select(this.renderTarget);
12425
11358
  targetElement.html("");
@@ -12773,20 +11706,102 @@ class TsqExpression extends ChartDataOptions {
12773
11706
  }
12774
11707
  }
12775
11708
 
11709
+ // Centralized renderer for the hierarchy tree. Keeps a stable D3 data-join and
11710
+ // updates existing DOM nodes instead of fully recreating them on each render.
11711
+ class TreeRenderer {
11712
+ static render(owner, data, target) {
11713
+ // Ensure an <ul> exists for this target (one list per level)
11714
+ let list = target.select('ul');
11715
+ if (list.empty()) {
11716
+ list = target.append('ul').attr('role', target === owner.hierarchyElem ? 'tree' : 'group');
11717
+ }
11718
+ const entries = Object.keys(data).map(k => ({ key: k, item: data[k] }));
11719
+ const liSelection = list.selectAll('li').data(entries, (d) => d && d.key);
11720
+ liSelection.exit().remove();
11721
+ const liEnter = liSelection.enter().append('li')
11722
+ .attr('role', 'none')
11723
+ .classed('tsi-leaf', (d) => !!d.item.isLeaf);
11724
+ const liMerged = liEnter.merge(liSelection);
11725
+ const setSize = entries.length;
11726
+ liMerged.each((d, i, nodes) => {
11727
+ const entry = d;
11728
+ const li = d3__namespace.select(nodes[i]);
11729
+ if (owner.selectedIds && owner.selectedIds.includes(entry.item.id)) {
11730
+ li.classed('tsi-selected', true);
11731
+ }
11732
+ else {
11733
+ li.classed('tsi-selected', false);
11734
+ }
11735
+ // determine instance vs hierarchy node by presence of isLeaf flag
11736
+ const isInstance = !!entry.item.isLeaf;
11737
+ const nodeNameToCheckIfExists = isInstance ? owner.instanceNodeString(entry.item) : entry.key;
11738
+ const displayName = (entry.item && (entry.item.displayName || nodeNameToCheckIfExists)) || nodeNameToCheckIfExists;
11739
+ li.attr('data-display-name', displayName);
11740
+ let itemElem = li.select('.tsi-hierarchyItem');
11741
+ if (itemElem.empty()) {
11742
+ const newListElem = owner.createHierarchyItemElem(entry.item, entry.key);
11743
+ li.node().appendChild(newListElem.node());
11744
+ itemElem = li.select('.tsi-hierarchyItem');
11745
+ }
11746
+ itemElem.attr('aria-label', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
11747
+ itemElem.attr('title', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
11748
+ itemElem.attr('aria-expanded', String(entry.item.isExpanded));
11749
+ // accessibility: set treeitem level and position in set
11750
+ const ariaLevel = String(((entry.item && typeof entry.item.level === 'number') ? entry.item.level : 0) + 1);
11751
+ itemElem.attr('aria-level', ariaLevel);
11752
+ itemElem.attr('aria-posinset', String(i + 1));
11753
+ itemElem.attr('aria-setsize', String(setSize));
11754
+ if (!isInstance) {
11755
+ itemElem.select('.tsi-caret-icon').attr('style', `left: ${(entry.item.level) * 18 + 20}px`);
11756
+ itemElem.select('.tsi-name').text(entry.key);
11757
+ itemElem.select('.tsi-instanceCount').text(entry.item.cumulativeInstanceCount);
11758
+ }
11759
+ else {
11760
+ const nameSpan = itemElem.select('.tsi-name');
11761
+ nameSpan.html('');
11762
+ Utils.appendFormattedElementsFromString(nameSpan, owner.instanceNodeStringToDisplay(entry.item));
11763
+ }
11764
+ entry.item.node = li;
11765
+ if (entry.item.children) {
11766
+ entry.item.isExpanded = true;
11767
+ li.classed('tsi-expanded', true);
11768
+ // recurse using TreeRenderer to keep rendering logic centralized
11769
+ TreeRenderer.render(owner, entry.item.children, li);
11770
+ }
11771
+ else {
11772
+ li.classed('tsi-expanded', false);
11773
+ li.selectAll('ul').remove();
11774
+ }
11775
+ });
11776
+ }
11777
+ }
11778
+
12776
11779
  class HierarchyNavigation extends Component {
12777
11780
  constructor(renderTarget) {
12778
11781
  super(renderTarget);
12779
11782
  this.path = [];
11783
+ // debounce + request cancellation fields
11784
+ this.debounceTimer = null;
11785
+ this.debounceDelay = 250; // ms
11786
+ this.requestCounter = 0; // increments for each outgoing request
11787
+ this.latestRequestId = 0; // id of the most recent request
12780
11788
  //selectedIds
12781
11789
  this.selectedIds = [];
12782
11790
  this.searchEnabled = true;
12783
- this.renderSearchResult = (r, payload, target) => {
11791
+ this.autocompleteEnabled = true; // Enable/disable autocomplete suggestions
11792
+ // Search mode state
11793
+ this.isSearchMode = false;
11794
+ // Paths that should be auto-expanded (Set of path strings like "Factory North/Building A")
11795
+ this.pathsToAutoExpand = new Set();
11796
+ this.renderSearchResult = async (r, payload, target) => {
12784
11797
  const hierarchyData = r.hierarchyNodes?.hits?.length
12785
11798
  ? this.fillDataRecursively(r.hierarchyNodes, payload, payload)
12786
11799
  : {};
12787
11800
  const instancesData = r.instances?.hits?.length
12788
11801
  ? r.instances.hits.reduce((acc, i) => {
12789
- acc[this.instanceNodeIdentifier(i)] = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
11802
+ const inst = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
11803
+ inst.displayName = this.instanceNodeStringToDisplay(i) || '';
11804
+ acc[this.instanceNodeIdentifier(i)] = inst;
12790
11805
  return acc;
12791
11806
  }, {})
12792
11807
  : {};
@@ -12797,7 +11812,17 @@ class HierarchyNavigation extends Component {
12797
11812
  }
12798
11813
  hitCountElem.text(r.hierarchyNodes.hitCount);
12799
11814
  }
12800
- this.renderTree({ ...hierarchyData, ...instancesData }, target);
11815
+ const merged = { ...hierarchyData, ...instancesData };
11816
+ this.renderTree(merged, target);
11817
+ // Auto-expand nodes that should be expanded and load their children
11818
+ for (const key in hierarchyData) {
11819
+ const node = hierarchyData[key];
11820
+ if (node.isExpanded && !node.children) {
11821
+ // This node should be expanded but doesn't have children loaded yet
11822
+ // We need to trigger expansion after the node is rendered
11823
+ await this.autoExpandNode(node);
11824
+ }
11825
+ }
12801
11826
  };
12802
11827
  this.hierarchyNodeIdentifier = (hName) => {
12803
11828
  return hName ? hName : '(' + this.getString("Empty") + ')';
@@ -12819,12 +11844,20 @@ class HierarchyNavigation extends Component {
12819
11844
  const targetElement = d3__namespace.select(this.renderTarget).text('');
12820
11845
  this.hierarchyNavWrapper = this.createHierarchyNavWrapper(targetElement);
12821
11846
  this.selectedIds = preselectedIds;
11847
+ // Allow disabling autocomplete via options
11848
+ if (hierarchyNavOptions.autocompleteEnabled !== undefined) {
11849
+ this.autocompleteEnabled = hierarchyNavOptions.autocompleteEnabled;
11850
+ }
11851
+ // Pre-compute paths that need to be auto-expanded for preselected instances
11852
+ if (preselectedIds && preselectedIds.length > 0) {
11853
+ await this.computePathsToAutoExpand(preselectedIds);
11854
+ }
12822
11855
  //render search wrapper
12823
- //this.renderSearchBox()
11856
+ this.renderSearchBox();
12824
11857
  super.themify(this.hierarchyNavWrapper, this.chartOptions.theme);
12825
11858
  const results = this.createResultsWrapper(this.hierarchyNavWrapper);
12826
11859
  this.hierarchyElem = this.createHierarchyElem(results);
12827
- this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
11860
+ await this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
12828
11861
  }
12829
11862
  createHierarchyNavWrapper(targetElement) {
12830
11863
  return targetElement.append('div').attr('class', 'tsi-hierarchy-nav-wrapper');
@@ -12832,8 +11865,129 @@ class HierarchyNavigation extends Component {
12832
11865
  createResultsWrapper(hierarchyNavWrapper) {
12833
11866
  return hierarchyNavWrapper.append('div').classed('tsi-hierarchy-or-list-wrapper', true);
12834
11867
  }
11868
+ // create hierarchy container and attach keyboard handler
12835
11869
  createHierarchyElem(results) {
12836
- return results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
11870
+ const sel = results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
11871
+ // attach keydown listener for keyboard navigation (delegated)
11872
+ // use native event to preserve focus handling
11873
+ const node = sel.node();
11874
+ if (node) {
11875
+ node.addEventListener('keydown', (ev) => this.onKeyDown(ev));
11876
+ }
11877
+ return sel;
11878
+ }
11879
+ // Keyboard navigation handlers and helpers
11880
+ onKeyDown(ev) {
11881
+ const key = ev.key;
11882
+ const active = document.activeElement;
11883
+ const container = this.hierarchyElem?.node();
11884
+ if (!container)
11885
+ return;
11886
+ const isInside = active && container.contains(active);
11887
+ if (!isInside && (key === 'ArrowDown' || key === 'ArrowUp')) {
11888
+ // focus first visible item on navigation keys
11889
+ const visible = this.getVisibleItemElems();
11890
+ if (visible.length) {
11891
+ this.focusItem(visible[0]);
11892
+ ev.preventDefault();
11893
+ }
11894
+ return;
11895
+ }
11896
+ if (!active)
11897
+ return;
11898
+ const current = active.classList && active.classList.contains('tsi-hierarchyItem') ? active : active.closest('.tsi-hierarchyItem');
11899
+ if (!current)
11900
+ return;
11901
+ switch (key) {
11902
+ case 'ArrowDown':
11903
+ this.focusNext(current);
11904
+ ev.preventDefault();
11905
+ break;
11906
+ case 'ArrowUp':
11907
+ this.focusPrev(current);
11908
+ ev.preventDefault();
11909
+ break;
11910
+ case 'ArrowRight':
11911
+ this.handleArrowRight(current);
11912
+ ev.preventDefault();
11913
+ break;
11914
+ case 'ArrowLeft':
11915
+ this.handleArrowLeft(current);
11916
+ ev.preventDefault();
11917
+ break;
11918
+ case 'Enter':
11919
+ case ' ':
11920
+ // activate (toggle expand or select)
11921
+ current.click();
11922
+ ev.preventDefault();
11923
+ break;
11924
+ }
11925
+ }
11926
+ getVisibleItemElems() {
11927
+ if (!this.hierarchyElem)
11928
+ return [];
11929
+ const root = this.hierarchyElem.node();
11930
+ if (!root)
11931
+ return [];
11932
+ const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
11933
+ return items.filter(i => i.offsetParent !== null && getComputedStyle(i).display !== 'none');
11934
+ }
11935
+ focusItem(elem) {
11936
+ if (!this.hierarchyElem)
11937
+ return;
11938
+ const root = this.hierarchyElem.node();
11939
+ if (!root)
11940
+ return;
11941
+ const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
11942
+ items.forEach(i => i.setAttribute('tabindex', '-1'));
11943
+ elem.setAttribute('tabindex', '0');
11944
+ elem.focus();
11945
+ }
11946
+ focusNext(current) {
11947
+ const visible = this.getVisibleItemElems();
11948
+ const idx = visible.indexOf(current);
11949
+ if (idx >= 0 && idx < visible.length - 1) {
11950
+ this.focusItem(visible[idx + 1]);
11951
+ }
11952
+ }
11953
+ focusPrev(current) {
11954
+ const visible = this.getVisibleItemElems();
11955
+ const idx = visible.indexOf(current);
11956
+ if (idx > 0) {
11957
+ this.focusItem(visible[idx - 1]);
11958
+ }
11959
+ }
11960
+ handleArrowRight(current) {
11961
+ const caret = current.querySelector('.tsi-caret-icon');
11962
+ const expanded = current.getAttribute('aria-expanded') === 'true';
11963
+ if (caret && !expanded) {
11964
+ // expand
11965
+ current.click();
11966
+ return;
11967
+ }
11968
+ // if already expanded, move to first child
11969
+ if (caret && expanded) {
11970
+ const li = current.closest('li');
11971
+ const childLi = li?.querySelector('ul > li');
11972
+ const childItem = childLi?.querySelector('.tsi-hierarchyItem');
11973
+ if (childItem)
11974
+ this.focusItem(childItem);
11975
+ }
11976
+ }
11977
+ handleArrowLeft(current) {
11978
+ const caret = current.querySelector('.tsi-caret-icon');
11979
+ const expanded = current.getAttribute('aria-expanded') === 'true';
11980
+ if (caret && expanded) {
11981
+ // collapse
11982
+ current.click();
11983
+ return;
11984
+ }
11985
+ // move focus to parent
11986
+ const li = current.closest('li');
11987
+ const parentLi = li?.parentElement?.closest('li');
11988
+ const parentItem = parentLi?.querySelector('.tsi-hierarchyItem');
11989
+ if (parentItem)
11990
+ this.focusItem(parentItem);
12837
11991
  }
12838
11992
  // prepares the parameters for search request
12839
11993
  requestPayload(hierarchy = null) {
@@ -12842,32 +11996,7 @@ class HierarchyNavigation extends Component {
12842
11996
  }
12843
11997
  // 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
12844
11998
  renderTree(data, target) {
12845
- let list = target.append('ul').attr("role", target === this.hierarchyElem ? "tree" : "group");
12846
- Object.keys(data).forEach(el => {
12847
- let nodeNameToCheckIfExists = data[el] instanceof InstanceNode ? this.instanceNodeString(data[el]) : el;
12848
- let li;
12849
- if (list.selectAll(".tsi-name").nodes().find(e => e.innerText === nodeNameToCheckIfExists)) {
12850
- li = null;
12851
- }
12852
- else {
12853
- li = list.append('li').classed('tsi-leaf', data[el].isLeaf);
12854
- //if the node is already selected, we want to highlight it
12855
- if (this.selectedIds && this.selectedIds.includes(data[el].id)) {
12856
- li.classed('tsi-selected', true);
12857
- }
12858
- }
12859
- if (!li)
12860
- return;
12861
- li.attr("role", "none");
12862
- let newListElem = this.createHierarchyItemElem(data[el], el);
12863
- li.node().appendChild(newListElem.node());
12864
- data[el].node = li;
12865
- if (data[el].children) {
12866
- data[el].isExpanded = true;
12867
- data[el].node.classed('tsi-expanded', true);
12868
- this.renderTree(data[el].children, data[el].node);
12869
- }
12870
- });
11999
+ TreeRenderer.render(this, data, target);
12871
12000
  }
12872
12001
  renderSearchBox() {
12873
12002
  this.searchWrapperElem = this.hierarchyNavWrapper.append('div').classed('tsi-hierarchy-search', true);
@@ -12876,40 +12005,140 @@ class HierarchyNavigation extends Component {
12876
12005
  let input = inputWrapper
12877
12006
  .append("input")
12878
12007
  .attr("class", "tsi-searchInput")
12879
- .attr("aria-label", this.getString("Search Time Series Instances"))
12880
- .attr("aria-describedby", "tsi-search-desc")
12008
+ .attr("aria-label", this.getString("Search"))
12009
+ .attr("aria-describedby", "tsi-hierarchy-search-desc")
12881
12010
  .attr("role", "combobox")
12882
12011
  .attr("aria-owns", "tsi-search-results")
12883
12012
  .attr("aria-expanded", "false")
12884
12013
  .attr("aria-haspopup", "listbox")
12885
- .attr("placeholder", this.getString("Search Time Series Instances") + "...");
12014
+ .attr("placeholder", this.getString("Search") + "...");
12015
+ // Add ARIA description for screen readers
12016
+ inputWrapper
12017
+ .append("span")
12018
+ .attr("id", "tsi-hierarchy-search-desc")
12019
+ .style("display", "none")
12020
+ .text(this.getString("Search suggestion instruction") || "Use arrow keys to navigate suggestions");
12021
+ // Add live region for search results info
12022
+ inputWrapper
12023
+ .append("div")
12024
+ .attr("class", "tsi-search-results-info")
12025
+ .attr("aria-live", "assertive");
12026
+ // Add clear button
12027
+ let clear = inputWrapper
12028
+ .append("div")
12029
+ .attr("class", "tsi-clear")
12030
+ .attr("tabindex", "0")
12031
+ .attr("role", "button")
12032
+ .attr("aria-label", "Clear Search")
12033
+ .on("click keydown", function (event) {
12034
+ if (Utils.isKeyDownAndNotEnter(event)) {
12035
+ return;
12036
+ }
12037
+ input.node().value = "";
12038
+ self.exitSearchMode();
12039
+ self.ap.close();
12040
+ d3__namespace.select(this).classed("tsi-shown", false);
12041
+ });
12042
+ // Initialize Awesomplete for autocomplete (only if enabled)
12043
+ let Awesomplete = window.Awesomplete;
12044
+ if (this.autocompleteEnabled && Awesomplete) {
12045
+ this.ap = new Awesomplete(input.node(), {
12046
+ minChars: 1,
12047
+ maxItems: 10,
12048
+ autoFirst: true
12049
+ });
12050
+ }
12051
+ else {
12052
+ // Create a dummy object if autocomplete is disabled
12053
+ this.ap = {
12054
+ list: [],
12055
+ close: () => { },
12056
+ evaluate: () => { }
12057
+ };
12058
+ }
12886
12059
  let self = this;
12060
+ let noSuggest = false;
12061
+ let justAwesompleted = false;
12062
+ // Handle autocomplete selection (only if enabled)
12063
+ if (this.autocompleteEnabled) {
12064
+ input.node().addEventListener("awesomplete-selectcomplete", (event) => {
12065
+ noSuggest = true;
12066
+ const selectedValue = event.text.value;
12067
+ // Trigger search with selected value
12068
+ self.performDeepSearch(selectedValue);
12069
+ justAwesompleted = true;
12070
+ });
12071
+ }
12887
12072
  input.on("keydown", (event) => {
12073
+ // Handle ESC key to clear the search box
12074
+ if (event.key === 'Escape') {
12075
+ const inputElement = event.target;
12076
+ inputElement.value = '';
12077
+ // Trigger input event to clear search results
12078
+ self.exitSearchMode();
12079
+ self.ap.close();
12080
+ clear.classed("tsi-shown", false);
12081
+ return;
12082
+ }
12888
12083
  this.chartOptions.onKeydown(event, this.ap);
12889
12084
  });
12890
- var searchText;
12085
+ input.node().addEventListener("keyup", function (event) {
12086
+ if (justAwesompleted) {
12087
+ justAwesompleted = false;
12088
+ return;
12089
+ }
12090
+ let key = event.which || event.keyCode;
12091
+ if (key === 13) {
12092
+ noSuggest = true;
12093
+ }
12094
+ });
12095
+ // Debounced input handler to reduce work while typing
12891
12096
  input.on("input", function (event) {
12892
- searchText = event.target.value;
12893
- if (searchText.length === 0) {
12894
- //clear the tree
12895
- self.hierarchyElem.selectAll('ul').remove();
12896
- self.pathSearchAndRenderResult({ search: { payload: self.requestPayload() }, render: { target: self.hierarchyElem } });
12097
+ const val = event.target.value;
12098
+ // always clear existing timer
12099
+ if (self.debounceTimer) {
12100
+ clearTimeout(self.debounceTimer);
12101
+ self.debounceTimer = null;
12102
+ }
12103
+ // Show/hide clear button
12104
+ clear.classed("tsi-shown", val.length > 0);
12105
+ if (!val || val.length === 0) {
12106
+ // Exit search mode and restore navigation view
12107
+ self.exitSearchMode();
12108
+ self.ap.close();
12109
+ return;
12110
+ }
12111
+ // Populate autocomplete suggestions with instance leaves (only if enabled)
12112
+ if (self.autocompleteEnabled && !noSuggest && val.length >= 1) {
12113
+ self.fetchAutocompleteSuggestions(val);
12897
12114
  }
12898
12115
  else {
12899
- //filter the tree
12900
- self.filterTree(searchText);
12116
+ self.ap.close();
12901
12117
  }
12118
+ // Use deep search for comprehensive results
12119
+ self.debounceTimer = setTimeout(() => {
12120
+ self.performDeepSearch(val);
12121
+ }, self.debounceDelay);
12122
+ noSuggest = false;
12902
12123
  });
12903
12124
  }
12904
12125
  async pathSearchAndRenderResult({ search: { payload, bubbleUpReject = false }, render: { target, locInTarget = null } }) {
12126
+ const requestId = ++this.requestCounter;
12127
+ this.latestRequestId = requestId;
12905
12128
  try {
12906
12129
  const result = await this.searchFunction(payload);
12130
+ if (requestId !== this.latestRequestId) {
12131
+ return;
12132
+ }
12907
12133
  if (result.error) {
12908
12134
  throw result.error;
12909
12135
  }
12910
- this.renderSearchResult(result, payload, target);
12136
+ await this.renderSearchResult(result, payload, target);
12911
12137
  }
12912
12138
  catch (err) {
12139
+ if (requestId !== this.latestRequestId) {
12140
+ return;
12141
+ }
12913
12142
  this.chartOptions.onError("Error in hierarchy navigation", "Failed to complete search", err instanceof XMLHttpRequest ? err : null);
12914
12143
  if (bubbleUpReject) {
12915
12144
  throw err;
@@ -12917,11 +12146,18 @@ class HierarchyNavigation extends Component {
12917
12146
  }
12918
12147
  }
12919
12148
  filterTree(searchText) {
12920
- let tree = this.hierarchyElem.selectAll('ul').nodes()[0];
12921
- let list = tree.querySelectorAll('li');
12149
+ const nodes = this.hierarchyElem.selectAll('ul').nodes();
12150
+ if (!nodes || !nodes.length)
12151
+ return;
12152
+ const tree = nodes[0];
12153
+ if (!tree)
12154
+ return;
12155
+ const list = tree.querySelectorAll('li');
12156
+ const needle = searchText.toLowerCase();
12922
12157
  list.forEach((li) => {
12923
- let name = li.querySelector('.tsi-name').innerText;
12924
- if (name.toLowerCase().includes(searchText.toLowerCase())) {
12158
+ const attrName = li.getAttribute('data-display-name');
12159
+ let name = attrName && attrName.length ? attrName : (li.querySelector('.tsi-name')?.textContent || '');
12160
+ if (name.toLowerCase().includes(needle)) {
12925
12161
  li.style.display = 'block';
12926
12162
  }
12927
12163
  else {
@@ -12929,11 +12165,300 @@ class HierarchyNavigation extends Component {
12929
12165
  }
12930
12166
  });
12931
12167
  }
12168
+ // Fetch autocomplete suggestions for instances (leaves)
12169
+ async fetchAutocompleteSuggestions(searchText) {
12170
+ if (!searchText || searchText.length < 1) {
12171
+ this.ap.list = [];
12172
+ return;
12173
+ }
12174
+ try {
12175
+ // Call server search to get instance suggestions
12176
+ const payload = {
12177
+ path: this.path,
12178
+ searchTerm: searchText,
12179
+ recursive: true,
12180
+ includeInstances: true,
12181
+ // Limit results for autocomplete
12182
+ maxResults: 10
12183
+ };
12184
+ const results = await this.searchFunction(payload);
12185
+ if (results.error) {
12186
+ this.ap.list = [];
12187
+ return;
12188
+ }
12189
+ // Extract instance names for autocomplete suggestions
12190
+ const suggestions = [];
12191
+ if (results.instances?.hits) {
12192
+ results.instances.hits.forEach((i) => {
12193
+ const displayName = this.instanceNodeStringToDisplay(i);
12194
+ const pathStr = i.hierarchyPath && i.hierarchyPath.length > 0
12195
+ ? i.hierarchyPath.join(' > ') + ' > '
12196
+ : '';
12197
+ suggestions.push({
12198
+ label: pathStr + displayName,
12199
+ value: displayName
12200
+ });
12201
+ });
12202
+ }
12203
+ // Update Awesomplete list
12204
+ this.ap.list = suggestions;
12205
+ }
12206
+ catch (err) {
12207
+ // Silently fail for autocomplete - don't interrupt user experience
12208
+ this.ap.list = [];
12209
+ }
12210
+ }
12211
+ // Perform deep search across entire hierarchy using server-side search
12212
+ async performDeepSearch(searchText) {
12213
+ if (!searchText || searchText.length < 2) {
12214
+ this.exitSearchMode();
12215
+ return;
12216
+ }
12217
+ this.isSearchMode = true;
12218
+ const requestId = ++this.requestCounter;
12219
+ this.latestRequestId = requestId;
12220
+ try {
12221
+ // Call server search with recursive flag
12222
+ const payload = {
12223
+ path: this.path,
12224
+ searchTerm: searchText,
12225
+ recursive: true, // Search entire subtree
12226
+ includeInstances: true
12227
+ };
12228
+ const results = await this.searchFunction(payload);
12229
+ if (requestId !== this.latestRequestId)
12230
+ return; // Stale request
12231
+ if (results.error) {
12232
+ throw results.error;
12233
+ }
12234
+ // Render search results in flat list view
12235
+ this.renderSearchResults(results, searchText);
12236
+ }
12237
+ catch (err) {
12238
+ if (requestId !== this.latestRequestId)
12239
+ return;
12240
+ this.chartOptions.onError("Search failed", "Unable to search hierarchy", err instanceof XMLHttpRequest ? err : null);
12241
+ }
12242
+ }
12243
+ // Render search results with breadcrumb paths
12244
+ renderSearchResults(results, searchText) {
12245
+ this.hierarchyElem.selectAll('*').remove();
12246
+ const flatResults = [];
12247
+ // Flatten hierarchy results with full paths
12248
+ if (results.hierarchyNodes?.hits) {
12249
+ results.hierarchyNodes.hits.forEach((h) => {
12250
+ flatResults.push({
12251
+ type: 'hierarchy',
12252
+ name: h.name,
12253
+ path: h.path || [],
12254
+ id: h.id,
12255
+ cumulativeInstanceCount: h.cumulativeInstanceCount,
12256
+ highlightedName: this.highlightMatch(h.name, searchText),
12257
+ node: h
12258
+ });
12259
+ });
12260
+ }
12261
+ // Flatten instance results with full paths
12262
+ if (results.instances?.hits) {
12263
+ results.instances.hits.forEach((i) => {
12264
+ const displayName = this.instanceNodeStringToDisplay(i);
12265
+ flatResults.push({
12266
+ type: 'instance',
12267
+ name: i.name,
12268
+ path: i.hierarchyPath || [],
12269
+ id: i.id,
12270
+ timeSeriesId: i.timeSeriesId,
12271
+ description: i.description,
12272
+ highlightedName: this.highlightMatch(displayName, searchText),
12273
+ node: i
12274
+ });
12275
+ });
12276
+ }
12277
+ // Render flat list with breadcrumbs
12278
+ const searchList = this.hierarchyElem
12279
+ .append('div')
12280
+ .classed('tsi-search-results', true);
12281
+ if (flatResults.length === 0) {
12282
+ searchList.append('div')
12283
+ .classed('tsi-noResults', true)
12284
+ .text(this.getString('No results'));
12285
+ return;
12286
+ }
12287
+ searchList.append('div')
12288
+ .classed('tsi-search-results-header', true)
12289
+ .html(`<strong>${flatResults.length}</strong> ${this.getString('results found') || 'results found'}`);
12290
+ const resultItems = searchList.selectAll('.tsi-search-result-item')
12291
+ .data(flatResults)
12292
+ .enter()
12293
+ .append('div')
12294
+ .classed('tsi-search-result-item', true)
12295
+ .attr('tabindex', '0')
12296
+ .attr('role', 'option')
12297
+ .attr('aria-label', (d) => {
12298
+ const pathStr = d.path.length > 0 ? d.path.join(' > ') + ' > ' : '';
12299
+ return pathStr + d.name;
12300
+ });
12301
+ const self = this;
12302
+ resultItems.each(function (d) {
12303
+ const item = d3__namespace.select(this);
12304
+ // Breadcrumb path
12305
+ if (d.path.length > 0) {
12306
+ item.append('div')
12307
+ .classed('tsi-search-breadcrumb', true)
12308
+ .text(d.path.join(' > '));
12309
+ }
12310
+ // Highlighted name
12311
+ item.append('div')
12312
+ .classed('tsi-search-result-name', true)
12313
+ .html(d.highlightedName);
12314
+ // Instance description or count
12315
+ if (d.type === 'instance' && d.description) {
12316
+ item.append('div')
12317
+ .classed('tsi-search-result-description', true)
12318
+ .text(d.description);
12319
+ }
12320
+ else if (d.type === 'hierarchy') {
12321
+ item.append('div')
12322
+ .classed('tsi-search-result-count', true)
12323
+ .text(`${d.cumulativeInstanceCount || 0} instances`);
12324
+ }
12325
+ });
12326
+ // Click handlers
12327
+ resultItems.on('click keydown', function (event, d) {
12328
+ if (Utils.isKeyDownAndNotEnter(event))
12329
+ return;
12330
+ if (d.type === 'instance') {
12331
+ // Handle instance selection
12332
+ if (self.chartOptions.onInstanceClick) {
12333
+ const inst = new InstanceNode(d.timeSeriesId, d.name, d.path.length, d.id, d.description);
12334
+ // Update selection state
12335
+ if (self.selectedIds && self.selectedIds.includes(d.id)) {
12336
+ self.selectedIds = self.selectedIds.filter(id => id !== d.id);
12337
+ d3__namespace.select(this).classed('tsi-selected', false);
12338
+ }
12339
+ else {
12340
+ self.selectedIds.push(d.id);
12341
+ d3__namespace.select(this).classed('tsi-selected', true);
12342
+ }
12343
+ self.chartOptions.onInstanceClick(inst);
12344
+ }
12345
+ }
12346
+ else {
12347
+ // Navigate to hierarchy node - exit search and expand to that path
12348
+ self.navigateToPath(d.path);
12349
+ }
12350
+ });
12351
+ // Apply selection state to already-selected instances
12352
+ resultItems.each(function (d) {
12353
+ if (d.type === 'instance' && self.selectedIds && self.selectedIds.includes(d.id)) {
12354
+ d3__namespace.select(this).classed('tsi-selected', true);
12355
+ }
12356
+ });
12357
+ }
12358
+ // Exit search mode and restore tree
12359
+ exitSearchMode() {
12360
+ this.isSearchMode = false;
12361
+ this.hierarchyElem.selectAll('*').remove();
12362
+ this.pathSearchAndRenderResult({
12363
+ search: { payload: this.requestPayload() },
12364
+ render: { target: this.hierarchyElem }
12365
+ });
12366
+ }
12367
+ // Navigate to a specific path in the hierarchy
12368
+ async navigateToPath(targetPath) {
12369
+ this.exitSearchMode();
12370
+ // For now, just exit search mode and return to root
12371
+ // In a more advanced implementation, this would progressively
12372
+ // expand nodes along the path to reveal the target
12373
+ // This would require waiting for each level to load before expanding the next
12374
+ }
12375
+ // Pre-compute which paths need to be auto-expanded for preselected instances
12376
+ async computePathsToAutoExpand(instanceIds) {
12377
+ if (!instanceIds || instanceIds.length === 0) {
12378
+ return;
12379
+ }
12380
+ // console.log('[HierarchyNavigation] Computing paths to auto-expand for:', instanceIds);
12381
+ try {
12382
+ this.pathsToAutoExpand.clear();
12383
+ for (const instanceId of instanceIds) {
12384
+ // Search for this specific instance
12385
+ const result = await this.searchFunction({
12386
+ path: this.path,
12387
+ searchTerm: instanceId,
12388
+ recursive: true,
12389
+ includeInstances: true
12390
+ });
12391
+ if (result?.instances?.hits) {
12392
+ for (const instance of result.instances.hits) {
12393
+ // Match by ID
12394
+ if (instance.id === instanceId ||
12395
+ (instance.id && instance.id.includes(instanceId))) {
12396
+ if (instance.hierarchyPath && instance.hierarchyPath.length > 0) {
12397
+ // Add all parent paths that need to be expanded
12398
+ const hierarchyPath = instance.hierarchyPath;
12399
+ for (let i = 1; i <= hierarchyPath.length; i++) {
12400
+ const pathArray = hierarchyPath.slice(0, i);
12401
+ const pathKey = pathArray.join('/');
12402
+ this.pathsToAutoExpand.add(pathKey);
12403
+ }
12404
+ }
12405
+ }
12406
+ }
12407
+ }
12408
+ }
12409
+ // console.log('[HierarchyNavigation] Paths to auto-expand:', Array.from(this.pathsToAutoExpand));
12410
+ }
12411
+ catch (err) {
12412
+ console.warn('Failed to compute paths to auto-expand:', err);
12413
+ }
12414
+ }
12415
+ // Check if a path should be auto-expanded
12416
+ shouldAutoExpand(pathArray) {
12417
+ if (this.pathsToAutoExpand.size === 0) {
12418
+ return false;
12419
+ }
12420
+ const pathKey = pathArray.join('/');
12421
+ return this.pathsToAutoExpand.has(pathKey);
12422
+ }
12423
+ // Auto-expand a node by triggering its expand function
12424
+ async autoExpandNode(node) {
12425
+ if (!node || !node.expand || !node.node) {
12426
+ return;
12427
+ }
12428
+ try {
12429
+ // Wait for the DOM node to be available
12430
+ await new Promise(resolve => setTimeout(resolve, 10));
12431
+ // Mark as expanded visually
12432
+ node.node.classed('tsi-expanded', true);
12433
+ // Call the expand function to load children
12434
+ await node.expand();
12435
+ // console.log(`[HierarchyNavigation] Auto-expanded node: ${node.path.join('/')}`);
12436
+ }
12437
+ catch (err) {
12438
+ console.warn(`Failed to auto-expand node ${node.path.join('/')}:`, err);
12439
+ }
12440
+ }
12441
+ // Highlight search term in text
12442
+ highlightMatch(text, searchTerm) {
12443
+ if (!text)
12444
+ return '';
12445
+ const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12446
+ const regex = new RegExp(`(${escapedTerm})`, 'gi');
12447
+ return text.replace(regex, '<mark>$1</mark>');
12448
+ }
12932
12449
  // creates in-depth data object using the server response for hierarchyNodes to show in the tree all expanded, considering UntilChildren
12933
12450
  fillDataRecursively(hierarchyNodes, payload, payloadForContinuation = null) {
12934
12451
  let data = {};
12935
12452
  hierarchyNodes.hits.forEach((h) => {
12936
12453
  let hierarchy = new HierarchyNode(h.name, payload.path, payload.path.length - this.path.length, h.cumulativeInstanceCount, h.id);
12454
+ // cache display name on node for client-side filtering
12455
+ hierarchy.displayName = h.name || '';
12456
+ // Check if this path should be auto-expanded
12457
+ const shouldExpand = this.shouldAutoExpand(hierarchy.path);
12458
+ if (shouldExpand) {
12459
+ hierarchy.isExpanded = true;
12460
+ //console.log(`[HierarchyNavigation] Auto-expanding node: ${hierarchy.path.join('/')}`);
12461
+ }
12937
12462
  hierarchy.expand = () => {
12938
12463
  hierarchy.isExpanded = true;
12939
12464
  hierarchy.node.classed('tsi-expanded', true);
@@ -12957,7 +12482,7 @@ class HierarchyNavigation extends Component {
12957
12482
  .attr('style', `padding-left: ${hORi.isLeaf ? hORi.level * 18 + 20 : (hORi.level + 1) * 18 + 20}px`)
12958
12483
  .attr('tabindex', 0)
12959
12484
  //.attr('arialabel', isHierarchyNode ? key : Utils.getTimeSeriesIdString(hORi))
12960
- .attr('arialabel', isHierarchyNode ? key : self.getAriaLabel(hORi))
12485
+ .attr('aria-label', isHierarchyNode ? key : self.getAriaLabel(hORi))
12961
12486
  .attr('title', isHierarchyNode ? key : self.getAriaLabel(hORi))
12962
12487
  .attr("role", "treeitem").attr('aria-expanded', hORi.isExpanded)
12963
12488
  .on('click keydown', async function (event) {
@@ -13009,6 +12534,8 @@ class HierarchyNavigation extends Component {
13009
12534
  return hORi.description || hORi.name || hORi.id || Utils.getTimeSeriesIdString(hORi);
13010
12535
  }
13011
12536
  }
12537
+ // TreeRenderer has been moved to its own module: ./TreeRenderer
12538
+ // The rendering logic was extracted to reduce file size and improve testability.
13012
12539
  class HierarchyNode {
13013
12540
  constructor(name, parentPath, level, cumulativeInstanceCount = null, id = null) {
13014
12541
  this.name = name;
@@ -13253,6 +12780,7 @@ class SingleDateTimePicker extends ChartComponent {
13253
12780
  class DateTimeButtonSingle extends DateTimeButton {
13254
12781
  constructor(renderTarget) {
13255
12782
  super(renderTarget);
12783
+ this.clickOutsideHandler = null;
13256
12784
  this.sDTPOnSet = (millis = null) => {
13257
12785
  if (millis !== null) {
13258
12786
  this.dateTimeButton.text(this.buttonDateTimeFormat(millis));
@@ -13265,6 +12793,32 @@ class DateTimeButtonSingle extends DateTimeButton {
13265
12793
  closeSDTP() {
13266
12794
  this.dateTimePickerContainer.style("display", "none");
13267
12795
  this.dateTimeButton.node().focus();
12796
+ this.removeClickOutsideHandler();
12797
+ }
12798
+ removeClickOutsideHandler() {
12799
+ if (this.clickOutsideHandler) {
12800
+ document.removeEventListener('click', this.clickOutsideHandler);
12801
+ this.clickOutsideHandler = null;
12802
+ }
12803
+ }
12804
+ setupClickOutsideHandler() {
12805
+ // Remove any existing handler first
12806
+ this.removeClickOutsideHandler();
12807
+ // Add handler after a small delay to prevent the opening click from immediately closing the picker
12808
+ setTimeout(() => {
12809
+ this.clickOutsideHandler = (event) => {
12810
+ const pickerElement = this.dateTimePickerContainer.node();
12811
+ const buttonElement = this.dateTimeButton.node();
12812
+ const target = event.target;
12813
+ // Check if click is outside both the picker and the button
12814
+ if (pickerElement && buttonElement &&
12815
+ !pickerElement.contains(target) &&
12816
+ !buttonElement.contains(target)) {
12817
+ this.closeSDTP();
12818
+ }
12819
+ };
12820
+ document.addEventListener('click', this.clickOutsideHandler);
12821
+ }, 0);
13268
12822
  }
13269
12823
  render(chartOptions = {}, minMillis, maxMillis, selectedMillis = null, onSet = null) {
13270
12824
  super.render(chartOptions, minMillis, maxMillis, onSet);
@@ -13274,12 +12828,11 @@ class DateTimeButtonSingle extends DateTimeButton {
13274
12828
  if (!this.dateTimePicker) {
13275
12829
  this.dateTimePicker = new SingleDateTimePicker(this.dateTimePickerContainer.node());
13276
12830
  }
13277
- let targetElement = d3__namespace.select(this.renderTarget);
13278
- (targetElement.select(".tsi-dateTimePickerContainer")).selectAll("*");
13279
12831
  this.dateTimeButton.on("click", () => {
13280
12832
  this.chartOptions.dTPIsModal = true;
13281
12833
  this.dateTimePickerContainer.style("display", "block");
13282
12834
  this.dateTimePicker.render(this.chartOptions, this.minMillis, this.maxMillis, this.selectedMillis, this.sDTPOnSet);
12835
+ this.setupClickOutsideHandler();
13283
12836
  });
13284
12837
  }
13285
12838
  }
@@ -13577,8 +13130,16 @@ class ProcessGraphic extends HistoryPlayback {
13577
13130
  class PlaybackControls extends Component {
13578
13131
  constructor(renderTarget, initialTimeStamp = null) {
13579
13132
  super(renderTarget);
13580
- this.handleRadius = 7;
13581
- this.minimumPlaybackInterval = 1000; // 1 second
13133
+ this.playbackInterval = null;
13134
+ this.playButton = null;
13135
+ this.handleElement = null;
13136
+ this.controlsContainer = null;
13137
+ this.track = null;
13138
+ this.selectTimeStampCallback = null;
13139
+ this.wasPlayingWhenDragStarted = false;
13140
+ this.rafId = null;
13141
+ this.handleRadius = PlaybackControls.CONSTANTS.HANDLE_RADIUS;
13142
+ this.minimumPlaybackInterval = PlaybackControls.CONSTANTS.MINIMUM_PLAYBACK_INTERVAL_MS;
13582
13143
  this.playbackInterval = null;
13583
13144
  this.selectedTimeStamp = initialTimeStamp;
13584
13145
  }
@@ -13586,6 +13147,21 @@ class PlaybackControls extends Component {
13586
13147
  return this.selectedTimeStamp;
13587
13148
  }
13588
13149
  render(start, end, onSelectTimeStamp, options, playbackSettings) {
13150
+ // Validate inputs
13151
+ if (!(start instanceof Date) || !(end instanceof Date)) {
13152
+ throw new TypeError('start and end must be Date objects');
13153
+ }
13154
+ if (start >= end) {
13155
+ throw new RangeError('start must be before end');
13156
+ }
13157
+ if (!onSelectTimeStamp || typeof onSelectTimeStamp !== 'function') {
13158
+ throw new TypeError('onSelectTimeStamp must be a function');
13159
+ }
13160
+ // Clean up any pending animation frames before re-rendering
13161
+ if (this.rafId !== null) {
13162
+ cancelAnimationFrame(this.rafId);
13163
+ this.rafId = null;
13164
+ }
13589
13165
  this.end = end;
13590
13166
  this.selectTimeStampCallback = onSelectTimeStamp;
13591
13167
  this.chartOptions.setOptions(options);
@@ -13647,6 +13223,9 @@ class PlaybackControls extends Component {
13647
13223
  this.playButton = this.controlsContainer.append('button')
13648
13224
  .classed('tsi-play-button', this.playbackInterval === null)
13649
13225
  .classed('tsi-pause-button', this.playbackInterval !== null)
13226
+ // Accessibility attributes
13227
+ .attr('aria-label', 'Play/Pause playback')
13228
+ .attr('title', 'Play/Pause playback')
13650
13229
  .on('click', () => {
13651
13230
  if (this.playbackInterval === null) {
13652
13231
  this.play();
@@ -13698,6 +13277,27 @@ class PlaybackControls extends Component {
13698
13277
  this.updateSelection(handlePosition, this.selectedTimeStamp);
13699
13278
  this.selectTimeStampCallback(this.selectedTimeStamp);
13700
13279
  }
13280
+ /**
13281
+ * Cleanup resources to prevent memory leaks
13282
+ */
13283
+ destroy() {
13284
+ this.pause();
13285
+ // Cancel any pending animation frames
13286
+ if (this.rafId !== null) {
13287
+ cancelAnimationFrame(this.rafId);
13288
+ this.rafId = null;
13289
+ }
13290
+ // Remove event listeners
13291
+ if (this.controlsContainer) {
13292
+ this.controlsContainer.selectAll('*').on('.', null);
13293
+ }
13294
+ // Clear DOM references
13295
+ this.playButton = null;
13296
+ this.handleElement = null;
13297
+ this.controlsContainer = null;
13298
+ this.track = null;
13299
+ this.selectTimeStampCallback = null;
13300
+ }
13701
13301
  clamp(number, min, max) {
13702
13302
  let clamped = Math.max(number, min);
13703
13303
  return Math.min(clamped, max);
@@ -13706,9 +13306,17 @@ class PlaybackControls extends Component {
13706
13306
  this.wasPlayingWhenDragStarted = this.wasPlayingWhenDragStarted ||
13707
13307
  (this.playbackInterval !== null);
13708
13308
  this.pause();
13709
- let handlePosition = this.clamp(positionX, 0, this.trackWidth);
13710
- this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
13711
- this.updateSelection(handlePosition, this.selectedTimeStamp);
13309
+ // Use requestAnimationFrame to batch DOM updates for better performance
13310
+ // Cancel any pending animation frame to prevent stacking updates
13311
+ if (this.rafId !== null) {
13312
+ cancelAnimationFrame(this.rafId);
13313
+ }
13314
+ this.rafId = requestAnimationFrame(() => {
13315
+ const handlePosition = this.clamp(positionX, 0, this.trackWidth);
13316
+ this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
13317
+ this.updateSelection(handlePosition, this.selectedTimeStamp);
13318
+ this.rafId = null;
13319
+ });
13712
13320
  }
13713
13321
  onDragEnd() {
13714
13322
  this.selectTimeStampCallback(this.selectedTimeStamp);
@@ -13731,6 +13339,12 @@ class PlaybackControls extends Component {
13731
13339
  .text(this.timeFormatter(timeStamp));
13732
13340
  }
13733
13341
  }
13342
+ PlaybackControls.CONSTANTS = {
13343
+ HANDLE_RADIUS: 7,
13344
+ MINIMUM_PLAYBACK_INTERVAL_MS: 1000,
13345
+ HANDLE_PADDING: 8,
13346
+ AXIS_OFFSET: 6,
13347
+ };
13734
13348
  class TimeAxis extends TemporalXAxisComponent {
13735
13349
  constructor(renderTarget) {
13736
13350
  super(renderTarget);
@@ -13997,6 +13611,10 @@ class GeoProcessGraphic extends HistoryPlayback {
13997
13611
  }
13998
13612
  }
13999
13613
 
13614
+ // Ensure moment is available globally for Pikaday and other components
13615
+ if (typeof window !== 'undefined') {
13616
+ window.moment = moment$1;
13617
+ }
14000
13618
  class UXClient {
14001
13619
  constructor() {
14002
13620
  // Public facing components have class constructors exposed as public UXClient members.