Refactor grid InfoPanel: sticky, responsive, new icons
- Redesigned MgGridInfoPanel to use a sticky, scroll-aware layout via JavaScript for better UX when scrolling. - InfoPanel now uses a responsive CSS grid layout with container queries for 1/2/3 column display based on width. - Added new toolbar icons using SVG masks for a modern, consistent look; updated toolbar item class names. - Added "Prev Row" and "Next Row" navigation buttons to the grid toolbar, with corresponding methods in grid base classes. - Unified edit state enum naming to MgGridEditState and updated all references. - Improved InfoPanel cell rendering for better text overflow handling and tooltips. - Updated CSS for InfoPanel and grid, including sticky pane support and icon styles. - Registered mgGridInfoPanel.js in App.razor and index.html for JS interop. - Minor UI/UX tweaks: InfoPanel header, background colors, and panel sizing.
This commit is contained in:
parent
45294199cf
commit
109a4b82b4
|
|
@ -3,7 +3,7 @@ namespace AyCode.Blazor.Components.Components.Grids;
|
|||
/// <summary>
|
||||
/// Represents the current edit state of the MgGrid
|
||||
/// </summary>
|
||||
public enum MgEditState
|
||||
public enum MgGridEditState
|
||||
{
|
||||
/// <summary>
|
||||
/// No edit operation in progress
|
||||
|
|
|
|||
|
|
@ -25,7 +25,17 @@ public interface IMgGridBase : IGrid
|
|||
/// <summary>
|
||||
/// Current edit state of the grid (None, New, Edit)
|
||||
/// </summary>
|
||||
MgEditState EditState { get; }
|
||||
MgGridEditState GridEditState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the previous row in the grid
|
||||
/// </summary>
|
||||
void StepPrevRow();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the next row in the grid
|
||||
/// </summary>
|
||||
void StepNextRow();
|
||||
}
|
||||
|
||||
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
|
||||
|
|
@ -49,7 +59,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
public bool IsSyncing => _dataSource?.IsSyncing ?? false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public MgEditState EditState { get; private set; } = MgEditState.None;
|
||||
public MgGridEditState GridEditState { get; private set; } = MgGridEditState.None;
|
||||
|
||||
[Parameter] public bool ShowInfoPanel { get; set; } = true;
|
||||
|
||||
|
|
@ -137,7 +147,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
|
||||
builder.OpenComponent<DxSplitter>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "100%");
|
||||
builder.AddAttribute(seq++, "Height", "100%");
|
||||
builder.AddAttribute(seq++, "CssClass", "mg-grid-splitter");
|
||||
builder.AddAttribute(seq++, "Orientation", Orientation.Horizontal);
|
||||
|
||||
builder.AddAttribute(seq++, "Panes", (RenderFragment)(panesBuilder =>
|
||||
{
|
||||
|
|
@ -151,12 +162,13 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
}));
|
||||
panesBuilder.CloseComponent();
|
||||
|
||||
// Right pane - InfoPanel
|
||||
// Right pane - InfoPanel (sticky to viewport)
|
||||
panesBuilder.OpenComponent<DxSplitterPane>(paneSeq++);
|
||||
panesBuilder.AddAttribute(paneSeq++, "Size", "350px");
|
||||
panesBuilder.AddAttribute(paneSeq++, "MinSize", "300px");
|
||||
panesBuilder.AddAttribute(paneSeq++, "MaxSize", "800px");
|
||||
panesBuilder.AddAttribute(paneSeq++, "Size", "400px");
|
||||
panesBuilder.AddAttribute(paneSeq++, "MinSize", "0px");
|
||||
panesBuilder.AddAttribute(paneSeq++, "MaxSize", "100%");
|
||||
panesBuilder.AddAttribute(paneSeq++, "AllowCollapse", true);
|
||||
panesBuilder.AddAttribute(paneSeq++, "CssClass", "mg-info-panel-pane");
|
||||
|
||||
panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(infoPanelBuilder =>
|
||||
{
|
||||
|
|
@ -434,7 +446,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
}
|
||||
|
||||
// Set edit state
|
||||
EditState = e.IsNew ? MgEditState.New : MgEditState.Edit;
|
||||
GridEditState = e.IsNew ? MgGridEditState.New : MgGridEditState.Edit;
|
||||
|
||||
await OnGridCustomizeEditModel.InvokeAsync(e);
|
||||
|
||||
|
|
@ -443,6 +455,9 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
{
|
||||
_infoPanelInstance.SetEditMode(editModel);
|
||||
}
|
||||
|
||||
// Force grid refresh to apply edit mode styling
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnEditStart(GridEditStartEventArgs e)
|
||||
|
|
@ -455,10 +470,21 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
{
|
||||
_focusedDataItem = e.DataItem;
|
||||
|
||||
if (ShowInfoPanel && _infoPanelInstance != null && e.DataItem is TDataItem dataItem)
|
||||
if (ShowInfoPanel && _infoPanelInstance != null)
|
||||
{
|
||||
// Ha edit módban vagyunk, de a felhasználó egy másik sorra kattintott,
|
||||
// akkor kilépünk az edit módból
|
||||
if (GridEditState != MgGridEditState.None)
|
||||
{
|
||||
_infoPanelInstance.ClearEditMode();
|
||||
}
|
||||
|
||||
// Frissítjük az InfoPanel-t az új sor adataival
|
||||
if (e.DataItem is TDataItem dataItem)
|
||||
{
|
||||
_infoPanelInstance.RefreshData(this, dataItem, e.VisibleIndex);
|
||||
}
|
||||
}
|
||||
|
||||
await OnGridFocusedRowChanged.InvokeAsync(e);
|
||||
}
|
||||
|
|
@ -491,23 +517,29 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
else await UpdateDataItemAsync(dataItem);
|
||||
|
||||
// Kilépés edit módból
|
||||
EditState = MgEditState.None;
|
||||
GridEditState = MgGridEditState.None;
|
||||
|
||||
if (ShowInfoPanel && _infoPanelInstance != null)
|
||||
{
|
||||
_infoPanelInstance.ClearEditMode();
|
||||
}
|
||||
|
||||
// Force grid refresh to remove edit mode styling
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnEditCanceling(GridEditCancelingEventArgs e)
|
||||
{
|
||||
// Kilépés edit módból
|
||||
EditState = MgEditState.None;
|
||||
GridEditState = MgGridEditState.None;
|
||||
|
||||
if (ShowInfoPanel && _infoPanelInstance != null)
|
||||
{
|
||||
_infoPanelInstance.ClearEditMode();
|
||||
}
|
||||
|
||||
// Force grid refresh to remove edit mode styling
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task SaveChangesToServerAsync()
|
||||
|
|
@ -570,6 +602,27 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
e.Column.Visible = AcDomain.IsDeveloperVersion;
|
||||
e.Column.ShowInColumnChooser = AcDomain.IsDeveloperVersion;
|
||||
}
|
||||
|
||||
// Apply edit mode background to the row being edited
|
||||
if (e.ElementType == GridElementType.DataRow && GridEditState != MgGridEditState.None)
|
||||
{
|
||||
if (e.VisibleIndex == GetFocusedRowIndex())
|
||||
{
|
||||
e.Style = string.IsNullOrEmpty(e.Style)
|
||||
? "background-color: #fffbeb;"
|
||||
: e.Style + " background-color: #fffbeb;";
|
||||
}
|
||||
}
|
||||
// Apply edit mode background to cells in the edited row
|
||||
else if (e.ElementType == GridElementType.DataCell && GridEditState != MgGridEditState.None)
|
||||
{
|
||||
if (e.VisibleIndex == GetFocusedRowIndex())
|
||||
{
|
||||
e.Style = string.IsNullOrEmpty(e.Style)
|
||||
? "background-color: #fffbeb;"
|
||||
: e.Style + " background-color: #fffbeb;";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SetParametersAsyncCore(ParameterView parameters)
|
||||
|
|
@ -647,6 +700,31 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
return _dataSource.LoadDataSourceAsync(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the previous row in the grid
|
||||
/// </summary>
|
||||
public void StepPrevRow()
|
||||
{
|
||||
var currentIndex = GetFocusedRowIndex();
|
||||
if (currentIndex > 0)
|
||||
{
|
||||
SetFocusedRowIndex(currentIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the next row in the grid
|
||||
/// </summary>
|
||||
public void StepNextRow()
|
||||
{
|
||||
var currentIndex = GetFocusedRowIndex();
|
||||
var visibleRowCount = GetVisibleRowCount();
|
||||
if (currentIndex >= 0 && currentIndex < visibleRowCount - 1)
|
||||
{
|
||||
SetFocusedRowIndex(currentIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,19 @@
|
|||
@using System.Reflection
|
||||
@typeparam TDataItem where TDataItem : class
|
||||
|
||||
<div class="mg-grid-info-panel @(_isEditMode ? "edit-mode" : "view-mode")">
|
||||
<div @ref="_panelElement" class="mg-grid-info-panel @(_isEditMode ? "edit-mode" : "view-mode")">
|
||||
@* Header - matches grid toolbar height *@
|
||||
<div class="dxbl-grid-header-panel px-3 py-2 border-bottom">
|
||||
<span class="fw-semibold">Sor részletei</span>
|
||||
</div>
|
||||
|
||||
@* Content - scrollable area *@
|
||||
<div class="mg-info-panel-content">
|
||||
@if (GetActiveDataItem() != null && _currentGrid != null)
|
||||
{
|
||||
var colSpan = _allDataColumns.Count > 10 ? 6 : 12;
|
||||
var dataItem = GetActiveDataItem()!;
|
||||
|
||||
<DxFormLayout CssClass="info-panel-form"
|
||||
CaptionPosition="CaptionPosition.Vertical"
|
||||
SizeMode="SizeMode.Small">
|
||||
<div class="mg-info-panel-grid">
|
||||
@foreach (var column in _allDataColumns)
|
||||
{
|
||||
var displayText = GetDisplayTextFromGrid(column);
|
||||
|
|
@ -19,15 +23,12 @@
|
|||
var settingsType = GetEditSettingsType(column);
|
||||
var isReadOnly = !_isEditMode || column.ReadOnly;
|
||||
|
||||
<DxFormLayoutItem Caption="@GetColumnCaption(column)"
|
||||
CaptionCssClass="@GetCaptionCssClass(isReadOnly)"
|
||||
ColSpanXxl="@colSpan"
|
||||
ColSpanXl="@colSpan"
|
||||
ColSpanLg="@colSpan"
|
||||
ColSpanMd="@colSpan"
|
||||
ColSpanSm="@colSpan"
|
||||
ColSpanXs="@colSpan">
|
||||
<Template>
|
||||
<div class="mg-info-panel-item">
|
||||
<div class="dxbl-form-layout-item">
|
||||
<label class="dxbl-fl-lc @GetCaptionCssClass(isReadOnly) d-block mb-1 small">
|
||||
@GetColumnCaption(column)
|
||||
</label>
|
||||
<div class="dxbl-fl-ec">
|
||||
@if (_isEditMode && !column.ReadOnly)
|
||||
{
|
||||
@RenderEditableCell(column, dataItem, value, displayText, settingsType)
|
||||
|
|
@ -36,17 +37,19 @@
|
|||
{
|
||||
@RenderCellContent(column, value, displayText, settingsType)
|
||||
}
|
||||
</Template>
|
||||
</DxFormLayoutItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</DxFormLayout>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="info-panel-empty">
|
||||
<p>Válasszon ki egy sort az adatok megtekintéséhez</p>
|
||||
<div class="text-center text-muted py-5">
|
||||
<p>Válasszon ki egy sort az adatok megtekintéséhez</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
|
@ -539,10 +542,14 @@
|
|||
{
|
||||
case EditSettingsType.ComboBox:
|
||||
// ComboBox columns show resolved display text
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "title", displayText);
|
||||
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
|
||||
builder.OpenComponent<DxTextBox>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", displayText);
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
return;
|
||||
|
||||
case EditSettingsType.CheckBox when value is bool boolVal:
|
||||
|
|
@ -561,19 +568,26 @@
|
|||
return;
|
||||
|
||||
case EditSettingsType.SpinEdit:
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "title", displayText);
|
||||
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
|
||||
builder.OpenComponent<DxTextBox>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", displayText);
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.AddAttribute(seq++, "CssClass", "text-end");
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
return;
|
||||
|
||||
case EditSettingsType.Memo:
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "title", displayText);
|
||||
builder.OpenComponent<DxMemo>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", displayText);
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.AddAttribute(seq++, "Rows", 3);
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -618,18 +632,26 @@
|
|||
break;
|
||||
|
||||
case decimal or double or float or int or long or short:
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "title", displayText);
|
||||
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
|
||||
builder.OpenComponent<DxTextBox>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", displayText);
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.AddAttribute(seq++, "CssClass", "text-end");
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
break;
|
||||
|
||||
default:
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "title", displayText);
|
||||
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
|
||||
builder.OpenComponent<DxTextBox>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", displayText);
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.CloseComponent();
|
||||
builder.CloseElement();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
using DevExpress.Blazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
public partial class MgGridInfoPanel<TDataItem> : ComponentBase where TDataItem : class
|
||||
public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposable where TDataItem : class
|
||||
{
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
|
||||
private ElementReference _panelElement;
|
||||
private IJSObjectReference? _jsModule;
|
||||
private bool _isJsInitialized;
|
||||
private const int DefaultTopOffset = 300; // Increased from 180 to account for header + tabs + toolbar
|
||||
|
||||
private DxGrid? _currentGrid;
|
||||
private TDataItem? _currentDataItem;
|
||||
private int _focusedRowVisibleIndex = -1;
|
||||
|
|
@ -17,6 +25,30 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase where TDataItem
|
|||
// Cache for edit settings to avoid repeated lookups
|
||||
private readonly Dictionary<string, IEditSettings?> _editSettingsCache = [];
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await InitializeStickyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeStickyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync(
|
||||
"MgGridInfoPanel.initSticky",
|
||||
_panelElement,
|
||||
DefaultTopOffset);
|
||||
_isJsInitialized = true;
|
||||
}
|
||||
catch (JSException)
|
||||
{
|
||||
// JS might not be loaded yet, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the InfoPanel with data from the specified grid row (view mode)
|
||||
/// </summary>
|
||||
|
|
@ -87,6 +119,26 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase where TDataItem
|
|||
StateHasChanged();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isJsInitialized)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("MgGridInfoPanel.disposeSticky", _panelElement);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore disposal errors
|
||||
}
|
||||
}
|
||||
|
||||
if (_jsModule != null)
|
||||
{
|
||||
await _jsModule.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem)
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,78 @@
|
|||
/* Shared edit mode background color configuration - change only here */
|
||||
/* Grid row background: #fffbeb (see MgGridBase.cs OnCustomizeElement) */
|
||||
/* InfoPanel background: #fffbeb (see below .edit-mode) */
|
||||
/* Border color: #f59e0b */
|
||||
|
||||
/* Breakpoint configuration - CHANGE ONLY THESE VALUES */
|
||||
/* 2 column breakpoint: 500px */
|
||||
/* 3 column breakpoint: 800px */
|
||||
|
||||
/* Main panel - contained within splitter pane */
|
||||
.mg-grid-info-panel {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
container-type: inline-size;
|
||||
container-name: infopanel;
|
||||
background-color: var(--dxbl-bg-secondary, #f8f9fa);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
/* Prevent panel from pushing out the splitter */
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.mg-grid-info-panel.edit-mode {
|
||||
background-color: #fffbeb;
|
||||
border-left: 3px solid #f59e0b;
|
||||
background-color: #fffbeb !important;
|
||||
border-left: 3px solid #f59e0b !important;
|
||||
}
|
||||
|
||||
.mg-grid-info-panel.view-mode {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 3px solid transparent;
|
||||
background-color: #f8f9fa !important;
|
||||
border-left: 3px solid transparent !important;
|
||||
}
|
||||
|
||||
/* Content area - scrollable, takes remaining space */
|
||||
.mg-info-panel-content {
|
||||
flex: 1 1 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 1rem;
|
||||
min-height: 0; /* Critical for flex child to allow shrinking */
|
||||
}
|
||||
|
||||
/* Grid layout with responsive column wrapping based on panel width */
|
||||
.mg-info-panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* 1 column for narrow panels (< 500px) */
|
||||
@container infopanel (max-width: 499px) {
|
||||
.mg-info-panel-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2 columns for medium width (500px - 799px) */
|
||||
@container infopanel (min-width: 500px) and (max-width: 799px) {
|
||||
.mg-info-panel-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 3 columns for wider panels (>= 800px) */
|
||||
@container infopanel (min-width: 800px) {
|
||||
.mg-info-panel-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mg-info-panel-item {
|
||||
min-width: 0; /* Prevent grid blowout */
|
||||
}
|
||||
|
||||
/* Fallback styles */
|
||||
.info-panel-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -30,17 +87,13 @@
|
|||
color: var(--dxbl-primary, #0d6efd);
|
||||
}
|
||||
|
||||
.info-panel-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--dxbl-text-secondary, #6c757d);
|
||||
font-style: italic;
|
||||
/* Text overflow handling - show ellipsis and full text in tooltip */
|
||||
.info-panel-text-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-panel-empty p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.info-panel-text-wrapper input[readonly] {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
// MgGridInfoPanel - Sticky scroll handling
|
||||
// Makes the InfoPanel sticky to viewport when scrolling
|
||||
|
||||
window.MgGridInfoPanel = {
|
||||
observers: new Map(),
|
||||
|
||||
// Initialize sticky behavior for an InfoPanel element
|
||||
initSticky: function (element, topOffset) {
|
||||
if (!element) return;
|
||||
|
||||
const elementId = element.id || this.generateId(element);
|
||||
|
||||
// Clean up existing observer if any
|
||||
this.disposeSticky(element);
|
||||
|
||||
// Store the initial position of the element (relative to document)
|
||||
const rect = element.getBoundingClientRect();
|
||||
const initialTop = rect.top + window.scrollY;
|
||||
|
||||
// Calculate and set initial state
|
||||
this.updatePosition(element, initialTop);
|
||||
|
||||
// Handler to update position on scroll and resize
|
||||
const updateHandler = () => {
|
||||
this.updatePosition(element, initialTop);
|
||||
};
|
||||
|
||||
// Add event listeners - use passive to not block scrolling
|
||||
window.addEventListener('resize', updateHandler, { passive: true });
|
||||
window.addEventListener('scroll', updateHandler, { passive: true });
|
||||
|
||||
// Store cleanup info
|
||||
this.observers.set(elementId, {
|
||||
element: element,
|
||||
updateHandler: updateHandler,
|
||||
initialTop: initialTop
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// Dispose sticky behavior
|
||||
disposeSticky: function (element) {
|
||||
if (!element) return;
|
||||
|
||||
const elementId = element.id || this.findElementId(element);
|
||||
const observer = this.observers.get(elementId);
|
||||
|
||||
if (observer) {
|
||||
window.removeEventListener('resize', observer.updateHandler);
|
||||
window.removeEventListener('scroll', observer.updateHandler);
|
||||
|
||||
// Reset styles
|
||||
element.style.height = '';
|
||||
element.style.maxHeight = '';
|
||||
element.style.transform = '';
|
||||
|
||||
this.observers.delete(elementId);
|
||||
}
|
||||
},
|
||||
|
||||
// Update panel position and height based on scroll
|
||||
updatePosition: function (element, initialTop) {
|
||||
if (!element) return;
|
||||
|
||||
const scrollY = window.scrollY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const bottomPadding = 30; // 30px from bottom
|
||||
|
||||
// Calculate how much we've scrolled past the initial position
|
||||
const scrolledPast = Math.max(0, scrollY - initialTop);
|
||||
|
||||
// Get the splitter pane to know our container limits
|
||||
const paneContainer = element.closest('.dxbl-splitter-pane');
|
||||
let maxScrollOffset = Infinity;
|
||||
|
||||
if (paneContainer) {
|
||||
// Don't scroll past the bottom of the pane
|
||||
const paneHeight = paneContainer.offsetHeight;
|
||||
const elementHeight = element.offsetHeight;
|
||||
maxScrollOffset = Math.max(0, paneHeight - elementHeight);
|
||||
}
|
||||
|
||||
// Clamp the scroll offset
|
||||
const translateY = Math.min(scrolledPast, maxScrollOffset);
|
||||
|
||||
// Apply transform to make it "sticky"
|
||||
element.style.transform = `translateY(${translateY}px)`;
|
||||
|
||||
// Calculate height: from current visual position to viewport bottom
|
||||
const rect = element.getBoundingClientRect();
|
||||
const visualTop = rect.top; // This already accounts for transform
|
||||
|
||||
// Height from current visual top to viewport bottom minus padding
|
||||
const availableHeight = viewportHeight - visualTop - bottomPadding;
|
||||
|
||||
// Clamp height
|
||||
const finalHeight = Math.max(200, Math.min(availableHeight, viewportHeight - bottomPadding));
|
||||
|
||||
element.style.height = finalHeight + 'px';
|
||||
element.style.maxHeight = finalHeight + 'px';
|
||||
},
|
||||
|
||||
// Generate a unique ID for the element
|
||||
generateId: function (element) {
|
||||
const id = 'mg-info-panel-' + Math.random().toString(36).substr(2, 9);
|
||||
element.id = id;
|
||||
return id;
|
||||
},
|
||||
|
||||
// Find element ID from stored observers
|
||||
findElementId: function (element) {
|
||||
for (const [id, observer] of this.observers.entries()) {
|
||||
if (observer.element === element) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue