Compare commits
No commits in common. "518cfa686593e4274e13908fde3a548eb582e507" and "d5a908a46ff23b43bf37a04df2cb32ee3bbb34e7" have entirely different histories.
518cfa6865
...
d5a908a46f
|
|
@ -1,36 +0,0 @@
|
||||||
@typeparam TItem
|
|
||||||
|
|
||||||
<div class="mg-card-view-container" style="@ContainerStyle">
|
|
||||||
@if (ShowFilterPanel && FilterPanel is not null)
|
|
||||||
{
|
|
||||||
<div class="mg-card-filter-panel">
|
|
||||||
@FilterPanel
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Data is { Count: > 0 })
|
|
||||||
{
|
|
||||||
<div class="mg-card-scroll-area">
|
|
||||||
<div class="mg-card-grid @CssClass"
|
|
||||||
style="--cols-xs: @ColumnCountXs; --cols-sm: @ColumnCountSm; --cols-lg: @ColumnCountLg;">
|
|
||||||
@foreach (var item in PagedItems)
|
|
||||||
{
|
|
||||||
<div id="@GetCardElementId(item)"
|
|
||||||
class="mg-card @CardCssClass"
|
|
||||||
@onclick="() => OnCardClickInternal(item)"
|
|
||||||
style="@(OnCardClick.HasDelegate ? "cursor: pointer;" : "")">
|
|
||||||
@CardTemplate(item)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (ShowPager && Data.Count > PageSize)
|
|
||||||
{
|
|
||||||
<DxPager PageCount="@((int)Math.Ceiling((double)Data.Count / PageSize))"
|
|
||||||
ActivePageIndex="_activePageIndex"
|
|
||||||
ActivePageIndexChanged="OnActivePageIndexChanged"
|
|
||||||
CssClass="mt-2" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.JSInterop;
|
|
||||||
|
|
||||||
namespace AyCode.Blazor.Components.Components.CardViews;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generic card view component that displays items in a responsive CSS Grid layout
|
|
||||||
/// with optional pagination and scroll-to-item support.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TItem">The type of data item displayed in each card.</typeparam>
|
|
||||||
public partial class MgCardView<TItem> : ComponentBase
|
|
||||||
{
|
|
||||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The collection of items to display as cards.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter, EditorRequired]
|
|
||||||
public IReadOnlyList<TItem> Data { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Template for rendering each card's content.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter, EditorRequired]
|
|
||||||
public RenderFragment<TItem> CardTemplate { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fired when a card is clicked/tapped.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public EventCallback<TItem> OnCardClick { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of columns on extra-small screens (below 576px). Default: 1.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public int ColumnCountXs { get; set; } = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of columns on small screens (576–768px). Default: 2.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public int ColumnCountSm { get; set; } = 2;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of columns on medium+ screens (769px+). Default: 3.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public int ColumnCountLg { get; set; } = 3;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to show the pager below the cards. Default: false.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public bool ShowPager { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of items per page when paging is enabled. Default: 12.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public int PageSize { get; set; } = 12;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Additional CSS class for the card view container.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public string? CssClass { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Additional CSS class applied to each individual card wrapper.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public string? CardCssClass { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Height of the card view container (e.g., "500px", "70vh"). When set, the component uses its own scroll area.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public string? Height { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to show the filter panel above the cards. Default: false.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public bool ShowFilterPanel { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Custom content for the filter panel. Rendered above the card grid when ShowFilterPanel is true.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public RenderFragment? FilterPanel { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Item to scroll into view after render. Set to null to disable.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public TItem? ScrollToItem { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Key selector for identifying items (e.g., item => item.Id). Required when ScrollToItem is used.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
public Func<TItem, object>? ItemKeySelector { get; set; }
|
|
||||||
|
|
||||||
private int _activePageIndex;
|
|
||||||
private object? _lastScrolledKey;
|
|
||||||
|
|
||||||
private string? ContainerStyle => Height is not null ? $"height: {Height};" : null;
|
|
||||||
|
|
||||||
private IReadOnlyList<TItem> PagedItems
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!ShowPager)
|
|
||||||
return Data;
|
|
||||||
|
|
||||||
return Data
|
|
||||||
.Skip(_activePageIndex * PageSize)
|
|
||||||
.Take(PageSize)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnCardClickInternal(TItem item)
|
|
||||||
{
|
|
||||||
if (OnCardClick.HasDelegate)
|
|
||||||
await OnCardClick.InvokeAsync(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnActivePageIndexChanged(int newPageIndex)
|
|
||||||
{
|
|
||||||
_activePageIndex = newPageIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a stable DOM element id for a card item using the key selector.
|
|
||||||
/// </summary>
|
|
||||||
private string? GetCardElementId(TItem item)
|
|
||||||
{
|
|
||||||
return ItemKeySelector is null ? null : $"mg-card-{ItemKeySelector(item)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (ScrollToItem is not null && ItemKeySelector is not null)
|
|
||||||
{
|
|
||||||
var key = ItemKeySelector(ScrollToItem);
|
|
||||||
if (!Equals(key, _lastScrolledKey))
|
|
||||||
{
|
|
||||||
_lastScrolledKey = key;
|
|
||||||
var elementId = $"mg-card-{key}";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await JSRuntime.InvokeVoidAsync("MgCardView.scrollToElement", elementId);
|
|
||||||
}
|
|
||||||
catch (JSException)
|
|
||||||
{
|
|
||||||
// JS might not be loaded yet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
.mg-card-view-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card-scroll-area {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
grid-template-columns: repeat(var(--cols-xs, 1), 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
.mg-card-grid {
|
|
||||||
grid-template-columns: repeat(var(--cols-sm, 2), 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.mg-card-grid {
|
|
||||||
grid-template-columns: repeat(var(--cols-lg, 3), 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card {
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
||||||
transition: box-shadow 0.2s ease, transform 0.15s ease;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card-filter-panel {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
@ -64,7 +64,7 @@ public class MgGridDataColumn : DxGridDataColumn
|
||||||
builder.OpenElement(0, "a");
|
builder.OpenElement(0, "a");
|
||||||
builder.AddAttribute(1, "href", url);
|
builder.AddAttribute(1, "href", url);
|
||||||
builder.AddAttribute(2, "target", "_blank");
|
builder.AddAttribute(2, "target", "_blank");
|
||||||
builder.AddAttribute(3, "style", "text-decoration: underline; color: inherit;");
|
builder.AddAttribute(3, "style", "text-decoration: none;");
|
||||||
builder.AddContent(4, context.DisplayText);
|
builder.AddContent(4, context.DisplayText);
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
<DxSplitterPane>
|
<DxSplitterPane>
|
||||||
@GridContent
|
@GridContent
|
||||||
</DxSplitterPane>
|
</DxSplitterPane>
|
||||||
<DxSplitterPane Size="@_currentInfoPanelSize" MinSize="0px" MaxSize="100%" AllowCollapse="true" CssClass="mg-info-panel-pane"
|
<DxSplitterPane Size="@_currentInfoPanelSize" MinSize="0px" MaxSize="100%" AllowCollapse="false" CssClass="mg-info-panel-pane"
|
||||||
SizeChanged="OnInfoPanelSizeChanged">
|
SizeChanged="OnInfoPanelSizeChanged">
|
||||||
@if (ChildContent != null)
|
@if (ChildContent != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// MgCardView - Scroll handling
|
|
||||||
window.MgCardView = {
|
|
||||||
scrollToElement: function (elementId) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
// Find the closest scroll container
|
|
||||||
const scrollArea = element.closest('.mg-card-scroll-area');
|
|
||||||
if (scrollArea) {
|
|
||||||
const containerRect = scrollArea.getBoundingClientRect();
|
|
||||||
const elementRect = element.getBoundingClientRect();
|
|
||||||
const offset = elementRect.top - containerRect.top - (containerRect.height - elementRect.height) / 2;
|
|
||||||
scrollArea.scrollBy({ top: offset, behavior: 'smooth' });
|
|
||||||
} else {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue