Merge pull request #3994 from sbwalker/dev

add static pager
This commit is contained in:
Shaun Walker 2024-03-14 20:13:50 -07:00 committed by GitHub
commit 6a80258163
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 625 additions and 0 deletions

View File

@ -0,0 +1,499 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@inject IStringLocalizerFactory LocalizerFactory
@inject IStringLocalizer<SharedResources> SharedLocalizer
@typeparam TableItem
@if (ItemList != null)
{
@if (!string.IsNullOrEmpty(SearchProperties))
{
<form method="post" autocomplete="off" @formname="PagerForm" @onsubmit="Search" data-enhance>
<input type="hidden" name="__RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="input-group my-3">
<input type="text" id="pagersearch" name="_search" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" />
<button type="submit" class="btn btn-primary">@SharedLocalizer["Search"]</button>
<a class="btn btn-secondary" href="@PageUrl(1, "")">@SharedLocalizer["Reset"]</a>
</div>
</form>
}
@if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{
<ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(1, _search)"><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_startPage - 1, _search)"><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li>
}
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page > 1) ? _page - 1 : _page), _search)"><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li>
@for (int i = _startPage; i <= _endPage; i++)
{
var pager = i;
if (pager == _page)
{
<li class="page-item app-pager-pointer active">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a>
</li>
}
else
{
<li class="page-item app-pager-pointer">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a>
</li>
}
}
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page < _pages) ? _page + 1 : _page), _search)"><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_endPage + 1, _search)"><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li>
}
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_pages, _search)"><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li>
<li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li>
</ul>
}
@if (Format == "Table" && Row != null)
{
<div class="table-responsive">
<table class="@Class">
<thead>
<tr class="@RowClass">@Header</tr>
</thead>
<tbody>
@foreach (var item in ItemList)
{
<tr class="@RowClass">@Row(item)</tr>
@if (Detail != null)
{
<tr>@Detail(item)</tr>
}
}
</tbody>
<tfoot>
<tr class="@RowClass">@Footer</tr>
</tfoot>
</table>
</div>
}
@if (Format == "Grid" && Row != null)
{
int count = 0;
int rows = 0;
int cols = 0;
if (ItemList != null)
{
if (_columns == 0)
{
count = ItemList.Count();
rows = 1;
cols = count;
}
else
{
count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns;
rows = count / _columns;
cols = _columns;
}
}
<div class="@Class">
@for (int row = 0; row < rows; row++)
{
<div class="@RowClass">
@for (int col = 0; col < cols; col++)
{
int index = (row * _columns) + col;
if (index < ItemList.Count())
{
<div class="@ColumnClass">@Row(ItemList.ElementAt(index))</div>
}
else
{
<div>&nbsp;</div>
}
}
</div>
}
</div>
}
@if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{
<ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(1, _search)"><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_startPage - 1, _search)"><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li>
}
<li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page > 1) ? _page - 1 : _page), _search)"><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li>
@for (int i = _startPage; i <= _endPage; i++)
{
var pager = i;
if (pager == _page)
{
<li class="page-item app-pager-pointer active">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a>
</li>
}
else
{
<li class="page-item app-pager-pointer">
<a class="page-link" href="@PageUrl(pager, _search)">@pager</a>
</li>
}
}
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(((_page < _pages) ? _page + 1 : _page), _search)"><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_endPage + 1, _search)"><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li>
}
<li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" href="@PageUrl(_pages, _search)"><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li>
<li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li>
</ul>
}
}
@code {
private IStringLocalizer Localizer;
private int _pages = 0;
private int _page = 1;
private int _maxItems = 10;
private int _displayPages = 5;
private int _startPage = 0;
private int _endPage = 0;
private int _columns = 0;
private string _search = "";
private IEnumerable<TableItem> AllItems;
[Parameter]
public string Format { get; set; } // Table or Grid
[Parameter]
public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter]
public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
[Parameter]
public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter]
public RenderFragment Footer { get; set; } = null; // only applicable to Table layouts
[Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter]
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter]
public string PageSize { get; set; } // number of items to display on a page
[Parameter]
public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter]
public string CurrentPage { get; set; } // sets the initial page to display
[Parameter]
public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter]
public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid
[Parameter]
public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid
[Parameter]
public string ColumnClass { get; set; } // class for column element - only applicable to Grid format
[Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes
[Parameter]
public string SearchProperties { get; set; } // comma delimited list of property names to include in search
[SupplyParameterFromForm(FormName = "PagerForm")]
public string _Search { get => ""; set => _search = value; }
private IEnumerable<TableItem> ItemList { get; set; }
protected override void OnInitialized()
{
Localizer = LocalizerFactory.Create(GetType().FullName);
}
protected override void OnParametersSet()
{
if (string.IsNullOrEmpty(Format))
{
Format = "Table";
}
if (string.IsNullOrEmpty(Toolbar))
{
Toolbar = "Top";
}
if (string.IsNullOrEmpty(Class))
{
if (Format == "Table")
{
Class = "table table-borderless";
}
else
{
Class = "container-fluid";
}
}
if (string.IsNullOrEmpty(RowClass))
{
if (Format == "Table")
{
RowClass = "";
}
else
{
RowClass = "row";
}
}
if (string.IsNullOrEmpty(ColumnClass))
{
if (Format == "Table")
{
ColumnClass = "";
}
else
{
ColumnClass = "col";
}
}
if (PageState.QueryString.ContainsKey("search"))
{
_search = PageState.QueryString["search"];
}
if (!string.IsNullOrEmpty(SearchProperties))
{
AllItems = Items; // only used in search
if (!string.IsNullOrEmpty(_search))
{
Search();
}
}
if (!string.IsNullOrEmpty(PageSize))
{
_maxItems = int.Parse(PageSize);
}
if (!string.IsNullOrEmpty(Columns))
{
_columns = int.Parse(Columns);
}
if (!string.IsNullOrEmpty(DisplayPages))
{
_displayPages = int.Parse(DisplayPages);
}
if (PageState.QueryString.ContainsKey("page"))
{
_page = int.Parse(PageState.QueryString["page"]);
}
else
{
if (!string.IsNullOrEmpty(CurrentPage))
{
_page = int.Parse(CurrentPage);
}
else
{
_page = 1;
}
}
if (_page < 1) _page = 1;
_startPage = 0;
_endPage = 0;
if (Items != null)
{
_pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems);
if (_page > _pages)
{
_page = _pages;
}
SetPagerSize();
}
}
public void SetPagerSize()
{
_startPage = ((_page - 1) / _displayPages) * _displayPages + 1;
_endPage = _startPage + _displayPages - 1;
if (_endPage > _pages)
{
_endPage = _pages;
}
ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
StateHasChanged();
OnPageChange?.Invoke(_page);
}
public void UpdateList(int page)
{
_page = page;
SetPagerSize();
}
public void SkipPages(string direction)
{
switch (direction)
{
case "forward":
_page = _endPage + 1;
break;
case "back":
_page = _startPage - 1;
break;
}
SetPagerSize();
}
public void NavigateToPage(string direction)
{
switch (direction)
{
case "next":
if (_page < _pages)
{
_page += 1;
}
break;
case "previous":
if (_page > 1)
{
_page -= 1;
}
break;
}
UpdateList(_page);
}
public void Search()
{
if (!string.IsNullOrEmpty(_search))
{
Items = AllItems.Where(item =>
{
var values = SearchProperties.Split(',')
.Select(itemType => GetPropertyValue(item, itemType))
.Where(value => value != null)
.Select(value => value.ToString().ToLower());
return values.Any(value => value.Contains(_search.ToLower()));
}).ToList();
}
else
{
Items = AllItems;
}
_pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems);
UpdateList(1);
}
private object GetPropertyValue(object obj, string propertyName)
{
var index = propertyName.IndexOf(".");
if (index != -1)
{
var propertyInfo = obj.GetType().GetProperty(propertyName.Substring(0, index));
if (propertyInfo != null)
{
return GetPropertyValue(propertyInfo.GetValue(obj), propertyName.Substring(index + 1));
}
return null;
}
else
{
var propertyInfo = obj.GetType().GetProperty(propertyName);
if (propertyInfo != null)
{
return propertyInfo.GetValue(obj);
}
return null;
}
}
public void Reset()
{
_search = "";
Items = AllItems;
_pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems);
UpdateList(1);
}
private string FormatSearchProperties()
{
var properties = new List<string>();
foreach (var property in SearchProperties.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
var index = property.LastIndexOf(".");
if (index != -1)
{
properties.Add(property.Substring(index + 1));
}
else
{
properties.Add(property);
}
}
return string.Join(",", properties);
}
private string PageUrl(int page, string search)
{
var parameters = new Dictionary<string, string>(PageState.QueryString);
if (parameters.ContainsKey("page")) parameters.Remove("page");
parameters.Add("page", page.ToString());
if (parameters.ContainsKey("search")) parameters.Remove("search");
if (!string.IsNullOrEmpty(search))
{
parameters.Add("search", search);
}
return PageState.Route.AbsolutePath + Utilities.CreateQueryString(parameters);
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PageOfPages" xml:space="preserve">
<value>Page {0} of {1}</value>
</data>
<data name="SearchPlaceholder" xml:space="preserve">
<value>Search: {0}</value>
</data>
</root>