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