diff --git a/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs b/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs index e0411da..b65c3fa 100644 --- a/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs +++ b/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs @@ -3,7 +3,7 @@ namespace AyCode.Blazor.Components.Components.Grids; /// /// Represents the current edit state of the MgGrid /// -public enum MgEditState +public enum MgGridEditState { /// /// No edit operation in progress diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index 930b5af..3bc7653 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -21,11 +21,21 @@ public interface IMgGridBase : IGrid /// Indicates whether any synchronization operation is in progress /// bool IsSyncing { get; } - + /// /// Current edit state of the grid (None, New, Edit) /// - MgEditState EditState { get; } + MgGridEditState GridEditState { get; } + + /// + /// Navigates to the previous row in the grid + /// + void StepPrevRow(); + + /// + /// Navigates to the next row in the grid + /// + void StepNextRow(); } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable @@ -49,7 +59,7 @@ public abstract class MgGridBase _dataSource?.IsSyncing ?? false; /// - 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(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(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,15 +446,18 @@ public abstract class MgGridBase + /// Navigates to the previous row in the grid + /// + public void StepPrevRow() + { + var currentIndex = GetFocusedRowIndex(); + if (currentIndex > 0) + { + SetFocusedRowIndex(currentIndex - 1); + } + } + + /// + /// Navigates to the next row in the grid + /// + 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; diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor index 2fa1052..22b7ad2 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor @@ -3,50 +3,53 @@ @using System.Reflection @typeparam TDataItem where TDataItem : class -
- @if (GetActiveDataItem() != null && _currentGrid != null) - { - var colSpan = _allDataColumns.Count > 10 ? 6 : 12; - var dataItem = GetActiveDataItem()!; +
+ @* Header - matches grid toolbar height *@ +
+ Sor részletei +
- - @foreach (var column in _allDataColumns) - { - var displayText = GetDisplayTextFromGrid(column); - var value = GetCellValue(column); - var settingsType = GetEditSettingsType(column); - var isReadOnly = !_isEditMode || column.ReadOnly; - - - - - } - - } - else - { -
-

Válasszon ki egy sort az adatok megtekintéséhez

-
- } + @* Content - scrollable area *@ +
+ @if (GetActiveDataItem() != null && _currentGrid != null) + { + var dataItem = GetActiveDataItem()!; + +
+ @foreach (var column in _allDataColumns) + { + var displayText = GetDisplayTextFromGrid(column); + var value = GetCellValue(column); + var settingsType = GetEditSettingsType(column); + var isReadOnly = !_isEditMode || column.ReadOnly; + +
+
+ +
+ @if (_isEditMode && !column.ReadOnly) + { + @RenderEditableCell(column, dataItem, value, displayText, settingsType) + } + else + { + @RenderCellContent(column, value, displayText, settingsType) + } +
+
+
+ } +
+ } + else + { +
+

Válasszon ki egy sort az adatok megtekintéséhez

+
+ } +
@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(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(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(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(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(seq++); builder.AddAttribute(seq++, "Text", displayText); builder.AddAttribute(seq++, "ReadOnly", true); builder.CloseComponent(); + builder.CloseElement(); break; } }; diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs index f16eb24..fbe17ce 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs @@ -1,10 +1,18 @@ using DevExpress.Blazor; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; namespace AyCode.Blazor.Components.Components.Grids; -public partial class MgGridInfoPanel : ComponentBase where TDataItem : class +public partial class MgGridInfoPanel : 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 : ComponentBase where TDataItem // Cache for edit settings to avoid repeated lookups private readonly Dictionary _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 + } + } + /// /// Refreshes the InfoPanel with data from the specified grid row (view mode) /// @@ -87,6 +119,26 @@ public partial class MgGridInfoPanel : 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(); + } + } + /// /// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem) /// diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css index 73ec890..4d63be8 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css @@ -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; + } diff --git a/AyCode.Blazor.Components/wwwroot/js/mgGridInfoPanel.js b/AyCode.Blazor.Components/wwwroot/js/mgGridInfoPanel.js new file mode 100644 index 0000000..4d0a1ca --- /dev/null +++ b/AyCode.Blazor.Components/wwwroot/js/mgGridInfoPanel.js @@ -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; + } +};