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