@easyquery/ui 7.4.2 → 7.5.0-rc01
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/LICENSE.md +162 -162
- package/README.md +39 -39
- package/dist/assets/css/aggr-settings.css +240 -240
- package/dist/assets/css/easychart.css +71 -71
- package/dist/assets/css/easyquery-mobile.css +270 -270
- package/dist/assets/css/easyquery.css +1452 -1452
- package/dist/assets/css/easyquery.ui.css +2874 -2806
- package/dist/assets/css/eq-icons.css +114 -114
- package/dist/assets/css/themes.css +11 -11
- package/dist/assets/css/upgrade.css +486 -454
- package/dist/easyquery.ui.cjs.js +385 -75
- package/dist/easyquery.ui.esm.js +385 -75
- package/dist/types/utils/column_key_utils.d.ts +20 -0
- package/dist/types/utils/popup_menu_utils.d.ts +27 -0
- package/dist/types/views/view_options.d.ts +9 -0
- package/dist/types/widgets/charts/chart_widget.d.ts +8 -0
- package/dist/types/widgets/charts/chart_widget_chartjs.d.ts +9 -0
- package/dist/types/widgets/columns_panel.d.ts +4 -3
- package/dist/types/widgets/easy_grid_widget.d.ts +22 -0
- package/dist/types/widgets/entities_panel.d.ts +4 -2
- package/dist/types/widgets/sorting_bar.d.ts +3 -2
- package/package.json +4 -3
package/dist/easyquery.ui.esm.js
CHANGED
|
@@ -42,11 +42,15 @@ class EntitiesPanel extends Widget {
|
|
|
42
42
|
* The default constructor.
|
|
43
43
|
* @param slot The html element.
|
|
44
44
|
*/
|
|
45
|
-
constructor(slot) {
|
|
45
|
+
constructor(slot, customQuery) {
|
|
46
46
|
super(slot);
|
|
47
|
+
this.customQuery = null;
|
|
47
48
|
this.queryChangedCallbackId = null;
|
|
48
49
|
this.group = WidgetGroup.Model | WidgetGroup.Query;
|
|
49
50
|
this.panel = slot;
|
|
51
|
+
if (customQuery) {
|
|
52
|
+
this.customQuery = customQuery;
|
|
53
|
+
}
|
|
50
54
|
}
|
|
51
55
|
getWidgetType() {
|
|
52
56
|
return 'entitiesPanel';
|
|
@@ -61,6 +65,12 @@ class EntitiesPanel extends Widget {
|
|
|
61
65
|
this.setOptions(options);
|
|
62
66
|
this.renderProgressBlock();
|
|
63
67
|
}
|
|
68
|
+
getQuery() {
|
|
69
|
+
if (this.customQuery) {
|
|
70
|
+
return this.customQuery;
|
|
71
|
+
}
|
|
72
|
+
return this.context.getQuery();
|
|
73
|
+
}
|
|
64
74
|
/**
|
|
65
75
|
* Refresh widget implementation
|
|
66
76
|
*/
|
|
@@ -110,11 +120,11 @@ class EntitiesPanel extends Widget {
|
|
|
110
120
|
utils.assignDeep(this.options, options);
|
|
111
121
|
}
|
|
112
122
|
if (this.queryChangedCallbackId) {
|
|
113
|
-
const query = this.
|
|
123
|
+
const query = this.getQuery();
|
|
114
124
|
query.removeChangedCallback(this.queryChangedCallbackId);
|
|
115
125
|
}
|
|
116
126
|
if (this.options.syncWithColumns) {
|
|
117
|
-
const query = this.
|
|
127
|
+
const query = this.getQuery();
|
|
118
128
|
this.queryChangedCallbackId = query.addChangedCallback((ev) => {
|
|
119
129
|
const edata = ev.data;
|
|
120
130
|
if (!edata || (edata.part != QueryChangePart.Columns && edata.part != QueryChangePart.All)) {
|
|
@@ -225,7 +235,7 @@ class EntitiesPanel extends Widget {
|
|
|
225
235
|
if (this.options.syncWithColumns) {
|
|
226
236
|
attrInput.addEventListener('change', (ev) => {
|
|
227
237
|
const model = this.context.getModel();
|
|
228
|
-
const query = this.
|
|
238
|
+
const query = this.getQuery();
|
|
229
239
|
const checkbox = ev.currentTarget;
|
|
230
240
|
const attrId = checkbox.parentElement.parentElement.getAttribute('data-id');
|
|
231
241
|
if (model.checkAttrProperty(attrId, "useInResult")) {
|
|
@@ -324,7 +334,7 @@ class EntitiesPanel extends Widget {
|
|
|
324
334
|
entity.checked = element.checked;
|
|
325
335
|
}
|
|
326
336
|
if (this.options.syncWithColumns) {
|
|
327
|
-
const query = this.
|
|
337
|
+
const query = this.getQuery();
|
|
328
338
|
const model = this.context.getModel();
|
|
329
339
|
const attrList = [];
|
|
330
340
|
const attrElems = Array.from(entityChildren.querySelectorAll(`.${this.cssPrefix}-entity-attr`));
|
|
@@ -429,7 +439,7 @@ class EntitiesPanel extends Widget {
|
|
|
429
439
|
const attrElements = this.panel.querySelectorAll(`.${this.cssPrefix}-entity-attr`);
|
|
430
440
|
let attrList = [];
|
|
431
441
|
let attrId;
|
|
432
|
-
const query = this.
|
|
442
|
+
const query = this.getQuery();
|
|
433
443
|
query.fireProcessEvent({
|
|
434
444
|
source: self,
|
|
435
445
|
status: "start"
|
|
@@ -467,7 +477,7 @@ class EntitiesPanel extends Widget {
|
|
|
467
477
|
let attrElements = this.panel.querySelectorAll(`.${this.cssPrefix}-entity-attr`);
|
|
468
478
|
let attrList = [];
|
|
469
479
|
let attrId;
|
|
470
|
-
let query = this.
|
|
480
|
+
let query = this.getQuery();
|
|
471
481
|
query.fireProcessEvent({
|
|
472
482
|
source: self,
|
|
473
483
|
status: "start"
|
|
@@ -530,7 +540,7 @@ class EntitiesPanel extends Widget {
|
|
|
530
540
|
}
|
|
531
541
|
refreshCheckedStateByColumns() {
|
|
532
542
|
this.deselectAll();
|
|
533
|
-
const query = this.
|
|
543
|
+
const query = this.getQuery();
|
|
534
544
|
const exprColumns = query.getColumns()
|
|
535
545
|
.filter(col => col.expr.tag === ExprTag.EntityAttribute);
|
|
536
546
|
for (const column of exprColumns) {
|
|
@@ -647,6 +657,80 @@ var ConditionsEditMode;
|
|
|
647
657
|
ConditionsEditMode[ConditionsEditMode["Full"] = 255] = "Full";
|
|
648
658
|
})(ConditionsEditMode || (ConditionsEditMode = {}));
|
|
649
659
|
|
|
660
|
+
function computeMenuPlacement(input) {
|
|
661
|
+
const minOffset = input.minOffset != null ? input.minOffset : 15;
|
|
662
|
+
const alignOffset = input.alignOffset != null ? input.alignOffset : 0;
|
|
663
|
+
const { anchorY, anchorHeight, menuHeight, scrollDivHeight, scrollY, viewportHeight, levelIndex, useMaxSpace } = input;
|
|
664
|
+
const viewportTopDoc = scrollY;
|
|
665
|
+
const viewportBottomDoc = scrollY + viewportHeight;
|
|
666
|
+
// A sub-level sits beside its parent item, so it aligns top-with-top when
|
|
667
|
+
// opening downward and bottom-with-bottom when opening upward. The root
|
|
668
|
+
// level opens below or above its anchor element.
|
|
669
|
+
const sideAttached = levelIndex > 0;
|
|
670
|
+
const downTop = sideAttached ? anchorY - alignOffset : anchorY + anchorHeight;
|
|
671
|
+
const upBottom = sideAttached ? anchorY + anchorHeight + alignOffset : anchorY;
|
|
672
|
+
const spaceBelow = (viewportBottomDoc - minOffset) - downTop;
|
|
673
|
+
const spaceAbove = upBottom - (viewportTopDoc + minOffset);
|
|
674
|
+
// A. fits below the anchor without scrolling
|
|
675
|
+
if (menuHeight < spaceBelow) {
|
|
676
|
+
return { top: downTop, maxHeight: null };
|
|
677
|
+
}
|
|
678
|
+
// B. fits above without scrolling
|
|
679
|
+
if (menuHeight < spaceAbove) {
|
|
680
|
+
return { top: upBottom - menuHeight, maxHeight: null };
|
|
681
|
+
}
|
|
682
|
+
// C. useMaxSpace: occupy as much of the viewport as the list needs,
|
|
683
|
+
// growing from the anchor toward the side with more room and sliding
|
|
684
|
+
// past the anchor only as far as the viewport edge requires; a list
|
|
685
|
+
// taller than the viewport fills it and scrolls internally. With the
|
|
686
|
+
// option off, every level stays attached to its trigger instead (D/E).
|
|
687
|
+
if (useMaxSpace) {
|
|
688
|
+
if (spaceAbove <= spaceBelow) {
|
|
689
|
+
let top = sideAttached ? downTop : anchorY;
|
|
690
|
+
const overflowBottom = (top + menuHeight) - (viewportBottomDoc - minOffset);
|
|
691
|
+
if (overflowBottom > 0) {
|
|
692
|
+
top -= overflowBottom;
|
|
693
|
+
}
|
|
694
|
+
let maxHeight = null;
|
|
695
|
+
const overflowTop = (viewportTopDoc + minOffset) - top;
|
|
696
|
+
if (overflowTop > 0) {
|
|
697
|
+
top += overflowTop;
|
|
698
|
+
maxHeight = scrollDivHeight - overflowTop;
|
|
699
|
+
}
|
|
700
|
+
return { top, maxHeight };
|
|
701
|
+
}
|
|
702
|
+
let top = upBottom - menuHeight;
|
|
703
|
+
const overflowTop = (viewportTopDoc + minOffset) - top;
|
|
704
|
+
if (overflowTop > 0) {
|
|
705
|
+
top += overflowTop;
|
|
706
|
+
}
|
|
707
|
+
let maxHeight = null;
|
|
708
|
+
const overflowBottom = (top + menuHeight) - (viewportBottomDoc - minOffset);
|
|
709
|
+
if (overflowBottom > 0) {
|
|
710
|
+
maxHeight = scrollDivHeight - overflowBottom;
|
|
711
|
+
}
|
|
712
|
+
return { top, maxHeight };
|
|
713
|
+
}
|
|
714
|
+
// D. drop down with an internal scroll
|
|
715
|
+
if (spaceAbove <= spaceBelow) {
|
|
716
|
+
let maxHeight = null;
|
|
717
|
+
const overflow = menuHeight - spaceBelow;
|
|
718
|
+
if (overflow > 0) {
|
|
719
|
+
maxHeight = scrollDivHeight - overflow;
|
|
720
|
+
}
|
|
721
|
+
return { top: downTop, maxHeight };
|
|
722
|
+
}
|
|
723
|
+
// E. drop up with an internal scroll
|
|
724
|
+
let top = upBottom - menuHeight;
|
|
725
|
+
let maxHeight = null;
|
|
726
|
+
const overflowTop = (viewportTopDoc + minOffset) - top;
|
|
727
|
+
if (overflowTop > 0) {
|
|
728
|
+
maxHeight = scrollDivHeight - overflowTop;
|
|
729
|
+
top += overflowTop;
|
|
730
|
+
}
|
|
731
|
+
return { top, maxHeight };
|
|
732
|
+
}
|
|
733
|
+
|
|
650
734
|
class MenuLevel {
|
|
651
735
|
get applyItem() {
|
|
652
736
|
return this._applyItem;
|
|
@@ -823,6 +907,7 @@ class MenuLevel {
|
|
|
823
907
|
.addClass(getMobileCssClass());
|
|
824
908
|
scrollDiv.style.overflowX = "hidden";
|
|
825
909
|
scrollDiv.style.overflowY = "auto";
|
|
910
|
+
scrollDiv.style.setProperty("overscroll-behavior", "contain");
|
|
826
911
|
scrollDiv.style.position = "relative";
|
|
827
912
|
baseDiv.appendChild(scrollDiv);
|
|
828
913
|
this.levelDiv = baseDiv;
|
|
@@ -914,7 +999,7 @@ class MenuLevel {
|
|
|
914
999
|
}
|
|
915
1000
|
}
|
|
916
1001
|
focusScrollDiv() {
|
|
917
|
-
this.scrollDiv.focus();
|
|
1002
|
+
this.scrollDiv.focus({ preventScroll: true });
|
|
918
1003
|
}
|
|
919
1004
|
turnCheckboxes(items) {
|
|
920
1005
|
//turn checkboxes on for selected items
|
|
@@ -1431,8 +1516,7 @@ class MenuLevel {
|
|
|
1431
1516
|
const leftSpace = pos.x - scroll.left;
|
|
1432
1517
|
const rightSpace = winSize.width - leftSpace - menuItem.itemDiv.offsetWidth;
|
|
1433
1518
|
pos.x += menuItem.itemDiv.offsetParent.offsetWidth;
|
|
1434
|
-
pos.y
|
|
1435
|
-
menuItem.subLevel.showAt(pos.x, pos.y, true, true);
|
|
1519
|
+
menuItem.subLevel.showAt(pos.x, pos.y, true, true, menuItem.itemDiv);
|
|
1436
1520
|
menuItem.subLevel.levelDiv.style.width = '';
|
|
1437
1521
|
menuItem.subLevel.levelDiv.style.right = '';
|
|
1438
1522
|
if ((rightSpace >= menuItem.subLevel.levelDiv.offsetWidth) || (rightSpace >= leftSpace)) { //show level to the right
|
|
@@ -1487,48 +1571,25 @@ class MenuLevel {
|
|
|
1487
1571
|
const viewportHeight = getViewportSize().height;
|
|
1488
1572
|
const anchorRect = anchor ? anchor.getBoundingClientRect() : null;
|
|
1489
1573
|
const anchorHeight = anchorRect ? anchorRect.height : 0;
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const scrollDivRect = this.scrollDiv.getBoundingClientRect();
|
|
1510
|
-
this.scrollDiv.style.maxHeight = scrollDivRect.height - outsideTop + 'px';
|
|
1511
|
-
levelStyle.top = levelDivRect.top + outsideTop + 'px';
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
else if (upperSpace <= lowerSpace) { //root level. Drop down with scroll
|
|
1515
|
-
levelStyle.top = (y + anchorHeight) + 'px';
|
|
1516
|
-
const outsideBottom = levelDivRect.height + minOffset - lowerSpace;
|
|
1517
|
-
if (outsideBottom > 0) {
|
|
1518
|
-
const scrollDivRect = this.scrollDiv.getBoundingClientRect();
|
|
1519
|
-
this.scrollDiv.style.maxHeight = scrollDivRect.height - outsideBottom + 'px';
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
else { //root level. Drop up with scroll
|
|
1523
|
-
y -= levelDivRect.height;
|
|
1524
|
-
const outsideTop = scrollY + minOffset - y;
|
|
1525
|
-
if (outsideTop > 0) {
|
|
1526
|
-
const scrollDivRect = this.scrollDiv.getBoundingClientRect();
|
|
1527
|
-
this.scrollDiv.style.maxHeight = scrollDivRect.height - outsideTop + 'px';
|
|
1528
|
-
y += outsideTop;
|
|
1529
|
-
}
|
|
1530
|
-
levelStyle.top = y + 'px';
|
|
1531
|
-
}
|
|
1574
|
+
// Measure natural sizes (maxHeight was just reset above), then delegate
|
|
1575
|
+
// all vertical placement to a pure, document-coordinate helper.
|
|
1576
|
+
const levelDivRect = this.levelDiv.getBoundingClientRect();
|
|
1577
|
+
const scrollDivRect = this.scrollDiv.getBoundingClientRect();
|
|
1578
|
+
const placement = computeMenuPlacement({
|
|
1579
|
+
anchorY: y,
|
|
1580
|
+
anchorHeight,
|
|
1581
|
+
menuHeight: levelDivRect.height,
|
|
1582
|
+
scrollDivHeight: scrollDivRect.height,
|
|
1583
|
+
scrollY,
|
|
1584
|
+
viewportHeight,
|
|
1585
|
+
levelIndex: this.levelIndex,
|
|
1586
|
+
useMaxSpace,
|
|
1587
|
+
// sub-levels sit beside their parent item; compensate the menu
|
|
1588
|
+
// padding so the first/last sub-item lines up with the item
|
|
1589
|
+
alignOffset: this.levelIndex > 0 ? 5 : 0,
|
|
1590
|
+
});
|
|
1591
|
+
levelStyle.top = placement.top + 'px';
|
|
1592
|
+
this.scrollDiv.style.maxHeight = placement.maxHeight == null ? '' : placement.maxHeight + 'px';
|
|
1532
1593
|
//setting minimal level width if it is defined
|
|
1533
1594
|
let minItemWidth = this.parentMenu.minItemWidth;
|
|
1534
1595
|
if (minItemWidth > 0 && this.scrollDiv.offsetWidth < minItemWidth) {
|
|
@@ -1547,10 +1608,10 @@ class MenuLevel {
|
|
|
1547
1608
|
}
|
|
1548
1609
|
focus() {
|
|
1549
1610
|
window.setTimeout(() => {
|
|
1550
|
-
this.scrollDiv.focus();
|
|
1611
|
+
this.scrollDiv.focus({ preventScroll: true });
|
|
1551
1612
|
if (this.searchBox != null) {
|
|
1552
1613
|
if (this.parentMenu.options.searchBoxAutoFocus) {
|
|
1553
|
-
this.searchBox.focus();
|
|
1614
|
+
this.searchBox.focus({ preventScroll: true });
|
|
1554
1615
|
}
|
|
1555
1616
|
this.searchBox.value = "";
|
|
1556
1617
|
}
|
|
@@ -1832,6 +1893,10 @@ class PopupMenu {
|
|
|
1832
1893
|
this.rootLevel.levelDiv.style.position = 'fixed';
|
|
1833
1894
|
this.rootLevel.levelDiv.style.fontSize = '20px';
|
|
1834
1895
|
this.rootLevel.showAt(0, 0, true, true);
|
|
1896
|
+
//the full-screen menu is a column: the list of items takes all the space left
|
|
1897
|
+
//after the search box and the "Close" button (see the .eqjs-mobile rules in the CSS).
|
|
1898
|
+
//It's set after showAt() which turns any level into a plain block.
|
|
1899
|
+
this.rootLevel.levelDiv.style.display = 'flex';
|
|
1835
1900
|
}
|
|
1836
1901
|
else {
|
|
1837
1902
|
const anchor = options.anchor || document.documentElement;
|
|
@@ -3369,7 +3434,7 @@ class ColumnsPanel extends Widget {
|
|
|
3369
3434
|
get cssPrefix() {
|
|
3370
3435
|
return 'eqjs-qc';
|
|
3371
3436
|
}
|
|
3372
|
-
constructor(slot) {
|
|
3437
|
+
constructor(slot, customQuery) {
|
|
3373
3438
|
super(slot);
|
|
3374
3439
|
this.options = {
|
|
3375
3440
|
isSubQuery: false,
|
|
@@ -3403,6 +3468,7 @@ class ColumnsPanel extends Widget {
|
|
|
3403
3468
|
showIndicatorOnLoad: true,
|
|
3404
3469
|
editMode: ColumnsEditMode.Full
|
|
3405
3470
|
};
|
|
3471
|
+
this.customQuery = null;
|
|
3406
3472
|
this.landingSlot = domel('div')
|
|
3407
3473
|
.addClass(`${this.cssPrefix}-col-landing-slot`)
|
|
3408
3474
|
.addChildElement(domel('div')
|
|
@@ -3411,6 +3477,9 @@ class ColumnsPanel extends Widget {
|
|
|
3411
3477
|
this.landingIndex = -1;
|
|
3412
3478
|
this.slot.classList.add(`${this.cssPrefix}-panel`);
|
|
3413
3479
|
this.group = WidgetGroup.Query;
|
|
3480
|
+
if (customQuery) {
|
|
3481
|
+
this.customQuery = customQuery;
|
|
3482
|
+
}
|
|
3414
3483
|
}
|
|
3415
3484
|
getWidgetType() {
|
|
3416
3485
|
return 'columnsPanel';
|
|
@@ -3659,6 +3728,9 @@ class ColumnsPanel extends Widget {
|
|
|
3659
3728
|
}
|
|
3660
3729
|
}
|
|
3661
3730
|
getQuery() {
|
|
3731
|
+
if (this.customQuery) {
|
|
3732
|
+
return this.customQuery;
|
|
3733
|
+
}
|
|
3662
3734
|
return this.getContext().getQuery();
|
|
3663
3735
|
}
|
|
3664
3736
|
showLandingSlot(pageX, pageY) {
|
|
@@ -5620,7 +5692,7 @@ class SortingBar extends Widget {
|
|
|
5620
5692
|
get cssPrefix() {
|
|
5621
5693
|
return "eqjs-sb";
|
|
5622
5694
|
}
|
|
5623
|
-
constructor(slot) {
|
|
5695
|
+
constructor(slot, customQuery) {
|
|
5624
5696
|
super(slot);
|
|
5625
5697
|
this.options = {
|
|
5626
5698
|
showAddRow: true,
|
|
@@ -5633,6 +5705,7 @@ class SortingBar extends Widget {
|
|
|
5633
5705
|
itemRenderedCallback: null
|
|
5634
5706
|
},
|
|
5635
5707
|
};
|
|
5708
|
+
this.customQuery = null;
|
|
5636
5709
|
this.sortMenuList = [
|
|
5637
5710
|
{ id: 'None', text: i18n.getText('CmdNotSorted') },
|
|
5638
5711
|
{ id: 'Ascending', text: i18n.getText('CmdAscending') },
|
|
@@ -5651,6 +5724,9 @@ class SortingBar extends Widget {
|
|
|
5651
5724
|
.addClass(`${this.cssPrefix}-panel`)
|
|
5652
5725
|
.addClass(getMobileCssClass());
|
|
5653
5726
|
this.group = WidgetGroup.Query;
|
|
5727
|
+
if (customQuery) {
|
|
5728
|
+
this.customQuery = customQuery;
|
|
5729
|
+
}
|
|
5654
5730
|
}
|
|
5655
5731
|
getWidgetType() {
|
|
5656
5732
|
return 'sortingBar';
|
|
@@ -5716,6 +5792,9 @@ class SortingBar extends Widget {
|
|
|
5716
5792
|
}
|
|
5717
5793
|
}
|
|
5718
5794
|
getQuery() {
|
|
5795
|
+
if (this.customQuery) {
|
|
5796
|
+
return this.customQuery;
|
|
5797
|
+
}
|
|
5719
5798
|
return this.getContext().getQuery();
|
|
5720
5799
|
}
|
|
5721
5800
|
getModel() {
|
|
@@ -8978,6 +9057,25 @@ class Grid extends Widget {
|
|
|
8978
9057
|
}
|
|
8979
9058
|
}
|
|
8980
9059
|
|
|
9060
|
+
/**
|
|
9061
|
+
* Computes a stable per-column identity used to persist column widths:
|
|
9062
|
+
* the query column's expression (attribute id, else the serialized expression),
|
|
9063
|
+
* falling back to the server-provided origin attribute id when the query has
|
|
9064
|
+
* no matching column (e.g. a data-filtering fetch with an empty column list,
|
|
9065
|
+
* where the server reflects all entity properties into auto-columns).
|
|
9066
|
+
* Returns null when the column has no stable identity — such columns are not
|
|
9067
|
+
* persisted (transient ids would only pollute the store).
|
|
9068
|
+
*/
|
|
9069
|
+
function stableColumnKey(qcol, dataColumn) {
|
|
9070
|
+
if (qcol) {
|
|
9071
|
+
const exprData = qcol.expr.saveToData();
|
|
9072
|
+
if (exprData) {
|
|
9073
|
+
return exprData.id || JSON.stringify(exprData);
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
return (dataColumn && dataColumn.originAttrId) || null;
|
|
9077
|
+
}
|
|
9078
|
+
|
|
8981
9079
|
class EasyGridWidget extends Grid {
|
|
8982
9080
|
/**
|
|
8983
9081
|
* The default constructor.
|
|
@@ -8986,6 +9084,10 @@ class EasyGridWidget extends Grid {
|
|
|
8986
9084
|
constructor(slot) {
|
|
8987
9085
|
super(slot);
|
|
8988
9086
|
this.hasCustomTotalsSettings = false;
|
|
9087
|
+
this.columnWidthCache = {};
|
|
9088
|
+
this.columnWidthScope = 'model';
|
|
9089
|
+
this.columnWidthsEnabled = true;
|
|
9090
|
+
this.loadedWidthScopeKey = null;
|
|
8989
9091
|
//!!!! DO NOT forget to add types for options
|
|
8990
9092
|
this.options = { autoRefresh: true };
|
|
8991
9093
|
this.group = WidgetGroup.Result;
|
|
@@ -9005,6 +9107,25 @@ class EasyGridWidget extends Grid {
|
|
|
9005
9107
|
this.options = utils.assign(this.options, gridOptions);
|
|
9006
9108
|
gridOptions.onPlusButtonClick = (ev) => this.addColumnClickHandler(ev);
|
|
9007
9109
|
gridOptions.onColumnMoved = (ev) => this.columnMovedHandler(ev);
|
|
9110
|
+
const userSync = gridOptions.onSyncGridColumn;
|
|
9111
|
+
gridOptions.onSyncGridColumn = (col) => {
|
|
9112
|
+
this.syncGridColumn(col);
|
|
9113
|
+
this.applyColumnWidth(col);
|
|
9114
|
+
if (userSync)
|
|
9115
|
+
userSync(col);
|
|
9116
|
+
};
|
|
9117
|
+
gridOptions.onColumnSort = (ev) => this.columnSortHandler(ev);
|
|
9118
|
+
// gridOptions already merges widgets.resultGrid / widgets.easyGrid / options
|
|
9119
|
+
// (spread above), with `options` winning, so a single read honours priority.
|
|
9120
|
+
const cwOpt = gridOptions.columnWidthPersistence || {};
|
|
9121
|
+
this.columnWidthsEnabled = cwOpt.enabled !== false;
|
|
9122
|
+
this.columnWidthScope = cwOpt.scope === 'query' ? 'query' : 'model';
|
|
9123
|
+
const userResize = gridOptions.onColumnResize;
|
|
9124
|
+
gridOptions.onColumnResize = (ev) => {
|
|
9125
|
+
this.columnResizeHandler(ev);
|
|
9126
|
+
if (userResize)
|
|
9127
|
+
userResize(ev);
|
|
9128
|
+
};
|
|
9008
9129
|
if (gridOptions.aggregates) {
|
|
9009
9130
|
this.hasCustomTotalsSettings = true;
|
|
9010
9131
|
}
|
|
@@ -9017,8 +9138,12 @@ class EasyGridWidget extends Grid {
|
|
|
9017
9138
|
if (this.options.autoRefresh) {
|
|
9018
9139
|
this.attachQueryObserver();
|
|
9019
9140
|
}
|
|
9141
|
+
this.loadColumnWidths();
|
|
9020
9142
|
}
|
|
9021
9143
|
refreshCore() {
|
|
9144
|
+
// Re-checks the scope key (no-ops when unchanged) so a query switch under
|
|
9145
|
+
// scope:'query', or a model that loads after init, picks up stored widths.
|
|
9146
|
+
this.loadColumnWidths();
|
|
9022
9147
|
this.clear();
|
|
9023
9148
|
this.updateTotalsSettings();
|
|
9024
9149
|
this.render();
|
|
@@ -9108,6 +9233,119 @@ class EasyGridWidget extends Grid {
|
|
|
9108
9233
|
});
|
|
9109
9234
|
return col;
|
|
9110
9235
|
}
|
|
9236
|
+
toGridSort(dir) {
|
|
9237
|
+
if (dir === SortDirection.Ascending)
|
|
9238
|
+
return 'asc';
|
|
9239
|
+
if (dir === SortDirection.Descending)
|
|
9240
|
+
return 'desc';
|
|
9241
|
+
return 'none';
|
|
9242
|
+
}
|
|
9243
|
+
fromGridSort(dir) {
|
|
9244
|
+
if (dir === 'asc')
|
|
9245
|
+
return SortDirection.Ascending;
|
|
9246
|
+
if (dir === 'desc')
|
|
9247
|
+
return SortDirection.Descending;
|
|
9248
|
+
return SortDirection.None;
|
|
9249
|
+
}
|
|
9250
|
+
/** Reflects the query's current sorting in the grid header indicator. */
|
|
9251
|
+
syncGridColumn(col) {
|
|
9252
|
+
if (col.isRowNum || !col.dataColumn)
|
|
9253
|
+
return;
|
|
9254
|
+
const query = this.getQuery();
|
|
9255
|
+
const qcol = query.getColumns().find(c => c.id === col.dataColumn.id);
|
|
9256
|
+
col.sortDirection = qcol ? this.toGridSort(query.getColumnSorting(qcol)) : 'none';
|
|
9257
|
+
}
|
|
9258
|
+
/** Handles a header-sort click: multi-column server-side sort, then reload. */
|
|
9259
|
+
columnSortHandler(ev) {
|
|
9260
|
+
const query = this.getQuery();
|
|
9261
|
+
const target = query.getColumns().find(c => c.id === ev.columnId);
|
|
9262
|
+
if (!target)
|
|
9263
|
+
return;
|
|
9264
|
+
// Multi-column sort: write the new direction (already cycled by EasyGrid:
|
|
9265
|
+
// none -> asc -> desc -> none) to only the clicked column. The query keeps
|
|
9266
|
+
// the full sorted set; when syncSortOrderWithColumnsOrder is enabled (the
|
|
9267
|
+
// default), Query.adjustColumnsSorting aligns sort priority with the column
|
|
9268
|
+
// order, matching ColumnsPanel.
|
|
9269
|
+
const prevSorting = query.getColumnSorting(target);
|
|
9270
|
+
query.setColumnSorting(target, this.fromGridSort(ev.direction));
|
|
9271
|
+
// Pure result re-ordering: reload sorted data and re-render the result grid
|
|
9272
|
+
// (which re-applies the header sort indicators via onSyncGridColumn), then
|
|
9273
|
+
// refresh the query-side widgets (SortingBar/ColumnsPanel). This deliberately
|
|
9274
|
+
// does not route through the view's clear/refetch path, so there is no
|
|
9275
|
+
// result-clearing flash and no double fetch.
|
|
9276
|
+
this.context.fetchData({
|
|
9277
|
+
success: () => this.context.refreshWidgets(WidgetGroup.Result),
|
|
9278
|
+
error: () => {
|
|
9279
|
+
// The failed sorting must not stay in the query, otherwise every
|
|
9280
|
+
// subsequent fetch would repeat the failing ORDER BY (e.g. a column
|
|
9281
|
+
// the server cannot sort by). Only the query-side widgets need a
|
|
9282
|
+
// refresh: the grid header changes its arrows only on re-render,
|
|
9283
|
+
// which never happened for the failed direction - re-rendering the
|
|
9284
|
+
// grid here would just make it blink.
|
|
9285
|
+
query.setColumnSorting(target, prevSorting);
|
|
9286
|
+
this.context.refreshWidgets(WidgetGroup.Query);
|
|
9287
|
+
}
|
|
9288
|
+
});
|
|
9289
|
+
this.context.refreshWidgets(WidgetGroup.Query);
|
|
9290
|
+
}
|
|
9291
|
+
/** Computes the storage scope key for the current model/query. */
|
|
9292
|
+
columnWidthScopeKey() {
|
|
9293
|
+
return this.columnWidthScope === 'query'
|
|
9294
|
+
? this.getQuery().getId()
|
|
9295
|
+
: this.getContext().getModel().getId();
|
|
9296
|
+
}
|
|
9297
|
+
/** Stable per-column identity: the query column's expression, else the
|
|
9298
|
+
* server-provided originAttrId (covers the data-filtering scenario where
|
|
9299
|
+
* the query has no columns and the server auto-generates them). */
|
|
9300
|
+
columnKey(dataColumn) {
|
|
9301
|
+
const qcol = this.getQuery().getColumns().find(c => c.id === dataColumn.id);
|
|
9302
|
+
return stableColumnKey(qcol, dataColumn);
|
|
9303
|
+
}
|
|
9304
|
+
/** Applies a cached width to a grid column (called from onSyncGridColumn). */
|
|
9305
|
+
applyColumnWidth(col) {
|
|
9306
|
+
if (!this.columnWidthsEnabled || col.isRowNum || !col.dataColumn)
|
|
9307
|
+
return;
|
|
9308
|
+
const key = this.columnKey(col.dataColumn);
|
|
9309
|
+
if (key && this.columnWidthCache[key] != null) {
|
|
9310
|
+
col.width = this.columnWidthCache[key];
|
|
9311
|
+
col.manualWidth = true;
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
/** Persists a width when the user finishes a resize drag. */
|
|
9315
|
+
columnResizeHandler(ev) {
|
|
9316
|
+
if (!this.columnWidthsEnabled)
|
|
9317
|
+
return;
|
|
9318
|
+
const gridCol = this.grid.getColumns().getItems()
|
|
9319
|
+
.find(c => c.dataColumn && c.dataColumn.id === ev.columnId);
|
|
9320
|
+
if (!gridCol)
|
|
9321
|
+
return;
|
|
9322
|
+
const key = this.columnKey(gridCol.dataColumn);
|
|
9323
|
+
if (!key)
|
|
9324
|
+
return;
|
|
9325
|
+
const scopeKey = this.columnWidthScopeKey();
|
|
9326
|
+
if (!scopeKey)
|
|
9327
|
+
return; // no model/query id -> avoid a colliding empty key
|
|
9328
|
+
this.columnWidthCache[key] = ev.width;
|
|
9329
|
+
this.getContext().getColumnWidthStore().save(scopeKey, key, ev.width);
|
|
9330
|
+
}
|
|
9331
|
+
/** Loads the width map once per scope key, then refreshes to apply. */
|
|
9332
|
+
loadColumnWidths() {
|
|
9333
|
+
if (!this.columnWidthsEnabled)
|
|
9334
|
+
return;
|
|
9335
|
+
const model = this.getContext().getModel();
|
|
9336
|
+
if (!model || model.isEmpty())
|
|
9337
|
+
return; // no model yet -> nothing to scope by
|
|
9338
|
+
const scopeKey = this.columnWidthScopeKey();
|
|
9339
|
+
if (scopeKey === this.loadedWidthScopeKey)
|
|
9340
|
+
return;
|
|
9341
|
+
this.loadedWidthScopeKey = scopeKey;
|
|
9342
|
+
this.getContext().getColumnWidthStore().load(scopeKey).then((map) => {
|
|
9343
|
+
this.columnWidthCache = map || {};
|
|
9344
|
+
if (Object.keys(this.columnWidthCache).length > 0 && this.grid) {
|
|
9345
|
+
this.grid.refresh();
|
|
9346
|
+
}
|
|
9347
|
+
});
|
|
9348
|
+
}
|
|
9111
9349
|
}
|
|
9112
9350
|
|
|
9113
9351
|
/**
|
|
@@ -9714,7 +9952,25 @@ class ChartWidget extends Widget {
|
|
|
9714
9952
|
getCurrentChartType() {
|
|
9715
9953
|
return this.chartType;
|
|
9716
9954
|
}
|
|
9955
|
+
/**
|
|
9956
|
+
* Changes the current chart type and redraws the chart in place.
|
|
9957
|
+
* Deliberately avoids a full re-render: emptying the widget (even for
|
|
9958
|
+
* the drawing delay) shrinks the scrollable parent, so the browser
|
|
9959
|
+
* clamps - and loses - its scroll position.
|
|
9960
|
+
* @param type The new chart type.
|
|
9961
|
+
*/
|
|
9962
|
+
changeChartType(type) {
|
|
9963
|
+
this.chartType = type;
|
|
9964
|
+
this.drawChart(0);
|
|
9965
|
+
}
|
|
9717
9966
|
render() {
|
|
9967
|
+
// hold the current height while the content is being rebuilt:
|
|
9968
|
+
// an emptied widget shrinks the scrollable parent and the browser
|
|
9969
|
+
// clamps (loses) its scroll position. drawChart() releases the lock.
|
|
9970
|
+
const prevHeight = this.element.offsetHeight;
|
|
9971
|
+
if (prevHeight > 0) {
|
|
9972
|
+
this.element.style.minHeight = prevHeight + 'px';
|
|
9973
|
+
}
|
|
9718
9974
|
this.clear();
|
|
9719
9975
|
if (this.hasData() && this.hasColumnsForChart()
|
|
9720
9976
|
&& (this.context.resultTable.getTotal()
|
|
@@ -9735,8 +9991,7 @@ class ChartWidget extends Widget {
|
|
|
9735
9991
|
headerDiv.appendChild(chartTypeSelector);
|
|
9736
9992
|
chartTypeSelector.addEventListener('change', (ev) => {
|
|
9737
9993
|
let ctkey = ev.target.value;
|
|
9738
|
-
this.
|
|
9739
|
-
this.refresh();
|
|
9994
|
+
this.changeChartType(parseInt(ctkey));
|
|
9740
9995
|
});
|
|
9741
9996
|
this.settingsDiv = document.createElement('div');
|
|
9742
9997
|
addCssClass(this.settingsDiv, `${this.cssPrefix}-settings`);
|
|
@@ -9758,6 +10013,7 @@ class ChartWidget extends Widget {
|
|
|
9758
10013
|
}
|
|
9759
10014
|
else {
|
|
9760
10015
|
this.addContentDiv(mainDiv, 'Unable to render the chart. None of the supported chart libraries (ChartJS, Google Charts, etc) is found.');
|
|
10016
|
+
this.element.style.minHeight = '';
|
|
9761
10017
|
}
|
|
9762
10018
|
if (!this.options.hideSettings) {
|
|
9763
10019
|
this.element.appendChild(headerDiv);
|
|
@@ -9767,6 +10023,7 @@ class ChartWidget extends Widget {
|
|
|
9767
10023
|
}
|
|
9768
10024
|
else {
|
|
9769
10025
|
this.element.style.display = "none";
|
|
10026
|
+
this.element.style.minHeight = '';
|
|
9770
10027
|
}
|
|
9771
10028
|
}
|
|
9772
10029
|
addContentDiv(mainDiv, text) {
|
|
@@ -9787,11 +10044,16 @@ class ChartWidget extends Widget {
|
|
|
9787
10044
|
*/
|
|
9788
10045
|
drawChart(delay) {
|
|
9789
10046
|
showElement(this.chartDiv);
|
|
10047
|
+
const draw = () => {
|
|
10048
|
+
this.drawCore();
|
|
10049
|
+
//release the height lock held by render() for the redraw gap
|
|
10050
|
+
this.element.style.minHeight = '';
|
|
10051
|
+
};
|
|
9790
10052
|
if (delay > 0) {
|
|
9791
|
-
setTimeout(
|
|
10053
|
+
setTimeout(draw, delay);
|
|
9792
10054
|
}
|
|
9793
10055
|
else {
|
|
9794
|
-
|
|
10056
|
+
draw();
|
|
9795
10057
|
}
|
|
9796
10058
|
}
|
|
9797
10059
|
/**
|
|
@@ -9910,6 +10172,7 @@ class ChartJsWidget extends ChartWidget {
|
|
|
9910
10172
|
];
|
|
9911
10173
|
this.chartLabels = [];
|
|
9912
10174
|
this.chartColumns = [];
|
|
10175
|
+
this.chart = null;
|
|
9913
10176
|
this.hasChartTypes = [
|
|
9914
10177
|
ChartType.Column,
|
|
9915
10178
|
ChartType.Bar,
|
|
@@ -9934,6 +10197,16 @@ class ChartJsWidget extends ChartWidget {
|
|
|
9934
10197
|
static hasChartJs() {
|
|
9935
10198
|
return typeof Chart !== 'undefined';
|
|
9936
10199
|
}
|
|
10200
|
+
/**
|
|
10201
|
+
* Returns the major version of the Chart.js library loaded on the page.
|
|
10202
|
+
* `Chart.version` was introduced in 3.0, so its absence means 2.x.
|
|
10203
|
+
*/
|
|
10204
|
+
static getChartJsMajorVersion() {
|
|
10205
|
+
if (!ChartJsWidget.hasChartJs())
|
|
10206
|
+
return 0;
|
|
10207
|
+
const major = parseInt(Chart.version, 10);
|
|
10208
|
+
return isNaN(major) ? 2 : major;
|
|
10209
|
+
}
|
|
9937
10210
|
canDraw() {
|
|
9938
10211
|
return ChartJsWidget.hasChartJs();
|
|
9939
10212
|
}
|
|
@@ -9949,7 +10222,20 @@ class ChartJsWidget extends ChartWidget {
|
|
|
9949
10222
|
let config = this.generateConfig();
|
|
9950
10223
|
let chartCanvas = this.getChartCanvas(this.chartDiv);
|
|
9951
10224
|
let chartContext = chartCanvas.getContext('2d');
|
|
9952
|
-
|
|
10225
|
+
//we redraw into the same canvas when the chart settings change,
|
|
10226
|
+
//and Chart.js 3+ refuses to reuse a canvas that still holds a chart
|
|
10227
|
+
this.destroyChart();
|
|
10228
|
+
this.chart = new Chart(chartContext, config);
|
|
10229
|
+
}
|
|
10230
|
+
}
|
|
10231
|
+
destroyCore() {
|
|
10232
|
+
this.destroyChart();
|
|
10233
|
+
super.destroyCore();
|
|
10234
|
+
}
|
|
10235
|
+
destroyChart() {
|
|
10236
|
+
if (this.chart) {
|
|
10237
|
+
this.chart.destroy();
|
|
10238
|
+
this.chart = null;
|
|
9953
10239
|
}
|
|
9954
10240
|
}
|
|
9955
10241
|
updateChartColumns() {
|
|
@@ -10018,24 +10304,39 @@ class ChartJsWidget extends ChartWidget {
|
|
|
10018
10304
|
datasets: [dataset1],
|
|
10019
10305
|
labels: this.chartLabels
|
|
10020
10306
|
},
|
|
10021
|
-
options:
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10307
|
+
options: this.generateOptions(datasetName, displayLegend)
|
|
10308
|
+
};
|
|
10309
|
+
}
|
|
10310
|
+
generateOptions(datasetName, displayLegend) {
|
|
10311
|
+
const legend = {
|
|
10312
|
+
display: displayLegend,
|
|
10313
|
+
position: this.options.legend.position || 'bottom'
|
|
10314
|
+
};
|
|
10315
|
+
const title = {
|
|
10316
|
+
display: true,
|
|
10317
|
+
text: datasetName
|
|
10031
10318
|
};
|
|
10319
|
+
if (ChartJsWidget.getChartJsMajorVersion() < 3) {
|
|
10320
|
+
return { legend, title };
|
|
10321
|
+
}
|
|
10322
|
+
//both were moved under `plugins` in 3.0 - left at the root
|
|
10323
|
+
//they are silently ignored
|
|
10324
|
+
const options = { plugins: { legend, title } };
|
|
10325
|
+
//3.0 dropped the `horizontalBar` type in favour of flipping a bar chart
|
|
10326
|
+
if (this.chartType == ChartType.Bar) {
|
|
10327
|
+
options.indexAxis = 'y';
|
|
10328
|
+
}
|
|
10329
|
+
return options;
|
|
10032
10330
|
}
|
|
10033
10331
|
getChartType() {
|
|
10034
10332
|
switch (this.chartType) {
|
|
10035
10333
|
case ChartType.Column:
|
|
10036
10334
|
return "bar";
|
|
10037
10335
|
case ChartType.Bar:
|
|
10038
|
-
|
|
10336
|
+
//3.0 replaced this type with `bar` + `indexAxis: 'y'`
|
|
10337
|
+
return ChartJsWidget.getChartJsMajorVersion() < 3
|
|
10338
|
+
? "horizontalBar"
|
|
10339
|
+
: "bar";
|
|
10039
10340
|
case ChartType.Area:
|
|
10040
10341
|
case ChartType.SteppedLine:
|
|
10041
10342
|
case ChartType.Line:
|
|
@@ -12480,7 +12781,8 @@ class View {
|
|
|
12480
12781
|
},
|
|
12481
12782
|
widgets: {
|
|
12482
12783
|
resultGrid: {
|
|
12483
|
-
addColumns: false
|
|
12784
|
+
addColumns: false,
|
|
12785
|
+
sortable: true
|
|
12484
12786
|
}
|
|
12485
12787
|
},
|
|
12486
12788
|
handlers: {}
|
|
@@ -13703,7 +14005,15 @@ class DataFilterView extends View {
|
|
|
13703
14005
|
this.context.syncQuery();
|
|
13704
14006
|
}
|
|
13705
14007
|
});
|
|
13706
|
-
this.context.addEventListener('ready',
|
|
14008
|
+
this.context.addEventListener('ready', () => {
|
|
14009
|
+
// Turn "show the whole entity" into an ordinary query with explicit
|
|
14010
|
+
// columns (silently - the fetch below is the only one). A query
|
|
14011
|
+
// loaded from storage keeps its own columns.
|
|
14012
|
+
if (options.autoFillColumns !== false) {
|
|
14013
|
+
this.context.getQuery().fillColumnsWithRootAttributes();
|
|
14014
|
+
}
|
|
14015
|
+
this.fetchData();
|
|
14016
|
+
});
|
|
13707
14017
|
return options;
|
|
13708
14018
|
}
|
|
13709
14019
|
createWidgets(options) {
|