tsichart-core 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1254 -5
- package/dist/index.js +1006 -1416
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1006 -1416
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1040 -1446
- package/dist/index.umd.js.map +1 -1
- package/dist/styles/index.css +9151 -7048
- package/dist/styles/index.css.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -108,7 +108,7 @@ const swimlaneLabelConstants = {
|
|
|
108
108
|
swimLaneLabelHeightPadding: 8,
|
|
109
109
|
labelLeftPadding: 28
|
|
110
110
|
};
|
|
111
|
-
const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '
|
|
111
|
+
const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '}', ']', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
|
|
112
112
|
const NONNUMERICTOPMARGIN = 8;
|
|
113
113
|
const LINECHARTTOPPADDING = 16;
|
|
114
114
|
const GRIDCONTAINERCLASS = 'tsi-gridContainer';
|
|
@@ -1544,35 +1544,41 @@ class Component {
|
|
|
1544
1544
|
}
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1547
|
+
/**
|
|
1548
|
+
* Constants for Legend component layout and behavior
|
|
1549
|
+
*/
|
|
1550
|
+
const LEGEND_CONSTANTS = {
|
|
1551
|
+
/** Height in pixels for each numeric split-by item (includes type selector dropdown) */
|
|
1552
|
+
NUMERIC_SPLITBY_HEIGHT: 44,
|
|
1553
|
+
/** Height in pixels for each non-numeric (categorical/events) split-by item */
|
|
1554
|
+
NON_NUMERIC_SPLITBY_HEIGHT: 24,
|
|
1555
|
+
/** Height in pixels for the series name label header */
|
|
1556
|
+
NAME_LABEL_HEIGHT: 24,
|
|
1557
|
+
/** Buffer distance in pixels from scroll edge before triggering "load more" */
|
|
1558
|
+
SCROLL_BUFFER: 40,
|
|
1559
|
+
/** Number of split-by items to load per batch when paginating */
|
|
1560
|
+
BATCH_SIZE: 20,
|
|
1561
|
+
/** Minimum height in pixels for aggregate container */
|
|
1562
|
+
MIN_AGGREGATE_HEIGHT: 201,
|
|
1563
|
+
/** Minimum width in pixels for each series label in compact mode */
|
|
1564
|
+
MIN_SERIES_WIDTH: 124,
|
|
1565
|
+
};
|
|
1549
1566
|
class Legend extends Component {
|
|
1550
1567
|
constructor(drawChart, renderTarget, legendWidth) {
|
|
1551
1568
|
super(renderTarget);
|
|
1552
1569
|
this.renderSplitBys = (aggKey, aggSelection, dataType, noSplitBys) => {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
var firstSplitByType = firstSplitBy ? firstSplitBy.visibleType : null;
|
|
1556
|
-
Object.keys(this.chartComponentData.displayState[aggKey].splitBys).reduce((isSame, curr) => {
|
|
1557
|
-
return (firstSplitByType == this.chartComponentData.displayState[aggKey].splitBys[curr].visibleType) && isSame;
|
|
1558
|
-
}, true);
|
|
1559
|
-
let showMoreSplitBys = () => {
|
|
1560
|
-
const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
|
|
1561
|
-
this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
|
|
1562
|
-
if (oldShownSplitBys != this.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
1563
|
-
this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
1564
|
-
}
|
|
1565
|
-
};
|
|
1570
|
+
const splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
|
|
1571
|
+
const showMoreSplitBys = () => this.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
|
|
1566
1572
|
let splitByContainer = aggSelection.selectAll(".tsi-splitByContainer").data([aggKey]);
|
|
1567
|
-
|
|
1573
|
+
const splitByContainerEntered = splitByContainer.enter().append("div")
|
|
1568
1574
|
.merge(splitByContainer)
|
|
1569
1575
|
.classed("tsi-splitByContainer", true);
|
|
1570
|
-
|
|
1576
|
+
const splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
|
|
1571
1577
|
.data(splitByLabelData.slice(0, this.chartComponentData.displayState[aggKey].shownSplitBys), function (d) {
|
|
1572
1578
|
return d;
|
|
1573
1579
|
});
|
|
1574
|
-
|
|
1575
|
-
|
|
1580
|
+
const self = this;
|
|
1581
|
+
const splitByLabelsEntered = splitByLabels
|
|
1576
1582
|
.enter()
|
|
1577
1583
|
.append("div")
|
|
1578
1584
|
.merge(splitByLabels)
|
|
@@ -1586,135 +1592,60 @@ class Legend extends Component {
|
|
|
1586
1592
|
}
|
|
1587
1593
|
})
|
|
1588
1594
|
.on("click", function (event, splitBy) {
|
|
1589
|
-
|
|
1590
|
-
self.toggleSplitByVisible(aggKey, splitBy);
|
|
1591
|
-
}
|
|
1592
|
-
else {
|
|
1593
|
-
self.toggleSticky(aggKey, splitBy);
|
|
1594
|
-
}
|
|
1595
|
-
self.drawChart();
|
|
1595
|
+
self.handleSplitByClick(aggKey, splitBy);
|
|
1596
1596
|
})
|
|
1597
1597
|
.on("mouseover", function (event, splitBy) {
|
|
1598
1598
|
event.stopPropagation();
|
|
1599
|
-
self.
|
|
1599
|
+
self.handleSplitByMouseOver(aggKey, splitBy);
|
|
1600
1600
|
})
|
|
1601
1601
|
.on("mouseout", function (event) {
|
|
1602
1602
|
event.stopPropagation();
|
|
1603
|
-
self.
|
|
1604
|
-
.attr("stroke-opacity", 1)
|
|
1605
|
-
.attr("fill-opacity", 1);
|
|
1606
|
-
self.labelMouseout(self.svgSelection, aggKey);
|
|
1603
|
+
self.handleSplitByMouseOut(aggKey);
|
|
1607
1604
|
})
|
|
1608
1605
|
.attr("class", (splitBy, i) => {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
return `tsi-splitByLabel
|
|
1606
|
+
const compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
|
|
1607
|
+
const shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
|
|
1608
|
+
return `tsi-splitByLabel ${compact} ${shown}`;
|
|
1612
1609
|
})
|
|
1613
|
-
.classed("stickied", (splitBy, i) =>
|
|
1614
|
-
|
|
1615
|
-
return aggKey == self.chartComponentData.stickiedKey.aggregateKey && splitBy == self.chartComponentData.stickiedKey.splitBy;
|
|
1616
|
-
}
|
|
1617
|
-
});
|
|
1618
|
-
var colors = Utils.createSplitByColors(self.chartComponentData.displayState, aggKey, self.chartOptions.keepSplitByColor);
|
|
1610
|
+
.classed("stickied", (splitBy, i) => self.isStickied(aggKey, splitBy));
|
|
1611
|
+
// Use helper methods to render each split-by element
|
|
1619
1612
|
splitByLabelsEntered.each(function (splitBy, j) {
|
|
1620
|
-
|
|
1613
|
+
const selection = d3.select(this);
|
|
1614
|
+
// Add color key (conditionally based on data type and legend state)
|
|
1621
1615
|
if (dataType === DataTypes.Numeric || noSplitBys || self.legendState === 'compact') {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
.append("div")
|
|
1625
|
-
.attr("class", 'tsi-colorKey')
|
|
1626
|
-
.merge(colorKey);
|
|
1627
|
-
if (dataType === DataTypes.Numeric) {
|
|
1628
|
-
colorKeyEntered.style('background-color', (d) => {
|
|
1629
|
-
return d;
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
else {
|
|
1633
|
-
self.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
|
|
1634
|
-
}
|
|
1635
|
-
d3.select(this).classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && this.legendState !== 'compact');
|
|
1636
|
-
colorKey.exit().remove();
|
|
1616
|
+
self.addColorKey(selection, aggKey, splitBy, dataType);
|
|
1617
|
+
selection.classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && self.legendState !== 'compact');
|
|
1637
1618
|
}
|
|
1638
1619
|
else {
|
|
1639
|
-
|
|
1640
|
-
}
|
|
1641
|
-
if (d3.select(this).select('.tsi-eyeIcon').empty()) {
|
|
1642
|
-
d3.select(this).append("button")
|
|
1643
|
-
.attr("class", "tsi-eyeIcon")
|
|
1644
|
-
.attr('aria-label', () => {
|
|
1645
|
-
let showOrHide = self.chartComponentData.displayState[aggKey].splitBys[splitBy].visible ? self.getString('hide series') : self.getString('show series');
|
|
1646
|
-
return `${showOrHide} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`;
|
|
1647
|
-
})
|
|
1648
|
-
.attr('title', () => self.getString('Show/Hide values'))
|
|
1649
|
-
.on("click", function (event) {
|
|
1650
|
-
event.stopPropagation();
|
|
1651
|
-
self.toggleSplitByVisible(aggKey, splitBy);
|
|
1652
|
-
d3.select(this)
|
|
1653
|
-
.classed("shown", Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy));
|
|
1654
|
-
self.drawChart();
|
|
1655
|
-
});
|
|
1656
|
-
}
|
|
1657
|
-
if (d3.select(this).select('.tsi-seriesName').empty()) {
|
|
1658
|
-
let seriesName = d3.select(this)
|
|
1659
|
-
.append('div')
|
|
1660
|
-
.attr('class', 'tsi-seriesName');
|
|
1661
|
-
Utils.appendFormattedElementsFromString(seriesName, noSplitBys ? (self.chartComponentData.displayState[aggKey].name) : splitBy);
|
|
1620
|
+
selection.selectAll('.tsi-colorKey').remove();
|
|
1662
1621
|
}
|
|
1622
|
+
// Add eye icon
|
|
1623
|
+
self.addEyeIcon(selection, aggKey, splitBy);
|
|
1624
|
+
// Add series name
|
|
1625
|
+
self.addSeriesName(selection, aggKey, splitBy);
|
|
1626
|
+
// Add series type selection for numeric data
|
|
1663
1627
|
if (dataType === DataTypes.Numeric) {
|
|
1664
|
-
|
|
1665
|
-
d3.select(this).append("select")
|
|
1666
|
-
.attr('aria-label', `${self.getString("Series type selection for")} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`)
|
|
1667
|
-
.attr('class', 'tsi-seriesTypeSelection')
|
|
1668
|
-
.on("change", function (data) {
|
|
1669
|
-
var seriesType = d3.select(this).property("value");
|
|
1670
|
-
self.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
|
|
1671
|
-
self.drawChart();
|
|
1672
|
-
})
|
|
1673
|
-
.on("click", (event) => {
|
|
1674
|
-
event.stopPropagation();
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
d3.select(this).select('.tsi-seriesTypeSelection')
|
|
1678
|
-
.each(function (d) {
|
|
1679
|
-
var typeLabels = d3.select(this).selectAll('option')
|
|
1680
|
-
.data(data => self.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map((type) => {
|
|
1681
|
-
return {
|
|
1682
|
-
type: type,
|
|
1683
|
-
aggKey: aggKey,
|
|
1684
|
-
splitBy: splitBy,
|
|
1685
|
-
visibleMeasure: Utils.getAgVisibleMeasure(self.chartComponentData.displayState, aggKey, splitBy)
|
|
1686
|
-
};
|
|
1687
|
-
}));
|
|
1688
|
-
typeLabels
|
|
1689
|
-
.enter()
|
|
1690
|
-
.append("option")
|
|
1691
|
-
.attr("class", "seriesTypeLabel")
|
|
1692
|
-
.merge(typeLabels)
|
|
1693
|
-
.property("selected", (data) => {
|
|
1694
|
-
return ((data.type == Utils.getAgVisibleMeasure(self.chartComponentData.displayState, data.aggKey, data.splitBy)) ?
|
|
1695
|
-
" selected" : "");
|
|
1696
|
-
})
|
|
1697
|
-
.text((data) => data.type);
|
|
1698
|
-
typeLabels.exit().remove();
|
|
1699
|
-
});
|
|
1628
|
+
self.addSeriesTypeSelection(selection, aggKey, splitBy);
|
|
1700
1629
|
}
|
|
1701
1630
|
else {
|
|
1702
|
-
|
|
1631
|
+
selection.selectAll('.tsi-seriesTypeSelection').remove();
|
|
1703
1632
|
}
|
|
1704
1633
|
});
|
|
1705
1634
|
splitByLabels.exit().remove();
|
|
1706
|
-
|
|
1635
|
+
// Show more button
|
|
1636
|
+
const shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
|
|
1707
1637
|
splitByContainerEntered.selectAll('.tsi-legendShowMore').remove();
|
|
1708
1638
|
if (this.legendState === 'shown' && shouldShowMore) {
|
|
1709
1639
|
splitByContainerEntered.append('button')
|
|
1710
1640
|
.text(this.getString('Show more'))
|
|
1711
1641
|
.attr('class', 'tsi-legendShowMore')
|
|
1712
|
-
.style('display',
|
|
1642
|
+
.style('display', 'block')
|
|
1713
1643
|
.on('click', showMoreSplitBys);
|
|
1714
1644
|
}
|
|
1645
|
+
// Scroll handler for infinite scrolling
|
|
1715
1646
|
splitByContainerEntered.on("scroll", function () {
|
|
1716
1647
|
if (self.chartOptions.legend === 'shown') {
|
|
1717
|
-
if (this.scrollTop + this.clientHeight +
|
|
1648
|
+
if (this.scrollTop + this.clientHeight + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollHeight) {
|
|
1718
1649
|
showMoreSplitBys();
|
|
1719
1650
|
}
|
|
1720
1651
|
}
|
|
@@ -1739,10 +1670,125 @@ class Legend extends Component {
|
|
|
1739
1670
|
};
|
|
1740
1671
|
this.drawChart = drawChart;
|
|
1741
1672
|
this.legendWidth = legendWidth;
|
|
1742
|
-
this.legendElement = d3.select(renderTarget)
|
|
1673
|
+
this.legendElement = d3.select(renderTarget)
|
|
1674
|
+
.insert("div", ":first-child")
|
|
1743
1675
|
.attr("class", "tsi-legend")
|
|
1744
|
-
.style("left", "0px")
|
|
1745
|
-
|
|
1676
|
+
.style("left", "0px");
|
|
1677
|
+
// Note: width is set conditionally in draw() based on legendState
|
|
1678
|
+
// to allow CSS to control width in compact mode
|
|
1679
|
+
}
|
|
1680
|
+
getHeightPerSplitBy(aggKey) {
|
|
1681
|
+
const dataType = this.chartComponentData.displayState[aggKey].dataType;
|
|
1682
|
+
return dataType === DataTypes.Numeric
|
|
1683
|
+
? LEGEND_CONSTANTS.NUMERIC_SPLITBY_HEIGHT
|
|
1684
|
+
: LEGEND_CONSTANTS.NON_NUMERIC_SPLITBY_HEIGHT;
|
|
1685
|
+
}
|
|
1686
|
+
addColorKey(selection, aggKey, splitBy, dataType) {
|
|
1687
|
+
const colors = Utils.createSplitByColors(this.chartComponentData.displayState, aggKey, this.chartOptions.keepSplitByColor);
|
|
1688
|
+
const splitByKeys = Object.keys(this.chartComponentData.timeArrays[aggKey]);
|
|
1689
|
+
const splitByIndex = splitByKeys.indexOf(splitBy);
|
|
1690
|
+
const color = this.chartComponentData.isFromHeatmap
|
|
1691
|
+
? this.chartComponentData.displayState[aggKey].color
|
|
1692
|
+
: colors[splitByIndex];
|
|
1693
|
+
const colorKey = selection.selectAll('.tsi-colorKey').data([color]);
|
|
1694
|
+
const colorKeyEntered = colorKey.enter()
|
|
1695
|
+
.append('div')
|
|
1696
|
+
.attr('class', 'tsi-colorKey')
|
|
1697
|
+
.merge(colorKey);
|
|
1698
|
+
if (dataType === DataTypes.Numeric) {
|
|
1699
|
+
colorKeyEntered.style('background-color', d => d);
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
this.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
|
|
1703
|
+
}
|
|
1704
|
+
colorKey.exit().remove();
|
|
1705
|
+
}
|
|
1706
|
+
addEyeIcon(selection, aggKey, splitBy) {
|
|
1707
|
+
if (selection.select('.tsi-eyeIcon').empty()) {
|
|
1708
|
+
selection.append('button')
|
|
1709
|
+
.attr('class', 'tsi-eyeIcon')
|
|
1710
|
+
.attr('aria-label', () => {
|
|
1711
|
+
const showOrHide = this.chartComponentData.displayState[aggKey].splitBys[splitBy].visible
|
|
1712
|
+
? this.getString('hide series')
|
|
1713
|
+
: this.getString('show series');
|
|
1714
|
+
return `${showOrHide} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`;
|
|
1715
|
+
})
|
|
1716
|
+
.attr('title', () => this.getString('Show/Hide values'))
|
|
1717
|
+
.on('click', (event) => {
|
|
1718
|
+
event.stopPropagation();
|
|
1719
|
+
this.toggleSplitByVisible(aggKey, splitBy);
|
|
1720
|
+
this.drawChart();
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
selection.select('.tsi-eyeIcon')
|
|
1724
|
+
.classed('shown', Utils.getAgVisible(this.chartComponentData.displayState, aggKey, splitBy));
|
|
1725
|
+
}
|
|
1726
|
+
addSeriesName(selection, aggKey, splitBy) {
|
|
1727
|
+
if (selection.select('.tsi-seriesName').empty()) {
|
|
1728
|
+
const seriesName = selection.append('div')
|
|
1729
|
+
.attr('class', 'tsi-seriesName');
|
|
1730
|
+
const noSplitBys = Object.keys(this.chartComponentData.timeArrays[aggKey]).length === 1
|
|
1731
|
+
&& Object.keys(this.chartComponentData.timeArrays[aggKey])[0] === '';
|
|
1732
|
+
const displayText = noSplitBys
|
|
1733
|
+
? this.chartComponentData.displayState[aggKey].name
|
|
1734
|
+
: splitBy;
|
|
1735
|
+
Utils.appendFormattedElementsFromString(seriesName, displayText);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
addSeriesTypeSelection(selection, aggKey, splitBy) {
|
|
1739
|
+
if (selection.select('.tsi-seriesTypeSelection').empty()) {
|
|
1740
|
+
selection.append('select')
|
|
1741
|
+
.attr('aria-label', `${this.getString('Series type selection for')} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`)
|
|
1742
|
+
.attr('class', 'tsi-seriesTypeSelection')
|
|
1743
|
+
.on('change', (event) => {
|
|
1744
|
+
const seriesType = d3.select(event.target).property('value');
|
|
1745
|
+
this.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
|
|
1746
|
+
this.drawChart();
|
|
1747
|
+
})
|
|
1748
|
+
.on('click', (event) => {
|
|
1749
|
+
event.stopPropagation();
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
selection.select('.tsi-seriesTypeSelection')
|
|
1753
|
+
.each((d, i, nodes) => {
|
|
1754
|
+
const typeLabels = d3.select(nodes[i])
|
|
1755
|
+
.selectAll('option')
|
|
1756
|
+
.data(this.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map(type => ({
|
|
1757
|
+
type,
|
|
1758
|
+
aggKey,
|
|
1759
|
+
splitBy,
|
|
1760
|
+
visibleMeasure: Utils.getAgVisibleMeasure(this.chartComponentData.displayState, aggKey, splitBy)
|
|
1761
|
+
})));
|
|
1762
|
+
typeLabels.enter()
|
|
1763
|
+
.append('option')
|
|
1764
|
+
.attr('class', 'seriesTypeLabel')
|
|
1765
|
+
.merge(typeLabels)
|
|
1766
|
+
.property('selected', (data) => data.type === Utils.getAgVisibleMeasure(this.chartComponentData.displayState, data.aggKey, data.splitBy))
|
|
1767
|
+
.text((data) => data.type);
|
|
1768
|
+
typeLabels.exit().remove();
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
handleSplitByClick(aggKey, splitBy) {
|
|
1772
|
+
if (this.legendState === 'compact') {
|
|
1773
|
+
this.toggleSplitByVisible(aggKey, splitBy);
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
this.toggleSticky(aggKey, splitBy);
|
|
1777
|
+
}
|
|
1778
|
+
this.drawChart();
|
|
1779
|
+
}
|
|
1780
|
+
handleSplitByMouseOver(aggKey, splitBy) {
|
|
1781
|
+
this.labelMouseover(aggKey, splitBy);
|
|
1782
|
+
}
|
|
1783
|
+
handleSplitByMouseOut(aggKey) {
|
|
1784
|
+
this.svgSelection.selectAll(".tsi-valueElement")
|
|
1785
|
+
.attr("stroke-opacity", 1)
|
|
1786
|
+
.attr("fill-opacity", 1);
|
|
1787
|
+
this.labelMouseout(this.svgSelection, aggKey);
|
|
1788
|
+
}
|
|
1789
|
+
isStickied(aggKey, splitBy) {
|
|
1790
|
+
const stickied = this.chartComponentData.stickiedKey;
|
|
1791
|
+
return stickied?.aggregateKey === aggKey && stickied?.splitBy === splitBy;
|
|
1746
1792
|
}
|
|
1747
1793
|
labelMouseoutWrapper(labelMouseout, svgSelection, event) {
|
|
1748
1794
|
return (svgSelection, aggKey) => {
|
|
@@ -1784,14 +1830,11 @@ class Legend extends Component {
|
|
|
1784
1830
|
return d == aggKey;
|
|
1785
1831
|
}).node();
|
|
1786
1832
|
var prospectiveScrollTop = Math.max((indexOfSplitBy - 1) * this.getHeightPerSplitBy(aggKey), 0);
|
|
1787
|
-
if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight -
|
|
1833
|
+
if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - LEGEND_CONSTANTS.SCROLL_BUFFER) || splitByNode.scrollTop > prospectiveScrollTop) {
|
|
1788
1834
|
splitByNode.scrollTop = prospectiveScrollTop;
|
|
1789
1835
|
}
|
|
1790
1836
|
}
|
|
1791
1837
|
}
|
|
1792
|
-
getHeightPerSplitBy(aggKey) {
|
|
1793
|
-
return (this.chartComponentData.displayState[aggKey].dataType === DataTypes.Numeric ? NUMERICSPLITBYHEIGHT : NONNUMERICSPLITBYHEIGHT);
|
|
1794
|
-
}
|
|
1795
1838
|
createGradient(gradientKey, svg, values) {
|
|
1796
1839
|
let gradient = svg.append('defs').append('linearGradient')
|
|
1797
1840
|
.attr('id', gradientKey).attr('x1', '0%').attr('x2', '0%').attr('y1', '0%').attr('y2', '100%');
|
|
@@ -1810,10 +1853,6 @@ class Legend extends Component {
|
|
|
1810
1853
|
.attr("stop-opacity", 1);
|
|
1811
1854
|
});
|
|
1812
1855
|
}
|
|
1813
|
-
isNonNumeric(aggKey) {
|
|
1814
|
-
let dataType = this.chartComponentData.displayState[aggKey].dataType;
|
|
1815
|
-
return (dataType === DataTypes.Categorical || dataType === DataTypes.Events);
|
|
1816
|
-
}
|
|
1817
1856
|
createNonNumericColorKey(dataType, colorKey, aggKey) {
|
|
1818
1857
|
if (dataType === DataTypes.Categorical) {
|
|
1819
1858
|
this.createCategoricalColorKey(colorKey, aggKey);
|
|
@@ -1869,6 +1908,13 @@ class Legend extends Component {
|
|
|
1869
1908
|
rect.attr('fill', "url(#" + gradientKey + ")");
|
|
1870
1909
|
}
|
|
1871
1910
|
}
|
|
1911
|
+
handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys) {
|
|
1912
|
+
const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
|
|
1913
|
+
this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + LEGEND_CONSTANTS.BATCH_SIZE, splitByLabelData.length);
|
|
1914
|
+
if (oldShownSplitBys !== this.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
1915
|
+
this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1872
1918
|
draw(legendState, chartComponentData, labelMouseover, svgSelection, options, labelMouseoutAction = null, stickySeriesAction = null, event) {
|
|
1873
1919
|
this.chartOptions.setOptions(options);
|
|
1874
1920
|
this.chartComponentData = chartComponentData;
|
|
@@ -1883,6 +1929,13 @@ class Legend extends Component {
|
|
|
1883
1929
|
legend.style('visibility', this.legendState != 'hidden')
|
|
1884
1930
|
.classed('compact', this.legendState == 'compact')
|
|
1885
1931
|
.classed('hidden', this.legendState == 'hidden');
|
|
1932
|
+
// Set width conditionally - let CSS handle compact mode width
|
|
1933
|
+
if (this.legendState !== 'compact') {
|
|
1934
|
+
legend.style('width', `${this.legendWidth}px`);
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
legend.style('width', null); // Remove inline width style in compact mode
|
|
1938
|
+
}
|
|
1886
1939
|
let seriesNames = Object.keys(this.chartComponentData.displayState);
|
|
1887
1940
|
var seriesLabels = legend.selectAll(".tsi-seriesLabel")
|
|
1888
1941
|
.data(seriesNames, d => d);
|
|
@@ -1893,7 +1946,7 @@ class Legend extends Component {
|
|
|
1893
1946
|
return "tsi-seriesLabel " + (this.chartComponentData.displayState[d]["visible"] ? " shown" : "");
|
|
1894
1947
|
})
|
|
1895
1948
|
.style("min-width", () => {
|
|
1896
|
-
return Math.min(
|
|
1949
|
+
return Math.min(LEGEND_CONSTANTS.MIN_SERIES_WIDTH, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
|
|
1897
1950
|
})
|
|
1898
1951
|
.style("border-color", function (d, i) {
|
|
1899
1952
|
if (d3.select(this).classed("shown"))
|
|
@@ -1901,9 +1954,8 @@ class Legend extends Component {
|
|
|
1901
1954
|
return "lightgray";
|
|
1902
1955
|
});
|
|
1903
1956
|
var self = this;
|
|
1904
|
-
const heightPerNameLabel = 25;
|
|
1905
1957
|
const usableLegendHeight = legend.node().clientHeight;
|
|
1906
|
-
var prospectiveAggregateHeight = Math.ceil(Math.max(
|
|
1958
|
+
var prospectiveAggregateHeight = Math.ceil(Math.max(LEGEND_CONSTANTS.MIN_AGGREGATE_HEIGHT, (usableLegendHeight / seriesLabelsEntered.size())));
|
|
1907
1959
|
var contentHeight = 0;
|
|
1908
1960
|
seriesLabelsEntered.each(function (aggKey, i) {
|
|
1909
1961
|
let heightPerSplitBy = self.getHeightPerSplitBy(aggKey);
|
|
@@ -1959,12 +2011,12 @@ class Legend extends Component {
|
|
|
1959
2011
|
seriesNameLabel.exit().remove();
|
|
1960
2012
|
var splitByContainerHeight;
|
|
1961
2013
|
if (splitByLabelData.length > (prospectiveAggregateHeight / heightPerSplitBy)) {
|
|
1962
|
-
splitByContainerHeight = prospectiveAggregateHeight -
|
|
1963
|
-
contentHeight += splitByContainerHeight +
|
|
2014
|
+
splitByContainerHeight = prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
2015
|
+
contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
1964
2016
|
}
|
|
1965
2017
|
else if (splitByLabelData.length > 1 || (splitByLabelData.length === 1 && splitByLabelData[0] !== "")) {
|
|
1966
|
-
splitByContainerHeight = splitByLabelData.length * heightPerSplitBy +
|
|
1967
|
-
contentHeight += splitByContainerHeight +
|
|
2018
|
+
splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
2019
|
+
contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
1968
2020
|
}
|
|
1969
2021
|
else {
|
|
1970
2022
|
splitByContainerHeight = heightPerSplitBy;
|
|
@@ -1977,43 +2029,28 @@ class Legend extends Component {
|
|
|
1977
2029
|
d3.select(this).style("height", "unset");
|
|
1978
2030
|
}
|
|
1979
2031
|
var splitByContainer = d3.select(this).selectAll(".tsi-splitByContainer").data([aggKey]);
|
|
1980
|
-
|
|
2032
|
+
splitByContainer.enter().append("div")
|
|
1981
2033
|
.merge(splitByContainer)
|
|
1982
2034
|
.classed("tsi-splitByContainer", true);
|
|
1983
2035
|
let aggSelection = d3.select(this);
|
|
1984
2036
|
self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
1985
|
-
|
|
1986
|
-
if (self.chartOptions.legend == "shown") {
|
|
1987
|
-
if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
|
|
1988
|
-
const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
|
|
1989
|
-
self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
|
|
1990
|
-
if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
1991
|
-
self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
});
|
|
2037
|
+
// Compact mode horizontal scroll handler
|
|
1996
2038
|
d3.select(this).on('scroll', function () {
|
|
1997
2039
|
if (self.chartOptions.legend == "compact") {
|
|
1998
|
-
if (this.scrollLeft + this.clientWidth +
|
|
1999
|
-
|
|
2000
|
-
self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
|
|
2001
|
-
if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
2002
|
-
this.renderSplitBys(dataType);
|
|
2003
|
-
}
|
|
2040
|
+
if (this.scrollLeft + this.clientWidth + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollWidth) {
|
|
2041
|
+
self.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
|
|
2004
2042
|
}
|
|
2005
2043
|
}
|
|
2006
2044
|
});
|
|
2007
2045
|
splitByContainer.exit().remove();
|
|
2008
2046
|
});
|
|
2009
2047
|
if (this.chartOptions.legend == 'shown') {
|
|
2010
|
-
legend.node().clientHeight;
|
|
2011
2048
|
//minSplitBysForFlexGrow: the minimum number of split bys for flex-grow to be triggered
|
|
2012
2049
|
if (contentHeight < usableLegendHeight) {
|
|
2013
2050
|
this.legendElement.classed("tsi-flexLegend", true);
|
|
2014
2051
|
seriesLabelsEntered.each(function (d) {
|
|
2015
2052
|
let heightPerSplitBy = self.getHeightPerSplitBy(d);
|
|
2016
|
-
var minSplitByForFlexGrow = (prospectiveAggregateHeight -
|
|
2053
|
+
var minSplitByForFlexGrow = (prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT) / heightPerSplitBy;
|
|
2017
2054
|
var splitBysCount = Object.keys(self.chartComponentData.displayState[String(d3.select(this).data()[0])].splitBys).length;
|
|
2018
2055
|
if (splitBysCount > minSplitByForFlexGrow) {
|
|
2019
2056
|
d3.select(this).style("flex-grow", 1);
|
|
@@ -2026,6 +2063,12 @@ class Legend extends Component {
|
|
|
2026
2063
|
}
|
|
2027
2064
|
seriesLabels.exit().remove();
|
|
2028
2065
|
}
|
|
2066
|
+
destroy() {
|
|
2067
|
+
this.legendElement.remove();
|
|
2068
|
+
// Note: Virtual list cleanup will be added when virtual scrolling is implemented
|
|
2069
|
+
// this.virtualLists.forEach(list => list.destroy());
|
|
2070
|
+
// this.virtualLists.clear();
|
|
2071
|
+
}
|
|
2029
2072
|
}
|
|
2030
2073
|
|
|
2031
2074
|
class ChartComponentData {
|
|
@@ -6301,6 +6344,8 @@ class LineChart extends TemporalXAxisComponent {
|
|
|
6301
6344
|
.append("text")
|
|
6302
6345
|
.attr("class", (d) => `tsi-swimLaneLabel-${lane} tsi-swimLaneLabel ${onClickPresentAndValid(d) ? 'tsi-boldOnHover' : ''}`)
|
|
6303
6346
|
.attr("role", "heading")
|
|
6347
|
+
.attr("aria-roledescription", this.getString("Swimlane label"))
|
|
6348
|
+
.attr("aria-label", d => d.label)
|
|
6304
6349
|
.attr("aria-level", "3")
|
|
6305
6350
|
.merge(label)
|
|
6306
6351
|
.style("text-anchor", "middle")
|
|
@@ -6839,1205 +6884,6 @@ class TimezonePicker extends ChartComponent {
|
|
|
6839
6884
|
}
|
|
6840
6885
|
}
|
|
6841
6886
|
|
|
6842
|
-
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
6843
|
-
|
|
6844
|
-
function getDefaultExportFromCjs (x) {
|
|
6845
|
-
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
6846
|
-
}
|
|
6847
|
-
|
|
6848
|
-
var pikaday$1 = {exports: {}};
|
|
6849
|
-
|
|
6850
|
-
/*!
|
|
6851
|
-
* Pikaday
|
|
6852
|
-
*
|
|
6853
|
-
* Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday
|
|
6854
|
-
*/
|
|
6855
|
-
var pikaday = pikaday$1.exports;
|
|
6856
|
-
|
|
6857
|
-
var hasRequiredPikaday;
|
|
6858
|
-
|
|
6859
|
-
function requirePikaday () {
|
|
6860
|
-
if (hasRequiredPikaday) return pikaday$1.exports;
|
|
6861
|
-
hasRequiredPikaday = 1;
|
|
6862
|
-
(function (module, exports) {
|
|
6863
|
-
(function (root, factory) {
|
|
6864
|
-
|
|
6865
|
-
var moment;
|
|
6866
|
-
{
|
|
6867
|
-
// CommonJS module
|
|
6868
|
-
// Load moment.js as an optional dependency
|
|
6869
|
-
try { moment = require('moment'); } catch (e) { moment = (typeof window !== 'undefined' && window.moment) || undefined; }
|
|
6870
|
-
module.exports = factory(moment);
|
|
6871
|
-
}
|
|
6872
|
-
}(typeof self !== 'undefined' ? self :
|
|
6873
|
-
typeof window !== 'undefined' ? window :
|
|
6874
|
-
typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
|
|
6875
|
-
pikaday, function (moment) {
|
|
6876
|
-
|
|
6877
|
-
/**
|
|
6878
|
-
* feature detection and helper functions
|
|
6879
|
-
*/
|
|
6880
|
-
var hasMoment = typeof moment === 'function' || (moment && typeof moment.version === 'string'),
|
|
6881
|
-
|
|
6882
|
-
hasEventListeners = !!window.addEventListener,
|
|
6883
|
-
|
|
6884
|
-
document = window.document,
|
|
6885
|
-
|
|
6886
|
-
sto = window.setTimeout,
|
|
6887
|
-
|
|
6888
|
-
addEvent = function (el, e, callback, capture) {
|
|
6889
|
-
if (hasEventListeners) {
|
|
6890
|
-
el.addEventListener(e, callback, !!capture);
|
|
6891
|
-
} else {
|
|
6892
|
-
el.attachEvent('on' + e, callback);
|
|
6893
|
-
}
|
|
6894
|
-
},
|
|
6895
|
-
|
|
6896
|
-
removeEvent = function (el, e, callback, capture) {
|
|
6897
|
-
if (hasEventListeners) {
|
|
6898
|
-
el.removeEventListener(e, callback, !!capture);
|
|
6899
|
-
} else {
|
|
6900
|
-
el.detachEvent('on' + e, callback);
|
|
6901
|
-
}
|
|
6902
|
-
},
|
|
6903
|
-
|
|
6904
|
-
trim = function (str) {
|
|
6905
|
-
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
|
|
6906
|
-
},
|
|
6907
|
-
|
|
6908
|
-
hasClass = function (el, cn) {
|
|
6909
|
-
return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1;
|
|
6910
|
-
},
|
|
6911
|
-
|
|
6912
|
-
addClass = function (el, cn) {
|
|
6913
|
-
if (!hasClass(el, cn)) {
|
|
6914
|
-
el.className = (el.className === '') ? cn : el.className + ' ' + cn;
|
|
6915
|
-
}
|
|
6916
|
-
},
|
|
6917
|
-
|
|
6918
|
-
removeClass = function (el, cn) {
|
|
6919
|
-
el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' '));
|
|
6920
|
-
},
|
|
6921
|
-
|
|
6922
|
-
isArray = function (obj) {
|
|
6923
|
-
return (/Array/).test(Object.prototype.toString.call(obj));
|
|
6924
|
-
},
|
|
6925
|
-
|
|
6926
|
-
isDate = function (obj) {
|
|
6927
|
-
return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime());
|
|
6928
|
-
},
|
|
6929
|
-
|
|
6930
|
-
isWeekend = function (date) {
|
|
6931
|
-
var day = date.getDay();
|
|
6932
|
-
return day === 0 || day === 6;
|
|
6933
|
-
},
|
|
6934
|
-
|
|
6935
|
-
isLeapYear = function (year) {
|
|
6936
|
-
// solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
|
|
6937
|
-
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
6938
|
-
},
|
|
6939
|
-
|
|
6940
|
-
getDaysInMonth = function (year, month) {
|
|
6941
|
-
return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
|
|
6942
|
-
},
|
|
6943
|
-
|
|
6944
|
-
setToStartOfDay = function (date) {
|
|
6945
|
-
if (isDate(date)) date.setHours(0, 0, 0, 0);
|
|
6946
|
-
},
|
|
6947
|
-
|
|
6948
|
-
compareDates = function (a, b) {
|
|
6949
|
-
// weak date comparison (use setToStartOfDay(date) to ensure correct result)
|
|
6950
|
-
return a.getTime() === b.getTime();
|
|
6951
|
-
},
|
|
6952
|
-
|
|
6953
|
-
extend = function (to, from, overwrite) {
|
|
6954
|
-
var prop, hasProp;
|
|
6955
|
-
for (prop in from) {
|
|
6956
|
-
hasProp = to[prop] !== undefined;
|
|
6957
|
-
if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) {
|
|
6958
|
-
if (isDate(from[prop])) {
|
|
6959
|
-
if (overwrite) {
|
|
6960
|
-
to[prop] = new Date(from[prop].getTime());
|
|
6961
|
-
}
|
|
6962
|
-
}
|
|
6963
|
-
else if (isArray(from[prop])) {
|
|
6964
|
-
if (overwrite) {
|
|
6965
|
-
to[prop] = from[prop].slice(0);
|
|
6966
|
-
}
|
|
6967
|
-
} else {
|
|
6968
|
-
to[prop] = extend({}, from[prop], overwrite);
|
|
6969
|
-
}
|
|
6970
|
-
} else if (overwrite || !hasProp) {
|
|
6971
|
-
to[prop] = from[prop];
|
|
6972
|
-
}
|
|
6973
|
-
}
|
|
6974
|
-
return to;
|
|
6975
|
-
},
|
|
6976
|
-
|
|
6977
|
-
fireEvent = function (el, eventName, data) {
|
|
6978
|
-
var ev;
|
|
6979
|
-
|
|
6980
|
-
if (document.createEvent) {
|
|
6981
|
-
ev = document.createEvent('HTMLEvents');
|
|
6982
|
-
ev.initEvent(eventName, true, false);
|
|
6983
|
-
ev = extend(ev, data);
|
|
6984
|
-
el.dispatchEvent(ev);
|
|
6985
|
-
} else if (document.createEventObject) {
|
|
6986
|
-
ev = document.createEventObject();
|
|
6987
|
-
ev = extend(ev, data);
|
|
6988
|
-
el.fireEvent('on' + eventName, ev);
|
|
6989
|
-
}
|
|
6990
|
-
},
|
|
6991
|
-
|
|
6992
|
-
adjustCalendar = function (calendar) {
|
|
6993
|
-
if (calendar.month < 0) {
|
|
6994
|
-
calendar.year -= Math.ceil(Math.abs(calendar.month) / 12);
|
|
6995
|
-
calendar.month += 12;
|
|
6996
|
-
}
|
|
6997
|
-
if (calendar.month > 11) {
|
|
6998
|
-
calendar.year += Math.floor(Math.abs(calendar.month) / 12);
|
|
6999
|
-
calendar.month -= 12;
|
|
7000
|
-
}
|
|
7001
|
-
return calendar;
|
|
7002
|
-
},
|
|
7003
|
-
|
|
7004
|
-
/**
|
|
7005
|
-
* defaults and localisation
|
|
7006
|
-
*/
|
|
7007
|
-
defaults = {
|
|
7008
|
-
|
|
7009
|
-
// bind the picker to a form field
|
|
7010
|
-
field: null,
|
|
7011
|
-
|
|
7012
|
-
// automatically show/hide the picker on `field` focus (default `true` if `field` is set)
|
|
7013
|
-
bound: undefined,
|
|
7014
|
-
|
|
7015
|
-
// position of the datepicker, relative to the field (default to bottom & left)
|
|
7016
|
-
// ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position)
|
|
7017
|
-
position: 'bottom left',
|
|
7018
|
-
|
|
7019
|
-
// automatically fit in the viewport even if it means repositioning from the position option
|
|
7020
|
-
reposition: true,
|
|
7021
|
-
|
|
7022
|
-
// the default output format for `.toString()` and `field` value
|
|
7023
|
-
format: 'YYYY-MM-DD',
|
|
7024
|
-
|
|
7025
|
-
// the toString function which gets passed a current date object and format
|
|
7026
|
-
// and returns a string
|
|
7027
|
-
toString: null,
|
|
7028
|
-
|
|
7029
|
-
// used to create date object from current input string
|
|
7030
|
-
parse: null,
|
|
7031
|
-
|
|
7032
|
-
// the initial date to view when first opened
|
|
7033
|
-
defaultDate: null,
|
|
7034
|
-
|
|
7035
|
-
// make the `defaultDate` the initial selected value
|
|
7036
|
-
setDefaultDate: false,
|
|
7037
|
-
|
|
7038
|
-
// first day of week (0: Sunday, 1: Monday etc)
|
|
7039
|
-
firstDay: 0,
|
|
7040
|
-
|
|
7041
|
-
// the default flag for moment's strict date parsing
|
|
7042
|
-
formatStrict: false,
|
|
7043
|
-
|
|
7044
|
-
// the minimum/earliest date that can be selected
|
|
7045
|
-
minDate: null,
|
|
7046
|
-
// the maximum/latest date that can be selected
|
|
7047
|
-
maxDate: null,
|
|
7048
|
-
|
|
7049
|
-
// number of years either side, or array of upper/lower range
|
|
7050
|
-
yearRange: 10,
|
|
7051
|
-
|
|
7052
|
-
// show week numbers at head of row
|
|
7053
|
-
showWeekNumber: false,
|
|
7054
|
-
|
|
7055
|
-
// Week picker mode
|
|
7056
|
-
pickWholeWeek: false,
|
|
7057
|
-
|
|
7058
|
-
// used internally (don't config outside)
|
|
7059
|
-
minYear: 0,
|
|
7060
|
-
maxYear: 9999,
|
|
7061
|
-
minMonth: undefined,
|
|
7062
|
-
maxMonth: undefined,
|
|
7063
|
-
|
|
7064
|
-
startRange: null,
|
|
7065
|
-
endRange: null,
|
|
7066
|
-
|
|
7067
|
-
isRTL: false,
|
|
7068
|
-
|
|
7069
|
-
// Additional text to append to the year in the calendar title
|
|
7070
|
-
yearSuffix: '',
|
|
7071
|
-
|
|
7072
|
-
// Render the month after year in the calendar title
|
|
7073
|
-
showMonthAfterYear: false,
|
|
7074
|
-
|
|
7075
|
-
// Render days of the calendar grid that fall in the next or previous month
|
|
7076
|
-
showDaysInNextAndPreviousMonths: false,
|
|
7077
|
-
|
|
7078
|
-
// Allows user to select days that fall in the next or previous month
|
|
7079
|
-
enableSelectionDaysInNextAndPreviousMonths: false,
|
|
7080
|
-
|
|
7081
|
-
// how many months are visible
|
|
7082
|
-
numberOfMonths: 1,
|
|
7083
|
-
|
|
7084
|
-
// when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`)
|
|
7085
|
-
// only used for the first display or when a selected date is not visible
|
|
7086
|
-
mainCalendar: 'left',
|
|
7087
|
-
|
|
7088
|
-
// Specify a DOM element to render the calendar in
|
|
7089
|
-
container: undefined,
|
|
7090
|
-
|
|
7091
|
-
// Blur field when date is selected
|
|
7092
|
-
blurFieldOnSelect: true,
|
|
7093
|
-
|
|
7094
|
-
// internationalization
|
|
7095
|
-
i18n: {
|
|
7096
|
-
previousMonth: 'Previous Month',
|
|
7097
|
-
nextMonth: 'Next Month',
|
|
7098
|
-
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
7099
|
-
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
7100
|
-
weekdaysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
7101
|
-
},
|
|
7102
|
-
|
|
7103
|
-
// Theme Classname
|
|
7104
|
-
theme: null,
|
|
7105
|
-
|
|
7106
|
-
// events array
|
|
7107
|
-
events: [],
|
|
7108
|
-
|
|
7109
|
-
// callback function
|
|
7110
|
-
onSelect: null,
|
|
7111
|
-
onOpen: null,
|
|
7112
|
-
onClose: null,
|
|
7113
|
-
onDraw: null,
|
|
7114
|
-
|
|
7115
|
-
// Enable keyboard input
|
|
7116
|
-
keyboardInput: true
|
|
7117
|
-
},
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
/**
|
|
7121
|
-
* templating functions to abstract HTML rendering
|
|
7122
|
-
*/
|
|
7123
|
-
renderDayName = function (opts, day, abbr) {
|
|
7124
|
-
day += opts.firstDay;
|
|
7125
|
-
while (day >= 7) {
|
|
7126
|
-
day -= 7;
|
|
7127
|
-
}
|
|
7128
|
-
return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
|
|
7129
|
-
},
|
|
7130
|
-
|
|
7131
|
-
renderDay = function (opts) {
|
|
7132
|
-
var arr = [];
|
|
7133
|
-
var ariaSelected = 'false';
|
|
7134
|
-
if (opts.isEmpty) {
|
|
7135
|
-
if (opts.showDaysInNextAndPreviousMonths) {
|
|
7136
|
-
arr.push('is-outside-current-month');
|
|
7137
|
-
|
|
7138
|
-
if (!opts.enableSelectionDaysInNextAndPreviousMonths) {
|
|
7139
|
-
arr.push('is-selection-disabled');
|
|
7140
|
-
}
|
|
7141
|
-
|
|
7142
|
-
} else {
|
|
7143
|
-
return '<td class="is-empty"></td>';
|
|
7144
|
-
}
|
|
7145
|
-
}
|
|
7146
|
-
if (opts.isDisabled) {
|
|
7147
|
-
arr.push('is-disabled');
|
|
7148
|
-
}
|
|
7149
|
-
if (opts.isToday) {
|
|
7150
|
-
arr.push('is-today');
|
|
7151
|
-
}
|
|
7152
|
-
if (opts.isSelected) {
|
|
7153
|
-
arr.push('is-selected');
|
|
7154
|
-
ariaSelected = 'true';
|
|
7155
|
-
}
|
|
7156
|
-
if (opts.hasEvent) {
|
|
7157
|
-
arr.push('has-event');
|
|
7158
|
-
}
|
|
7159
|
-
if (opts.isInRange) {
|
|
7160
|
-
arr.push('is-inrange');
|
|
7161
|
-
}
|
|
7162
|
-
if (opts.isStartRange) {
|
|
7163
|
-
arr.push('is-startrange');
|
|
7164
|
-
}
|
|
7165
|
-
if (opts.isEndRange) {
|
|
7166
|
-
arr.push('is-endrange');
|
|
7167
|
-
}
|
|
7168
|
-
return '<td data-day="' + opts.day + '" class="' + arr.join(' ') + '" aria-selected="' + ariaSelected + '">' +
|
|
7169
|
-
'<button tabIndex="-1" class="pika-button pika-day" type="button" ' +
|
|
7170
|
-
'data-pika-year="' + opts.year + '" data-pika-month="' + opts.month + '" data-pika-day="' + opts.day + '">' +
|
|
7171
|
-
opts.day +
|
|
7172
|
-
'</button>' +
|
|
7173
|
-
'</td>';
|
|
7174
|
-
},
|
|
7175
|
-
|
|
7176
|
-
renderWeek = function (d, m, y) {
|
|
7177
|
-
// Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified.
|
|
7178
|
-
var onejan = new Date(y, 0, 1),
|
|
7179
|
-
weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay() + 1) / 7);
|
|
7180
|
-
return '<td class="pika-week">' + weekNum + '</td>';
|
|
7181
|
-
},
|
|
7182
|
-
|
|
7183
|
-
renderRow = function (days, isRTL, pickWholeWeek, isRowSelected) {
|
|
7184
|
-
return '<tr class="pika-row' + (pickWholeWeek ? ' pick-whole-week' : '') + (isRowSelected ? ' is-selected' : '') + '">' + (isRTL ? days.reverse() : days).join('') + '</tr>';
|
|
7185
|
-
},
|
|
7186
|
-
|
|
7187
|
-
renderBody = function (rows) {
|
|
7188
|
-
return '<tbody>' + rows.join('') + '</tbody>';
|
|
7189
|
-
},
|
|
7190
|
-
|
|
7191
|
-
renderHead = function (opts) {
|
|
7192
|
-
var i, arr = [];
|
|
7193
|
-
if (opts.showWeekNumber) {
|
|
7194
|
-
arr.push('<th></th>');
|
|
7195
|
-
}
|
|
7196
|
-
for (i = 0; i < 7; i++) {
|
|
7197
|
-
arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
|
|
7198
|
-
}
|
|
7199
|
-
return '<thead><tr>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</tr></thead>';
|
|
7200
|
-
},
|
|
7201
|
-
|
|
7202
|
-
renderTitle = function (instance, c, year, month, refYear, randId) {
|
|
7203
|
-
var i, j, arr,
|
|
7204
|
-
opts = instance._o,
|
|
7205
|
-
isMinYear = year === opts.minYear,
|
|
7206
|
-
isMaxYear = year === opts.maxYear,
|
|
7207
|
-
html = '<div id="' + randId + '" class="pika-title">',
|
|
7208
|
-
monthHtml,
|
|
7209
|
-
yearHtml,
|
|
7210
|
-
prev = true,
|
|
7211
|
-
next = true;
|
|
7212
|
-
|
|
7213
|
-
for (arr = [], i = 0; i < 12; i++) {
|
|
7214
|
-
arr.push('<option value="' + (year === refYear ? i - c : 12 + i - c) + '"' +
|
|
7215
|
-
(i === month ? ' selected="selected"' : '') +
|
|
7216
|
-
((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled="disabled"' : '') + '>' +
|
|
7217
|
-
opts.i18n.months[i] + '</option>');
|
|
7218
|
-
}
|
|
7219
|
-
|
|
7220
|
-
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>';
|
|
7221
|
-
|
|
7222
|
-
if (isArray(opts.yearRange)) {
|
|
7223
|
-
i = opts.yearRange[0];
|
|
7224
|
-
j = opts.yearRange[1] + 1;
|
|
7225
|
-
} else {
|
|
7226
|
-
i = year - opts.yearRange;
|
|
7227
|
-
j = 1 + year + opts.yearRange;
|
|
7228
|
-
}
|
|
7229
|
-
|
|
7230
|
-
for (arr = []; i < j && i <= opts.maxYear; i++) {
|
|
7231
|
-
if (i >= opts.minYear) {
|
|
7232
|
-
arr.push('<option value="' + i + '"' + (i === year ? ' selected="selected"' : '') + '>' + (i) + '</option>');
|
|
7233
|
-
}
|
|
7234
|
-
}
|
|
7235
|
-
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>';
|
|
7236
|
-
|
|
7237
|
-
if (opts.showMonthAfterYear) {
|
|
7238
|
-
html += yearHtml + monthHtml;
|
|
7239
|
-
} else {
|
|
7240
|
-
html += monthHtml + yearHtml;
|
|
7241
|
-
}
|
|
7242
|
-
|
|
7243
|
-
if (isMinYear && (month === 0 || opts.minMonth >= month)) {
|
|
7244
|
-
prev = false;
|
|
7245
|
-
}
|
|
7246
|
-
|
|
7247
|
-
if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
|
|
7248
|
-
next = false;
|
|
7249
|
-
}
|
|
7250
|
-
|
|
7251
|
-
if (c === 0) {
|
|
7252
|
-
html += '<button tabIndex="-1" class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
|
|
7253
|
-
}
|
|
7254
|
-
if (c === (instance._o.numberOfMonths - 1)) {
|
|
7255
|
-
html += '<button tabIndex="-1" class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
|
|
7256
|
-
}
|
|
7257
|
-
|
|
7258
|
-
return html += '</div>';
|
|
7259
|
-
},
|
|
7260
|
-
|
|
7261
|
-
renderTable = function (opts, data, randId) {
|
|
7262
|
-
return '<table cellpadding="0" cellspacing="0" class="pika-table" role="grid" aria-labelledby="' + randId + '">' + renderHead(opts) + renderBody(data) + '</table>';
|
|
7263
|
-
},
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
/**
|
|
7267
|
-
* Pikaday constructor
|
|
7268
|
-
*/
|
|
7269
|
-
Pikaday = function (options) {
|
|
7270
|
-
var self = this,
|
|
7271
|
-
opts = self.config(options);
|
|
7272
|
-
|
|
7273
|
-
self._onMouseDown = function (e) {
|
|
7274
|
-
if (!self._v) {
|
|
7275
|
-
return;
|
|
7276
|
-
}
|
|
7277
|
-
e = e || window.event;
|
|
7278
|
-
var target = e.target || e.srcElement;
|
|
7279
|
-
if (!target) {
|
|
7280
|
-
return;
|
|
7281
|
-
}
|
|
7282
|
-
|
|
7283
|
-
if (!hasClass(target, 'is-disabled')) {
|
|
7284
|
-
if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) {
|
|
7285
|
-
self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day')));
|
|
7286
|
-
if (opts.bound) {
|
|
7287
|
-
sto(function () {
|
|
7288
|
-
self.hide();
|
|
7289
|
-
if (opts.blurFieldOnSelect && opts.field) {
|
|
7290
|
-
opts.field.blur();
|
|
7291
|
-
}
|
|
7292
|
-
}, 100);
|
|
7293
|
-
}
|
|
7294
|
-
}
|
|
7295
|
-
else if (hasClass(target, 'pika-prev')) {
|
|
7296
|
-
self.prevMonth();
|
|
7297
|
-
}
|
|
7298
|
-
else if (hasClass(target, 'pika-next')) {
|
|
7299
|
-
self.nextMonth();
|
|
7300
|
-
}
|
|
7301
|
-
}
|
|
7302
|
-
if (!hasClass(target, 'pika-select')) {
|
|
7303
|
-
// if this is touch event prevent mouse events emulation
|
|
7304
|
-
if (e.preventDefault) {
|
|
7305
|
-
e.preventDefault();
|
|
7306
|
-
} else {
|
|
7307
|
-
e.returnValue = false;
|
|
7308
|
-
return false;
|
|
7309
|
-
}
|
|
7310
|
-
} else {
|
|
7311
|
-
self._c = true;
|
|
7312
|
-
}
|
|
7313
|
-
};
|
|
7314
|
-
|
|
7315
|
-
self._onChange = function (e) {
|
|
7316
|
-
e = e || window.event;
|
|
7317
|
-
var target = e.target || e.srcElement;
|
|
7318
|
-
if (!target) {
|
|
7319
|
-
return;
|
|
7320
|
-
}
|
|
7321
|
-
if (hasClass(target, 'pika-select-month')) {
|
|
7322
|
-
self.gotoMonth(target.value);
|
|
7323
|
-
}
|
|
7324
|
-
else if (hasClass(target, 'pika-select-year')) {
|
|
7325
|
-
self.gotoYear(target.value);
|
|
7326
|
-
}
|
|
7327
|
-
};
|
|
7328
|
-
|
|
7329
|
-
self._onKeyChange = function (e) {
|
|
7330
|
-
e = e || window.event;
|
|
7331
|
-
// ignore if event comes from input box
|
|
7332
|
-
if (self.isVisible() && e.target && e.target.type !== 'text') {
|
|
7333
|
-
|
|
7334
|
-
switch (e.keyCode) {
|
|
7335
|
-
case 13:
|
|
7336
|
-
case 27:
|
|
7337
|
-
if (opts.field) {
|
|
7338
|
-
opts.field.blur();
|
|
7339
|
-
}
|
|
7340
|
-
break;
|
|
7341
|
-
case 37:
|
|
7342
|
-
e.preventDefault();
|
|
7343
|
-
self.adjustDate('subtract', 1);
|
|
7344
|
-
break;
|
|
7345
|
-
case 38:
|
|
7346
|
-
self.adjustDate('subtract', 7);
|
|
7347
|
-
break;
|
|
7348
|
-
case 39:
|
|
7349
|
-
self.adjustDate('add', 1);
|
|
7350
|
-
break;
|
|
7351
|
-
case 40:
|
|
7352
|
-
self.adjustDate('add', 7);
|
|
7353
|
-
break;
|
|
7354
|
-
}
|
|
7355
|
-
}
|
|
7356
|
-
};
|
|
7357
|
-
|
|
7358
|
-
self._onInputChange = function (e) {
|
|
7359
|
-
var date;
|
|
7360
|
-
|
|
7361
|
-
if (e.firedBy === self) {
|
|
7362
|
-
return;
|
|
7363
|
-
}
|
|
7364
|
-
if (opts.parse) {
|
|
7365
|
-
date = opts.parse(opts.field.value, opts.format);
|
|
7366
|
-
} else if (hasMoment) {
|
|
7367
|
-
date = moment(opts.field.value, opts.format, opts.formatStrict);
|
|
7368
|
-
date = (date && date.isValid()) ? date.toDate() : null;
|
|
7369
|
-
}
|
|
7370
|
-
else {
|
|
7371
|
-
date = new Date(Date.parse(opts.field.value));
|
|
7372
|
-
}
|
|
7373
|
-
// if (isDate(date)) {
|
|
7374
|
-
// self.setDate(date);
|
|
7375
|
-
// }
|
|
7376
|
-
// if (!self._v) {
|
|
7377
|
-
// self.show();
|
|
7378
|
-
// }
|
|
7379
|
-
};
|
|
7380
|
-
|
|
7381
|
-
self._onInputFocus = function () {
|
|
7382
|
-
self.show();
|
|
7383
|
-
};
|
|
7384
|
-
|
|
7385
|
-
self._onInputClick = function () {
|
|
7386
|
-
self.show();
|
|
7387
|
-
};
|
|
7388
|
-
|
|
7389
|
-
self._onInputBlur = function () {
|
|
7390
|
-
// IE allows pika div to gain focus; catch blur the input field
|
|
7391
|
-
var pEl = document.activeElement;
|
|
7392
|
-
do {
|
|
7393
|
-
if (hasClass(pEl, 'pika-single')) {
|
|
7394
|
-
return;
|
|
7395
|
-
}
|
|
7396
|
-
}
|
|
7397
|
-
while ((pEl = pEl.parentNode));
|
|
7398
|
-
|
|
7399
|
-
if (!self._c) {
|
|
7400
|
-
self._b = sto(function () {
|
|
7401
|
-
self.hide();
|
|
7402
|
-
}, 50);
|
|
7403
|
-
}
|
|
7404
|
-
self._c = false;
|
|
7405
|
-
};
|
|
7406
|
-
|
|
7407
|
-
self._onClick = function (e) {
|
|
7408
|
-
e = e || window.event;
|
|
7409
|
-
var target = e.target || e.srcElement,
|
|
7410
|
-
pEl = target;
|
|
7411
|
-
if (!target) {
|
|
7412
|
-
return;
|
|
7413
|
-
}
|
|
7414
|
-
if (!hasEventListeners && hasClass(target, 'pika-select')) {
|
|
7415
|
-
if (!target.onchange) {
|
|
7416
|
-
target.setAttribute('onchange', 'return;');
|
|
7417
|
-
addEvent(target, 'change', self._onChange);
|
|
7418
|
-
}
|
|
7419
|
-
}
|
|
7420
|
-
do {
|
|
7421
|
-
if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) {
|
|
7422
|
-
return;
|
|
7423
|
-
}
|
|
7424
|
-
}
|
|
7425
|
-
while ((pEl = pEl.parentNode));
|
|
7426
|
-
if (self._v && target !== opts.trigger && pEl !== opts.trigger) {
|
|
7427
|
-
self.hide();
|
|
7428
|
-
}
|
|
7429
|
-
};
|
|
7430
|
-
|
|
7431
|
-
self.el = document.createElement('div');
|
|
7432
|
-
self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : '');
|
|
7433
|
-
|
|
7434
|
-
addEvent(self.el, 'mousedown', self._onMouseDown, true);
|
|
7435
|
-
addEvent(self.el, 'touchend', self._onMouseDown, true);
|
|
7436
|
-
addEvent(self.el, 'change', self._onChange);
|
|
7437
|
-
|
|
7438
|
-
if (opts.keyboardInput) {
|
|
7439
|
-
addEvent(document, 'keydown', self._onKeyChange);
|
|
7440
|
-
}
|
|
7441
|
-
|
|
7442
|
-
if (opts.field) {
|
|
7443
|
-
if (opts.container) {
|
|
7444
|
-
opts.container.appendChild(self.el);
|
|
7445
|
-
} else if (opts.bound) {
|
|
7446
|
-
document.body.appendChild(self.el);
|
|
7447
|
-
} else {
|
|
7448
|
-
opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
|
|
7449
|
-
}
|
|
7450
|
-
addEvent(opts.field, 'change', self._onInputChange);
|
|
7451
|
-
|
|
7452
|
-
if (!opts.defaultDate) {
|
|
7453
|
-
if (hasMoment && opts.field.value) {
|
|
7454
|
-
opts.defaultDate = moment(opts.field.value, opts.format).toDate();
|
|
7455
|
-
} else {
|
|
7456
|
-
opts.defaultDate = new Date(Date.parse(opts.field.value));
|
|
7457
|
-
}
|
|
7458
|
-
opts.setDefaultDate = true;
|
|
7459
|
-
}
|
|
7460
|
-
}
|
|
7461
|
-
|
|
7462
|
-
var defDate = opts.defaultDate;
|
|
7463
|
-
|
|
7464
|
-
if (isDate(defDate)) {
|
|
7465
|
-
if (opts.setDefaultDate) {
|
|
7466
|
-
self.setDate(defDate, true);
|
|
7467
|
-
} else {
|
|
7468
|
-
self.gotoDate(defDate);
|
|
7469
|
-
}
|
|
7470
|
-
} else {
|
|
7471
|
-
self.gotoDate(new Date());
|
|
7472
|
-
}
|
|
7473
|
-
|
|
7474
|
-
if (opts.bound) {
|
|
7475
|
-
this.hide();
|
|
7476
|
-
self.el.className += ' is-bound';
|
|
7477
|
-
addEvent(opts.trigger, 'click', self._onInputClick);
|
|
7478
|
-
addEvent(opts.trigger, 'focus', self._onInputFocus);
|
|
7479
|
-
addEvent(opts.trigger, 'blur', self._onInputBlur);
|
|
7480
|
-
} else {
|
|
7481
|
-
this.show();
|
|
7482
|
-
}
|
|
7483
|
-
};
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
/**
|
|
7487
|
-
* public Pikaday API
|
|
7488
|
-
*/
|
|
7489
|
-
Pikaday.prototype = {
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
/**
|
|
7493
|
-
* configure functionality
|
|
7494
|
-
*/
|
|
7495
|
-
config: function (options) {
|
|
7496
|
-
if (!this._o) {
|
|
7497
|
-
this._o = extend({}, defaults, true);
|
|
7498
|
-
}
|
|
7499
|
-
|
|
7500
|
-
var opts = extend(this._o, options, true);
|
|
7501
|
-
|
|
7502
|
-
opts.isRTL = !!opts.isRTL;
|
|
7503
|
-
|
|
7504
|
-
opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
|
|
7505
|
-
|
|
7506
|
-
opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null;
|
|
7507
|
-
|
|
7508
|
-
opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
|
|
7509
|
-
|
|
7510
|
-
opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field;
|
|
7511
|
-
|
|
7512
|
-
opts.disableWeekends = !!opts.disableWeekends;
|
|
7513
|
-
|
|
7514
|
-
opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null;
|
|
7515
|
-
|
|
7516
|
-
var nom = parseInt(opts.numberOfMonths, 10) || 1;
|
|
7517
|
-
opts.numberOfMonths = nom > 4 ? 4 : nom;
|
|
7518
|
-
|
|
7519
|
-
if (!isDate(opts.minDate)) {
|
|
7520
|
-
opts.minDate = false;
|
|
7521
|
-
}
|
|
7522
|
-
if (!isDate(opts.maxDate)) {
|
|
7523
|
-
opts.maxDate = false;
|
|
7524
|
-
}
|
|
7525
|
-
if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
|
|
7526
|
-
opts.maxDate = opts.minDate = false;
|
|
7527
|
-
}
|
|
7528
|
-
if (opts.minDate) {
|
|
7529
|
-
this.setMinDate(opts.minDate);
|
|
7530
|
-
}
|
|
7531
|
-
if (opts.maxDate) {
|
|
7532
|
-
this.setMaxDate(opts.maxDate);
|
|
7533
|
-
}
|
|
7534
|
-
|
|
7535
|
-
if (isArray(opts.yearRange)) {
|
|
7536
|
-
var fallback = new Date().getFullYear() - 10;
|
|
7537
|
-
opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
|
|
7538
|
-
opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
|
|
7539
|
-
} else {
|
|
7540
|
-
opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
|
|
7541
|
-
if (opts.yearRange > 100) {
|
|
7542
|
-
opts.yearRange = 100;
|
|
7543
|
-
}
|
|
7544
|
-
}
|
|
7545
|
-
|
|
7546
|
-
return opts;
|
|
7547
|
-
},
|
|
7548
|
-
|
|
7549
|
-
/**
|
|
7550
|
-
* return a formatted string of the current selection (using Moment.js if available)
|
|
7551
|
-
*/
|
|
7552
|
-
toString: function (format) {
|
|
7553
|
-
format = format || this._o.format;
|
|
7554
|
-
if (!isDate(this._d)) {
|
|
7555
|
-
return '';
|
|
7556
|
-
}
|
|
7557
|
-
if (this._o.toString) {
|
|
7558
|
-
return this._o.toString(this._d, format);
|
|
7559
|
-
}
|
|
7560
|
-
if (hasMoment) {
|
|
7561
|
-
return moment(this._d).format(format);
|
|
7562
|
-
}
|
|
7563
|
-
return this._d.toDateString();
|
|
7564
|
-
},
|
|
7565
|
-
|
|
7566
|
-
/**
|
|
7567
|
-
* return a Moment.js object of the current selection (if available)
|
|
7568
|
-
*/
|
|
7569
|
-
getMoment: function () {
|
|
7570
|
-
return hasMoment ? moment(this._d) : null;
|
|
7571
|
-
},
|
|
7572
|
-
|
|
7573
|
-
/**
|
|
7574
|
-
* set the current selection from a Moment.js object (if available)
|
|
7575
|
-
*/
|
|
7576
|
-
setMoment: function (date, preventOnSelect) {
|
|
7577
|
-
if (hasMoment && moment.isMoment(date)) {
|
|
7578
|
-
this.setDate(date.toDate(), preventOnSelect);
|
|
7579
|
-
}
|
|
7580
|
-
},
|
|
7581
|
-
|
|
7582
|
-
/**
|
|
7583
|
-
* return a Date object of the current selection
|
|
7584
|
-
*/
|
|
7585
|
-
getDate: function () {
|
|
7586
|
-
return isDate(this._d) ? new Date(this._d.getTime()) : null;
|
|
7587
|
-
},
|
|
7588
|
-
|
|
7589
|
-
/**
|
|
7590
|
-
* set the current selection
|
|
7591
|
-
*/
|
|
7592
|
-
setDate: function (date, preventOnSelect) {
|
|
7593
|
-
if (!date) {
|
|
7594
|
-
this._d = null;
|
|
7595
|
-
|
|
7596
|
-
if (this._o.field) {
|
|
7597
|
-
this._o.field.value = '';
|
|
7598
|
-
fireEvent(this._o.field, 'change', { firedBy: this });
|
|
7599
|
-
}
|
|
7600
|
-
|
|
7601
|
-
return this.draw();
|
|
7602
|
-
}
|
|
7603
|
-
if (typeof date === 'string') {
|
|
7604
|
-
date = new Date(Date.parse(date));
|
|
7605
|
-
}
|
|
7606
|
-
if (!isDate(date)) {
|
|
7607
|
-
return;
|
|
7608
|
-
}
|
|
7609
|
-
|
|
7610
|
-
var min = this._o.minDate,
|
|
7611
|
-
max = this._o.maxDate;
|
|
7612
|
-
|
|
7613
|
-
if (isDate(min) && date < min) {
|
|
7614
|
-
date = min;
|
|
7615
|
-
} else if (isDate(max) && date > max) {
|
|
7616
|
-
date = max;
|
|
7617
|
-
}
|
|
7618
|
-
|
|
7619
|
-
this._d = new Date(date.getTime());
|
|
7620
|
-
setToStartOfDay(this._d);
|
|
7621
|
-
this.gotoDate(this._d);
|
|
7622
|
-
|
|
7623
|
-
if (this._o.field) {
|
|
7624
|
-
this._o.field.value = this.toString();
|
|
7625
|
-
fireEvent(this._o.field, 'change', { firedBy: this });
|
|
7626
|
-
}
|
|
7627
|
-
if (!preventOnSelect && typeof this._o.onSelect === 'function') {
|
|
7628
|
-
this._o.onSelect.call(this, this.getDate());
|
|
7629
|
-
}
|
|
7630
|
-
},
|
|
7631
|
-
|
|
7632
|
-
/**
|
|
7633
|
-
* change view to a specific date
|
|
7634
|
-
*/
|
|
7635
|
-
gotoDate: function (date) {
|
|
7636
|
-
var newCalendar = true;
|
|
7637
|
-
|
|
7638
|
-
if (!isDate(date)) {
|
|
7639
|
-
return;
|
|
7640
|
-
}
|
|
7641
|
-
|
|
7642
|
-
if (this.calendars) {
|
|
7643
|
-
var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1),
|
|
7644
|
-
lastVisibleDate = new Date(this.calendars[this.calendars.length - 1].year, this.calendars[this.calendars.length - 1].month, 1),
|
|
7645
|
-
visibleDate = date.getTime();
|
|
7646
|
-
// get the end of the month
|
|
7647
|
-
lastVisibleDate.setMonth(lastVisibleDate.getMonth() + 1);
|
|
7648
|
-
lastVisibleDate.setDate(lastVisibleDate.getDate() - 1);
|
|
7649
|
-
newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate);
|
|
7650
|
-
}
|
|
7651
|
-
|
|
7652
|
-
if (newCalendar) {
|
|
7653
|
-
this.calendars = [{
|
|
7654
|
-
month: date.getMonth(),
|
|
7655
|
-
year: date.getFullYear()
|
|
7656
|
-
}];
|
|
7657
|
-
if (this._o.mainCalendar === 'right') {
|
|
7658
|
-
this.calendars[0].month += 1 - this._o.numberOfMonths;
|
|
7659
|
-
}
|
|
7660
|
-
}
|
|
7661
|
-
|
|
7662
|
-
this.adjustCalendars();
|
|
7663
|
-
},
|
|
7664
|
-
|
|
7665
|
-
adjustDate: function (sign, days) {
|
|
7666
|
-
|
|
7667
|
-
var day = this.getDate() || new Date();
|
|
7668
|
-
var difference = parseInt(days) * 24 * 60 * 60 * 1000;
|
|
7669
|
-
|
|
7670
|
-
var newDay;
|
|
7671
|
-
|
|
7672
|
-
if (sign === 'add') {
|
|
7673
|
-
newDay = new Date(day.valueOf() + difference);
|
|
7674
|
-
} else if (sign === 'subtract') {
|
|
7675
|
-
newDay = new Date(day.valueOf() - difference);
|
|
7676
|
-
}
|
|
7677
|
-
|
|
7678
|
-
this.setDate(newDay);
|
|
7679
|
-
},
|
|
7680
|
-
|
|
7681
|
-
adjustCalendars: function () {
|
|
7682
|
-
this.calendars[0] = adjustCalendar(this.calendars[0]);
|
|
7683
|
-
for (var c = 1; c < this._o.numberOfMonths; c++) {
|
|
7684
|
-
this.calendars[c] = adjustCalendar({
|
|
7685
|
-
month: this.calendars[0].month + c,
|
|
7686
|
-
year: this.calendars[0].year
|
|
7687
|
-
});
|
|
7688
|
-
}
|
|
7689
|
-
this.draw();
|
|
7690
|
-
},
|
|
7691
|
-
|
|
7692
|
-
gotoToday: function () {
|
|
7693
|
-
this.gotoDate(new Date());
|
|
7694
|
-
},
|
|
7695
|
-
|
|
7696
|
-
/**
|
|
7697
|
-
* change view to a specific month (zero-index, e.g. 0: January)
|
|
7698
|
-
*/
|
|
7699
|
-
gotoMonth: function (month) {
|
|
7700
|
-
if (!isNaN(month)) {
|
|
7701
|
-
this.calendars[0].month = parseInt(month, 10);
|
|
7702
|
-
this.adjustCalendars();
|
|
7703
|
-
}
|
|
7704
|
-
},
|
|
7705
|
-
|
|
7706
|
-
nextMonth: function () {
|
|
7707
|
-
this.calendars[0].month++;
|
|
7708
|
-
this.adjustCalendars();
|
|
7709
|
-
},
|
|
7710
|
-
|
|
7711
|
-
prevMonth: function () {
|
|
7712
|
-
this.calendars[0].month--;
|
|
7713
|
-
this.adjustCalendars();
|
|
7714
|
-
},
|
|
7715
|
-
|
|
7716
|
-
/**
|
|
7717
|
-
* change view to a specific full year (e.g. "2012")
|
|
7718
|
-
*/
|
|
7719
|
-
gotoYear: function (year) {
|
|
7720
|
-
if (!isNaN(year)) {
|
|
7721
|
-
this.calendars[0].year = parseInt(year, 10);
|
|
7722
|
-
this.adjustCalendars();
|
|
7723
|
-
}
|
|
7724
|
-
},
|
|
7725
|
-
|
|
7726
|
-
/**
|
|
7727
|
-
* change the minDate
|
|
7728
|
-
*/
|
|
7729
|
-
setMinDate: function (value) {
|
|
7730
|
-
if (value instanceof Date) {
|
|
7731
|
-
setToStartOfDay(value);
|
|
7732
|
-
this._o.minDate = value;
|
|
7733
|
-
this._o.minYear = value.getFullYear();
|
|
7734
|
-
this._o.minMonth = value.getMonth();
|
|
7735
|
-
} else {
|
|
7736
|
-
this._o.minDate = defaults.minDate;
|
|
7737
|
-
this._o.minYear = defaults.minYear;
|
|
7738
|
-
this._o.minMonth = defaults.minMonth;
|
|
7739
|
-
this._o.startRange = defaults.startRange;
|
|
7740
|
-
}
|
|
7741
|
-
|
|
7742
|
-
this.draw();
|
|
7743
|
-
},
|
|
7744
|
-
|
|
7745
|
-
/**
|
|
7746
|
-
* change the maxDate
|
|
7747
|
-
*/
|
|
7748
|
-
setMaxDate: function (value) {
|
|
7749
|
-
if (value instanceof Date) {
|
|
7750
|
-
setToStartOfDay(value);
|
|
7751
|
-
this._o.maxDate = value;
|
|
7752
|
-
this._o.maxYear = value.getFullYear();
|
|
7753
|
-
this._o.maxMonth = value.getMonth();
|
|
7754
|
-
} else {
|
|
7755
|
-
this._o.maxDate = defaults.maxDate;
|
|
7756
|
-
this._o.maxYear = defaults.maxYear;
|
|
7757
|
-
this._o.maxMonth = defaults.maxMonth;
|
|
7758
|
-
this._o.endRange = defaults.endRange;
|
|
7759
|
-
}
|
|
7760
|
-
|
|
7761
|
-
this.draw();
|
|
7762
|
-
},
|
|
7763
|
-
|
|
7764
|
-
setStartRange: function (value) {
|
|
7765
|
-
this._o.startRange = value;
|
|
7766
|
-
},
|
|
7767
|
-
|
|
7768
|
-
setEndRange: function (value) {
|
|
7769
|
-
this._o.endRange = value;
|
|
7770
|
-
},
|
|
7771
|
-
|
|
7772
|
-
/**
|
|
7773
|
-
* refresh the HTML
|
|
7774
|
-
*/
|
|
7775
|
-
draw: function (force) {
|
|
7776
|
-
if (!this._v && !force) {
|
|
7777
|
-
return;
|
|
7778
|
-
}
|
|
7779
|
-
var opts = this._o,
|
|
7780
|
-
minYear = opts.minYear,
|
|
7781
|
-
maxYear = opts.maxYear,
|
|
7782
|
-
minMonth = opts.minMonth,
|
|
7783
|
-
maxMonth = opts.maxMonth,
|
|
7784
|
-
html = '',
|
|
7785
|
-
randId;
|
|
7786
|
-
|
|
7787
|
-
if (this._y <= minYear) {
|
|
7788
|
-
this._y = minYear;
|
|
7789
|
-
if (!isNaN(minMonth) && this._m < minMonth) {
|
|
7790
|
-
this._m = minMonth;
|
|
7791
|
-
}
|
|
7792
|
-
}
|
|
7793
|
-
if (this._y >= maxYear) {
|
|
7794
|
-
this._y = maxYear;
|
|
7795
|
-
if (!isNaN(maxMonth) && this._m > maxMonth) {
|
|
7796
|
-
this._m = maxMonth;
|
|
7797
|
-
}
|
|
7798
|
-
}
|
|
7799
|
-
|
|
7800
|
-
for (var c = 0; c < opts.numberOfMonths; c++) {
|
|
7801
|
-
randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2);
|
|
7802
|
-
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>';
|
|
7803
|
-
}
|
|
7804
|
-
|
|
7805
|
-
this.el.innerHTML = html;
|
|
7806
|
-
|
|
7807
|
-
if (opts.bound) {
|
|
7808
|
-
if (opts.field.type !== 'hidden') {
|
|
7809
|
-
sto(function () {
|
|
7810
|
-
opts.trigger.focus();
|
|
7811
|
-
}, 1);
|
|
7812
|
-
}
|
|
7813
|
-
}
|
|
7814
|
-
|
|
7815
|
-
if (typeof this._o.onDraw === 'function') {
|
|
7816
|
-
this._o.onDraw(this);
|
|
7817
|
-
}
|
|
7818
|
-
|
|
7819
|
-
if (opts.bound) {
|
|
7820
|
-
// let the screen reader user know to use arrow keys
|
|
7821
|
-
opts.field.setAttribute('aria-label', 'Use the arrow keys to pick a date');
|
|
7822
|
-
}
|
|
7823
|
-
},
|
|
7824
|
-
|
|
7825
|
-
adjustPosition: function () {
|
|
7826
|
-
var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect;
|
|
7827
|
-
|
|
7828
|
-
if (this._o.container) return;
|
|
7829
|
-
|
|
7830
|
-
this.el.style.position = 'absolute';
|
|
7831
|
-
|
|
7832
|
-
field = this._o.trigger;
|
|
7833
|
-
pEl = field;
|
|
7834
|
-
width = this.el.offsetWidth;
|
|
7835
|
-
height = this.el.offsetHeight;
|
|
7836
|
-
viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
7837
|
-
viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
7838
|
-
scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
|
|
7839
|
-
|
|
7840
|
-
if (typeof field.getBoundingClientRect === 'function') {
|
|
7841
|
-
clientRect = field.getBoundingClientRect();
|
|
7842
|
-
left = clientRect.left + window.pageXOffset;
|
|
7843
|
-
top = clientRect.bottom + window.pageYOffset;
|
|
7844
|
-
} else {
|
|
7845
|
-
left = pEl.offsetLeft;
|
|
7846
|
-
top = pEl.offsetTop + pEl.offsetHeight;
|
|
7847
|
-
while ((pEl = pEl.offsetParent)) {
|
|
7848
|
-
left += pEl.offsetLeft;
|
|
7849
|
-
top += pEl.offsetTop;
|
|
7850
|
-
}
|
|
7851
|
-
}
|
|
7852
|
-
|
|
7853
|
-
// default position is bottom & left
|
|
7854
|
-
if ((this._o.reposition && left + width > viewportWidth) ||
|
|
7855
|
-
(
|
|
7856
|
-
this._o.position.indexOf('right') > -1 &&
|
|
7857
|
-
left - width + field.offsetWidth > 0
|
|
7858
|
-
)
|
|
7859
|
-
) {
|
|
7860
|
-
left = left - width + field.offsetWidth;
|
|
7861
|
-
}
|
|
7862
|
-
if ((this._o.reposition && top + height > viewportHeight + scrollTop) ||
|
|
7863
|
-
(
|
|
7864
|
-
this._o.position.indexOf('top') > -1 &&
|
|
7865
|
-
top - height - field.offsetHeight > 0
|
|
7866
|
-
)
|
|
7867
|
-
) {
|
|
7868
|
-
top = top - height - field.offsetHeight;
|
|
7869
|
-
}
|
|
7870
|
-
|
|
7871
|
-
this.el.style.left = left + 'px';
|
|
7872
|
-
this.el.style.top = top + 'px';
|
|
7873
|
-
},
|
|
7874
|
-
|
|
7875
|
-
/**
|
|
7876
|
-
* render HTML for a particular month
|
|
7877
|
-
*/
|
|
7878
|
-
render: function (year, month, randId) {
|
|
7879
|
-
var opts = this._o,
|
|
7880
|
-
now = new Date(),
|
|
7881
|
-
days = getDaysInMonth(year, month),
|
|
7882
|
-
before = new Date(year, month, 1).getDay(),
|
|
7883
|
-
data = [],
|
|
7884
|
-
row = [];
|
|
7885
|
-
setToStartOfDay(now);
|
|
7886
|
-
if (opts.firstDay > 0) {
|
|
7887
|
-
before -= opts.firstDay;
|
|
7888
|
-
if (before < 0) {
|
|
7889
|
-
before += 7;
|
|
7890
|
-
}
|
|
7891
|
-
}
|
|
7892
|
-
var previousMonth = month === 0 ? 11 : month - 1,
|
|
7893
|
-
nextMonth = month === 11 ? 0 : month + 1,
|
|
7894
|
-
yearOfPreviousMonth = month === 0 ? year - 1 : year,
|
|
7895
|
-
yearOfNextMonth = month === 11 ? year + 1 : year,
|
|
7896
|
-
daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth);
|
|
7897
|
-
var cells = days + before,
|
|
7898
|
-
after = cells;
|
|
7899
|
-
while (after > 7) {
|
|
7900
|
-
after -= 7;
|
|
7901
|
-
}
|
|
7902
|
-
cells += 7 - after;
|
|
7903
|
-
var isWeekSelected = false;
|
|
7904
|
-
for (var i = 0, r = 0; i < cells; i++) {
|
|
7905
|
-
var day = new Date(year, month, 1 + (i - before)),
|
|
7906
|
-
isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
|
|
7907
|
-
isToday = compareDates(day, now),
|
|
7908
|
-
hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false,
|
|
7909
|
-
isEmpty = i < before || i >= (days + before),
|
|
7910
|
-
dayNumber = 1 + (i - before),
|
|
7911
|
-
monthNumber = month,
|
|
7912
|
-
yearNumber = year,
|
|
7913
|
-
isStartRange = opts.startRange && compareDates(opts.startRange, day),
|
|
7914
|
-
isEndRange = opts.endRange && compareDates(opts.endRange, day),
|
|
7915
|
-
isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange,
|
|
7916
|
-
isDisabled = (opts.minDate && day < opts.minDate) ||
|
|
7917
|
-
(opts.maxDate && day > opts.maxDate) ||
|
|
7918
|
-
(opts.disableWeekends && isWeekend(day)) ||
|
|
7919
|
-
(opts.disableDayFn && opts.disableDayFn(day));
|
|
7920
|
-
|
|
7921
|
-
if (isEmpty) {
|
|
7922
|
-
if (i < before) {
|
|
7923
|
-
dayNumber = daysInPreviousMonth + dayNumber;
|
|
7924
|
-
monthNumber = previousMonth;
|
|
7925
|
-
yearNumber = yearOfPreviousMonth;
|
|
7926
|
-
} else {
|
|
7927
|
-
dayNumber = dayNumber - days;
|
|
7928
|
-
monthNumber = nextMonth;
|
|
7929
|
-
yearNumber = yearOfNextMonth;
|
|
7930
|
-
}
|
|
7931
|
-
}
|
|
7932
|
-
|
|
7933
|
-
var dayConfig = {
|
|
7934
|
-
day: dayNumber,
|
|
7935
|
-
month: monthNumber,
|
|
7936
|
-
year: yearNumber,
|
|
7937
|
-
hasEvent: hasEvent,
|
|
7938
|
-
isSelected: isSelected,
|
|
7939
|
-
isToday: isToday,
|
|
7940
|
-
isDisabled: isDisabled,
|
|
7941
|
-
isEmpty: isEmpty,
|
|
7942
|
-
isStartRange: isStartRange,
|
|
7943
|
-
isEndRange: isEndRange,
|
|
7944
|
-
isInRange: isInRange,
|
|
7945
|
-
showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths,
|
|
7946
|
-
enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths
|
|
7947
|
-
};
|
|
7948
|
-
|
|
7949
|
-
if (opts.pickWholeWeek && isSelected) {
|
|
7950
|
-
isWeekSelected = true;
|
|
7951
|
-
}
|
|
7952
|
-
|
|
7953
|
-
row.push(renderDay(dayConfig));
|
|
7954
|
-
|
|
7955
|
-
if (++r === 7) {
|
|
7956
|
-
if (opts.showWeekNumber) {
|
|
7957
|
-
row.unshift(renderWeek(i - before, month, year));
|
|
7958
|
-
}
|
|
7959
|
-
data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected));
|
|
7960
|
-
row = [];
|
|
7961
|
-
r = 0;
|
|
7962
|
-
isWeekSelected = false;
|
|
7963
|
-
}
|
|
7964
|
-
}
|
|
7965
|
-
return renderTable(opts, data, randId);
|
|
7966
|
-
},
|
|
7967
|
-
|
|
7968
|
-
isVisible: function () {
|
|
7969
|
-
return this._v;
|
|
7970
|
-
},
|
|
7971
|
-
|
|
7972
|
-
show: function () {
|
|
7973
|
-
if (!this.isVisible()) {
|
|
7974
|
-
this._v = true;
|
|
7975
|
-
this.draw();
|
|
7976
|
-
removeClass(this.el, 'is-hidden');
|
|
7977
|
-
if (this._o.bound) {
|
|
7978
|
-
addEvent(document, 'click', this._onClick);
|
|
7979
|
-
this.adjustPosition();
|
|
7980
|
-
}
|
|
7981
|
-
if (typeof this._o.onOpen === 'function') {
|
|
7982
|
-
this._o.onOpen.call(this);
|
|
7983
|
-
}
|
|
7984
|
-
}
|
|
7985
|
-
},
|
|
7986
|
-
|
|
7987
|
-
hide: function () {
|
|
7988
|
-
var v = this._v;
|
|
7989
|
-
if (v !== false) {
|
|
7990
|
-
if (this._o.bound) {
|
|
7991
|
-
removeEvent(document, 'click', this._onClick);
|
|
7992
|
-
}
|
|
7993
|
-
this.el.style.position = 'static'; // reset
|
|
7994
|
-
this.el.style.left = 'auto';
|
|
7995
|
-
this.el.style.top = 'auto';
|
|
7996
|
-
addClass(this.el, 'is-hidden');
|
|
7997
|
-
this._v = false;
|
|
7998
|
-
if (v !== undefined && typeof this._o.onClose === 'function') {
|
|
7999
|
-
this._o.onClose.call(this);
|
|
8000
|
-
}
|
|
8001
|
-
}
|
|
8002
|
-
},
|
|
8003
|
-
|
|
8004
|
-
/**
|
|
8005
|
-
* GAME OVER
|
|
8006
|
-
*/
|
|
8007
|
-
destroy: function () {
|
|
8008
|
-
var opts = this._o;
|
|
8009
|
-
|
|
8010
|
-
this.hide();
|
|
8011
|
-
removeEvent(this.el, 'mousedown', this._onMouseDown, true);
|
|
8012
|
-
removeEvent(this.el, 'touchend', this._onMouseDown, true);
|
|
8013
|
-
removeEvent(this.el, 'change', this._onChange);
|
|
8014
|
-
if (opts.keyboardInput) {
|
|
8015
|
-
removeEvent(document, 'keydown', this._onKeyChange);
|
|
8016
|
-
}
|
|
8017
|
-
if (opts.field) {
|
|
8018
|
-
removeEvent(opts.field, 'change', this._onInputChange);
|
|
8019
|
-
if (opts.bound) {
|
|
8020
|
-
removeEvent(opts.trigger, 'click', this._onInputClick);
|
|
8021
|
-
removeEvent(opts.trigger, 'focus', this._onInputFocus);
|
|
8022
|
-
removeEvent(opts.trigger, 'blur', this._onInputBlur);
|
|
8023
|
-
}
|
|
8024
|
-
}
|
|
8025
|
-
if (this.el.parentNode) {
|
|
8026
|
-
this.el.parentNode.removeChild(this.el);
|
|
8027
|
-
}
|
|
8028
|
-
}
|
|
8029
|
-
|
|
8030
|
-
};
|
|
8031
|
-
|
|
8032
|
-
return Pikaday;
|
|
8033
|
-
}));
|
|
8034
|
-
} (pikaday$1));
|
|
8035
|
-
return pikaday$1.exports;
|
|
8036
|
-
}
|
|
8037
|
-
|
|
8038
|
-
var pikadayExports = /*@__PURE__*/ requirePikaday();
|
|
8039
|
-
var Pikaday = /*@__PURE__*/getDefaultExportFromCjs(pikadayExports);
|
|
8040
|
-
|
|
8041
6887
|
// Ensure moment is available globally for Pikaday
|
|
8042
6888
|
if (typeof window !== 'undefined') {
|
|
8043
6889
|
window.moment = moment$1;
|
|
@@ -8632,7 +7478,30 @@ class DateTimeButton extends ChartComponent {
|
|
|
8632
7478
|
this.pickerIsVisible = false;
|
|
8633
7479
|
}
|
|
8634
7480
|
buttonDateTimeFormat(millis) {
|
|
8635
|
-
|
|
7481
|
+
const date = new Date(millis);
|
|
7482
|
+
const locale = this.chartOptions.dateLocale || 'en-US';
|
|
7483
|
+
const is24Hour = this.chartOptions.is24HourTime !== false;
|
|
7484
|
+
const formatOptions = {
|
|
7485
|
+
year: 'numeric',
|
|
7486
|
+
month: '2-digit',
|
|
7487
|
+
day: '2-digit',
|
|
7488
|
+
hour: '2-digit',
|
|
7489
|
+
minute: '2-digit',
|
|
7490
|
+
second: '2-digit',
|
|
7491
|
+
hour12: !is24Hour
|
|
7492
|
+
};
|
|
7493
|
+
try {
|
|
7494
|
+
if (this.chartOptions.offset && this.chartOptions.offset !== 'Local') {
|
|
7495
|
+
formatOptions.timeZone = this.getTimezoneFromOffset(this.chartOptions.offset);
|
|
7496
|
+
}
|
|
7497
|
+
const baseFormat = date.toLocaleString(locale, formatOptions);
|
|
7498
|
+
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
7499
|
+
return `${baseFormat}.${milliseconds}`;
|
|
7500
|
+
}
|
|
7501
|
+
catch (error) {
|
|
7502
|
+
console.warn(`Failed to format date for locale ${locale}:`, error);
|
|
7503
|
+
return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
|
|
7504
|
+
}
|
|
8636
7505
|
}
|
|
8637
7506
|
render(chartOptions, minMillis, maxMillis, onSet = null) {
|
|
8638
7507
|
this.chartOptions.setOptions(chartOptions);
|
|
@@ -8652,11 +7521,22 @@ class DateTimeButton extends ChartComponent {
|
|
|
8652
7521
|
}
|
|
8653
7522
|
super.themify(d3.select(this.renderTarget), this.chartOptions.theme);
|
|
8654
7523
|
}
|
|
7524
|
+
getTimezoneFromOffset(offset) {
|
|
7525
|
+
const timezoneMap = {
|
|
7526
|
+
'UTC': 'UTC',
|
|
7527
|
+
'EST': 'America/New_York',
|
|
7528
|
+
'PST': 'America/Los_Angeles',
|
|
7529
|
+
'CST': 'America/Chicago',
|
|
7530
|
+
'MST': 'America/Denver'
|
|
7531
|
+
};
|
|
7532
|
+
return timezoneMap[offset] || 'UTC';
|
|
7533
|
+
}
|
|
8655
7534
|
}
|
|
8656
7535
|
|
|
8657
7536
|
class DateTimeButtonRange extends DateTimeButton {
|
|
8658
7537
|
constructor(renderTarget) {
|
|
8659
7538
|
super(renderTarget);
|
|
7539
|
+
this.clickOutsideHandler = null;
|
|
8660
7540
|
}
|
|
8661
7541
|
setButtonText(fromMillis, toMillis, isRelative, quickTime) {
|
|
8662
7542
|
let fromString = this.buttonDateTimeFormat(fromMillis);
|
|
@@ -8676,10 +7556,38 @@ class DateTimeButtonRange extends DateTimeButton {
|
|
|
8676
7556
|
onClose() {
|
|
8677
7557
|
this.dateTimePickerContainer.style("display", "none");
|
|
8678
7558
|
this.dateTimeButton.node().focus();
|
|
7559
|
+
this.removeClickOutsideHandler();
|
|
7560
|
+
}
|
|
7561
|
+
removeClickOutsideHandler() {
|
|
7562
|
+
if (this.clickOutsideHandler) {
|
|
7563
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
7564
|
+
this.clickOutsideHandler = null;
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
setupClickOutsideHandler() {
|
|
7568
|
+
// Remove any existing handler first
|
|
7569
|
+
this.removeClickOutsideHandler();
|
|
7570
|
+
// Add handler after a small delay to prevent the opening click from immediately closing the picker
|
|
7571
|
+
setTimeout(() => {
|
|
7572
|
+
this.clickOutsideHandler = (event) => {
|
|
7573
|
+
const pickerElement = this.dateTimePickerContainer.node();
|
|
7574
|
+
const buttonElement = this.dateTimeButton.node();
|
|
7575
|
+
const target = event.target;
|
|
7576
|
+
// Check if click is outside both the picker and the button
|
|
7577
|
+
if (pickerElement && buttonElement &&
|
|
7578
|
+
!pickerElement.contains(target) &&
|
|
7579
|
+
!buttonElement.contains(target)) {
|
|
7580
|
+
this.onClose();
|
|
7581
|
+
}
|
|
7582
|
+
};
|
|
7583
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
7584
|
+
}, 0);
|
|
8679
7585
|
}
|
|
8680
7586
|
render(chartOptions = {}, minMillis, maxMillis, fromMillis = null, toMillis = null, onSet = null, onCancel = null) {
|
|
8681
7587
|
super.render(chartOptions, minMillis, maxMillis, onSet);
|
|
8682
|
-
d3.select(this.renderTarget)
|
|
7588
|
+
let container = d3.select(this.renderTarget);
|
|
7589
|
+
container.classed('tsi-dateTimeContainerRange', true);
|
|
7590
|
+
container.style('position', 'relative');
|
|
8683
7591
|
this.fromMillis = fromMillis;
|
|
8684
7592
|
this.toMillis = toMillis;
|
|
8685
7593
|
this.onCancel = onCancel ? onCancel : () => { };
|
|
@@ -8705,6 +7613,7 @@ class DateTimeButtonRange extends DateTimeButton {
|
|
|
8705
7613
|
this.onClose();
|
|
8706
7614
|
this.onCancel();
|
|
8707
7615
|
});
|
|
7616
|
+
this.setupClickOutsideHandler();
|
|
8708
7617
|
}
|
|
8709
7618
|
});
|
|
8710
7619
|
}
|
|
@@ -12419,7 +11328,7 @@ class ModelAutocomplete extends Component {
|
|
|
12419
11328
|
super(renderTarget);
|
|
12420
11329
|
this.chartOptions = new ChartOptions(); // TODO handle onkeyup and oninput in chart options
|
|
12421
11330
|
}
|
|
12422
|
-
render(
|
|
11331
|
+
render(chartOptions) {
|
|
12423
11332
|
this.chartOptions.setOptions(chartOptions);
|
|
12424
11333
|
let targetElement = d3.select(this.renderTarget);
|
|
12425
11334
|
targetElement.html("");
|
|
@@ -12773,20 +11682,102 @@ class TsqExpression extends ChartDataOptions {
|
|
|
12773
11682
|
}
|
|
12774
11683
|
}
|
|
12775
11684
|
|
|
11685
|
+
// Centralized renderer for the hierarchy tree. Keeps a stable D3 data-join and
|
|
11686
|
+
// updates existing DOM nodes instead of fully recreating them on each render.
|
|
11687
|
+
class TreeRenderer {
|
|
11688
|
+
static render(owner, data, target) {
|
|
11689
|
+
// Ensure an <ul> exists for this target (one list per level)
|
|
11690
|
+
let list = target.select('ul');
|
|
11691
|
+
if (list.empty()) {
|
|
11692
|
+
list = target.append('ul').attr('role', target === owner.hierarchyElem ? 'tree' : 'group');
|
|
11693
|
+
}
|
|
11694
|
+
const entries = Object.keys(data).map(k => ({ key: k, item: data[k] }));
|
|
11695
|
+
const liSelection = list.selectAll('li').data(entries, (d) => d && d.key);
|
|
11696
|
+
liSelection.exit().remove();
|
|
11697
|
+
const liEnter = liSelection.enter().append('li')
|
|
11698
|
+
.attr('role', 'none')
|
|
11699
|
+
.classed('tsi-leaf', (d) => !!d.item.isLeaf);
|
|
11700
|
+
const liMerged = liEnter.merge(liSelection);
|
|
11701
|
+
const setSize = entries.length;
|
|
11702
|
+
liMerged.each((d, i, nodes) => {
|
|
11703
|
+
const entry = d;
|
|
11704
|
+
const li = d3.select(nodes[i]);
|
|
11705
|
+
if (owner.selectedIds && owner.selectedIds.includes(entry.item.id)) {
|
|
11706
|
+
li.classed('tsi-selected', true);
|
|
11707
|
+
}
|
|
11708
|
+
else {
|
|
11709
|
+
li.classed('tsi-selected', false);
|
|
11710
|
+
}
|
|
11711
|
+
// determine instance vs hierarchy node by presence of isLeaf flag
|
|
11712
|
+
const isInstance = !!entry.item.isLeaf;
|
|
11713
|
+
const nodeNameToCheckIfExists = isInstance ? owner.instanceNodeString(entry.item) : entry.key;
|
|
11714
|
+
const displayName = (entry.item && (entry.item.displayName || nodeNameToCheckIfExists)) || nodeNameToCheckIfExists;
|
|
11715
|
+
li.attr('data-display-name', displayName);
|
|
11716
|
+
let itemElem = li.select('.tsi-hierarchyItem');
|
|
11717
|
+
if (itemElem.empty()) {
|
|
11718
|
+
const newListElem = owner.createHierarchyItemElem(entry.item, entry.key);
|
|
11719
|
+
li.node().appendChild(newListElem.node());
|
|
11720
|
+
itemElem = li.select('.tsi-hierarchyItem');
|
|
11721
|
+
}
|
|
11722
|
+
itemElem.attr('aria-label', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
|
|
11723
|
+
itemElem.attr('title', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
|
|
11724
|
+
itemElem.attr('aria-expanded', String(entry.item.isExpanded));
|
|
11725
|
+
// accessibility: set treeitem level and position in set
|
|
11726
|
+
const ariaLevel = String(((entry.item && typeof entry.item.level === 'number') ? entry.item.level : 0) + 1);
|
|
11727
|
+
itemElem.attr('aria-level', ariaLevel);
|
|
11728
|
+
itemElem.attr('aria-posinset', String(i + 1));
|
|
11729
|
+
itemElem.attr('aria-setsize', String(setSize));
|
|
11730
|
+
if (!isInstance) {
|
|
11731
|
+
itemElem.select('.tsi-caret-icon').attr('style', `left: ${(entry.item.level) * 18 + 20}px`);
|
|
11732
|
+
itemElem.select('.tsi-name').text(entry.key);
|
|
11733
|
+
itemElem.select('.tsi-instanceCount').text(entry.item.cumulativeInstanceCount);
|
|
11734
|
+
}
|
|
11735
|
+
else {
|
|
11736
|
+
const nameSpan = itemElem.select('.tsi-name');
|
|
11737
|
+
nameSpan.html('');
|
|
11738
|
+
Utils.appendFormattedElementsFromString(nameSpan, owner.instanceNodeStringToDisplay(entry.item));
|
|
11739
|
+
}
|
|
11740
|
+
entry.item.node = li;
|
|
11741
|
+
if (entry.item.children) {
|
|
11742
|
+
entry.item.isExpanded = true;
|
|
11743
|
+
li.classed('tsi-expanded', true);
|
|
11744
|
+
// recurse using TreeRenderer to keep rendering logic centralized
|
|
11745
|
+
TreeRenderer.render(owner, entry.item.children, li);
|
|
11746
|
+
}
|
|
11747
|
+
else {
|
|
11748
|
+
li.classed('tsi-expanded', false);
|
|
11749
|
+
li.selectAll('ul').remove();
|
|
11750
|
+
}
|
|
11751
|
+
});
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
|
|
12776
11755
|
class HierarchyNavigation extends Component {
|
|
12777
11756
|
constructor(renderTarget) {
|
|
12778
11757
|
super(renderTarget);
|
|
12779
11758
|
this.path = [];
|
|
11759
|
+
// debounce + request cancellation fields
|
|
11760
|
+
this.debounceTimer = null;
|
|
11761
|
+
this.debounceDelay = 250; // ms
|
|
11762
|
+
this.requestCounter = 0; // increments for each outgoing request
|
|
11763
|
+
this.latestRequestId = 0; // id of the most recent request
|
|
12780
11764
|
//selectedIds
|
|
12781
11765
|
this.selectedIds = [];
|
|
12782
11766
|
this.searchEnabled = true;
|
|
12783
|
-
this.
|
|
11767
|
+
this.autocompleteEnabled = true; // Enable/disable autocomplete suggestions
|
|
11768
|
+
// Search mode state
|
|
11769
|
+
this.isSearchMode = false;
|
|
11770
|
+
// Paths that should be auto-expanded (Set of path strings like "Factory North/Building A")
|
|
11771
|
+
this.pathsToAutoExpand = new Set();
|
|
11772
|
+
this.renderSearchResult = async (r, payload, target) => {
|
|
12784
11773
|
const hierarchyData = r.hierarchyNodes?.hits?.length
|
|
12785
11774
|
? this.fillDataRecursively(r.hierarchyNodes, payload, payload)
|
|
12786
11775
|
: {};
|
|
12787
11776
|
const instancesData = r.instances?.hits?.length
|
|
12788
11777
|
? r.instances.hits.reduce((acc, i) => {
|
|
12789
|
-
|
|
11778
|
+
const inst = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
|
|
11779
|
+
inst.displayName = this.instanceNodeStringToDisplay(i) || '';
|
|
11780
|
+
acc[this.instanceNodeIdentifier(i)] = inst;
|
|
12790
11781
|
return acc;
|
|
12791
11782
|
}, {})
|
|
12792
11783
|
: {};
|
|
@@ -12797,7 +11788,17 @@ class HierarchyNavigation extends Component {
|
|
|
12797
11788
|
}
|
|
12798
11789
|
hitCountElem.text(r.hierarchyNodes.hitCount);
|
|
12799
11790
|
}
|
|
12800
|
-
|
|
11791
|
+
const merged = { ...hierarchyData, ...instancesData };
|
|
11792
|
+
this.renderTree(merged, target);
|
|
11793
|
+
// Auto-expand nodes that should be expanded and load their children
|
|
11794
|
+
for (const key in hierarchyData) {
|
|
11795
|
+
const node = hierarchyData[key];
|
|
11796
|
+
if (node.isExpanded && !node.children) {
|
|
11797
|
+
// This node should be expanded but doesn't have children loaded yet
|
|
11798
|
+
// We need to trigger expansion after the node is rendered
|
|
11799
|
+
await this.autoExpandNode(node);
|
|
11800
|
+
}
|
|
11801
|
+
}
|
|
12801
11802
|
};
|
|
12802
11803
|
this.hierarchyNodeIdentifier = (hName) => {
|
|
12803
11804
|
return hName ? hName : '(' + this.getString("Empty") + ')';
|
|
@@ -12819,12 +11820,20 @@ class HierarchyNavigation extends Component {
|
|
|
12819
11820
|
const targetElement = d3.select(this.renderTarget).text('');
|
|
12820
11821
|
this.hierarchyNavWrapper = this.createHierarchyNavWrapper(targetElement);
|
|
12821
11822
|
this.selectedIds = preselectedIds;
|
|
11823
|
+
// Allow disabling autocomplete via options
|
|
11824
|
+
if (hierarchyNavOptions.autocompleteEnabled !== undefined) {
|
|
11825
|
+
this.autocompleteEnabled = hierarchyNavOptions.autocompleteEnabled;
|
|
11826
|
+
}
|
|
11827
|
+
// Pre-compute paths that need to be auto-expanded for preselected instances
|
|
11828
|
+
if (preselectedIds && preselectedIds.length > 0) {
|
|
11829
|
+
await this.computePathsToAutoExpand(preselectedIds);
|
|
11830
|
+
}
|
|
12822
11831
|
//render search wrapper
|
|
12823
|
-
|
|
11832
|
+
this.renderSearchBox();
|
|
12824
11833
|
super.themify(this.hierarchyNavWrapper, this.chartOptions.theme);
|
|
12825
11834
|
const results = this.createResultsWrapper(this.hierarchyNavWrapper);
|
|
12826
11835
|
this.hierarchyElem = this.createHierarchyElem(results);
|
|
12827
|
-
this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
|
|
11836
|
+
await this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
|
|
12828
11837
|
}
|
|
12829
11838
|
createHierarchyNavWrapper(targetElement) {
|
|
12830
11839
|
return targetElement.append('div').attr('class', 'tsi-hierarchy-nav-wrapper');
|
|
@@ -12832,8 +11841,129 @@ class HierarchyNavigation extends Component {
|
|
|
12832
11841
|
createResultsWrapper(hierarchyNavWrapper) {
|
|
12833
11842
|
return hierarchyNavWrapper.append('div').classed('tsi-hierarchy-or-list-wrapper', true);
|
|
12834
11843
|
}
|
|
11844
|
+
// create hierarchy container and attach keyboard handler
|
|
12835
11845
|
createHierarchyElem(results) {
|
|
12836
|
-
|
|
11846
|
+
const sel = results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
|
|
11847
|
+
// attach keydown listener for keyboard navigation (delegated)
|
|
11848
|
+
// use native event to preserve focus handling
|
|
11849
|
+
const node = sel.node();
|
|
11850
|
+
if (node) {
|
|
11851
|
+
node.addEventListener('keydown', (ev) => this.onKeyDown(ev));
|
|
11852
|
+
}
|
|
11853
|
+
return sel;
|
|
11854
|
+
}
|
|
11855
|
+
// Keyboard navigation handlers and helpers
|
|
11856
|
+
onKeyDown(ev) {
|
|
11857
|
+
const key = ev.key;
|
|
11858
|
+
const active = document.activeElement;
|
|
11859
|
+
const container = this.hierarchyElem?.node();
|
|
11860
|
+
if (!container)
|
|
11861
|
+
return;
|
|
11862
|
+
const isInside = active && container.contains(active);
|
|
11863
|
+
if (!isInside && (key === 'ArrowDown' || key === 'ArrowUp')) {
|
|
11864
|
+
// focus first visible item on navigation keys
|
|
11865
|
+
const visible = this.getVisibleItemElems();
|
|
11866
|
+
if (visible.length) {
|
|
11867
|
+
this.focusItem(visible[0]);
|
|
11868
|
+
ev.preventDefault();
|
|
11869
|
+
}
|
|
11870
|
+
return;
|
|
11871
|
+
}
|
|
11872
|
+
if (!active)
|
|
11873
|
+
return;
|
|
11874
|
+
const current = active.classList && active.classList.contains('tsi-hierarchyItem') ? active : active.closest('.tsi-hierarchyItem');
|
|
11875
|
+
if (!current)
|
|
11876
|
+
return;
|
|
11877
|
+
switch (key) {
|
|
11878
|
+
case 'ArrowDown':
|
|
11879
|
+
this.focusNext(current);
|
|
11880
|
+
ev.preventDefault();
|
|
11881
|
+
break;
|
|
11882
|
+
case 'ArrowUp':
|
|
11883
|
+
this.focusPrev(current);
|
|
11884
|
+
ev.preventDefault();
|
|
11885
|
+
break;
|
|
11886
|
+
case 'ArrowRight':
|
|
11887
|
+
this.handleArrowRight(current);
|
|
11888
|
+
ev.preventDefault();
|
|
11889
|
+
break;
|
|
11890
|
+
case 'ArrowLeft':
|
|
11891
|
+
this.handleArrowLeft(current);
|
|
11892
|
+
ev.preventDefault();
|
|
11893
|
+
break;
|
|
11894
|
+
case 'Enter':
|
|
11895
|
+
case ' ':
|
|
11896
|
+
// activate (toggle expand or select)
|
|
11897
|
+
current.click();
|
|
11898
|
+
ev.preventDefault();
|
|
11899
|
+
break;
|
|
11900
|
+
}
|
|
11901
|
+
}
|
|
11902
|
+
getVisibleItemElems() {
|
|
11903
|
+
if (!this.hierarchyElem)
|
|
11904
|
+
return [];
|
|
11905
|
+
const root = this.hierarchyElem.node();
|
|
11906
|
+
if (!root)
|
|
11907
|
+
return [];
|
|
11908
|
+
const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
|
|
11909
|
+
return items.filter(i => i.offsetParent !== null && getComputedStyle(i).display !== 'none');
|
|
11910
|
+
}
|
|
11911
|
+
focusItem(elem) {
|
|
11912
|
+
if (!this.hierarchyElem)
|
|
11913
|
+
return;
|
|
11914
|
+
const root = this.hierarchyElem.node();
|
|
11915
|
+
if (!root)
|
|
11916
|
+
return;
|
|
11917
|
+
const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
|
|
11918
|
+
items.forEach(i => i.setAttribute('tabindex', '-1'));
|
|
11919
|
+
elem.setAttribute('tabindex', '0');
|
|
11920
|
+
elem.focus();
|
|
11921
|
+
}
|
|
11922
|
+
focusNext(current) {
|
|
11923
|
+
const visible = this.getVisibleItemElems();
|
|
11924
|
+
const idx = visible.indexOf(current);
|
|
11925
|
+
if (idx >= 0 && idx < visible.length - 1) {
|
|
11926
|
+
this.focusItem(visible[idx + 1]);
|
|
11927
|
+
}
|
|
11928
|
+
}
|
|
11929
|
+
focusPrev(current) {
|
|
11930
|
+
const visible = this.getVisibleItemElems();
|
|
11931
|
+
const idx = visible.indexOf(current);
|
|
11932
|
+
if (idx > 0) {
|
|
11933
|
+
this.focusItem(visible[idx - 1]);
|
|
11934
|
+
}
|
|
11935
|
+
}
|
|
11936
|
+
handleArrowRight(current) {
|
|
11937
|
+
const caret = current.querySelector('.tsi-caret-icon');
|
|
11938
|
+
const expanded = current.getAttribute('aria-expanded') === 'true';
|
|
11939
|
+
if (caret && !expanded) {
|
|
11940
|
+
// expand
|
|
11941
|
+
current.click();
|
|
11942
|
+
return;
|
|
11943
|
+
}
|
|
11944
|
+
// if already expanded, move to first child
|
|
11945
|
+
if (caret && expanded) {
|
|
11946
|
+
const li = current.closest('li');
|
|
11947
|
+
const childLi = li?.querySelector('ul > li');
|
|
11948
|
+
const childItem = childLi?.querySelector('.tsi-hierarchyItem');
|
|
11949
|
+
if (childItem)
|
|
11950
|
+
this.focusItem(childItem);
|
|
11951
|
+
}
|
|
11952
|
+
}
|
|
11953
|
+
handleArrowLeft(current) {
|
|
11954
|
+
const caret = current.querySelector('.tsi-caret-icon');
|
|
11955
|
+
const expanded = current.getAttribute('aria-expanded') === 'true';
|
|
11956
|
+
if (caret && expanded) {
|
|
11957
|
+
// collapse
|
|
11958
|
+
current.click();
|
|
11959
|
+
return;
|
|
11960
|
+
}
|
|
11961
|
+
// move focus to parent
|
|
11962
|
+
const li = current.closest('li');
|
|
11963
|
+
const parentLi = li?.parentElement?.closest('li');
|
|
11964
|
+
const parentItem = parentLi?.querySelector('.tsi-hierarchyItem');
|
|
11965
|
+
if (parentItem)
|
|
11966
|
+
this.focusItem(parentItem);
|
|
12837
11967
|
}
|
|
12838
11968
|
// prepares the parameters for search request
|
|
12839
11969
|
requestPayload(hierarchy = null) {
|
|
@@ -12842,32 +11972,7 @@ class HierarchyNavigation extends Component {
|
|
|
12842
11972
|
}
|
|
12843
11973
|
// 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
11974
|
renderTree(data, target) {
|
|
12845
|
-
|
|
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
|
-
});
|
|
11975
|
+
TreeRenderer.render(this, data, target);
|
|
12871
11976
|
}
|
|
12872
11977
|
renderSearchBox() {
|
|
12873
11978
|
this.searchWrapperElem = this.hierarchyNavWrapper.append('div').classed('tsi-hierarchy-search', true);
|
|
@@ -12876,40 +11981,140 @@ class HierarchyNavigation extends Component {
|
|
|
12876
11981
|
let input = inputWrapper
|
|
12877
11982
|
.append("input")
|
|
12878
11983
|
.attr("class", "tsi-searchInput")
|
|
12879
|
-
.attr("aria-label", this.getString("Search
|
|
12880
|
-
.attr("aria-describedby", "tsi-search-desc")
|
|
11984
|
+
.attr("aria-label", this.getString("Search"))
|
|
11985
|
+
.attr("aria-describedby", "tsi-hierarchy-search-desc")
|
|
12881
11986
|
.attr("role", "combobox")
|
|
12882
11987
|
.attr("aria-owns", "tsi-search-results")
|
|
12883
11988
|
.attr("aria-expanded", "false")
|
|
12884
11989
|
.attr("aria-haspopup", "listbox")
|
|
12885
|
-
.attr("placeholder", this.getString("Search
|
|
11990
|
+
.attr("placeholder", this.getString("Search") + "...");
|
|
11991
|
+
// Add ARIA description for screen readers
|
|
11992
|
+
inputWrapper
|
|
11993
|
+
.append("span")
|
|
11994
|
+
.attr("id", "tsi-hierarchy-search-desc")
|
|
11995
|
+
.style("display", "none")
|
|
11996
|
+
.text(this.getString("Search suggestion instruction") || "Use arrow keys to navigate suggestions");
|
|
11997
|
+
// Add live region for search results info
|
|
11998
|
+
inputWrapper
|
|
11999
|
+
.append("div")
|
|
12000
|
+
.attr("class", "tsi-search-results-info")
|
|
12001
|
+
.attr("aria-live", "assertive");
|
|
12002
|
+
// Add clear button
|
|
12003
|
+
let clear = inputWrapper
|
|
12004
|
+
.append("div")
|
|
12005
|
+
.attr("class", "tsi-clear")
|
|
12006
|
+
.attr("tabindex", "0")
|
|
12007
|
+
.attr("role", "button")
|
|
12008
|
+
.attr("aria-label", "Clear Search")
|
|
12009
|
+
.on("click keydown", function (event) {
|
|
12010
|
+
if (Utils.isKeyDownAndNotEnter(event)) {
|
|
12011
|
+
return;
|
|
12012
|
+
}
|
|
12013
|
+
input.node().value = "";
|
|
12014
|
+
self.exitSearchMode();
|
|
12015
|
+
self.ap.close();
|
|
12016
|
+
d3.select(this).classed("tsi-shown", false);
|
|
12017
|
+
});
|
|
12018
|
+
// Initialize Awesomplete for autocomplete (only if enabled)
|
|
12019
|
+
let Awesomplete = window.Awesomplete;
|
|
12020
|
+
if (this.autocompleteEnabled && Awesomplete) {
|
|
12021
|
+
this.ap = new Awesomplete(input.node(), {
|
|
12022
|
+
minChars: 1,
|
|
12023
|
+
maxItems: 10,
|
|
12024
|
+
autoFirst: true
|
|
12025
|
+
});
|
|
12026
|
+
}
|
|
12027
|
+
else {
|
|
12028
|
+
// Create a dummy object if autocomplete is disabled
|
|
12029
|
+
this.ap = {
|
|
12030
|
+
list: [],
|
|
12031
|
+
close: () => { },
|
|
12032
|
+
evaluate: () => { }
|
|
12033
|
+
};
|
|
12034
|
+
}
|
|
12886
12035
|
let self = this;
|
|
12036
|
+
let noSuggest = false;
|
|
12037
|
+
let justAwesompleted = false;
|
|
12038
|
+
// Handle autocomplete selection (only if enabled)
|
|
12039
|
+
if (this.autocompleteEnabled) {
|
|
12040
|
+
input.node().addEventListener("awesomplete-selectcomplete", (event) => {
|
|
12041
|
+
noSuggest = true;
|
|
12042
|
+
const selectedValue = event.text.value;
|
|
12043
|
+
// Trigger search with selected value
|
|
12044
|
+
self.performDeepSearch(selectedValue);
|
|
12045
|
+
justAwesompleted = true;
|
|
12046
|
+
});
|
|
12047
|
+
}
|
|
12887
12048
|
input.on("keydown", (event) => {
|
|
12049
|
+
// Handle ESC key to clear the search box
|
|
12050
|
+
if (event.key === 'Escape') {
|
|
12051
|
+
const inputElement = event.target;
|
|
12052
|
+
inputElement.value = '';
|
|
12053
|
+
// Trigger input event to clear search results
|
|
12054
|
+
self.exitSearchMode();
|
|
12055
|
+
self.ap.close();
|
|
12056
|
+
clear.classed("tsi-shown", false);
|
|
12057
|
+
return;
|
|
12058
|
+
}
|
|
12888
12059
|
this.chartOptions.onKeydown(event, this.ap);
|
|
12889
12060
|
});
|
|
12890
|
-
|
|
12061
|
+
input.node().addEventListener("keyup", function (event) {
|
|
12062
|
+
if (justAwesompleted) {
|
|
12063
|
+
justAwesompleted = false;
|
|
12064
|
+
return;
|
|
12065
|
+
}
|
|
12066
|
+
let key = event.which || event.keyCode;
|
|
12067
|
+
if (key === 13) {
|
|
12068
|
+
noSuggest = true;
|
|
12069
|
+
}
|
|
12070
|
+
});
|
|
12071
|
+
// Debounced input handler to reduce work while typing
|
|
12891
12072
|
input.on("input", function (event) {
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
self.
|
|
12896
|
-
self.
|
|
12073
|
+
const val = event.target.value;
|
|
12074
|
+
// always clear existing timer
|
|
12075
|
+
if (self.debounceTimer) {
|
|
12076
|
+
clearTimeout(self.debounceTimer);
|
|
12077
|
+
self.debounceTimer = null;
|
|
12078
|
+
}
|
|
12079
|
+
// Show/hide clear button
|
|
12080
|
+
clear.classed("tsi-shown", val.length > 0);
|
|
12081
|
+
if (!val || val.length === 0) {
|
|
12082
|
+
// Exit search mode and restore navigation view
|
|
12083
|
+
self.exitSearchMode();
|
|
12084
|
+
self.ap.close();
|
|
12085
|
+
return;
|
|
12086
|
+
}
|
|
12087
|
+
// Populate autocomplete suggestions with instance leaves (only if enabled)
|
|
12088
|
+
if (self.autocompleteEnabled && !noSuggest && val.length >= 1) {
|
|
12089
|
+
self.fetchAutocompleteSuggestions(val);
|
|
12897
12090
|
}
|
|
12898
12091
|
else {
|
|
12899
|
-
|
|
12900
|
-
self.filterTree(searchText);
|
|
12092
|
+
self.ap.close();
|
|
12901
12093
|
}
|
|
12094
|
+
// Use deep search for comprehensive results
|
|
12095
|
+
self.debounceTimer = setTimeout(() => {
|
|
12096
|
+
self.performDeepSearch(val);
|
|
12097
|
+
}, self.debounceDelay);
|
|
12098
|
+
noSuggest = false;
|
|
12902
12099
|
});
|
|
12903
12100
|
}
|
|
12904
12101
|
async pathSearchAndRenderResult({ search: { payload, bubbleUpReject = false }, render: { target, locInTarget = null } }) {
|
|
12102
|
+
const requestId = ++this.requestCounter;
|
|
12103
|
+
this.latestRequestId = requestId;
|
|
12905
12104
|
try {
|
|
12906
12105
|
const result = await this.searchFunction(payload);
|
|
12106
|
+
if (requestId !== this.latestRequestId) {
|
|
12107
|
+
return;
|
|
12108
|
+
}
|
|
12907
12109
|
if (result.error) {
|
|
12908
12110
|
throw result.error;
|
|
12909
12111
|
}
|
|
12910
|
-
this.renderSearchResult(result, payload, target);
|
|
12112
|
+
await this.renderSearchResult(result, payload, target);
|
|
12911
12113
|
}
|
|
12912
12114
|
catch (err) {
|
|
12115
|
+
if (requestId !== this.latestRequestId) {
|
|
12116
|
+
return;
|
|
12117
|
+
}
|
|
12913
12118
|
this.chartOptions.onError("Error in hierarchy navigation", "Failed to complete search", err instanceof XMLHttpRequest ? err : null);
|
|
12914
12119
|
if (bubbleUpReject) {
|
|
12915
12120
|
throw err;
|
|
@@ -12917,11 +12122,18 @@ class HierarchyNavigation extends Component {
|
|
|
12917
12122
|
}
|
|
12918
12123
|
}
|
|
12919
12124
|
filterTree(searchText) {
|
|
12920
|
-
|
|
12921
|
-
|
|
12125
|
+
const nodes = this.hierarchyElem.selectAll('ul').nodes();
|
|
12126
|
+
if (!nodes || !nodes.length)
|
|
12127
|
+
return;
|
|
12128
|
+
const tree = nodes[0];
|
|
12129
|
+
if (!tree)
|
|
12130
|
+
return;
|
|
12131
|
+
const list = tree.querySelectorAll('li');
|
|
12132
|
+
const needle = searchText.toLowerCase();
|
|
12922
12133
|
list.forEach((li) => {
|
|
12923
|
-
|
|
12924
|
-
|
|
12134
|
+
const attrName = li.getAttribute('data-display-name');
|
|
12135
|
+
let name = attrName && attrName.length ? attrName : (li.querySelector('.tsi-name')?.textContent || '');
|
|
12136
|
+
if (name.toLowerCase().includes(needle)) {
|
|
12925
12137
|
li.style.display = 'block';
|
|
12926
12138
|
}
|
|
12927
12139
|
else {
|
|
@@ -12929,11 +12141,300 @@ class HierarchyNavigation extends Component {
|
|
|
12929
12141
|
}
|
|
12930
12142
|
});
|
|
12931
12143
|
}
|
|
12144
|
+
// Fetch autocomplete suggestions for instances (leaves)
|
|
12145
|
+
async fetchAutocompleteSuggestions(searchText) {
|
|
12146
|
+
if (!searchText || searchText.length < 1) {
|
|
12147
|
+
this.ap.list = [];
|
|
12148
|
+
return;
|
|
12149
|
+
}
|
|
12150
|
+
try {
|
|
12151
|
+
// Call server search to get instance suggestions
|
|
12152
|
+
const payload = {
|
|
12153
|
+
path: this.path,
|
|
12154
|
+
searchTerm: searchText,
|
|
12155
|
+
recursive: true,
|
|
12156
|
+
includeInstances: true,
|
|
12157
|
+
// Limit results for autocomplete
|
|
12158
|
+
maxResults: 10
|
|
12159
|
+
};
|
|
12160
|
+
const results = await this.searchFunction(payload);
|
|
12161
|
+
if (results.error) {
|
|
12162
|
+
this.ap.list = [];
|
|
12163
|
+
return;
|
|
12164
|
+
}
|
|
12165
|
+
// Extract instance names for autocomplete suggestions
|
|
12166
|
+
const suggestions = [];
|
|
12167
|
+
if (results.instances?.hits) {
|
|
12168
|
+
results.instances.hits.forEach((i) => {
|
|
12169
|
+
const displayName = this.instanceNodeStringToDisplay(i);
|
|
12170
|
+
const pathStr = i.hierarchyPath && i.hierarchyPath.length > 0
|
|
12171
|
+
? i.hierarchyPath.join(' > ') + ' > '
|
|
12172
|
+
: '';
|
|
12173
|
+
suggestions.push({
|
|
12174
|
+
label: pathStr + displayName,
|
|
12175
|
+
value: displayName
|
|
12176
|
+
});
|
|
12177
|
+
});
|
|
12178
|
+
}
|
|
12179
|
+
// Update Awesomplete list
|
|
12180
|
+
this.ap.list = suggestions;
|
|
12181
|
+
}
|
|
12182
|
+
catch (err) {
|
|
12183
|
+
// Silently fail for autocomplete - don't interrupt user experience
|
|
12184
|
+
this.ap.list = [];
|
|
12185
|
+
}
|
|
12186
|
+
}
|
|
12187
|
+
// Perform deep search across entire hierarchy using server-side search
|
|
12188
|
+
async performDeepSearch(searchText) {
|
|
12189
|
+
if (!searchText || searchText.length < 2) {
|
|
12190
|
+
this.exitSearchMode();
|
|
12191
|
+
return;
|
|
12192
|
+
}
|
|
12193
|
+
this.isSearchMode = true;
|
|
12194
|
+
const requestId = ++this.requestCounter;
|
|
12195
|
+
this.latestRequestId = requestId;
|
|
12196
|
+
try {
|
|
12197
|
+
// Call server search with recursive flag
|
|
12198
|
+
const payload = {
|
|
12199
|
+
path: this.path,
|
|
12200
|
+
searchTerm: searchText,
|
|
12201
|
+
recursive: true, // Search entire subtree
|
|
12202
|
+
includeInstances: true
|
|
12203
|
+
};
|
|
12204
|
+
const results = await this.searchFunction(payload);
|
|
12205
|
+
if (requestId !== this.latestRequestId)
|
|
12206
|
+
return; // Stale request
|
|
12207
|
+
if (results.error) {
|
|
12208
|
+
throw results.error;
|
|
12209
|
+
}
|
|
12210
|
+
// Render search results in flat list view
|
|
12211
|
+
this.renderSearchResults(results, searchText);
|
|
12212
|
+
}
|
|
12213
|
+
catch (err) {
|
|
12214
|
+
if (requestId !== this.latestRequestId)
|
|
12215
|
+
return;
|
|
12216
|
+
this.chartOptions.onError("Search failed", "Unable to search hierarchy", err instanceof XMLHttpRequest ? err : null);
|
|
12217
|
+
}
|
|
12218
|
+
}
|
|
12219
|
+
// Render search results with breadcrumb paths
|
|
12220
|
+
renderSearchResults(results, searchText) {
|
|
12221
|
+
this.hierarchyElem.selectAll('*').remove();
|
|
12222
|
+
const flatResults = [];
|
|
12223
|
+
// Flatten hierarchy results with full paths
|
|
12224
|
+
if (results.hierarchyNodes?.hits) {
|
|
12225
|
+
results.hierarchyNodes.hits.forEach((h) => {
|
|
12226
|
+
flatResults.push({
|
|
12227
|
+
type: 'hierarchy',
|
|
12228
|
+
name: h.name,
|
|
12229
|
+
path: h.path || [],
|
|
12230
|
+
id: h.id,
|
|
12231
|
+
cumulativeInstanceCount: h.cumulativeInstanceCount,
|
|
12232
|
+
highlightedName: this.highlightMatch(h.name, searchText),
|
|
12233
|
+
node: h
|
|
12234
|
+
});
|
|
12235
|
+
});
|
|
12236
|
+
}
|
|
12237
|
+
// Flatten instance results with full paths
|
|
12238
|
+
if (results.instances?.hits) {
|
|
12239
|
+
results.instances.hits.forEach((i) => {
|
|
12240
|
+
const displayName = this.instanceNodeStringToDisplay(i);
|
|
12241
|
+
flatResults.push({
|
|
12242
|
+
type: 'instance',
|
|
12243
|
+
name: i.name,
|
|
12244
|
+
path: i.hierarchyPath || [],
|
|
12245
|
+
id: i.id,
|
|
12246
|
+
timeSeriesId: i.timeSeriesId,
|
|
12247
|
+
description: i.description,
|
|
12248
|
+
highlightedName: this.highlightMatch(displayName, searchText),
|
|
12249
|
+
node: i
|
|
12250
|
+
});
|
|
12251
|
+
});
|
|
12252
|
+
}
|
|
12253
|
+
// Render flat list with breadcrumbs
|
|
12254
|
+
const searchList = this.hierarchyElem
|
|
12255
|
+
.append('div')
|
|
12256
|
+
.classed('tsi-search-results', true);
|
|
12257
|
+
if (flatResults.length === 0) {
|
|
12258
|
+
searchList.append('div')
|
|
12259
|
+
.classed('tsi-noResults', true)
|
|
12260
|
+
.text(this.getString('No results'));
|
|
12261
|
+
return;
|
|
12262
|
+
}
|
|
12263
|
+
searchList.append('div')
|
|
12264
|
+
.classed('tsi-search-results-header', true)
|
|
12265
|
+
.html(`<strong>${flatResults.length}</strong> ${this.getString('results found') || 'results found'}`);
|
|
12266
|
+
const resultItems = searchList.selectAll('.tsi-search-result-item')
|
|
12267
|
+
.data(flatResults)
|
|
12268
|
+
.enter()
|
|
12269
|
+
.append('div')
|
|
12270
|
+
.classed('tsi-search-result-item', true)
|
|
12271
|
+
.attr('tabindex', '0')
|
|
12272
|
+
.attr('role', 'option')
|
|
12273
|
+
.attr('aria-label', (d) => {
|
|
12274
|
+
const pathStr = d.path.length > 0 ? d.path.join(' > ') + ' > ' : '';
|
|
12275
|
+
return pathStr + d.name;
|
|
12276
|
+
});
|
|
12277
|
+
const self = this;
|
|
12278
|
+
resultItems.each(function (d) {
|
|
12279
|
+
const item = d3.select(this);
|
|
12280
|
+
// Breadcrumb path
|
|
12281
|
+
if (d.path.length > 0) {
|
|
12282
|
+
item.append('div')
|
|
12283
|
+
.classed('tsi-search-breadcrumb', true)
|
|
12284
|
+
.text(d.path.join(' > '));
|
|
12285
|
+
}
|
|
12286
|
+
// Highlighted name
|
|
12287
|
+
item.append('div')
|
|
12288
|
+
.classed('tsi-search-result-name', true)
|
|
12289
|
+
.html(d.highlightedName);
|
|
12290
|
+
// Instance description or count
|
|
12291
|
+
if (d.type === 'instance' && d.description) {
|
|
12292
|
+
item.append('div')
|
|
12293
|
+
.classed('tsi-search-result-description', true)
|
|
12294
|
+
.text(d.description);
|
|
12295
|
+
}
|
|
12296
|
+
else if (d.type === 'hierarchy') {
|
|
12297
|
+
item.append('div')
|
|
12298
|
+
.classed('tsi-search-result-count', true)
|
|
12299
|
+
.text(`${d.cumulativeInstanceCount || 0} instances`);
|
|
12300
|
+
}
|
|
12301
|
+
});
|
|
12302
|
+
// Click handlers
|
|
12303
|
+
resultItems.on('click keydown', function (event, d) {
|
|
12304
|
+
if (Utils.isKeyDownAndNotEnter(event))
|
|
12305
|
+
return;
|
|
12306
|
+
if (d.type === 'instance') {
|
|
12307
|
+
// Handle instance selection
|
|
12308
|
+
if (self.chartOptions.onInstanceClick) {
|
|
12309
|
+
const inst = new InstanceNode(d.timeSeriesId, d.name, d.path.length, d.id, d.description);
|
|
12310
|
+
// Update selection state
|
|
12311
|
+
if (self.selectedIds && self.selectedIds.includes(d.id)) {
|
|
12312
|
+
self.selectedIds = self.selectedIds.filter(id => id !== d.id);
|
|
12313
|
+
d3.select(this).classed('tsi-selected', false);
|
|
12314
|
+
}
|
|
12315
|
+
else {
|
|
12316
|
+
self.selectedIds.push(d.id);
|
|
12317
|
+
d3.select(this).classed('tsi-selected', true);
|
|
12318
|
+
}
|
|
12319
|
+
self.chartOptions.onInstanceClick(inst);
|
|
12320
|
+
}
|
|
12321
|
+
}
|
|
12322
|
+
else {
|
|
12323
|
+
// Navigate to hierarchy node - exit search and expand to that path
|
|
12324
|
+
self.navigateToPath(d.path);
|
|
12325
|
+
}
|
|
12326
|
+
});
|
|
12327
|
+
// Apply selection state to already-selected instances
|
|
12328
|
+
resultItems.each(function (d) {
|
|
12329
|
+
if (d.type === 'instance' && self.selectedIds && self.selectedIds.includes(d.id)) {
|
|
12330
|
+
d3.select(this).classed('tsi-selected', true);
|
|
12331
|
+
}
|
|
12332
|
+
});
|
|
12333
|
+
}
|
|
12334
|
+
// Exit search mode and restore tree
|
|
12335
|
+
exitSearchMode() {
|
|
12336
|
+
this.isSearchMode = false;
|
|
12337
|
+
this.hierarchyElem.selectAll('*').remove();
|
|
12338
|
+
this.pathSearchAndRenderResult({
|
|
12339
|
+
search: { payload: this.requestPayload() },
|
|
12340
|
+
render: { target: this.hierarchyElem }
|
|
12341
|
+
});
|
|
12342
|
+
}
|
|
12343
|
+
// Navigate to a specific path in the hierarchy
|
|
12344
|
+
async navigateToPath(targetPath) {
|
|
12345
|
+
this.exitSearchMode();
|
|
12346
|
+
// For now, just exit search mode and return to root
|
|
12347
|
+
// In a more advanced implementation, this would progressively
|
|
12348
|
+
// expand nodes along the path to reveal the target
|
|
12349
|
+
// This would require waiting for each level to load before expanding the next
|
|
12350
|
+
}
|
|
12351
|
+
// Pre-compute which paths need to be auto-expanded for preselected instances
|
|
12352
|
+
async computePathsToAutoExpand(instanceIds) {
|
|
12353
|
+
if (!instanceIds || instanceIds.length === 0) {
|
|
12354
|
+
return;
|
|
12355
|
+
}
|
|
12356
|
+
// console.log('[HierarchyNavigation] Computing paths to auto-expand for:', instanceIds);
|
|
12357
|
+
try {
|
|
12358
|
+
this.pathsToAutoExpand.clear();
|
|
12359
|
+
for (const instanceId of instanceIds) {
|
|
12360
|
+
// Search for this specific instance
|
|
12361
|
+
const result = await this.searchFunction({
|
|
12362
|
+
path: this.path,
|
|
12363
|
+
searchTerm: instanceId,
|
|
12364
|
+
recursive: true,
|
|
12365
|
+
includeInstances: true
|
|
12366
|
+
});
|
|
12367
|
+
if (result?.instances?.hits) {
|
|
12368
|
+
for (const instance of result.instances.hits) {
|
|
12369
|
+
// Match by ID
|
|
12370
|
+
if (instance.id === instanceId ||
|
|
12371
|
+
(instance.id && instance.id.includes(instanceId))) {
|
|
12372
|
+
if (instance.hierarchyPath && instance.hierarchyPath.length > 0) {
|
|
12373
|
+
// Add all parent paths that need to be expanded
|
|
12374
|
+
const hierarchyPath = instance.hierarchyPath;
|
|
12375
|
+
for (let i = 1; i <= hierarchyPath.length; i++) {
|
|
12376
|
+
const pathArray = hierarchyPath.slice(0, i);
|
|
12377
|
+
const pathKey = pathArray.join('/');
|
|
12378
|
+
this.pathsToAutoExpand.add(pathKey);
|
|
12379
|
+
}
|
|
12380
|
+
}
|
|
12381
|
+
}
|
|
12382
|
+
}
|
|
12383
|
+
}
|
|
12384
|
+
}
|
|
12385
|
+
// console.log('[HierarchyNavigation] Paths to auto-expand:', Array.from(this.pathsToAutoExpand));
|
|
12386
|
+
}
|
|
12387
|
+
catch (err) {
|
|
12388
|
+
console.warn('Failed to compute paths to auto-expand:', err);
|
|
12389
|
+
}
|
|
12390
|
+
}
|
|
12391
|
+
// Check if a path should be auto-expanded
|
|
12392
|
+
shouldAutoExpand(pathArray) {
|
|
12393
|
+
if (this.pathsToAutoExpand.size === 0) {
|
|
12394
|
+
return false;
|
|
12395
|
+
}
|
|
12396
|
+
const pathKey = pathArray.join('/');
|
|
12397
|
+
return this.pathsToAutoExpand.has(pathKey);
|
|
12398
|
+
}
|
|
12399
|
+
// Auto-expand a node by triggering its expand function
|
|
12400
|
+
async autoExpandNode(node) {
|
|
12401
|
+
if (!node || !node.expand || !node.node) {
|
|
12402
|
+
return;
|
|
12403
|
+
}
|
|
12404
|
+
try {
|
|
12405
|
+
// Wait for the DOM node to be available
|
|
12406
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
12407
|
+
// Mark as expanded visually
|
|
12408
|
+
node.node.classed('tsi-expanded', true);
|
|
12409
|
+
// Call the expand function to load children
|
|
12410
|
+
await node.expand();
|
|
12411
|
+
// console.log(`[HierarchyNavigation] Auto-expanded node: ${node.path.join('/')}`);
|
|
12412
|
+
}
|
|
12413
|
+
catch (err) {
|
|
12414
|
+
console.warn(`Failed to auto-expand node ${node.path.join('/')}:`, err);
|
|
12415
|
+
}
|
|
12416
|
+
}
|
|
12417
|
+
// Highlight search term in text
|
|
12418
|
+
highlightMatch(text, searchTerm) {
|
|
12419
|
+
if (!text)
|
|
12420
|
+
return '';
|
|
12421
|
+
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12422
|
+
const regex = new RegExp(`(${escapedTerm})`, 'gi');
|
|
12423
|
+
return text.replace(regex, '<mark>$1</mark>');
|
|
12424
|
+
}
|
|
12932
12425
|
// creates in-depth data object using the server response for hierarchyNodes to show in the tree all expanded, considering UntilChildren
|
|
12933
12426
|
fillDataRecursively(hierarchyNodes, payload, payloadForContinuation = null) {
|
|
12934
12427
|
let data = {};
|
|
12935
12428
|
hierarchyNodes.hits.forEach((h) => {
|
|
12936
12429
|
let hierarchy = new HierarchyNode(h.name, payload.path, payload.path.length - this.path.length, h.cumulativeInstanceCount, h.id);
|
|
12430
|
+
// cache display name on node for client-side filtering
|
|
12431
|
+
hierarchy.displayName = h.name || '';
|
|
12432
|
+
// Check if this path should be auto-expanded
|
|
12433
|
+
const shouldExpand = this.shouldAutoExpand(hierarchy.path);
|
|
12434
|
+
if (shouldExpand) {
|
|
12435
|
+
hierarchy.isExpanded = true;
|
|
12436
|
+
//console.log(`[HierarchyNavigation] Auto-expanding node: ${hierarchy.path.join('/')}`);
|
|
12437
|
+
}
|
|
12937
12438
|
hierarchy.expand = () => {
|
|
12938
12439
|
hierarchy.isExpanded = true;
|
|
12939
12440
|
hierarchy.node.classed('tsi-expanded', true);
|
|
@@ -12957,7 +12458,7 @@ class HierarchyNavigation extends Component {
|
|
|
12957
12458
|
.attr('style', `padding-left: ${hORi.isLeaf ? hORi.level * 18 + 20 : (hORi.level + 1) * 18 + 20}px`)
|
|
12958
12459
|
.attr('tabindex', 0)
|
|
12959
12460
|
//.attr('arialabel', isHierarchyNode ? key : Utils.getTimeSeriesIdString(hORi))
|
|
12960
|
-
.attr('
|
|
12461
|
+
.attr('aria-label', isHierarchyNode ? key : self.getAriaLabel(hORi))
|
|
12961
12462
|
.attr('title', isHierarchyNode ? key : self.getAriaLabel(hORi))
|
|
12962
12463
|
.attr("role", "treeitem").attr('aria-expanded', hORi.isExpanded)
|
|
12963
12464
|
.on('click keydown', async function (event) {
|
|
@@ -13009,6 +12510,8 @@ class HierarchyNavigation extends Component {
|
|
|
13009
12510
|
return hORi.description || hORi.name || hORi.id || Utils.getTimeSeriesIdString(hORi);
|
|
13010
12511
|
}
|
|
13011
12512
|
}
|
|
12513
|
+
// TreeRenderer has been moved to its own module: ./TreeRenderer
|
|
12514
|
+
// The rendering logic was extracted to reduce file size and improve testability.
|
|
13012
12515
|
class HierarchyNode {
|
|
13013
12516
|
constructor(name, parentPath, level, cumulativeInstanceCount = null, id = null) {
|
|
13014
12517
|
this.name = name;
|
|
@@ -13253,6 +12756,7 @@ class SingleDateTimePicker extends ChartComponent {
|
|
|
13253
12756
|
class DateTimeButtonSingle extends DateTimeButton {
|
|
13254
12757
|
constructor(renderTarget) {
|
|
13255
12758
|
super(renderTarget);
|
|
12759
|
+
this.clickOutsideHandler = null;
|
|
13256
12760
|
this.sDTPOnSet = (millis = null) => {
|
|
13257
12761
|
if (millis !== null) {
|
|
13258
12762
|
this.dateTimeButton.text(this.buttonDateTimeFormat(millis));
|
|
@@ -13265,6 +12769,32 @@ class DateTimeButtonSingle extends DateTimeButton {
|
|
|
13265
12769
|
closeSDTP() {
|
|
13266
12770
|
this.dateTimePickerContainer.style("display", "none");
|
|
13267
12771
|
this.dateTimeButton.node().focus();
|
|
12772
|
+
this.removeClickOutsideHandler();
|
|
12773
|
+
}
|
|
12774
|
+
removeClickOutsideHandler() {
|
|
12775
|
+
if (this.clickOutsideHandler) {
|
|
12776
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
12777
|
+
this.clickOutsideHandler = null;
|
|
12778
|
+
}
|
|
12779
|
+
}
|
|
12780
|
+
setupClickOutsideHandler() {
|
|
12781
|
+
// Remove any existing handler first
|
|
12782
|
+
this.removeClickOutsideHandler();
|
|
12783
|
+
// Add handler after a small delay to prevent the opening click from immediately closing the picker
|
|
12784
|
+
setTimeout(() => {
|
|
12785
|
+
this.clickOutsideHandler = (event) => {
|
|
12786
|
+
const pickerElement = this.dateTimePickerContainer.node();
|
|
12787
|
+
const buttonElement = this.dateTimeButton.node();
|
|
12788
|
+
const target = event.target;
|
|
12789
|
+
// Check if click is outside both the picker and the button
|
|
12790
|
+
if (pickerElement && buttonElement &&
|
|
12791
|
+
!pickerElement.contains(target) &&
|
|
12792
|
+
!buttonElement.contains(target)) {
|
|
12793
|
+
this.closeSDTP();
|
|
12794
|
+
}
|
|
12795
|
+
};
|
|
12796
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
12797
|
+
}, 0);
|
|
13268
12798
|
}
|
|
13269
12799
|
render(chartOptions = {}, minMillis, maxMillis, selectedMillis = null, onSet = null) {
|
|
13270
12800
|
super.render(chartOptions, minMillis, maxMillis, onSet);
|
|
@@ -13274,12 +12804,11 @@ class DateTimeButtonSingle extends DateTimeButton {
|
|
|
13274
12804
|
if (!this.dateTimePicker) {
|
|
13275
12805
|
this.dateTimePicker = new SingleDateTimePicker(this.dateTimePickerContainer.node());
|
|
13276
12806
|
}
|
|
13277
|
-
let targetElement = d3.select(this.renderTarget);
|
|
13278
|
-
(targetElement.select(".tsi-dateTimePickerContainer")).selectAll("*");
|
|
13279
12807
|
this.dateTimeButton.on("click", () => {
|
|
13280
12808
|
this.chartOptions.dTPIsModal = true;
|
|
13281
12809
|
this.dateTimePickerContainer.style("display", "block");
|
|
13282
12810
|
this.dateTimePicker.render(this.chartOptions, this.minMillis, this.maxMillis, this.selectedMillis, this.sDTPOnSet);
|
|
12811
|
+
this.setupClickOutsideHandler();
|
|
13283
12812
|
});
|
|
13284
12813
|
}
|
|
13285
12814
|
}
|
|
@@ -13577,8 +13106,16 @@ class ProcessGraphic extends HistoryPlayback {
|
|
|
13577
13106
|
class PlaybackControls extends Component {
|
|
13578
13107
|
constructor(renderTarget, initialTimeStamp = null) {
|
|
13579
13108
|
super(renderTarget);
|
|
13580
|
-
this.
|
|
13581
|
-
this.
|
|
13109
|
+
this.playbackInterval = null;
|
|
13110
|
+
this.playButton = null;
|
|
13111
|
+
this.handleElement = null;
|
|
13112
|
+
this.controlsContainer = null;
|
|
13113
|
+
this.track = null;
|
|
13114
|
+
this.selectTimeStampCallback = null;
|
|
13115
|
+
this.wasPlayingWhenDragStarted = false;
|
|
13116
|
+
this.rafId = null;
|
|
13117
|
+
this.handleRadius = PlaybackControls.CONSTANTS.HANDLE_RADIUS;
|
|
13118
|
+
this.minimumPlaybackInterval = PlaybackControls.CONSTANTS.MINIMUM_PLAYBACK_INTERVAL_MS;
|
|
13582
13119
|
this.playbackInterval = null;
|
|
13583
13120
|
this.selectedTimeStamp = initialTimeStamp;
|
|
13584
13121
|
}
|
|
@@ -13586,6 +13123,21 @@ class PlaybackControls extends Component {
|
|
|
13586
13123
|
return this.selectedTimeStamp;
|
|
13587
13124
|
}
|
|
13588
13125
|
render(start, end, onSelectTimeStamp, options, playbackSettings) {
|
|
13126
|
+
// Validate inputs
|
|
13127
|
+
if (!(start instanceof Date) || !(end instanceof Date)) {
|
|
13128
|
+
throw new TypeError('start and end must be Date objects');
|
|
13129
|
+
}
|
|
13130
|
+
if (start >= end) {
|
|
13131
|
+
throw new RangeError('start must be before end');
|
|
13132
|
+
}
|
|
13133
|
+
if (!onSelectTimeStamp || typeof onSelectTimeStamp !== 'function') {
|
|
13134
|
+
throw new TypeError('onSelectTimeStamp must be a function');
|
|
13135
|
+
}
|
|
13136
|
+
// Clean up any pending animation frames before re-rendering
|
|
13137
|
+
if (this.rafId !== null) {
|
|
13138
|
+
cancelAnimationFrame(this.rafId);
|
|
13139
|
+
this.rafId = null;
|
|
13140
|
+
}
|
|
13589
13141
|
this.end = end;
|
|
13590
13142
|
this.selectTimeStampCallback = onSelectTimeStamp;
|
|
13591
13143
|
this.chartOptions.setOptions(options);
|
|
@@ -13647,6 +13199,9 @@ class PlaybackControls extends Component {
|
|
|
13647
13199
|
this.playButton = this.controlsContainer.append('button')
|
|
13648
13200
|
.classed('tsi-play-button', this.playbackInterval === null)
|
|
13649
13201
|
.classed('tsi-pause-button', this.playbackInterval !== null)
|
|
13202
|
+
// Accessibility attributes
|
|
13203
|
+
.attr('aria-label', 'Play/Pause playback')
|
|
13204
|
+
.attr('title', 'Play/Pause playback')
|
|
13650
13205
|
.on('click', () => {
|
|
13651
13206
|
if (this.playbackInterval === null) {
|
|
13652
13207
|
this.play();
|
|
@@ -13698,6 +13253,27 @@ class PlaybackControls extends Component {
|
|
|
13698
13253
|
this.updateSelection(handlePosition, this.selectedTimeStamp);
|
|
13699
13254
|
this.selectTimeStampCallback(this.selectedTimeStamp);
|
|
13700
13255
|
}
|
|
13256
|
+
/**
|
|
13257
|
+
* Cleanup resources to prevent memory leaks
|
|
13258
|
+
*/
|
|
13259
|
+
destroy() {
|
|
13260
|
+
this.pause();
|
|
13261
|
+
// Cancel any pending animation frames
|
|
13262
|
+
if (this.rafId !== null) {
|
|
13263
|
+
cancelAnimationFrame(this.rafId);
|
|
13264
|
+
this.rafId = null;
|
|
13265
|
+
}
|
|
13266
|
+
// Remove event listeners
|
|
13267
|
+
if (this.controlsContainer) {
|
|
13268
|
+
this.controlsContainer.selectAll('*').on('.', null);
|
|
13269
|
+
}
|
|
13270
|
+
// Clear DOM references
|
|
13271
|
+
this.playButton = null;
|
|
13272
|
+
this.handleElement = null;
|
|
13273
|
+
this.controlsContainer = null;
|
|
13274
|
+
this.track = null;
|
|
13275
|
+
this.selectTimeStampCallback = null;
|
|
13276
|
+
}
|
|
13701
13277
|
clamp(number, min, max) {
|
|
13702
13278
|
let clamped = Math.max(number, min);
|
|
13703
13279
|
return Math.min(clamped, max);
|
|
@@ -13706,9 +13282,17 @@ class PlaybackControls extends Component {
|
|
|
13706
13282
|
this.wasPlayingWhenDragStarted = this.wasPlayingWhenDragStarted ||
|
|
13707
13283
|
(this.playbackInterval !== null);
|
|
13708
13284
|
this.pause();
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
this.
|
|
13285
|
+
// Use requestAnimationFrame to batch DOM updates for better performance
|
|
13286
|
+
// Cancel any pending animation frame to prevent stacking updates
|
|
13287
|
+
if (this.rafId !== null) {
|
|
13288
|
+
cancelAnimationFrame(this.rafId);
|
|
13289
|
+
}
|
|
13290
|
+
this.rafId = requestAnimationFrame(() => {
|
|
13291
|
+
const handlePosition = this.clamp(positionX, 0, this.trackWidth);
|
|
13292
|
+
this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
|
|
13293
|
+
this.updateSelection(handlePosition, this.selectedTimeStamp);
|
|
13294
|
+
this.rafId = null;
|
|
13295
|
+
});
|
|
13712
13296
|
}
|
|
13713
13297
|
onDragEnd() {
|
|
13714
13298
|
this.selectTimeStampCallback(this.selectedTimeStamp);
|
|
@@ -13731,6 +13315,12 @@ class PlaybackControls extends Component {
|
|
|
13731
13315
|
.text(this.timeFormatter(timeStamp));
|
|
13732
13316
|
}
|
|
13733
13317
|
}
|
|
13318
|
+
PlaybackControls.CONSTANTS = {
|
|
13319
|
+
HANDLE_RADIUS: 7,
|
|
13320
|
+
MINIMUM_PLAYBACK_INTERVAL_MS: 1000,
|
|
13321
|
+
HANDLE_PADDING: 8,
|
|
13322
|
+
AXIS_OFFSET: 6,
|
|
13323
|
+
};
|
|
13734
13324
|
class TimeAxis extends TemporalXAxisComponent {
|
|
13735
13325
|
constructor(renderTarget) {
|
|
13736
13326
|
super(renderTarget);
|