@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.
@@ -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.context.getQuery();
125
+ const query = this.getQuery();
116
126
  query.removeChangedCallback(this.queryChangedCallbackId);
117
127
  }
118
128
  if (this.options.syncWithColumns) {
119
- const query = this.context.getQuery();
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.context.getQuery();
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.context.getQuery();
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.context.getQuery();
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.context.getQuery();
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.context.getQuery();
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 -= 5;
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
- const upperSpace = y - scrollY;
1493
- const lowerSpace = viewportHeight - upperSpace - anchorHeight;
1494
- const minOffset = 15;
1495
- // Рахуємо початковий контейнер
1496
- let levelDivRect = this.levelDiv.getBoundingClientRect();
1497
- if (this.levelIndex == 0 && levelDivRect.height < lowerSpace - minOffset) { //root level. Have enough space to drop down without scroll
1498
- levelStyle.top = (y + anchorHeight) + 'px';
1499
- }
1500
- else if (this.levelIndex == 0 && levelDivRect.height < upperSpace - minOffset) { //root level. Have enough space to drop up without scroll
1501
- levelStyle.top = (y - levelDivRect.height) + 'px';
1502
- }
1503
- else if (useMaxSpace || this.levelIndex > 0) { //sub-level or useMaxSpace option set. Use all the space
1504
- const outsideBottom = levelDivRect.bottom + minOffset - scrollY - viewportHeight;
1505
- if (outsideBottom > 0) {
1506
- levelStyle.top = levelDivRect.top - outsideBottom + 'px';
1507
- }
1508
- levelDivRect = this.levelDiv.getBoundingClientRect();
1509
- const outsideTop = scrollY + minOffset - levelDivRect.top;
1510
- if (outsideTop > 0) {
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.chartType = parseInt(ctkey);
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(() => this.drawCore(), delay);
10055
+ setTimeout(draw, delay);
9794
10056
  }
9795
10057
  else {
9796
- this.drawCore();
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
- new Chart(chartContext, config);
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
- legend: {
10025
- display: displayLegend,
10026
- position: this.options.legend.position || 'bottom'
10027
- },
10028
- title: {
10029
- display: true,
10030
- text: datasetName
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
- return "horizontalBar";
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', this.fetchData.bind(this));
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) {