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