Merge branch 'features/refactor' of https://git.tompit.com/Connected/Connected.Components into features/refactor

features/refactor
stm 2 years ago
commit 1dabf54e39

@ -21,7 +21,7 @@
@if (CloseGlyphVisible) @if (CloseGlyphVisible)
{ {
<div class="alert-close"> <div class="alert-close">
<IconButton ClassList="size-small" Icon="@CloseGlyph" Clicked="OnCloseGlyphClick" /> <GlyphButton ClassList="size-small" Glyph="@CloseGlyph" Clicked="OnCloseGlyphClick" />
</div> </div>
} }
</div> </div>

@ -11,7 +11,7 @@
{ {
@if (!String.IsNullOrEmpty(Icon)) @if (!String.IsNullOrEmpty(Icon))
{ {
<Icon Icon="@Icon" Class="icon-badge" /> <Icon Glyph="@Icon" Class="icon-badge" />
} }
else else
{ {

@ -7,7 +7,7 @@
<a href="@(Item?.Href ?? "#")"> <a href="@(Item?.Href ?? "#")">
@if (Item?.Icon != null) @if (Item?.Icon != null)
{ {
<Icon Icon="@Item?.Icon" Size="Size.Small" /> <Icon Glyph="@Item?.Icon" Size="Size.Small" />
} }
@Item?.Text @Item?.Text
</a> </a>

@ -13,7 +13,7 @@
<BreadcrumbLink Item="Items[0]"></BreadcrumbLink> <BreadcrumbLink Item="Items[0]"></BreadcrumbLink>
<BreadcrumbSeparator></BreadcrumbSeparator> <BreadcrumbSeparator></BreadcrumbSeparator>
<li class="breadcrumbs-expander" @onclick="Expand"> <li class="breadcrumbs-expander" @onclick="Expand">
<Icon Icon="@ExpanderIcon" Size="Size.Small"></Icon> <Icon Glyph="@ExpanderIcon" Size="Size.Small"></Icon>
</li> </li>
<BreadcrumbSeparator></BreadcrumbSeparator> <BreadcrumbSeparator></BreadcrumbSeparator>
<BreadcrumbLink Item="Items[Items.Count - 1]"></BreadcrumbLink> <BreadcrumbLink Item="Items[Items.Count - 1]"></BreadcrumbLink>

@ -14,12 +14,12 @@
<span class="fab-label"> <span class="fab-label">
@if (!string.IsNullOrWhiteSpace(StartIcon)) @if (!string.IsNullOrWhiteSpace(StartIcon))
{ {
<Icon Icon="@StartIcon" Color="@IconColor" Size="@IconSize" /> <Icon Glyph="@StartIcon" Color="@IconColor" Size="@IconSize" />
} }
@Label @Label
@if (!string.IsNullOrWhiteSpace(EndIcon)) @if (!string.IsNullOrWhiteSpace(EndIcon))
{ {
<Icon Icon="@EndIcon" Color="@IconColor" Size="@IconSize" /> <Icon Glyph="@EndIcon" Color="@IconColor" Size="@IconSize" />
} }
</span> </span>
</Element> </Element>

@ -1,27 +0,0 @@
@namespace Connected.Components
@inherits ButtonBase
@using Connected.Components;
<Element disabled="@Disabled"
title="@IconTitle"
type="@ButtonType.ToString()"
ClassList="@CompiledClassList.ToString()"
HtmlTag="@HtmlTag"
PreventOnClickPropagation="PreventOnClickPropagation"
@attributes="CustomAttributes"
@onclick="OnClick">
@if (!String.IsNullOrEmpty(Icon))
{
<span name="glyph-container" class="glyph-button-label">
<Icon Glyph="@Icon" />
</span>
}
else
{
<TextContent Typo="Typo.body2">
@ChildContent
</TextContent>
}
</Element>

@ -1,54 +0,0 @@
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
namespace Connected.Components;
public partial class IconButton : ButtonBase
{
#region Content
/// <summary>
/// The Icon that will be used in the component.
/// </summary>
[Parameter]
public string? Icon { get; set; }
/// <summary>
/// GlyphTitle of the icon used for accessibility.
/// </summary>
[Parameter]
public string? IconTitle { get; set; }
/// <summary>
/// Child content of component, only shows if Glyph is null or Empty.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
#endregion
#region Styling
/// <summary>
/// A space separated list of class names, added on top of the default class list.
/// </summary>
[Parameter]
public string? ClassList { get; set; }
/// <summary>
/// The variant to use.
/// </summary>
[Parameter]
public Variant Variant { get; set; } = Variant.Text;
/// <summary>
/// Contains the default container classlist and the user defined classes.
/// </summary>
private CssBuilder CompiledClassList
{
get
{
return new CssBuilder("button-root glyph-button")
.AddClass(ClassList);
}
}
#endregion
}

@ -1,13 +0,0 @@
@namespace Connected.Components
@inherits UIComponent
<IconButton aria-pressed="@Toggled.ToString()"
ClassList="@ClassList"
Clicked="Toggle"
Disabled="Disabled"
Icon="@(Toggled ? ToggledIcon : Icon)"
IconTitle="@(Toggled && ToggledIconTitle != null ? ToggledIconTitle : IconTitle)"
Variant="Variant"
@attributes="CustomAttributes"
/>

@ -1,89 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace Connected.Components;
public partial class ToggleIconButton : UIComponent
{
#region Events
/// <summary>
/// Fires whenever toggled is changed.
/// </summary>
[Parameter]
public EventCallback<bool> ToggledChanged { get; set; }
public async Task Toggle()
{
await SetToggledAsync(!Toggled);
}
protected internal async Task SetToggledAsync(bool toggled)
{
if (Disabled)
return;
if (Toggled != toggled)
{
Toggled = toggled;
if (!ToggledChanged.HasDelegate)
return;
await ToggledChanged.InvokeAsync(Toggled);
}
}
#endregion
#region Content
/// <summary>
/// The glyph that will be used in the untoggled state.
/// </summary>
[Parameter]
public string? Icon { get; set; }
/// <summary>
/// GlyphTitle of the icon used for accessibility.
/// </summary>
[Parameter]
public string? IconTitle { get; set; }
/// <summary>
/// The glyph that will be used in the toggled state.
/// </summary>
[Parameter]
public string? ToggledIcon { get; set; }
/// <summary>
/// GlyphTitle used in toggled state, if different.
/// </summary>
[Parameter]
public string? ToggledIconTitle { get; set; }
#endregion
#region Styling
/// <summary>
/// A space separated list of class names, added on top of the default class list.
/// </summary>
[Parameter]
public string? ClassList { get; set; }
/// <summary>
/// The variant to use.
/// </summary>
[Parameter]
public Variant Variant { get; set; } = Variant.Text;
/// <summary>
/// If true, the button will be disabled.
/// </summary>
[Parameter]
public bool Disabled { get; set; }
/// <summary>
/// The button toggled state.
/// </summary>
[Parameter]
public bool Toggled { get; set; }
#endregion
}

@ -31,7 +31,7 @@
{ {
@if (PreviousButtonTemplate == null) @if (PreviousButtonTemplate == null)
{ {
<IconButton tabindex="1" aria-label="Go to previous" Class="@NavigationButtonsClassName" Style="z-index:3;opacity:0.75" Icon="@PreviousIcon" Clicked="Previous" Color="ThemeColor.Inherit" /> <GlyphButton tabindex="1" aria-label="Go to previous" Class="@NavigationButtonsClassName" Style="z-index:3;opacity:0.75" Glyph="@PreviousIcon" Clicked="Previous" Color="ThemeColor.Inherit" />
} }
else else
{ {
@ -51,7 +51,7 @@
int current = i; int current = i;
if (BulletTemplate == null) if (BulletTemplate == null)
{ {
<IconButton tabindex="@(i+3)" aria-label="@(i+1)" Class="@BulletsButtonsClassName" Style="z-index:3;opacity:0.75" Icon="@(current == SelectedIndex ? CheckedIcon : UncheckedIcon)" Clicked="(() => MoveTo(current))" Color="ThemeColor.Inherit" /> <GlyphButton tabindex="@(i+3)" aria-label="@(i+1)" Class="@BulletsButtonsClassName" Style="z-index:3;opacity:0.75" Glyph="@(current == SelectedIndex ? CheckedIcon : UncheckedIcon)" Clicked="(() => MoveTo(current))" Color="ThemeColor.Inherit" />
} }
else else
{ {
@ -69,7 +69,7 @@
{ {
@if (NextButtonTemplate == null) @if (NextButtonTemplate == null)
{ {
<IconButton tabindex="2" aria-label="Go to next" Class="@NavigationButtonsClassName" Style="z-index:3;opacity:0.75" Icon="@NextIcon" Clicked="Next" Color="ThemeColor.Inherit" /> <GlyphButton tabindex="2" aria-label="Go to next" Class="@NavigationButtonsClassName" Style="z-index:3;opacity:0.75" Glyph="@NextIcon" Clicked="Next" Color="ThemeColor.Inherit" />
} }
else else
{ {

@ -8,7 +8,7 @@
<span tabindex="0" class="@CheckBoxClassname"> <span tabindex="0" class="@CheckBoxClassname">
@*note: stopping the click propagation is important here. otherwise checking the checkbox results in click events on its parent (i.e. table row), which is generally not what you would want*@ @*note: stopping the click propagation is important here. otherwise checking the checkbox results in click events on its parent (i.e. table row), which is generally not what you would want*@
<input tabindex="-1" @attributes="CustomAttributes" type="checkbox" class="checkbox-input" checked="@BoolValue" @onchange="@OnChange" disabled="@Disabled" @onclick:preventDefault="@ReadOnly" /> <input tabindex="-1" @attributes="CustomAttributes" type="checkbox" class="checkbox-input" checked="@BoolValue" @onchange="@OnChange" disabled="@Disabled" @onclick:preventDefault="@ReadOnly" />
<Icon Icon="@GetIcon()" Color="HasErrors ? ThemeColor.Error : ThemeColor.Inherit" Size="@Size" /> <Icon Glyph="@GetIcon()" Color="HasErrors ? ThemeColor.Error : ThemeColor.Inherit" Size="@Size" />
</span> </span>
@if (!String.IsNullOrEmpty(Label)) @if (!String.IsNullOrEmpty(Label))
{ {

@ -10,11 +10,11 @@
} }
else if (!String.IsNullOrEmpty(Icon) && !IsChecked) else if (!String.IsNullOrEmpty(Icon) && !IsChecked)
{ {
<Icon Icon="@Icon" Class="chip-icon" Size="Size.Small" Color="@IconColor" /> <Icon Glyph="@Icon" Class="chip-icon" Size="Size.Small" Color="@IconColor" />
} }
else if (IsChecked) else if (IsChecked)
{ {
<Icon Icon="@CheckedIcon" Class="chip-icon" Size="Size.Small" /> <Icon Glyph="@CheckedIcon" Class="chip-icon" Size="Size.Small" />
} }
<span class="chip-content"> <span class="chip-content">
@if (ChildContent == null) @if (ChildContent == null)
@ -27,7 +27,7 @@
} }
@if (OnClose.HasDelegate || ChipSet?.AllClosable==true) @if (OnClose.HasDelegate || ChipSet?.AllClosable==true)
{ {
<IconButton Class="chip-close-button" Icon="@(String.IsNullOrEmpty(CloseIcon) ? $"{Icons.Material.Filled.Cancel}" : $"{CloseIcon}")" Clicked="OnCloseHandler" Size="Size.Small"/> <GlyphButton Class="chip-close-button" Glyph="@(String.IsNullOrEmpty(CloseIcon) ? $"{Icons.Material.Filled.Cancel}" : $"{CloseIcon}")" Clicked="OnCloseHandler" Size="Size.Small"/>
} }
</span> </span>
</div> </div>

@ -12,12 +12,12 @@
<PickerToolbar DisableToolbar="@DisableToolbar" Class="picker-color-toolbar"> <PickerToolbar DisableToolbar="@DisableToolbar" Class="picker-color-toolbar">
@if (PickerVariant != PickerVariant.Static) @if (PickerVariant != PickerVariant.Static)
{ {
<IconButton Class="pa-1 mud-close-picker-button" Size="Size.Small" Color="ThemeColor.Inherit" Icon="@CloseIcon" Clicked="@GetEventCallback()" /> <GlyphButton Class="pa-1 mud-close-picker-button" Size="Size.Small" Color="ThemeColor.Inherit" Glyph="@CloseIcon" Clicked="@GetEventCallback()" />
} }
<Spacer /> <Spacer />
<IconButton Class="pa-1" Size="Size.Small" Color="GetButtonColor(ColorPickerView.Spectrum)" Icon="@SpectrumIcon" Clicked="(() => ChangeView(ColorPickerView.Spectrum))" /> <GlyphButton Class="pa-1" Size="Size.Small" Color="GetButtonColor(ColorPickerView.Spectrum)" Glyph="@SpectrumIcon" Clicked="(() => ChangeView(ColorPickerView.Spectrum))" />
<IconButton Class="pa-1 mx-1" Size="Size.Small" Color="GetButtonColor(ColorPickerView.Grid)" Icon="@GridIcon" Clicked="(() => ChangeView(ColorPickerView.Grid))" /> <GlyphButton Class="pa-1 mx-1" Size="Size.Small" Color="GetButtonColor(ColorPickerView.Grid)" Glyph="@GridIcon" Clicked="(() => ChangeView(ColorPickerView.Grid))" />
<IconButton Class="pa-1" Size="Size.Small" Color="GetButtonColor(ColorPickerView.Palette)" Icon="@PaletteIcon" Clicked="(() => ChangeView(ColorPickerView.Palette))" /> <GlyphButton Class="pa-1" Size="Size.Small" Color="GetButtonColor(ColorPickerView.Palette)" Glyph="@PaletteIcon" Clicked="(() => ChangeView(ColorPickerView.Palette))" />
</PickerToolbar> </PickerToolbar>
<PickerContent Class="picker-color-content"> <PickerContent Class="picker-color-content">
@if (!DisableColorField) @if (!DisableColorField)
@ -126,7 +126,7 @@
@if (!DisableModeSwitch) @if (!DisableModeSwitch)
{ {
<div class="picker-control-switch"> <div class="picker-control-switch">
<IconButton Clicked="ChangeMode" Icon="@ImportExportIcon" Class="pa-1 me-n1"></IconButton> <GlyphButton Clicked="ChangeMode" Glyph="@ImportExportIcon" Class="pa-1 me-n1"></GlyphButton>
</div> </div>
} }
</div> </div>

@ -126,9 +126,9 @@
@{ var groupStyle = new StyleBuilder().AddStyle(GroupStyle).AddStyle(GroupStyleFunc?.Invoke(g)).Build(); } @{ var groupStyle = new StyleBuilder().AddStyle(GroupStyle).AddStyle(GroupStyleFunc?.Invoke(g)).Build(); }
<td class="table-cell @groupClass" colspan="1000" style="background-color:var(--mud-palette-background-grey);@groupStyle"> <td class="table-cell @groupClass" colspan="1000" style="background-color:var(--mud-palette-background-grey);@groupStyle">
<IconButton <GlyphButton
Class="table-row-expander" Class="table-row-expander"
Icon="@(g.IsExpanded ? Icons.Material.Filled.ExpandMore : Icons.Material.Filled.ChevronRight)" Glyph="@(g.IsExpanded ? Icons.Material.Filled.ExpandMore : Icons.Material.Filled.ChevronRight)"
Clicked="@(() => ToggleGroupExpansion(g))" /> Clicked="@(() => ToggleGroupExpansion(g))" />
@if (GroupedColumn.GroupTemplate == null) @if (GroupedColumn.GroupTemplate == null)
@ -377,7 +377,7 @@
@if (column == null) @if (column == null)
{ {
<Item xs="1" Class="d-flex"> <Item xs="1" Class="d-flex">
<IconButton Class="remove-filter-button" Icon="@Icons.Material.Filled.Close" Clicked="@filter.RemoveFilter" Size="@Size.Small" Style="align-self:flex-end"></IconButton> <GlyphButton Class="remove-filter-button" Glyph="@Icons.Material.Filled.Close" Clicked="@filter.RemoveFilter" Size="@Size.Small" Style="align-self:flex-end"></GlyphButton>
</Item> </Item>
<Item xs="4"> <Item xs="4">
<Select T="string" Value="@f.Field" ValueChanged="@filter.FieldChanged" FullWidth="true" Label="Column" Dense="true" Margin="@Margin.Dense" <Select T="string" Value="@f.Field" ValueChanged="@filter.FieldChanged" FullWidth="true" Label="Column" Dense="true" Margin="@Margin.Dense"

@ -20,10 +20,10 @@
@Info @Info
</TextContent> </TextContent>
<div class="table-pagination-actions"> <div class="table-pagination-actions">
<IconButton Class="flip-x-rtl" Icon="@Icons.Material.Filled.FirstPage" Disabled="@BackButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.First))" /> <GlyphButton Class="flip-x-rtl" Glyph="@Icons.Material.Filled.FirstPage" Disabled="@BackButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.First))" />
<IconButton Class="flip-x-rtl" Icon="@Icons.Material.Filled.NavigateBefore" Disabled="@BackButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.Previous))" /> <GlyphButton Class="flip-x-rtl" Glyph="@Icons.Material.Filled.NavigateBefore" Disabled="@BackButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.Previous))" />
<IconButton Class="flip-x-rtl" Icon="@Icons.Material.Filled.NavigateNext" Disabled="@ForwardButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.Next))" /> <GlyphButton Class="flip-x-rtl" Glyph="@Icons.Material.Filled.NavigateNext" Disabled="@ForwardButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.Next))" />
<IconButton Class="flip-x-rtl" Icon="@Icons.Material.Filled.LastPage" Disabled="@ForwardButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.Last))" /> <GlyphButton Class="flip-x-rtl" Glyph="@Icons.Material.Filled.LastPage" Disabled="@ForwardButtonsDisabled" Clicked="@(() => DataGrid.NavigateTo(Page.Last))" />
</div> </div>
</ToolBar> </ToolBar>

@ -58,7 +58,7 @@
} }
} }
</Menu> </Menu>
<IconButton Class="align-self-center" Icon="@Icons.Material.Filled.FilterAltOff" Size="@Size.Small" Clicked="@ClearFilter"></IconButton> <GlyphButton Class="align-self-center" Glyph="@Icons.Material.Filled.FilterAltOff" Size="@Size.Small" Clicked="@ClearFilter"></GlyphButton>
</Stack> </Stack>
} }
} }

@ -41,11 +41,11 @@ else if (Column != null && !Column.Hidden)
{ {
if (_initialDirection == SortDirection.None) if (_initialDirection == SortDirection.None)
{ {
<IconButton Icon="@Column.SortIcon" Class="@sortIconClass" Size="@Size.Small" Clicked="@SortChangedAsync"></IconButton> <GlyphButton Glyph="@Column.SortIcon" Class="@sortIconClass" Size="@Size.Small" Clicked="@SortChangedAsync"></GlyphButton>
} }
else else
{ {
<IconButton Icon="@Column.SortIcon" Class="@sortIconClass" Size="@Size.Small" Clicked="@SortChangedAsync"></IconButton> <GlyphButton Glyph="@Column.SortIcon" Class="@sortIconClass" Size="@Size.Small" Clicked="@SortChangedAsync"></GlyphButton>
if(DataGrid.SortMode == SortMode.Multiple) if(DataGrid.SortMode == SortMode.Multiple)
{ {
<span class="sort-index mud-text-disabled">@(Column.SortIndex + 1)</span> <span class="sort-index mud-text-disabled">@(Column.SortIndex + 1)</span>
@ -57,11 +57,11 @@ else if (Column != null && !Column.Hidden)
{ {
if (hasFilter) if (hasFilter)
{ {
<IconButton Class="filter-button filtered" Icon="@Icons.Material.Filled.FilterAlt" Size="@Size.Small" Clicked="@OpenFilters"></IconButton> <GlyphButton Class="filter-button filtered" Glyph="@Icons.Material.Filled.FilterAlt" Size="@Size.Small" Clicked="@OpenFilters"></GlyphButton>
} }
else if (showFilterIcon) else if (showFilterIcon)
{ {
<IconButton Class="filter-button" Icon="@Icons.Material.Outlined.FilterAlt" Size="@Size.Small" Clicked="@AddFilter"></IconButton> <GlyphButton Class="filter-button" Glyph="@Icons.Material.Outlined.FilterAlt" Size="@Size.Small" Clicked="@AddFilter"></GlyphButton>
} }
} }

@ -6,9 +6,8 @@
Filterable="false"> Filterable="false">
<CellTemplate> <CellTemplate>
<label class="ma-n3"> <label class="ma-n3">
<IconButton <GlyphButton Glyph="@(context.openHierarchies.Contains(context.Item) ? OpenIcon : ClosedIcon)"
Icon="@(context.openHierarchies.Contains(context.Item) ? OpenIcon : ClosedIcon)" OnClick="context.Actions.ToggleHierarchyVisibilityForItem"
OnClick="context.Actions.ToggleHierarchyVisibilityForItem"
Size="@IconSize" Size="@IconSize"
Disabled="ButtonDisabledFunc.Invoke(context.Item)"/> Disabled="ButtonDisabledFunc.Invoke(context.Item)"/>
</label> </label>

@ -47,11 +47,11 @@
<div class="picker-calendar-header-switch"> <div class="picker-calendar-header-switch">
@if (!FixYear.HasValue) @if (!FixYear.HasValue)
{ {
<IconButton aria-label="@prevLabel" Icon="@PreviousIcon" Clicked="OnPreviousYearClick" Class="flip-x-rtl" /> <GlyphButton aria-label="@prevLabel" Glyph="@PreviousIcon" Clicked="OnPreviousYearClick" Class="flip-x-rtl" />
<button type="button" class="picker-slide-transition mud-picker-calendar-header-transition" @onclick="OnYearClick" @onclick:stopPropagation="true"> <button type="button" class="picker-slide-transition mud-picker-calendar-header-transition" @onclick="OnYearClick" @onclick:stopPropagation="true">
<TextContent Typo="Typo.body1" Align="Align.Center">@calendarYear</TextContent> <TextContent Typo="Typo.body1" Align="Align.Center">@calendarYear</TextContent>
</button> </button>
<IconButton aria-label="@nextLabel" Icon="@NextIcon" Clicked="OnNextYearClick" Class="flip-x-rtl" /> <GlyphButton aria-label="@nextLabel" Glyph="@NextIcon" Clicked="OnNextYearClick" Class="flip-x-rtl" />
} }
else else
{ {
@ -77,11 +77,11 @@
<div class="picker-calendar-header-switch"> <div class="picker-calendar-header-switch">
@if (!FixMonth.HasValue) @if (!FixMonth.HasValue)
{ {
<IconButton aria-label="@prevLabel" Class="picker-nav-button-prev mud-flip-x-rtl" Icon="@PreviousIcon" Clicked="OnPreviousMonthClick" /> <GlyphButton aria-label="@prevLabel" Class="picker-nav-button-prev mud-flip-x-rtl" Glyph="@PreviousIcon" Clicked="OnPreviousMonthClick" />
<button type="button" class="picker-slide-transition mud-picker-calendar-header-transition mud-button-month" @onclick="(() => OnMonthClicked(tempMonth))" @onclick:stopPropagation="true"> <button type="button" class="picker-slide-transition mud-picker-calendar-header-transition mud-button-month" @onclick="(() => OnMonthClicked(tempMonth))" @onclick:stopPropagation="true">
<TextContent Typo="Typo.body1" Align="Align.Center">@GetMonthName(tempMonth)</TextContent> <TextContent Typo="Typo.body1" Align="Align.Center">@GetMonthName(tempMonth)</TextContent>
</button> </button>
<IconButton aria-label="@nextLabel" Class="picker-nav-button-next mud-flip-x-rtl" Icon="@NextIcon" Clicked="OnNextMonthClick" /> <GlyphButton aria-label="@nextLabel" Class="picker-nav-button-next mud-flip-x-rtl" Glyph="@NextIcon" Clicked="OnNextMonthClick" />
} }
else else
{ {

@ -17,7 +17,7 @@
} }
@if (CloseButton) @if (CloseButton)
{ {
<IconButton aria-label="close" Icon="@CloseIcon" Clicked="Cancel" /> <GlyphButton aria-label="close" Glyph="@CloseIcon" Clicked="Cancel" />
} }
</div> </div>
} }

@ -27,6 +27,12 @@ public class DragAndDropIndexChangedEventArgs : EventArgs
#endregion #endregion
} }
/// <summary>
/// Used to encapsulate data for a drag and drop transaction
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="T"></typeparam>
public class DragAndDropItemTransaction<T> public class DragAndDropItemTransaction<T>
{ {
#region Variables #region Variables
@ -113,23 +119,8 @@ public class DragAndDropItemTransaction<T>
#endregion #endregion
#region Styling
#endregion
#region Behavior
#endregion
#region Lifecycle
#endregion
} }
/// <summary>
/// Used to encapsulate data for a drag and drop transaction
/// </summary>
/// <typeparam name="T"></typeparam>
/// <summary> /// <summary>
/// Record encaplusalting data regaring a completed transaction /// Record encaplusalting data regaring a completed transaction
@ -140,9 +131,19 @@ public class DragAndDropItemTransaction<T>
/// <param name="IndexInZone">The index of the item within in the dropzone</param> /// <param name="IndexInZone">The index of the item within in the dropzone</param>
public record ItemDropInfo<T>(T Item, string DropzoneIdentifier, int IndexInZone); public record ItemDropInfo<T>(T Item, string DropzoneIdentifier, int IndexInZone);
public class DragAndDropTransactionFinishedEventArgs<T> : EventArgs public class DragAndDropTransactionFinishedEventArgs<T> : EventArgs
{ {
public DragAndDropTransactionFinishedEventArgs(DragAndDropItemTransaction<T> transaction) : #region Variables
public T Item { get; }
public bool Success { get; }
public string OriginatedDropzoneIdentifier { get; }
public string DestinationDropzoneIdentifier { get; }
public int OriginIndex { get; }
public int DestinationIndex { get; }
#endregion
#region Events
public DragAndDropTransactionFinishedEventArgs(DragAndDropItemTransaction<T> transaction) :
this(string.Empty, false, transaction) this(string.Empty, false, transaction)
{ {
@ -158,219 +159,227 @@ public record ItemDropInfo<T>(T Item, string DropzoneIdentifier, int IndexInZone
DestinationIndex = transaction.Index; DestinationIndex = transaction.Index;
} }
public T Item { get; } #endregion
public bool Success { get; }
public string OriginatedDropzoneIdentifier { get; } }
public string DestinationDropzoneIdentifier { get; }
public int OriginIndex { get; } /// <summary>
public int DestinationIndex { get; } /// The container of a drag and drop zones
} /// </summary>
/// <typeparam name="T">Datetype of items</typeparam>
public partial class DropContainer<T> : UIComponent
{
#region Variables
private DragAndDropItemTransaction<T> _transaction;
#endregion
#region Events
public event EventHandler<DragAndDropItemTransaction<T>> TransactionStarted;
public event EventHandler<DragAndDropIndexChangedEventArgs> TransactionIndexChanged;
public event EventHandler<DragAndDropTransactionFinishedEventArgs<T>> TransactionEnded;
public event EventHandler RefreshRequested;
#endregion
#region Content
/// <summary>
/// Child content of component. This should include the drop zones
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Appearance)]
public RenderFragment ChildContent { get; set; }
/// <summary> /// <summary>
/// The container of a drag and drop zones /// The items that can be drag and dropped within the container
/// </summary> /// </summary>
/// <typeparam name="T">Datetype of items</typeparam> [Parameter]
public partial class DropContainer<T> : UIComponent [Category(CategoryTypes.DropZone.Items)]
{ public IEnumerable<T> Items { get; set; }
private DragAndDropItemTransaction<T> _transaction; #endregion
#region Styling
protected string Classname => protected string Classname =>
new CssBuilder("drop-container") new CssBuilder("drop-container")
.AddClass(AdditionalClassList) .AddClass(AdditionalClassList)
.Build(); .Build();
/// <summary> /// <summary>
/// Child content of component. This should include the drop zones /// The CSS class(es), that is applied to drop zones that are a valid target for drag and drop transaction
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.DropZone.Appearance)] [Category(CategoryTypes.DropZone.DropRules)]
public RenderFragment ChildContent { get; set; } public string CanDropClass { get; set; }
/// <summary>
/// The items that can be drag and dropped within the container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public IEnumerable<T> Items { get; set; }
/// <summary>
/// The render fragment (template) that should be used to render the items within a drop zone
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public RenderFragment<T> ItemRenderer { get; set; }
/// <summary>
/// The method is used to determinate if an item can be dropped within a drop zone
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public Func<T, string, bool> ItemsSelector { get; set; }
/// <summary>
/// Callback that indicates that an item has been dropped on a drop zone. Should be used to update the "status" of the data item
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public EventCallback<ItemDropInfo<T>> ItemDropped { get; set; }
/// <summary>
/// The method is used to determinate if an item can be dropped within a drop zone
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public Func<T, string, bool> CanDrop { get; set; }
/// <summary>
/// The CSS class(es), that is applied to drop zones that are a valid target for drag and drop transaction
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string CanDropClass { get; set; }
/// <summary>
/// The CSS class(es), that is applied to drop zones that are NOT valid target for drag and drop transaction
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string NoDropClass { get; set; }
/// <summary>
/// If true, drop classes CanDropClass <see cref="CanDropClass"/> or NoDropClass <see cref="NoDropClass"/> or applied as soon, as a transaction has started
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public bool ApplyDropClassesOnDragStarted { get; set; } = false;
/// <summary>
/// The method is used to determinate if an item should be disabled for dragging. Defaults to allow all items
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public Func<T, bool> ItemIsDisabled { get; set; }
/// <summary>
/// If a drop item is disabled (determinate by <see cref="ItemIsDisabled"/>). This class is applied to the element
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public string DisabledClass { get; set; } = "disabled";
/// <summary>
/// An additional class that is applied to the drop zone where a drag operation started
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string DraggingClass { get; set; }
/// <summary>
/// An additional class that is applied to an drop item, when it is dragged
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string ItemDraggingClass { get; set; }
public event EventHandler<DragAndDropItemTransaction<T>> TransactionStarted;
public event EventHandler<DragAndDropIndexChangedEventArgs> TransactionIndexChanged;
public event EventHandler<DragAndDropTransactionFinishedEventArgs<T>> TransactionEnded;
public event EventHandler RefreshRequested;
public void StartTransaction(T item, string identifier, int index, Func<Task> commitCallback, Func<Task> cancelCallback)
{
_transaction = new DragAndDropItemTransaction<T>(item, identifier, index, commitCallback, cancelCallback);
TransactionStarted?.Invoke(this, _transaction);
}
public T GetTransactionItem() => _transaction.Item; /// <summary>
/// The CSS class(es), that is applied to drop zones that are NOT valid target for drag and drop transaction
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string NoDropClass { get; set; }
public bool TransactionInProgress() => _transaction != null; /// <summary>
public string GetTransactionOrignZoneIdentiifer() => _transaction?.SourceZoneIdentifier ?? string.Empty; /// The method is used to determinate if an item should be disabled for dragging. Defaults to allow all items
public string GetTransactionCurrentZoneIdentiifer() => _transaction?.CurrentZone ?? string.Empty; /// </summary>
public bool IsTransactionOriginatedFromInside(string identifier) => _transaction.SourceZoneIdentifier == identifier; [Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public Func<T, bool> ItemIsDisabled { get; set; }
public int GetTransactionIndex() => _transaction?.Index ?? -1; /// <summary>
public bool IsItemMovedDownwards() => _transaction.Index > _transaction.SourceIndex; /// If a drop item is disabled (determinate by <see cref="ItemIsDisabled"/>). This class is applied to the element
public bool HasTransactionIndexChanged() /// </summary>
{ [Parameter]
if (_transaction == null) [Category(CategoryTypes.DropZone.Disabled)]
{ public string DisabledClass { get; set; } = "disabled";
return false;
}
if (_transaction.CurrentZone != _transaction.SourceZoneIdentifier) /// <summary>
{ /// An additional class that is applied to the drop zone where a drag operation started
return true; /// </summary>
} [Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string DraggingClass { get; set; }
return _transaction.Index != _transaction.SourceIndex; /// <summary>
} /// An additional class that is applied to an drop item, when it is dragged
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string ItemDraggingClass { get; set; }
public bool IsOrign(int index, string identifier) #endregion
{
if (_transaction == null)
{
return false;
}
if (identifier != _transaction.SourceZoneIdentifier) #region Behavior
{ /// <summary>
return false; /// Callback that indicates that an item has been dropped on a drop zone. Should be used to update the "status" of the data item
} /// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public EventCallback<ItemDropInfo<T>> ItemDropped { get; set; }
return _transaction.SourceIndex == index || _transaction.SourceIndex - 1 == index; /// <summary>
} /// The method is used to determinate if an item can be dropped within a drop zone
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public Func<T, string, bool> CanDrop { get; set; }
public async Task CommitTransaction(string dropzoneIdentifier, bool reorderIsAllowed) /// <summary>
{ /// The render fragment (template) that should be used to render the items within a drop zone
await _transaction.Commit(); /// </summary>
var index = -1; [Parameter]
if (reorderIsAllowed == true) [Category(CategoryTypes.DropZone.Items)]
{ public RenderFragment<T> ItemRenderer { get; set; }
index = GetTransactionIndex() + 1;
if (_transaction.SourceZoneIdentifier == _transaction.CurrentZone && IsItemMovedDownwards() == true)
{
index -= 1;
}
}
await ItemDropped.InvokeAsync(new ItemDropInfo<T>(_transaction.Item, dropzoneIdentifier, index)); /// <summary>
var transactionFinishedEventArgs = new DragAndDropTransactionFinishedEventArgs<T>(dropzoneIdentifier, true, _transaction); /// The method is used to determinate if an item can be dropped within a drop zone
_transaction = null; /// </summary>
TransactionEnded?.Invoke(this, transactionFinishedEventArgs); [Parameter]
} [Category(CategoryTypes.DropZone.Items)]
public Func<T, string, bool> ItemsSelector { get; set; }
/// <summary>
/// If true, drop classes CanDropClass <see cref="CanDropClass"/> or NoDropClass <see cref="NoDropClass"/> or applied as soon, as a transaction has started
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public bool ApplyDropClassesOnDragStarted { get; set; } = false;
public void StartTransaction(T item, string identifier, int index, Func<Task> commitCallback, Func<Task> cancelCallback)
{
_transaction = new DragAndDropItemTransaction<T>(item, identifier, index, commitCallback, cancelCallback);
TransactionStarted?.Invoke(this, _transaction);
}
public T GetTransactionItem() => _transaction.Item;
public async Task CancelTransaction() public bool TransactionInProgress() => _transaction != null;
public string GetTransactionOrignZoneIdentiifer() => _transaction?.SourceZoneIdentifier ?? string.Empty;
public string GetTransactionCurrentZoneIdentiifer() => _transaction?.CurrentZone ?? string.Empty;
public bool IsTransactionOriginatedFromInside(string identifier) => _transaction.SourceZoneIdentifier == identifier;
public int GetTransactionIndex() => _transaction?.Index ?? -1;
public bool IsItemMovedDownwards() => _transaction.Index > _transaction.SourceIndex;
public bool HasTransactionIndexChanged()
{
if (_transaction == null)
{ {
await _transaction.Cancel(); return false;
var transactionFinishedEventArgs = new DragAndDropTransactionFinishedEventArgs<T>(_transaction);
_transaction = null;
TransactionEnded?.Invoke(this, transactionFinishedEventArgs);
} }
public void UpdateTransactionIndex(int index) if (_transaction.CurrentZone != _transaction.SourceZoneIdentifier)
{ {
var changed = _transaction.UpdateIndex(index); return true;
if (changed == false) { return; } }
return _transaction.Index != _transaction.SourceIndex;
}
TransactionIndexChanged?.Invoke(this, new DragAndDropIndexChangedEventArgs(_transaction.CurrentZone, _transaction.CurrentZone, _transaction.Index)); public bool IsOrign(int index, string identifier)
{
if (_transaction == null)
{
return false;
} }
internal void UpdateTransactionZone(string identifier) if (identifier != _transaction.SourceZoneIdentifier)
{ {
var oldValue = _transaction.CurrentZone; return false;
var changed = _transaction.UpdateZone(identifier); }
if (changed == false) { return; }
TransactionIndexChanged?.Invoke(this, new DragAndDropIndexChangedEventArgs(_transaction.CurrentZone, oldValue, _transaction.Index)); return _transaction.SourceIndex == index || _transaction.SourceIndex - 1 == index;
}
public async Task CommitTransaction(string dropzoneIdentifier, bool reorderIsAllowed)
{
await _transaction.Commit();
var index = -1;
if (reorderIsAllowed == true)
{
index = GetTransactionIndex() + 1;
if (_transaction.SourceZoneIdentifier == _transaction.CurrentZone && IsItemMovedDownwards() == true)
{
index -= 1;
}
} }
await ItemDropped.InvokeAsync(new ItemDropInfo<T>(_transaction.Item, dropzoneIdentifier, index));
var transactionFinishedEventArgs = new DragAndDropTransactionFinishedEventArgs<T>(dropzoneIdentifier, true, _transaction);
_transaction = null;
TransactionEnded?.Invoke(this, transactionFinishedEventArgs);
}
public async Task CancelTransaction()
{
await _transaction.Cancel();
var transactionFinishedEventArgs = new DragAndDropTransactionFinishedEventArgs<T>(_transaction);
_transaction = null;
TransactionEnded?.Invoke(this, transactionFinishedEventArgs);
}
public void UpdateTransactionIndex(int index)
{
var changed = _transaction.UpdateIndex(index);
if (changed == false) { return; }
TransactionIndexChanged?.Invoke(this, new DragAndDropIndexChangedEventArgs(_transaction.CurrentZone, _transaction.CurrentZone, _transaction.Index));
}
/// <summary> internal void UpdateTransactionZone(string identifier)
/// Refreshes the dropzone and all items within. This is neded in case of adding items to the collection or changed values of items {
/// </summary> var oldValue = _transaction.CurrentZone;
public void Refresh() => RefreshRequested?.Invoke(this, EventArgs.Empty); var changed = _transaction.UpdateZone(identifier);
if (changed == false) { return; }
TransactionIndexChanged?.Invoke(this, new DragAndDropIndexChangedEventArgs(_transaction.CurrentZone, oldValue, _transaction.Index));
}
/// <summary>
/// Refreshes the dropzone and all items within. This is neded in case of adding items to the collection or changed values of items
/// </summary>
public void Refresh() => RefreshRequested?.Invoke(this, EventArgs.Empty);
#endregion
} }

@ -11,12 +11,15 @@ namespace Connected.Components;
public partial class DropZone<T> : UIComponent, IDisposable public partial class DropZone<T> : UIComponent, IDisposable
{ {
#region Variables
private bool _containerIsInitialized = false; private bool _containerIsInitialized = false;
private bool _canDrop = false; private bool _canDrop = false;
private bool _dragInProgress = false; private bool _dragInProgress = false;
private bool _disposedValue = false; private bool _disposedValue = false;
private Guid _id = Guid.NewGuid(); private Guid _id = Guid.NewGuid();
private int _dragCounter = 0;
private Dictionary<T, int> _indicies = new(); private Dictionary<T, int> _indicies = new();
[Inject] private IJSRuntime JsRuntime { get; set; } [Inject] private IJSRuntime JsRuntime { get; set; }
@ -24,103 +27,9 @@ public partial class DropZone<T> : UIComponent, IDisposable
[CascadingParameter] [CascadingParameter]
protected DropContainer<T> Container { get; set; } protected DropContainer<T> Container { get; set; }
/// <summary> #endregion
/// Child content of component
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Appearance)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// The unique identifier of this drop zone. It is used within transaction to
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Appearance)]
public string Identifier { get; set; }
/// <summary>
/// The render fragment (template) that should be used to render the items within a drop zone. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public RenderFragment<T> ItemRenderer { get; set; }
/// <summary>
/// The method is used to determinate if an item can be dropped within a drop zone. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public Func<T, bool> ItemsSelector { get; set; }
/// <summary>
/// The method is used to determinate if an item can be dropped within a drop zone. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public Func<T, bool> CanDrop { get; set; }
/// <summary>
/// The CSS class(es), that is applied to drop zones that are a valid target for drag and drop transaction. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string CanDropClass { get; set; }
/// <summary>
/// The CSS class(es), that is applied to drop zones that are NOT valid target for drag and drop transaction. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string NoDropClass { get; set; }
/// <summary>
/// If true, drop classes CanDropClass <see cref="CanDropClass"/> or NoDropClass <see cref="NoDropClass"/> or applied as soon, as a transaction has started. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public bool? ApplyDropClassesOnDragStarted { get; set; }
/// <summary>
/// The method is used to determinate if an item should be disabled for dragging. Defaults to allow all items. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public Func<T, bool> ItemIsDisabled { get; set; }
/// <summary>
/// If a drop item is disabled (determinate by <see cref="ItemIsDisabled"/>). This class is applied to the element. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public string DisabledClass { get; set; }
/// <summary>
/// An additional class that is applied to the drop zone where a drag operation started
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string DraggingClass { get; set; }
/// <summary>
/// An additional class that is applied to an drop item, when it is dragged
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string ItemDraggingClass { get; set; }
[Parameter]
[Category(CategoryTypes.DropZone.Behavior)]
public bool AllowReorder { get; set; }
/// <summary>
/// If true, will only act as a dropable zone and not render any items.
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Behavior)]
public bool OnlyZone { get; set; }
#region view helper
#region Events
private int GetItemIndex(T item) private int GetItemIndex(T item)
{ {
if (_indicies.ContainsKey(item) == false) if (_indicies.ContainsKey(item) == false)
@ -178,52 +87,6 @@ public partial class DropZone<T> : UIComponent, IDisposable
return result; return result;
} }
protected string Classname =>
new CssBuilder("drop-zone")
//.AddClass("drop-zone-drag-block", Container?.TransactionInProgress() == true && Container.GetTransactionOrignZoneIdentiifer() != Identifier)
.AddClass(CanDropClass ?? Container?.CanDropClass, Container?.TransactionInProgress() == true && Container.GetTransactionOrignZoneIdentiifer() != Identifier && _canDrop == true && (_dragCounter > 0 || GetApplyDropClassesOnDragStarted() == true))
.AddClass(NoDropClass ?? Container?.NoDropClass, Container?.TransactionInProgress() == true && Container.GetTransactionOrignZoneIdentiifer() != Identifier && _canDrop == false && (_dragCounter > 0 || GetApplyDropClassesOnDragStarted() == true))
.AddClass(GetDragginClass(), _dragInProgress == true)
.AddClass(AdditionalClassList)
.Build();
protected string PlaceholderClassname =>
new CssBuilder("border-2 mud-border-primary border-dashed mud-chip-text mud-chip-color-primary pa-4 mud-dropitem-placeholder")
.AddClass("d-none", AllowReorder == false || (Container?.TransactionInProgress() == false || Container.GetTransactionCurrentZoneIdentiifer() != Identifier))
.Build();
#endregion
#region helper
private (T, bool) ItemCanBeDropped()
{
if (Container == null || Container.TransactionInProgress() == false)
{
return (default(T), false);
}
var item = Container.GetTransactionItem();
var result = true;
if (CanDrop != null)
{
result = CanDrop(item);
}
else if (Container.CanDrop != null)
{
result = Container.CanDrop(item, Identifier);
}
return (item, result);
}
private bool IsOrign(int index) => Container.IsOrign(index, Identifier);
#endregion
#region container event handling
private void Container_TransactionEnded(object sender, DragAndDropTransactionFinishedEventArgs<T> e) private void Container_TransactionEnded(object sender, DragAndDropTransactionFinishedEventArgs<T> e)
{ {
_dragCounter = 0; _dragCounter = 0;
@ -271,12 +134,6 @@ public partial class DropZone<T> : UIComponent, IDisposable
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
} }
#endregion
#region handling event callbacks
private int _dragCounter = 0;
private void HandleDragEnter() private void HandleDragEnter()
{ {
_dragCounter++; _dragCounter++;
@ -370,9 +227,160 @@ public partial class DropZone<T> : UIComponent, IDisposable
private void FinishedDragOperation() => _dragInProgress = false; private void FinishedDragOperation() => _dragInProgress = false;
private void DragOperationStarted() => _dragInProgress = true; private void DragOperationStarted() => _dragInProgress = true;
private void Container_TransactionIndexChanged(object sender, DragAndDropIndexChangedEventArgs e)
{
if (e.ZoneIdentifier != Identifier && e.OldZoneIdentifier != Identifier) { return; }
StateHasChanged();
}
#endregion
#region Content
/// <summary>
/// Child content of component
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Appearance)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// The unique identifier of this drop zone. It is used within transaction to
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Appearance)]
public string Identifier { get; set; }
/// <summary>
/// The render fragment (template) that should be used to render the items within a drop zone. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public RenderFragment<T> ItemRenderer { get; set; }
/// <summary>
/// The method is used to determinate if an item can be dropped within a drop zone. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Items)]
public Func<T, bool> ItemsSelector { get; set; }
#endregion
#region Styling
/// <summary>
/// The CSS class(es), that is applied to drop zones that are a valid target for drag and drop transaction. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string CanDropClass { get; set; }
/// <summary>
/// The CSS class(es), that is applied to drop zones that are NOT valid target for drag and drop transaction. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public string NoDropClass { get; set; }
/// <summary>
/// The method is used to determinate if an item should be disabled for dragging. Defaults to allow all items. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public Func<T, bool> ItemIsDisabled { get; set; }
/// <summary>
/// If a drop item is disabled (determinate by <see cref="ItemIsDisabled"/>). This class is applied to the element. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)]
public string DisabledClass { get; set; }
/// <summary>
/// An additional class that is applied to the drop zone where a drag operation started
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string DraggingClass { get; set; }
/// <summary>
/// An additional class that is applied to an drop item, when it is dragged
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DraggingClass)]
public string ItemDraggingClass { get; set; }
protected string Classname =>
new CssBuilder("drop-zone")
//.AddClass("drop-zone-drag-block", Container?.TransactionInProgress() == true && Container.GetTransactionOrignZoneIdentiifer() != Identifier)
.AddClass(CanDropClass ?? Container?.CanDropClass, Container?.TransactionInProgress() == true && Container.GetTransactionOrignZoneIdentiifer() != Identifier && _canDrop == true && (_dragCounter > 0 || GetApplyDropClassesOnDragStarted() == true))
.AddClass(NoDropClass ?? Container?.NoDropClass, Container?.TransactionInProgress() == true && Container.GetTransactionOrignZoneIdentiifer() != Identifier && _canDrop == false && (_dragCounter > 0 || GetApplyDropClassesOnDragStarted() == true))
.AddClass(GetDragginClass(), _dragInProgress == true)
.AddClass(AdditionalClassList)
.Build();
protected string PlaceholderClassname =>
new CssBuilder("border-2 mud-border-primary border-dashed mud-chip-text mud-chip-color-primary pa-4 mud-dropitem-placeholder")
.AddClass("d-none", AllowReorder == false || (Container?.TransactionInProgress() == false || Container.GetTransactionCurrentZoneIdentiifer() != Identifier))
.Build();
#endregion #endregion
#region life cycle #region Behavior
/// <summary>
/// The method is used to determinate if an item can be dropped within a drop zone. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public Func<T, bool> CanDrop { get; set; }
/// <summary>
/// If true, drop classes CanDropClass <see cref="CanDropClass"/> or NoDropClass <see cref="NoDropClass"/> or applied as soon, as a transaction has started. Overrides value provided by drop container
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.DropRules)]
public bool? ApplyDropClassesOnDragStarted { get; set; }
[Parameter]
[Category(CategoryTypes.DropZone.Behavior)]
public bool AllowReorder { get; set; }
/// <summary>
/// If true, will only act as a dropable zone and not render any items.
/// </summary>
[Parameter]
[Category(CategoryTypes.DropZone.Behavior)]
public bool OnlyZone { get; set; }
private (T, bool) ItemCanBeDropped()
{
if (Container == null || Container.TransactionInProgress() == false)
{
return (default(T), false);
}
var item = Container.GetTransactionItem();
var result = true;
if (CanDrop != null)
{
result = CanDrop(item);
}
else if (Container.CanDrop != null)
{
result = Container.CanDrop(item, Identifier);
}
return (item, result);
}
private bool IsOrign(int index) => Container.IsOrign(index, Identifier);
#endregion
#region Lifecycle
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
@ -388,12 +396,6 @@ public partial class DropZone<T> : UIComponent, IDisposable
base.OnParametersSet(); base.OnParametersSet();
} }
private void Container_TransactionIndexChanged(object sender, DragAndDropIndexChangedEventArgs e)
{
if (e.ZoneIdentifier != Identifier && e.OldZoneIdentifier != Identifier) { return; }
StateHasChanged();
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {

@ -11,133 +11,143 @@ namespace Connected.Components;
public partial class DynamicDropItem<T> : UIComponent public partial class DynamicDropItem<T> : UIComponent
{ {
private bool _dragOperationIsInProgress = false;
#region Variables
[CascadingParameter] private bool _dragOperationIsInProgress = false;
protected DropContainer<T> Container { get; set; } #endregion
/// <summary> #region Events
/// The zone identifier of the corresponding drop zone /// <summary>
/// </summary> /// An event callback set fires, when a drag operation has been started
[Parameter] /// </summary>
[Category(CategoryTypes.DropZone.Behavior)] [Parameter]
public string ZoneIdentifier { get; set; } [Category(CategoryTypes.DropZone.Behavior)]
public EventCallback<T> OnDragStarted { get; set; }
/// <summary>
/// the data item that is represneted by this item /// <summary>
/// </summary> /// An event callback set fires, when a drag operation has been eneded. This included also a cancelled transaction
[Parameter] /// </summary>
[Category(CategoryTypes.DropZone.Behavior)] [Parameter]
public T Item { get; set; } [Category(CategoryTypes.DropZone.Behavior)]
public EventCallback<T> OnDragEnded { get; set; }
/// <summary>
/// Child content of component private async Task DragStarted()
/// </summary> {
[Parameter] if (Container == null) { return; }
[Category(CategoryTypes.DropZone.Appearance)]
public RenderFragment ChildContent { get; set; } _dragOperationIsInProgress = true;
Container.StartTransaction(Item, ZoneIdentifier ?? string.Empty, Index, OnDroppedSucceeded, OnDroppedCanceled);
/// <summary> await OnDragStarted.InvokeAsync();
/// An additional class that is applied to this element when a drag operation is in progress }
/// </summary>
[Parameter] private async Task OnDroppedSucceeded()
[Category(CategoryTypes.DropZone.DraggingClass)] {
public string DraggingClass { get; set; } _dragOperationIsInProgress = false;
/// <summary> await OnDragEnded.InvokeAsync(Item);
/// An event callback set fires, when a drag operation has been started StateHasChanged();
/// </summary> }
[Parameter]
[Category(CategoryTypes.DropZone.Behavior)] private async Task OnDroppedCanceled()
public EventCallback<T> OnDragStarted { get; set; } {
_dragOperationIsInProgress = false;
/// <summary>
/// An event callback set fires, when a drag operation has been eneded. This included also a cancelled transaction await OnDragEnded.InvokeAsync(Item);
/// </summary> StateHasChanged();
[Parameter] }
[Category(CategoryTypes.DropZone.Behavior)]
public EventCallback<T> OnDragEnded { get; set; } private async Task DragEnded(DragEventArgs e)
{
/// <summary> if (_dragOperationIsInProgress == true)
/// When true, the item can't be dragged. defaults to false {
/// </summary> _dragOperationIsInProgress = false;
[Parameter] await Container?.CancelTransaction();
[Category(CategoryTypes.DropZone.Disabled)] }
public bool Disabled { get; set; } = false; else
{
/// <summary> await OnDragEnded.InvokeAsync(Item);
/// The class that is applied when disabled <see cref="Disabled"/> is set to true }
/// </summary> }
[Parameter]
[Category(CategoryTypes.DropZone.Disabled)] private void HandleDragEnter()
public string DisabledClass { get; set; } {
if (Container == null || Container.TransactionInProgress() == false) { return; }
[Parameter]
[Category(CategoryTypes.DropZone.Sorting)] Container.UpdateTransactionIndex(Index);
public int Index { get; set; } = -1; }
[Parameter] private void HandleDragLeave()
[Category(CategoryTypes.DropZone.Sorting)] {
public bool HideContent { get; set; } }
#region Event handling and callbacks #endregion
private async Task DragStarted() #region Content
{ [CascadingParameter]
if (Container == null) { return; } protected DropContainer<T> Container { get; set; }
_dragOperationIsInProgress = true; /// <summary>
Container.StartTransaction(Item, ZoneIdentifier ?? string.Empty, Index, OnDroppedSucceeded, OnDroppedCanceled); /// The zone identifier of the corresponding drop zone
await OnDragStarted.InvokeAsync(); /// </summary>
} [Parameter]
[Category(CategoryTypes.DropZone.Behavior)]
private async Task OnDroppedSucceeded() public string ZoneIdentifier { get; set; }
{
_dragOperationIsInProgress = false; /// <summary>
/// the data item that is represneted by this item
await OnDragEnded.InvokeAsync(Item); /// </summary>
StateHasChanged(); [Parameter]
} [Category(CategoryTypes.DropZone.Behavior)]
public T Item { get; set; }
private async Task OnDroppedCanceled()
{ /// <summary>
_dragOperationIsInProgress = false; /// Child content of component
/// </summary>
await OnDragEnded.InvokeAsync(Item); [Parameter]
StateHasChanged(); [Category(CategoryTypes.DropZone.Appearance)]
} public RenderFragment ChildContent { get; set; }
private async Task DragEnded(DragEventArgs e) #endregion
{
if (_dragOperationIsInProgress == true) #region Styling
{ /// <summary>
_dragOperationIsInProgress = false; /// An additional class that is applied to this element when a drag operation is in progress
await Container?.CancelTransaction(); /// </summary>
} [Parameter]
else [Category(CategoryTypes.DropZone.DraggingClass)]
{ public string DraggingClass { get; set; }
await OnDragEnded.InvokeAsync(Item);
} /// <summary>
} /// When true, the item can't be dragged. defaults to false
/// </summary>
private void HandleDragEnter() [Parameter]
{ [Category(CategoryTypes.DropZone.Disabled)]
if (Container == null || Container.TransactionInProgress() == false) { return; } public bool Disabled { get; set; } = false;
Container.UpdateTransactionIndex(Index); /// <summary>
} /// The class that is applied when disabled <see cref="Disabled"/> is set to true
/// </summary>
private void HandleDragLeave() [Parameter]
{ [Category(CategoryTypes.DropZone.Disabled)]
} public string DisabledClass { get; set; }
#endregion [Parameter]
[Category(CategoryTypes.DropZone.Sorting)]
protected string Classname => public int Index { get; set; } = -1;
new CssBuilder("drop-item") protected string Classname =>
.AddClass(DraggingClass, _dragOperationIsInProgress == true) new CssBuilder("drop-item")
.AddClass(DisabledClass, Disabled == true) .AddClass(DraggingClass, _dragOperationIsInProgress == true)
.AddClass(AdditionalClassList) .AddClass(DisabledClass, Disabled == true)
.Build(); .AddClass(AdditionalClassList)
.Build();
#endregion
#region Behavior
[Parameter]
[Category(CategoryTypes.DropZone.Sorting)]
public bool HideContent { get; set; }
#endregion
} }

@ -15,7 +15,7 @@
</div> </div>
@if (!HideIcon) @if (!HideIcon)
{ {
<Icon Icon="@Icon" class="@(IsExpanded? "expand-panel-icon mud-transform" : "expand-panel-icon")" /> <Icon Glyph="@Icon" class="@(IsExpanded? "expand-panel-icon mud-transform" : "expand-panel-icon")" />
} }
</div> </div>
<Collapse Expanded="@_collapseIsExpanded" MaxHeight="@MaxHeight"> <Collapse Expanded="@_collapseIsExpanded" MaxHeight="@MaxHeight">

@ -6,12 +6,84 @@ namespace Connected.Components;
public partial class ExpansionPanel : UIComponent, IDisposable public partial class ExpansionPanel : UIComponent, IDisposable
{ {
#region Variables
private bool _nextPanelExpanded; private bool _nextPanelExpanded;
private bool _isExpanded; private bool _isExpanded;
private bool _collapseIsExpanded; private bool _collapseIsExpanded;
#endregion
#region Events
public void ToggleExpansion()
{
if (Disabled)
{
return;
}
IsExpanded = !IsExpanded;
}
public void Expand(bool update_parent = true)
{
if (update_parent)
IsExpanded = true;
else
{
_isExpanded = true;
_collapseIsExpanded = true;
IsExpandedChanged.InvokeAsync(_isExpanded);
}
}
public void Collapse(bool update_parent = true)
{
if (update_parent)
IsExpanded = false;
else
{
_isExpanded = false;
_collapseIsExpanded = false;
IsExpandedChanged.InvokeAsync(_isExpanded);
}
}
/// <summary>
/// Raised when IsExpanded changes.
/// </summary>
[Parameter] public EventCallback<bool> IsExpandedChanged { get; set; }
internal event Action<ExpansionPanel> NotifyIsExpandedChanged;
#endregion
#region Content
[CascadingParameter] private ExpansionPanels Parent { get; set; } [CascadingParameter] private ExpansionPanels Parent { get; set; }
/// <summary>
/// RenderFragment to be displayed in the expansion panel which will override header text if defined.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public RenderFragment TitleContent { get; set; }
/// <summary>
/// The text to be displayed in the expansion panel.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public string Text { get; set; }
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public RenderFragment ChildContent { get; set; }
#endregion
#region Styling
protected string Classname => protected string Classname =>
new CssBuilder("expand-panel") new CssBuilder("expand-panel")
.AddClass("panel-expanded", IsExpanded) .AddClass("panel-expanded", IsExpanded)
@ -35,20 +107,6 @@ public partial class ExpansionPanel : UIComponent, IDisposable
[Category(CategoryTypes.ExpansionPanel.Appearance)] [Category(CategoryTypes.ExpansionPanel.Appearance)]
public int? MaxHeight { get; set; } public int? MaxHeight { get; set; }
/// <summary>
/// RenderFragment to be displayed in the expansion panel which will override header text if defined.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public RenderFragment TitleContent { get; set; }
/// <summary>
/// The text to be displayed in the expansion panel.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public string Text { get; set; }
/// <summary> /// <summary>
/// If true, expand icon will not show /// If true, expand icon will not show
/// </summary> /// </summary>
@ -77,12 +135,6 @@ public partial class ExpansionPanel : UIComponent, IDisposable
[Category(CategoryTypes.ExpansionPanel.Appearance)] [Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool DisableGutters { get; set; } public bool DisableGutters { get; set; }
/// <summary>
/// Raised when IsExpanded changes.
/// </summary>
[Parameter] public EventCallback<bool> IsExpandedChanged { get; set; }
internal event Action<ExpansionPanel> NotifyIsExpandedChanged;
/// <summary> /// <summary>
/// Expansion state of the panel (two-way bindable) /// Expansion state of the panel (two-way bindable)
/// </summary> /// </summary>
@ -109,14 +161,6 @@ public partial class ExpansionPanel : UIComponent, IDisposable
} }
} }
/// <summary>
/// Sets the initial expansion state. Do not use in combination with IsExpanded.
/// Combine with MultiExpansion to have more than one panel start open.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public bool IsInitiallyExpanded { get; set; }
/// <summary> /// <summary>
/// If true, the component will be disabled. /// If true, the component will be disabled.
/// </summary> /// </summary>
@ -124,12 +168,16 @@ public partial class ExpansionPanel : UIComponent, IDisposable
[Category(CategoryTypes.ExpansionPanel.Behavior)] [Category(CategoryTypes.ExpansionPanel.Behavior)]
public bool Disabled { get; set; } public bool Disabled { get; set; }
#endregion
#region Behavior
/// <summary> /// <summary>
/// Child content of component. /// Sets the initial expansion state. Do not use in combination with IsExpanded.
/// Combine with MultiExpansion to have more than one panel start open.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)] [Category(CategoryTypes.ExpansionPanel.Behavior)]
public RenderFragment ChildContent { get; set; } public bool IsInitiallyExpanded { get; set; }
public bool NextPanelExpanded public bool NextPanelExpanded
{ {
@ -143,39 +191,9 @@ public partial class ExpansionPanel : UIComponent, IDisposable
} }
} }
public void ToggleExpansion() #endregion
{
if (Disabled)
{
return;
}
IsExpanded = !IsExpanded; #region Lifecycle
}
public void Expand(bool update_parent = true)
{
if (update_parent)
IsExpanded = true;
else
{
_isExpanded = true;
_collapseIsExpanded = true;
IsExpandedChanged.InvokeAsync(_isExpanded);
}
}
public void Collapse(bool update_parent = true)
{
if (update_parent)
IsExpanded = false;
else
{
_isExpanded = false;
_collapseIsExpanded = false;
IsExpandedChanged.InvokeAsync(_isExpanded);
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -196,4 +214,6 @@ public partial class ExpansionPanel : UIComponent, IDisposable
{ {
Parent?.RemovePanel(this); Parent?.RemovePanel(this);
} }
#endregion
} }

@ -7,63 +7,8 @@ namespace Connected.Components;
public partial class ExpansionPanels : UIComponent public partial class ExpansionPanels : UIComponent
{ {
protected string Classname =>
new CssBuilder("expansion-panels")
.AddClass($"expansion-panels-square", Square)
.AddClass(AdditionalClassList)
.Build();
/// <summary>
/// If true, border-radius is set to 0.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool Square { get; set; }
/// <summary>
/// If true, multiple panels can be expanded at the same time.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public bool MultiExpansion { get; set; }
/// <summary>
/// The higher the number, the heavier the drop-shadow. 0 for no shadow.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public int Elevation { set; get; } = 1;
/// <summary>
/// If true, removes vertical padding from all panels' childcontent.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool Dense { get; set; }
/// <summary>
/// If true, the left and right padding is removed from all panels' childcontent.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool DisableGutters { get; set; }
/// <summary>
/// If true, the borders around each panel will be removed.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool DisableBorders { get; set; }
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public RenderFragment ChildContent { get; set; }
private List<ExpansionPanel> _panels = new();
#region Events
internal void AddPanel(ExpansionPanel panel) internal void AddPanel(ExpansionPanel panel)
{ {
if (MultiExpansion == false && _panels.Any(p => p.IsExpanded)) if (MultiExpansion == false && _panels.Any(p => p.IsExpanded))
@ -109,13 +54,6 @@ public partial class ExpansionPanels : UIComponent
StateHasChanged(); StateHasChanged();
} }
[Obsolete("Use CollapseAllExcept instead.")]
[ExcludeFromCodeCoverage]
public void CloseAllExcept(ExpansionPanel panel)
{
CollapseAllExcept(panel);
}
/// <summary> /// <summary>
/// Collapses all panels except the given one. /// Collapses all panels except the given one.
/// </summary> /// </summary>
@ -154,4 +92,69 @@ public partial class ExpansionPanels : UIComponent
} }
this.InvokeAsync(UpdateAll); this.InvokeAsync(UpdateAll);
} }
#endregion
#region Content
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public RenderFragment ChildContent { get; set; }
private List<ExpansionPanel> _panels = new();
#endregion
#region Styling
protected string Classname =>
new CssBuilder("expansion-panels")
.AddClass($"expansion-panels-square", Square)
.AddClass(AdditionalClassList)
.Build();
/// <summary>
/// If true, border-radius is set to 0.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool Square { get; set; }
/// <summary>
/// The higher the number, the heavier the drop-shadow. 0 for no shadow.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public int Elevation { set; get; } = 1;
/// <summary>
/// If true, removes vertical padding from all panels' childcontent.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool Dense { get; set; }
/// <summary>
/// If true, the left and right padding is removed from all panels' childcontent.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool DisableGutters { get; set; }
/// <summary>
/// If true, the borders around each panel will be removed.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Appearance)]
public bool DisableBorders { get; set; }
#endregion
#region Behavior
/// <summary>
/// If true, multiple panels can be expanded at the same time.
/// </summary>
[Parameter]
[Category(CategoryTypes.ExpansionPanel.Behavior)]
public bool MultiExpansion { get; set; }
#endregion
} }

@ -9,152 +9,163 @@ namespace Connected.Components;
//TODO Maybe can inherit from MudBaseInput? //TODO Maybe can inherit from MudBaseInput?
public partial class Field : UIComponent public partial class Field : UIComponent
{ {
protected string Classname => #region Events
new CssBuilder("input") /// <summary>
.AddClass($"input-{Variant.ToDescription()}") /// Button click event if set and Adornment used.
.AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) /// </summary>
.AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) [Parameter] public EventCallback<MouseEventArgs> OnAdornmentClick { get; set; }
.AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined) #endregion
.AddClass("shrink", when: () => !string.IsNullOrWhiteSpace(ChildContent?.ToString()) || Adornment == Adornment.Start)
.AddClass("disabled", Disabled) #region Content
.AddClass("input-error", Error && !string.IsNullOrEmpty(ErrorText)) /// <summary>
.Build(); /// Child content of component.
/// </summary>
protected string InnerClassname => [Parameter]
new CssBuilder("input-slot") [Category(CategoryTypes.Field.Data)]
.AddClass("input-root") public RenderFragment ChildContent { get; set; }
.AddClass("input-slot-nopadding", when: () => InnerPadding == false)
.AddClass($"input-root-{Variant.ToDescription()}") /// <summary>
.AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) /// The ErrorText that will be displayed if Error true
.AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) /// </summary>
.Build(); [Parameter]
[Category(CategoryTypes.Field.Validation)]
protected string AdornmentClassname => public string ErrorText { get; set; }
new CssBuilder("input-adornment")
.AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None) /// <summary>
.AddClass($"text", !string.IsNullOrEmpty(AdornmentText)) /// The HelperText will be displayed below the text field.
.AddClass($"input-root-filled-shrink", Variant == Variant.Filled) /// </summary>
.Build(); [Parameter]
[Category(CategoryTypes.Field.Behavior)]
protected string InputControlClassname => public string HelperText { get; set; }
new CssBuilder("field")
.AddClass(AdditionalClassList) /// <summary>
.Build(); /// If string has value the label text will be displayed in the input, and scaled down at the top if the field has value.
/// </summary>
/// <summary> [Parameter]
/// Child content of component. [Category(CategoryTypes.Field.Behavior)]
/// </summary> public string Label { get; set; }
[Parameter]
[Category(CategoryTypes.Field.Data)] /// <summary>
public RenderFragment ChildContent { get; set; } /// Text that will be used if Adornment is set to Start or End, the Text overrides Glyph.
/// </summary>
/// <summary> [Parameter]
/// Will adjust vertical spacing. [Category(CategoryTypes.Field.Behavior)]
/// </summary> public string AdornmentText { get; set; }
[Parameter]
[Category(CategoryTypes.Field.Appearance)]
public Margin Margin { get; set; } = Margin.None; #endregion
/// <summary> #region Styling
/// If true, the label will be displayed in an error state. protected string Classname =>
/// </summary> new CssBuilder("input")
[Parameter] .AddClass($"input-{Variant.ToDescription()}")
[Category(CategoryTypes.Field.Validation)] .AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
public bool Error { get; set; } .AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
.AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
/// <summary> .AddClass("shrink", when: () => !string.IsNullOrWhiteSpace(ChildContent?.ToString()) || Adornment == Adornment.Start)
/// The ErrorText that will be displayed if Error true .AddClass("disabled", Disabled)
/// </summary> .AddClass("input-error", Error && !string.IsNullOrEmpty(ErrorText))
[Parameter] .Build();
[Category(CategoryTypes.Field.Validation)]
public string ErrorText { get; set; } protected string InnerClassname =>
new CssBuilder("input-slot")
/// <summary> .AddClass("input-root")
/// The HelperText will be displayed below the text field. .AddClass("input-slot-nopadding", when: () => InnerPadding == false)
/// </summary> .AddClass($"input-root-{Variant.ToDescription()}")
[Parameter] .AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
[Category(CategoryTypes.Field.Behavior)] .AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
public string HelperText { get; set; } .Build();
/// <summary> protected string AdornmentClassname =>
/// If true, the field will take up the full width of its container. new CssBuilder("input-adornment")
/// </summary> .AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None)
[Parameter] .AddClass($"text", !string.IsNullOrEmpty(AdornmentText))
[Category(CategoryTypes.Field.Appearance)] .AddClass($"input-root-filled-shrink", Variant == Variant.Filled)
public bool FullWidth { get; set; } .Build();
/// <summary> protected string InputControlClassname =>
/// If string has value the label text will be displayed in the input, and scaled down at the top if the field has value. new CssBuilder("field")
/// </summary> .AddClass(AdditionalClassList)
[Parameter] .Build();
[Category(CategoryTypes.Field.Behavior)]
public string Label { get; set; }
/// <summary> /// <summary>
/// Variant can be Text, Filled or Outlined. /// Will adjust vertical spacing.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Field.Appearance)] [Category(CategoryTypes.Field.Appearance)]
public Variant Variant { get; set; } = Variant.Text; public Margin Margin { get; set; } = Margin.None;
/// <summary> /// <summary>
/// If true, the input element will be disabled. /// If true, the label will be displayed in an error state.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Field.Behavior)] [Category(CategoryTypes.Field.Validation)]
public bool Disabled { get; set; } public bool Error { get; set; }
/// <summary> /// <summary>
/// Glyph that will be used if Adornment is set to Start or End. /// If true, the field will take up the full width of its container.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Field.Behavior)] [Category(CategoryTypes.Field.Appearance)]
public string AdornmentIcon { get; set; } public bool FullWidth { get; set; }
/// <summary> /// <summary>
/// Text that will be used if Adornment is set to Start or End, the Text overrides Glyph. /// Variant can be Text, Filled or Outlined.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Field.Behavior)] [Category(CategoryTypes.Field.Appearance)]
public string AdornmentText { get; set; } public Variant Variant { get; set; } = Variant.Text;
/// <summary> /// <summary>
/// The Adornment if used. By default, it is set to None. /// If true, the input element will be disabled.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Field.Behavior)] [Category(CategoryTypes.Field.Behavior)]
public Adornment Adornment { get; set; } = Adornment.None; public bool Disabled { get; set; }
/// <summary> /// <summary>
/// The color of the adornment if used. It supports the theme colors. /// Glyph that will be used if Adornment is set to Start or End.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.FormComponent.Appearance)] [Category(CategoryTypes.Field.Behavior)]
public ThemeColor AdornmentColor { get; set; } = ThemeColor.Default; public string AdornmentIcon { get; set; }
/// <summary> /// <summary>
/// Sets the Glyph Size. /// The Adornment if used. By default, it is set to None.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Field.Appearance)] [Category(CategoryTypes.Field.Behavior)]
public Size IconSize { get; set; } = Size.Medium; public Adornment Adornment { get; set; } = Adornment.None;
/// <summary> /// <summary>
/// Button click event if set and Adornment used. /// The color of the adornment if used. It supports the theme colors.
/// </summary> /// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnAdornmentClick { get; set; } [Parameter]
[Category(CategoryTypes.FormComponent.Appearance)]
/// <summary> public ThemeColor AdornmentColor { get; set; } = ThemeColor.Default;
/// If true, the inner contents padding is removed.
/// </summary> /// <summary>
[Parameter] /// Sets the Glyph Size.
[Category(CategoryTypes.Field.Appearance)] /// </summary>
public bool InnerPadding { get; set; } = true; [Parameter]
[Category(CategoryTypes.Field.Appearance)]
/// <summary> public Size IconSize { get; set; } = Size.Medium;
/// If true, the field will not have an underline.
/// </summary> /// <summary>
[Parameter] /// If true, the inner contents padding is removed.
[Category(CategoryTypes.Field.Appearance)] /// </summary>
public bool DisableUnderLine { get; set; } [Parameter]
[Category(CategoryTypes.Field.Appearance)]
public bool InnerPadding { get; set; } = true;
/// <summary>
/// If true, the field will not have an underline.
/// </summary>
[Parameter]
[Category(CategoryTypes.Field.Appearance)]
public bool DisableUnderLine { get; set; }
#endregion
} }

@ -12,112 +12,132 @@ namespace Connected.Components;
public partial class FileUpload<T> : FormComponent<T, string> public partial class FileUpload<T> : FormComponent<T, string>
{ {
public FileUpload() : base(new DefaultConverter<T>()) { } #region Variables
private readonly string _id = $"mud_fileupload_{Guid.NewGuid()}";
private readonly string _id = $"mud_fileupload_{Guid.NewGuid()}"; #endregion
protected string Classname => #region Events
new CssBuilder("file-upload") /// <summary>
.AddClass(AdditionalClassList) /// Triggered when the internal OnChange event fires
.Build(); /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.FileUpload.Behavior)]
/// The value of the MudFileUpload component. public EventCallback<T> FilesChanged { get; set; }
/// If T is <see cref="IBrowserFile">IBrowserFile</see>, it represents a single file.
/// If T is <see cref="IReadOnlyCollection{IBrowserFile}">IReadOnlyList&lt;IBrowserFile&gt;</see>, it represents multiple files /// <summary>
/// </summary> /// Called when the internal files are changed
[Parameter] /// </summary>
[Category(CategoryTypes.FileUpload.Behavior)] [Parameter]
public T Files [Category(CategoryTypes.FileUpload.Behavior)]
{ public EventCallback<InputFileChangeEventArgs> OnFilesChanged { get; set; }
get => _value;
set private async Task OnChange(InputFileChangeEventArgs args)
{ {
if (_value != null && _value.Equals(value)) if (typeof(T) == typeof(IReadOnlyList<IBrowserFile>))
return; {
_value = value; _value = (T)args.GetMultipleFiles();
} }
} else if (typeof(T) == typeof(IBrowserFile))
/// <summary> {
/// Triggered when the internal OnChange event fires _value = (T)args.File;
/// </summary> }
[Parameter] else return;
[Category(CategoryTypes.FileUpload.Behavior)]
public EventCallback<T> FilesChanged { get; set; } await FilesChanged.InvokeAsync(_value);
BeginValidate();
/// <summary> FieldChanged(_value);
/// Called when the internal files are changed if (!HasError || !SuppressOnChangeWhenInvalid) //only trigger FilesChanged if validation passes or SuppressOnChangeWhenInvalid is false
/// </summary> await OnFilesChanged.InvokeAsync(args);
[Parameter] }
[Category(CategoryTypes.FileUpload.Behavior)] #endregion
public EventCallback<InputFileChangeEventArgs> OnFilesChanged { get; set; }
/// <summary> #region Content
/// Renders the button that triggers the input. Required for functioning. /// <summary>
/// </summary> /// The value of the MudFileUpload component.
[Parameter] /// If T is <see cref="IBrowserFile">IBrowserFile</see>, it represents a single file.
[Category(CategoryTypes.FileUpload.Appearance)] /// If T is <see cref="IReadOnlyCollection{IBrowserFile}">IReadOnlyList&lt;IBrowserFile&gt;</see>, it represents multiple files
public RenderFragment<string> ButtonTemplate { get; set; } /// </summary>
/// <summary> [Parameter]
/// Renders the selected files, if desired. [Category(CategoryTypes.FileUpload.Behavior)]
/// </summary> public T Files
[Parameter] {
[Category(CategoryTypes.FileUpload.Appearance)] get => _value;
public RenderFragment<T> SelectedTemplate { get; set; } set
/// <summary> {
/// If true, OnFilesChanged will not trigger if validation fails if (_value != null && _value.Equals(value))
/// </summary> return;
[Parameter] _value = value;
[Category(CategoryTypes.FileUpload.Behavior)] }
public bool SuppressOnChangeWhenInvalid { get; set; } }
/// <summary> #endregion
/// Sets the file types this input will accept
/// </summary> #region Styling
[Parameter] protected string Classname =>
[Category(CategoryTypes.FileUpload.Behavior)] new CssBuilder("file-upload")
public string Accept { get; set; } .AddClass(AdditionalClassList)
/// <summary> .Build();
/// If false, the inner FileInput will be visible
/// </summary>
[Parameter]
[Category(CategoryTypes.FileUpload.Appearance)] /// <summary>
public bool Hidden { get; set; } = true; /// Renders the button that triggers the input. Required for functioning.
/// <summary> /// </summary>
/// Css classes to apply to the internal InputFile [Parameter]
/// </summary> [Category(CategoryTypes.FileUpload.Appearance)]
[Parameter] public RenderFragment<string> ButtonTemplate { get; set; }
[Category(CategoryTypes.FileUpload.Appearance)] /// <summary>
public string InputClass { get; set; } /// Renders the selected files, if desired.
/// <summary> /// </summary>
/// Style to apply to the internal InputFile [Parameter]
/// </summary> [Category(CategoryTypes.FileUpload.Appearance)]
[Parameter] public RenderFragment<T> SelectedTemplate { get; set; }
[Category(CategoryTypes.FileUpload.Appearance)]
public string InputStyle { get; set; } /// <summary>
/// If false, the inner FileInput will be visible
private async Task OnChange(InputFileChangeEventArgs args) /// </summary>
{ [Parameter]
if (typeof(T) == typeof(IReadOnlyList<IBrowserFile>)) [Category(CategoryTypes.FileUpload.Appearance)]
{ public bool Hidden { get; set; } = true;
_value = (T)args.GetMultipleFiles(); /// <summary>
} /// Css classes to apply to the internal InputFile
else if (typeof(T) == typeof(IBrowserFile)) /// </summary>
{ [Parameter]
_value = (T)args.File; [Category(CategoryTypes.FileUpload.Appearance)]
} public string InputClass { get; set; }
else return; /// <summary>
/// Style to apply to the internal InputFile
await FilesChanged.InvokeAsync(_value); /// </summary>
BeginValidate(); [Parameter]
FieldChanged(_value); [Category(CategoryTypes.FileUpload.Appearance)]
if (!HasError || !SuppressOnChangeWhenInvalid) //only trigger FilesChanged if validation passes or SuppressOnChangeWhenInvalid is false public string InputStyle { get; set; }
await OnFilesChanged.InvokeAsync(args); #endregion
}
#region Behavior
protected override void OnInitialized() /// <summary>
{ /// If true, OnFilesChanged will not trigger if validation fails
//if (!(typeof(T) == typeof(IReadOnlyList<IBrowserFile>) || typeof(T) == typeof(IBrowserFile))) /// </summary>
// Logger.LogWarning("T must be of type {type1} or {type2}", typeof(IReadOnlyList<IBrowserFile>), typeof(IBrowserFile)); [Parameter]
[Category(CategoryTypes.FileUpload.Behavior)]
base.OnInitialized(); public bool SuppressOnChangeWhenInvalid { get; set; }
} /// <summary>
/// Sets the file types this input will accept
/// </summary>
[Parameter]
[Category(CategoryTypes.FileUpload.Behavior)]
public string Accept { get; set; }
#endregion
#region Lifecycle
public FileUpload() : base(new DefaultConverter<T>()) { }
protected override void OnInitialized()
{
//if (!(typeof(T) == typeof(IReadOnlyList<IBrowserFile>) || typeof(T) == typeof(IBrowserFile)))
// Logger.LogWarning("T must be of type {type1} or {type2}", typeof(IReadOnlyList<IBrowserFile>), typeof(IBrowserFile));
base.OnInitialized();
}
#endregion
} }

@ -7,11 +7,7 @@ namespace Connected.Components;
public partial class FocusTrap : IDisposable public partial class FocusTrap : IDisposable
{ {
protected string Classname => #region Variables
new CssBuilder("outline-none")
.AddClass(AdditionalClassList)
.Build();
protected ElementReference _firstBumper; protected ElementReference _firstBumper;
protected ElementReference _lastBumper; protected ElementReference _lastBumper;
protected ElementReference _fallback; protected ElementReference _fallback;
@ -21,50 +17,10 @@ public partial class FocusTrap : IDisposable
private bool _disabled; private bool _disabled;
private bool _initialized; private bool _initialized;
/// <summary> bool _shouldRender = true;
/// Child content of the component. #endregion
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// If true, the focus will no longer loop inside the component.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public bool Disabled
{
get => _disabled;
set
{
if (_disabled != value)
{
_disabled = value;
_initialized = false;
}
}
}
/// <summary>
/// Defines on which element to set the focus when the component is created or enabled.
/// When DefaultFocus.Element is used, the focus will be set to the FocusTrap itself, so the user will have to press TAB key once to focus the first tabbable element.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public DefaultFocus DefaultFocus { get; set; } = DefaultFocus.FirstChild;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
await SaveFocusAsync();
if (!_initialized)
await InitializeFocusAsync();
}
#region Events
private Task OnBottomFocusAsync(FocusEventArgs args) private Task OnBottomFocusAsync(FocusEventArgs args)
{ {
return FocusLastAsync(); return FocusLastAsync();
@ -142,9 +98,50 @@ public partial class FocusTrap : IDisposable
{ {
return _root.SaveFocusAsync().AsTask(); return _root.SaveFocusAsync().AsTask();
} }
#endregion
bool _shouldRender = true; #region Content
/// <summary>
/// Child content of the component.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public RenderFragment ChildContent { get; set; }
#endregion
#region Styling
protected string Classname =>
new CssBuilder("outline-none")
.AddClass(AdditionalClassList)
.Build();
/// <summary>
/// Defines on which element to set the focus when the component is created or enabled.
/// When DefaultFocus.Element is used, the focus will be set to the FocusTrap itself, so the user will have to press TAB key once to focus the first tabbable element.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public DefaultFocus DefaultFocus { get; set; } = DefaultFocus.FirstChild;
#endregion
#region Behavior
/// <summary>
/// If true, the focus will no longer loop inside the component.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public bool Disabled
{
get => _disabled;
set
{
if (_disabled != value)
{
_disabled = value;
_initialized = false;
}
}
}
protected override bool ShouldRender() protected override bool ShouldRender()
{ {
if (_shouldRender) if (_shouldRender)
@ -153,9 +150,25 @@ public partial class FocusTrap : IDisposable
return false; return false;
} }
#endregion
#region Lifecycle
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
await SaveFocusAsync();
if (!_initialized)
await InitializeFocusAsync();
}
public void Dispose() public void Dispose()
{ {
if (!_disabled) if (!_disabled)
RestoreFocusAsync().AndForget(TaskOption.Safe); RestoreFocusAsync().AndForget(TaskOption.Safe);
} }
#endregion
} }

@ -6,37 +6,23 @@ namespace Connected.Components;
public partial class Form : UIComponent, IDisposable, IForm public partial class Form : UIComponent, IDisposable, IForm
{ {
protected string Classname => #region Variables
new CssBuilder("form")
.AddClass(AdditionalClassList)
.Build();
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidatedData)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// Validation status. True if the form is valid and without errors. This parameter is two-way bindable.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidationResult)]
public bool IsValid
{
get => _valid && ChildForms.All(x => x.IsValid);
set
{
_valid = value;
}
}
// Note: w/o any children the form is automatically valid. // Note: w/o any children the form is automatically valid.
// It stays valid, as long as non-required fields are added or // It stays valid, as long as non-required fields are added or
// a required field is added or the user touches a field that fails validation. // a required field is added or the user touches a field that fails validation.
private bool _valid = true; private bool _valid = true;
private bool _touched = false;
protected HashSet<IFormComponent> _formControls = new();
protected HashSet<string> _errors = new();
private Timer _timer;
private bool _shouldRender = true; // <-- default is true, we need the form children to render
#endregion
#region Events
private void SetIsValid(bool value) private void SetIsValid(bool value)
{ {
if (IsValid == value) if (IsValid == value)
@ -45,47 +31,6 @@ public partial class Form : UIComponent, IDisposable, IForm
IsValidChanged.InvokeAsync(IsValid).AndForget(); IsValidChanged.InvokeAsync(IsValid).AndForget();
} }
// Note: w/o any children the form is automatically valid.
// It stays valid, as long as non-required fields are added or
// a required field is added or the user touches a field that fails validation.
/// <summary>
/// True if any field of the field was touched. This parameter is readonly.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public bool IsTouched { get => _touched; set {/* readonly parameter! */ } }
private bool _touched = false;
/// <summary>
/// Validation debounce delay in milliseconds. This can help improve rendering performance of forms with real-time validation of inputs
/// i.e. when textfields have ChangeTextImmediately="true".
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public int ValidationDelay { get; set; } = 300;
/// <summary>
/// When true, the form will not re-render its child contents on validation updates (i.e. when IsValid changes).
/// This is an optimization which can be necessary especially for larger forms on older devices.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public bool SuppressRenderingOnValidation { get; set; } = false;
/// <summary>
/// When true, will not cause a page refresh on Enter if any input has focus.
/// </summary>
/// <remarks>
/// https://www.w3.org/TR/2018/SPSD-html5-20180327/forms.html#implicit-submission
/// Usually this is not wanted, as it can cause a page refresh in the middle of editing a form.
/// When the form is in a dialog this will cause the dialog to close. So by default we suppress it.
/// </remarks>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public bool SuppressImplicitSubmission { get; set; } = true;
/// <summary> /// <summary>
/// Raised when IsValid changes. /// Raised when IsValid changes.
/// </summary> /// </summary>
@ -101,60 +46,8 @@ public partial class Form : UIComponent, IDisposable, IForm
/// </summary> /// </summary>
[Parameter] public EventCallback<FormFieldChangedEventArgs> FieldChanged { get; set; } [Parameter] public EventCallback<FormFieldChangedEventArgs> FieldChanged { get; set; }
// keeps track of validation. if the input was validated at least once the value will be true
protected HashSet<IFormComponent> _formControls = new();
protected HashSet<string> _errors = new();
/// <summary>
/// A default validation func or a validation attribute to use for form controls that don't have one.
/// Supported types are:
/// <para>Func&lt;T, bool&gt; ... will output the standard error message "Invalid" if false</para>
/// <para>Func&lt;T, string&gt; ... outputs the result as error message, no error if null </para>
/// <para>Func&lt;T, IEnumerable&lt; string &gt;&gt; ... outputs all the returned error messages, no error if empty</para>
/// <para>Func&lt;object, string, IEnumerable&lt; string &gt;&gt; input Form.Model, Full Path of Member ... outputs all the returned error messages, no error if empty</para>
/// <para>Func&lt;T, Task&lt; bool &gt;&gt; ... will output the standard error message "Invalid" if false</para>
/// <para>Func&lt;T, Task&lt; string &gt;&gt; ... outputs the result as error message, no error if null</para>
/// <para>Func&lt;T, Task&lt;IEnumerable&lt; string &gt;&gt;&gt; ... outputs all the returned error messages, no error if empty</para>
/// <para>Func&lt;object, string, Task&lt;IEnumerable&lt; string &gt;&gt;&gt; input Form.Model, Full Path of Member ... outputs all the returned error messages, no error if empty</para>
/// <para>System.ComponentModel.DataAnnotations.ValidationAttribute instances</para>
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public object Validation { get; set; }
/// <summary>
/// If a field already has a validation, override it with <see cref="Validation"/>.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public bool? OverrideFieldValidation { get; set; }
/// <summary>
/// Validation error messages.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidationResult)]
public string[] Errors
{
get => _errors.ToArray();
set { /* readonly */ }
}
[Parameter] public EventCallback<string[]> ErrorsChanged { get; set; } [Parameter] public EventCallback<string[]> ErrorsChanged { get; set; }
/// <summary>
/// Specifies the top-level model object for the form. Used with Fluent Validation
/// </summary>
#nullable enable
[Parameter]
[Category(CategoryTypes.Form.ValidatedData)]
public object? Model { get; set; }
#nullable disable
private HashSet<Form> ChildForms { get; set; } = new HashSet<Form>();
[CascadingParameter] private Form ParentForm { get; set; }
void IForm.FieldChanged(IFormComponent formControl, object newValue) void IForm.FieldChanged(IFormComponent formControl, object newValue)
{ {
FieldChanged.InvokeAsync(new FormFieldChangedEventArgs { Field = formControl, NewValue = newValue }).AndForget(); FieldChanged.InvokeAsync(new FormFieldChangedEventArgs { Field = formControl, NewValue = newValue }).AndForget();
@ -173,8 +66,6 @@ public partial class Form : UIComponent, IDisposable, IForm
_formControls.Remove(formControl); _formControls.Remove(formControl);
} }
private Timer _timer;
/// <summary> /// <summary>
/// Called by any input of the form to signal that its value changed. /// Called by any input of the form to signal that its value changed.
/// </summary> /// </summary>
@ -195,8 +86,6 @@ public partial class Form : UIComponent, IDisposable, IForm
private void OnTimerComplete(object stateInfo) => InvokeAsync(OnEvaluateForm); private void OnTimerComplete(object stateInfo) => InvokeAsync(OnEvaluateForm);
private bool _shouldRender = true; // <-- default is true, we need the form children to render
protected async Task OnEvaluateForm() protected async Task OnEvaluateForm()
{ {
_errors.Clear(); _errors.Clear();
@ -282,7 +171,143 @@ public partial class Form : UIComponent, IDisposable, IForm
EvaluateForm(debounce: false); EvaluateForm(debounce: false);
} }
private void SetDefaultControlValidation(IFormComponent formComponent)
{
if (Validation == null) return;
if (!formComponent.IsForNull && (formComponent.Validation == null || (OverrideFieldValidation ?? true)))
{
formComponent.Validation = Validation;
}
}
#endregion
#region Content
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidatedData)]
public RenderFragment ChildContent { get; set; }
private HashSet<Form> ChildForms { get; set; } = new HashSet<Form>();
[CascadingParameter] private Form ParentForm { get; set; }
// keeps track of validation. if the input was validated at least once the value will be true
/// <summary>
/// A default validation func or a validation attribute to use for form controls that don't have one.
/// Supported types are:
/// <para>Func&lt;T, bool&gt; ... will output the standard error message "Invalid" if false</para>
/// <para>Func&lt;T, string&gt; ... outputs the result as error message, no error if null </para>
/// <para>Func&lt;T, IEnumerable&lt; string &gt;&gt; ... outputs all the returned error messages, no error if empty</para>
/// <para>Func&lt;object, string, IEnumerable&lt; string &gt;&gt; input Form.Model, Full Path of Member ... outputs all the returned error messages, no error if empty</para>
/// <para>Func&lt;T, Task&lt; bool &gt;&gt; ... will output the standard error message "Invalid" if false</para>
/// <para>Func&lt;T, Task&lt; string &gt;&gt; ... outputs the result as error message, no error if null</para>
/// <para>Func&lt;T, Task&lt;IEnumerable&lt; string &gt;&gt;&gt; ... outputs all the returned error messages, no error if empty</para>
/// <para>Func&lt;object, string, Task&lt;IEnumerable&lt; string &gt;&gt;&gt; input Form.Model, Full Path of Member ... outputs all the returned error messages, no error if empty</para>
/// <para>System.ComponentModel.DataAnnotations.ValidationAttribute instances</para>
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public object Validation { get; set; }
/// <summary>
/// Validation error messages.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidationResult)]
public string[] Errors
{
get => _errors.ToArray();
set { /* readonly */ }
}
/// <summary>
/// Specifies the top-level model object for the form. Used with Fluent Validation
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidatedData)]
public object? Model { get; set; }
#endregion
#region Styling
protected string Classname =>
new CssBuilder("form")
.AddClass(AdditionalClassList)
.Build();
#endregion
#region Behavior
// Note: w/o any children the form is automatically valid.
// It stays valid, as long as non-required fields are added or
// a required field is added or the user touches a field that fails validation.
/// <summary>
/// Validation debounce delay in milliseconds. This can help improve rendering performance of forms with real-time validation of inputs
/// i.e. when textfields have ChangeTextImmediately="true".
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public int ValidationDelay { get; set; } = 300;
/// <summary>
/// Validation status. True if the form is valid and without errors. This parameter is two-way bindable.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.ValidationResult)]
public bool IsValid
{
get => _valid && ChildForms.All(x => x.IsValid);
set
{
_valid = value;
}
}
/// <summary>
/// True if any field of the field was touched. This parameter is readonly.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public bool IsTouched { get => _touched; set {/* readonly parameter! */ } }
/// <summary>
/// When true, the form will not re-render its child contents on validation updates (i.e. when IsValid changes).
/// This is an optimization which can be necessary especially for larger forms on older devices.
/// </summary>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public bool SuppressRenderingOnValidation { get; set; } = false;
/// <summary>
/// When true, will not cause a page refresh on Enter if any input has focus.
/// </summary>
/// <remarks>
/// https://www.w3.org/TR/2018/SPSD-html5-20180327/forms.html#implicit-submission
/// Usually this is not wanted, as it can cause a page refresh in the middle of editing a form.
/// When the form is in a dialog this will cause the dialog to close. So by default we suppress it.
/// </remarks>
[Parameter]
[Category(CategoryTypes.Form.Behavior)]
public bool SuppressImplicitSubmission { get; set; } = true;
/// <summary>
/// If a field already has a validation, override it with <see cref="Validation"/>.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public bool? OverrideFieldValidation { get; set; }
#endregion
#region Lifecycle
protected override Task OnAfterRenderAsync(bool firstRender) protected override Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
@ -299,16 +324,6 @@ public partial class Form : UIComponent, IDisposable, IForm
return base.OnAfterRenderAsync(firstRender); return base.OnAfterRenderAsync(firstRender);
} }
private void SetDefaultControlValidation(IFormComponent formComponent)
{
if (Validation == null) return;
if (!formComponent.IsForNull && (formComponent.Validation == null || (OverrideFieldValidation ?? true)))
{
formComponent.Validation = Validation;
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
if (ParentForm != null) if (ParentForm != null)
@ -323,4 +338,21 @@ public partial class Form : UIComponent, IDisposable, IForm
{ {
_timer?.Dispose(); _timer?.Dispose();
} }
#endregion
} }

@ -6,13 +6,7 @@ namespace Connected.Components;
public partial class Grid public partial class Grid
{ {
#region Event callbacks #region Styling
#endregion
#region Content placeholders
#endregion
#region Styling properties
private CssBuilder CompiledClassList private CssBuilder CompiledClassList
{ {
@ -42,8 +36,5 @@ public partial class Grid
#endregion #endregion
#region Lifecycle
#endregion
} }

@ -5,10 +5,7 @@ namespace Connected.Components;
public partial class Item public partial class Item
{ {
#region Event callbacks #region Content
#endregion
#region Content placeholders
[CascadingParameter] [CascadingParameter]
private Grid Parent { get; set; } private Grid Parent { get; set; }
@ -25,7 +22,7 @@ public partial class Item
#endregion #endregion
#region Styling properties #region Styling
private CssBuilder CompiledClassList private CssBuilder CompiledClassList
{ {
@ -50,7 +47,7 @@ public partial class Item
#endregion #endregion
#region Lifecycle events #region Lifecycle
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -58,19 +55,8 @@ public partial class Item
//if (Parent == null) //if (Parent == null)
// throw new ArgumentNullException(nameof(Parent), "Item must exist within a Grid"); // throw new ArgumentNullException(nameof(Parent), "Item must exist within a Grid");
base.OnInitialized(); base.OnInitialized();
#endregion
// ToDo false,auto,true on all sizes.
} }
#endregion
} }

@ -7,18 +7,60 @@ namespace Connected.Components;
public partial class Hidden : UIComponent, IAsyncDisposable public partial class Hidden : UIComponent, IAsyncDisposable
{ {
private Breakpoint _currentBreakpoint = Breakpoint.None; #region Variables
private bool _serviceIsReady = false; private Breakpoint _currentBreakpoint = Breakpoint.None;
private Guid _breakpointServiceSubscriptionId; private bool _serviceIsReady = false;
private Guid _breakpointServiceSubscriptionId;
[Inject] public IBreakpointService BreakpointService { get; set; } private bool _isHidden = true;
[CascadingParameter] public Breakpoint CurrentBreakpointFromProvider { get; set; } = Breakpoint.None; [Inject] public IBreakpointService BreakpointService { get; set; }
/// <summary> [CascadingParameter] public Breakpoint CurrentBreakpointFromProvider { get; set; } = Breakpoint.None;
/// The screen size(s) depending on which the ChildContent should not be rendered (or should be, if Invert is true)
/// </summary> #endregion
[Parameter]
#region Events
/// <summary>
/// Fires when the breakpoint changes visibility of the component
/// </summary>
[Parameter] public EventCallback<bool> IsHiddenChanged { get; set; }
protected void Update(Breakpoint currentBreakpoint)
{
if (CurrentBreakpointFromProvider != Breakpoint.None)
{
currentBreakpoint = CurrentBreakpointFromProvider;
}
else if (_serviceIsReady == false) { return; }
if (currentBreakpoint == Breakpoint.None) { return; }
_currentBreakpoint = currentBreakpoint;
var hidden = BreakpointService.IsMediaSize(Breakpoint, currentBreakpoint);
if (Invert == true)
{
hidden = !hidden;
}
IsHidden = hidden;
}
#endregion
#region Content
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.Hidden.Behavior)]
public RenderFragment ChildContent { get; set; }
#endregion
#region Styling
/// <summary>
/// The screen size(s) depending on which the ChildContent should not be rendered (or should be, if Invert is true)
/// </summary>
[Parameter]
[Category(CategoryTypes.Hidden.Behavior)] [Category(CategoryTypes.Hidden.Behavior)]
public Breakpoint Breakpoint { get; set; } public Breakpoint Breakpoint { get; set; }
@ -29,90 +71,60 @@ public partial class Hidden : UIComponent, IAsyncDisposable
[Category(CategoryTypes.Hidden.Behavior)] [Category(CategoryTypes.Hidden.Behavior)]
public bool Invert { get; set; } public bool Invert { get; set; }
private bool _isHidden = true;
/// <summary> /// <summary>
/// True if the component is not visible (two-way bindable) /// True if the component is not visible (two-way bindable)
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Hidden.Behavior)] [Category(CategoryTypes.Hidden.Behavior)]
public bool IsHidden public bool IsHidden
{ {
get => _isHidden; get => _isHidden;
set set
{ {
if (_isHidden != value) if (_isHidden != value)
{ {
_isHidden = value; _isHidden = value;
IsHiddenChanged.InvokeAsync(_isHidden); IsHiddenChanged.InvokeAsync(_isHidden);
} }
} }
} }
#endregion
#region Lifecycle
protected override void OnParametersSet()
{
base.OnParametersSet();
Update(_currentBreakpoint);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender == true)
{
if (CurrentBreakpointFromProvider == Breakpoint.None)
{
var attachResult = await BreakpointService.Subscribe((x) =>
{
Update(x);
InvokeAsync(StateHasChanged);
});
_serviceIsReady = true;
_breakpointServiceSubscriptionId = attachResult.SubscriptionId;
Update(attachResult.Breakpoint);
StateHasChanged();
}
else
{
_serviceIsReady = true;
}
}
}
public async ValueTask DisposeAsync() => await BreakpointService.Unsubscribe(_breakpointServiceSubscriptionId);
#endregion
/// <summary>
/// Fires when the breakpoint changes visibility of the component
/// </summary>
[Parameter] public EventCallback<bool> IsHiddenChanged { get; set; }
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.Hidden.Behavior)]
public RenderFragment ChildContent { get; set; }
protected void Update(Breakpoint currentBreakpoint)
{
if (CurrentBreakpointFromProvider != Breakpoint.None)
{
currentBreakpoint = CurrentBreakpointFromProvider;
}
else if (_serviceIsReady == false) { return; }
if (currentBreakpoint == Breakpoint.None) { return; }
_currentBreakpoint = currentBreakpoint;
var hidden = BreakpointService.IsMediaSize(Breakpoint, currentBreakpoint);
if (Invert == true)
{
hidden = !hidden;
}
IsHidden = hidden;
}
protected override void OnParametersSet()
{
base.OnParametersSet();
Update(_currentBreakpoint);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender == true)
{
if (CurrentBreakpointFromProvider == Breakpoint.None)
{
var attachResult = await BreakpointService.Subscribe((x) =>
{
Update(x);
InvokeAsync(StateHasChanged);
});
_serviceIsReady = true;
_breakpointServiceSubscriptionId = attachResult.SubscriptionId;
Update(attachResult.Breakpoint);
StateHasChanged();
}
else
{
_serviceIsReady = true;
}
}
}
public async ValueTask DisposeAsync() => await BreakpointService.Unsubscribe(_breakpointServiceSubscriptionId);
} }

@ -10,50 +10,60 @@ namespace Connected.Components;
public partial class Highlighter : UIComponent public partial class Highlighter : UIComponent
{ {
private Memory<string> _fragments; #region Variables
private string _regex; private Memory<string> _fragments;
private string _regex;
/// <summary> #endregion
/// The whole text in which a fragment will be highlighted
/// </summary> #region Content
[Parameter] /// <summary>
[Category(CategoryTypes.Highlighter.Behavior)] /// The whole text in which a fragment will be highlighted
public string Text { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Highlighter.Behavior)]
/// The fragment of text to be highlighted public string Text { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Highlighter.Behavior)] /// The fragment of text to be highlighted
public string HighlightedText { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Highlighter.Behavior)]
/// The fragments of text to be highlighted public string HighlightedText { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Highlighter.Behavior)] /// The fragments of text to be highlighted
public IEnumerable<string> HighlightedTexts { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Highlighter.Behavior)]
/// Whether or not the highlighted text is case sensitive public IEnumerable<string> HighlightedTexts { get; set; }
/// </summary> #endregion
[Parameter]
[Category(CategoryTypes.Highlighter.Behavior)] #region Styling
public bool CaseSensitive { get; set; } /// <summary>
/// Whether or not the highlighted text is case sensitive
/// <summary> /// </summary>
/// If true, highlights the text until the next regex boundary [Parameter]
/// </summary> [Category(CategoryTypes.Highlighter.Behavior)]
[Parameter] public bool CaseSensitive { get; set; }
[Category(CategoryTypes.Highlighter.Behavior)] #endregion
public bool UntilNextBoundary { get; set; }
#region Behavior
//TODO /// <summary>
//Accept regex highlightings /// If true, highlights the text until the next regex boundary
// [Parameter] public bool IsRegex { get; set; } /// </summary>
[Parameter]
protected override void OnParametersSet() [Category(CategoryTypes.Highlighter.Behavior)]
{ public bool HighlightUntilNextBoundary { get; set; }
_fragments = GetFragments(Text, HighlightedText, HighlightedTexts, out _regex, CaseSensitive, UntilNextBoundary); #endregion
}
#region Lifecycle
//TODO
//Accept regex highlightings
// [Parameter] public bool IsRegex { get; set; }
protected override void OnParametersSet()
{
_fragments = GetFragments(Text, HighlightedText, HighlightedTexts, out _regex, CaseSensitive, HighlightUntilNextBoundary);
}
#endregion
} }

@ -7,54 +7,61 @@ namespace Connected.Components;
public partial class Icon : UIComponent public partial class Icon : UIComponent
{ {
protected string Classname =>
new CssBuilder("icon-root") #region Content
.AddClass($"icon-default", Color == ThemeColor.Default) /// <summary>
.AddClass($"svg-icon", !string.IsNullOrEmpty(Glyph) && Glyph.Trim().StartsWith("<")) /// GlyphTitle of the icon used for accessibility.
.AddClass($"{Color.ToDescription()}-text", Color != ThemeColor.Default && Color != ThemeColor.Inherit) /// </summary>
.AddClass($"icon-size-{Size.ToDescription()}") [Parameter]
.AddClass(AdditionalClassList) [Category(CategoryTypes.Icon.Behavior)]
.Build(); public string Title { get; set; }
/// <summary> /// <summary>
/// Glyph to be used can either be svg paths for font icons. /// Child content of component.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Icon.Behavior)] [Category(CategoryTypes.Icon.Behavior)]
public string Glyph { get; set; } public RenderFragment ChildContent { get; set; }
#endregion
/// <summary>
/// GlyphTitle of the icon used for accessibility. #region Styling
/// </summary> protected string Classname =>
[Parameter] new CssBuilder("icon-root")
[Category(CategoryTypes.Icon.Behavior)] .AddClass($"icon-default", Color == ThemeColor.Default)
public string Title { get; set; } .AddClass($"svg-icon", !string.IsNullOrEmpty(Glyph) && Glyph.Trim().StartsWith("<"))
.AddClass($"{Color.ToDescription()}-text", Color != ThemeColor.Default && Color != ThemeColor.Inherit)
/// <summary> .AddClass($"icon-size-{Size.ToDescription()}")
/// The Size of the icon. .AddClass(AdditionalClassList)
/// </summary> .Build();
[Parameter]
[Category(CategoryTypes.Icon.Appearance)] /// <summary>
public Size Size { get; set; } = Size.Medium; /// Glyph to be used can either be svg paths for font icons.
/// </summary>
/// <summary> [Parameter]
/// The color of the component. It supports the theme colors. [Category(CategoryTypes.Icon.Behavior)]
/// </summary> public string Glyph { get; set; }
[Parameter]
[Category(CategoryTypes.Icon.Appearance)] /// <summary>
public ThemeColor Color { get; set; } = ThemeColor.Inherit; /// The Size of the icon.
/// </summary>
/// <summary> [Parameter]
/// The viewbox size of an svg element. [Category(CategoryTypes.Icon.Appearance)]
/// </summary> public Size Size { get; set; } = Size.Medium;
[Parameter]
[Category(CategoryTypes.Icon.Behavior)] /// <summary>
public string ViewBox { get; set; } = "0 0 24 24"; /// The color of the component. It supports the theme colors.
/// </summary>
/// <summary> [Parameter]
/// Child content of component. [Category(CategoryTypes.Icon.Appearance)]
/// </summary> public ThemeColor Color { get; set; } = ThemeColor.Inherit;
[Parameter]
[Category(CategoryTypes.Icon.Behavior)] /// <summary>
public RenderFragment ChildContent { get; set; } /// The viewbox size of an svg element.
/// </summary>
[Parameter]
[Category(CategoryTypes.Icon.Behavior)]
public string ViewBox { get; set; } = "0 0 24 24";
#endregion
} }

@ -2,7 +2,6 @@
// MudBlazor licenses this file to you under the MIT license. // MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Connected.Annotations;
using Connected.Extensions; using Connected.Extensions;
using Connected.Utilities; using Connected.Utilities;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -11,7 +10,39 @@ namespace Connected.Components;
public partial class Image : UIComponent public partial class Image : UIComponent
{ {
#region Content
/// <summary>
/// Specifies the path to the image.
/// </summary>
[Parameter]
public string Src { get; set; }
/// <summary>
/// Specifies an alternate text for the image.
/// </summary>
[Parameter]
public string Alt { get; set; }
#endregion
#region Styling
/// <summary>
/// Specifies the height of the image in px.
/// </summary>
[Parameter]
public int? Height { get; set; }
/// <summary>
/// Specifies the width of the image in px.
/// </summary>
[Parameter]
public int? Width { get; set; }
/// <summary>
/// The higher the number, the heavier the drop-shadow.
/// </summary>
[Parameter]
public int Elevation { set; get; }
private CssBuilder CompiledClassList private CssBuilder CompiledClassList
{ {
get get
@ -25,57 +56,29 @@ public partial class Image : UIComponent
} }
} }
/// <summary> /// <summary>
/// Applies the fluid class so the image scales with the parent width. /// Applies the fluid class so the image scales with the parent width.
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool Fluid { get; set; } public bool Fluid { get; set; }
/// <summary>
/// Specifies the path to the image.
/// </summary>
[Parameter]
public string Src { get; set; }
/// <summary>
/// Specifies an alternate text for the image.
/// </summary>
[Parameter]
public string Alt { get; set; }
/// <summary>
/// Specifies the height of the image in px.
/// </summary>
[Parameter]
public int? Height { get; set; }
/// <summary>
/// Specifies the width of the image in px.
/// </summary>
[Parameter]
public int? Width { get; set; }
/// <summary>
/// The higher the number, the heavier the drop-shadow.
/// </summary>
[Parameter]
public int Elevation { set; get; }
/// <summary> /// <summary>
/// Controls how the image should be resized. /// Controls how the image should be resized.
/// </summary> /// </summary>
[Parameter] [Parameter]
public ObjectFit ObjectFit { set; get; } = ObjectFit.Fill; public ObjectFit ObjectFit { set; get; } = ObjectFit.Fill;
/// <summary> /// <summary>
/// Controls how the image should positioned within its container. /// Controls how the image should positioned within its container.
/// </summary> /// </summary>
[Parameter] [Parameter]
public Position ObjectPosition { set; get; } = Position.Center; public Position ObjectPosition { set; get; } = Position.Center;
/// <summary> /// <summary>
/// A space separated list of class names, added on top of the default class list. /// A space separated list of class names, added on top of the default class list.
/// </summary> /// </summary>
[Parameter] [Parameter]
public string? ClassList { get; set; } public string? ClassList { get; set; }
#endregion
} }

@ -2,44 +2,26 @@
@typeparam T @typeparam T
@inherits InputBase<T> @inherits InputBase<T>
<CascadingValue Name="SubscribeToParentForm" Value="@base.SubscribeToParentForm" IsFixed="true">
<div class="@CompiledWrapperClass.Build()">
<InputControl Label="@Label"
Variant="@Variant"
HelperText="@HelperText"
HelperTextOnFocus="@HelperTextOnFocus"
CounterText="@GetCounterText()"
FullWidth="@FullWidth"
class="@CompiledHelperContainerClassList.Build()"
Error="@HasErrors"
ErrorText="@GetErrorText()"
ErrorId="@ErrorId"
Disabled="@Disabled"
Margin="@Margin"
Required="@Required"
ForId="@FieldId">
<InputContent>
<CascadingValue Name="SubscribeToParentForm" Value="false" IsFixed="true">
<div class="@WrapperClassList" style="@Style">
@if (Adornment == Adornment.Start) @if (Adornment == Adornment.Start)
{ {
<InputAdornment Class="@CompiledAdornmentClass.Build()" <InputAdornment Class="@AdornmentClassList"
Icon="@AdornmentIcon" Icon="@AdornmentIcon"
Color="@AdornmentColor" Color="@AdornmentColor"
Size="@IconSize" Size="@IconSize"
Text="@AdornmentText" Text="@AdornmentText"
Edge="@Edge.Start" Edge="@Edge.Start"
AdornmentClick="@OnAdornmentClick" AdornmentClick="@OnAdornmentClick"
AriaLabel="@AdornmentAriaLabel" AriaLabel="@AdornmentAriaLabel"
/> />
} }
@if (NumberOfLines > 1) @if (Lines > 1)
{ {
<textarea class="@CompiledInputClass.Build()" <textarea class="@InputClassList"
@ref="ElementReference" @ref="ElementReference"
rows="@NumberOfLines" rows="@Lines"
@attributes="@Attributes"
type="@InputTypeString" type="@InputTypeString"
placeholder="@Placeholder" placeholder="@Placeholder"
disabled=@Disabled disabled=@Disabled
@ -59,7 +41,7 @@
@onkeyup:preventDefault="@KeyUpPreventDefault" @onkeyup:preventDefault="@KeyUpPreventDefault"
@onmousewheel="@OnMouseWheel" @onmousewheel="@OnMouseWheel"
@onwheel="@OnMouseWheel" @onwheel="@OnMouseWheel"
aria-invalid="@HasError.ToString().ToLower()" aria-invalid="@GetErrorText()"
aria-describedby="@ErrorId" aria-describedby="@ErrorId"
> >
@Text @Text
@ -69,13 +51,12 @@
} }
else else
{ {
<input class="@CompiledInputClass.Build()" <input class="@InputClassList"
@ref="ElementReference" @ref="ElementReference"
@attributes="@Attributes"
step="@Step"
type="@InputTypeString" type="@InputTypeString"
value="@_internalText" value="@_internalText"
@oninput="OnInput" @oninput="OnInput"
step="@Step"
@onchange="OnChange" @onchange="OnChange"
placeholder="@Placeholder" placeholder="@Placeholder"
disabled=@Disabled disabled=@Disabled
@ -92,7 +73,7 @@
@onkeyup:preventDefault="@KeyUpPreventDefault" @onkeyup:preventDefault="@KeyUpPreventDefault"
@onmousewheel="@OnMouseWheel" @onmousewheel="@OnMouseWheel"
@onwheel="@OnMouseWheel" @onwheel="@OnMouseWheel"
aria-invalid="@HasError.ToString().ToLower()" aria-invalid="@GetErrorText()"
aria-describedby="@ErrorId" aria-describedby="@ErrorId"
/> />
@*Note: double mouse wheel handlers needed for Firefox because it doesn't know onmousewheel*@ @*Note: double mouse wheel handlers needed for Firefox because it doesn't know onmousewheel*@
@ -101,7 +82,7 @@
@*Note: this div must always be there to avoid crashes in WASM, but it is hidden most of the time except if ChildContent should be shown. @*Note: this div must always be there to avoid crashes in WASM, but it is hidden most of the time except if ChildContent should be shown.
In Disabled state the tabindex attribute must NOT be set at all or else it will get focus on click In Disabled state the tabindex attribute must NOT be set at all or else it will get focus on click
*@ *@
<div class="@CompiledInputClass.Build()" <div class="@InputClassList"
style="@("display:"+(InputType == InputType.Hidden && ChildContent != null ? "inline" : "none"))" style="@("display:"+(InputType == InputType.Hidden && ChildContent != null ? "inline" : "none"))"
@onblur="@OnBlurred" @ref="@_elementReference1" @onblur="@OnBlurred" @ref="@_elementReference1"
> >
@ -111,7 +92,7 @@
else else
{ {
@*Note: this div must always be there to avoid crashes in WASM, but it is hidden most of the time except if ChildContent should be shown.*@ @*Note: this div must always be there to avoid crashes in WASM, but it is hidden most of the time except if ChildContent should be shown.*@
<div class="@CompiledInputClass.Build()" <div class="@InputClassList"
style="@("display:"+(InputType == InputType.Hidden && ChildContent != null ? "inline" : "none"))" style="@("display:"+(InputType == InputType.Hidden && ChildContent != null ? "inline" : "none"))"
tabindex="@(InputType == InputType.Hidden && ChildContent != null ? 0 : -1)" tabindex="@(InputType == InputType.Hidden && ChildContent != null ? 0 : -1)"
@onblur="@OnBlurred" @ref="@_elementReference1" @onblur="@OnBlurred" @ref="@_elementReference1"
@ -123,16 +104,17 @@
@if (_showClearable && !Disabled) @if (_showClearable && !Disabled)
{ {
<IconButton ClassList="@CompiledClearButtonClassList.Build()" <GlyphButton ClassList="@ClearButtonClassList"
Icon="@ClearIcon" Glyph="@ClearIcon"
Clicked="@ClearButtonClickHandlerAsync" Clicked="@ClearButtonClickHandlerAsync"
aria-label="Clear"
tabindex="-1" tabindex="-1"
/> />
} }
@if (Adornment == Adornment.End) @if (Adornment == Adornment.End)
{ {
<InputAdornment Class="@CompiledAdornmentClass.Build()" <InputAdornment Class="@AdornmentClassList"
Icon="@AdornmentIcon" Icon="@AdornmentIcon"
Color="@AdornmentColor" Color="@AdornmentColor"
Size="@IconSize" Size="@IconSize"
@ -145,25 +127,18 @@
@if (Variant == Variant.Outlined) @if (Variant == Variant.Outlined)
{ {
<div class="input-outlined-border"></div> <div class="mud-input-outlined-border"></div>
} }
@if (!HideSpinButtons) @if (!HideSpinButtons)
{ {
<div class="input-numeric-spin"> <div class="mud-input-numeric-spin">
<Button Variant="Variant.Text" Clicked="OnIncrement" Disabled="@(Disabled || ReadOnly)" tabindex="-1"> <Button Variant="Variant.Text" @onclick="OnIncrement" Disabled="@(Disabled || ReadOnly)" tabindex="-1">
<Icon Icon="@NumericUpIcon" Size="@GetButtonSize()" /> <Icon aria-label="Increment" Glyph="@NumericUpIcon" Size="@GetButtonSize()" />
</Button> </Button>
<Button Variant="Variant.Text" Clicked="OnDecrement" Disabled="@(Disabled || ReadOnly)" tabindex="-1"> <Button Variant="Variant.Text" @onclick="OnDecrement" Disabled="@(Disabled || ReadOnly)" tabindex="-1">
<Icon Icon="@NumericDownIcon" Size="@GetButtonSize()" /> <Icon aria-label="Decrement" Glyph="@NumericDownIcon" Size="@GetButtonSize()" />
</Button> </Button>
</div> </div>
} }
</CascadingValue> </div>
</InputContent>
</InputControl>
</div>
</CascadingValue>

@ -45,6 +45,7 @@ public partial class Input<T> : InputBase<T>
// This method is called when Value property needs to be refreshed from the current Text property, so typically because Text property has changed. // This method is called when Value property needs to be refreshed from the current Text property, so typically because Text property has changed.
// We want to debounce only text-input, not a value being set, so the debouncing is only done when updateText==false (because that indicates the // We want to debounce only text-input, not a value being set, so the debouncing is only done when updateText==false (because that indicates the
// change came from a Text setter) // change came from a Text setter)
if (updateText) if (updateText)
{ {
// we have a change coming not from the Text setter, no debouncing is needed // we have a change coming not from the Text setter, no debouncing is needed
@ -72,11 +73,36 @@ public partial class Input<T> : InputBase<T>
} }
protected Task OnInput(ChangeEventArgs args) protected Task OnInput(ChangeEventArgs args)
{ {
var input = args.Value.ToString();
/*if (InputType==InputType.Number)
{
if (!Helper.IsNumeric(input))
{
input = Regex.Replace(input, "[^0-9.]", "");
Text = input;
UpdateValuePropertyAsync(true);
}
}*/
if (!ChangeTextImmediately) if (!ChangeTextImmediately)
return Task.CompletedTask; return Task.CompletedTask;
_isFocused = true; _isFocused = true;
return SetTextAsync(args?.Value as string); return SetTextAsync(input);
}
protected virtual void OnKeyDown(KeyboardEventArgs obj)
{
if (InputType == InputType.Number)
{
if (Helper.IsNumeric(obj.Key))
{
_isFocused = true;
base.OnKeyDown.InvokeAsync(obj).AndForget();
}
} else
{
_isFocused = true;
base.OnKeyDown.InvokeAsync(obj).AndForget();
}
} }
protected async Task OnChange(ChangeEventArgs args) protected async Task OnChange(ChangeEventArgs args)
@ -95,7 +121,6 @@ public partial class Input<T> : InputBase<T>
await SetTextAsync(args?.Value as string); await SetTextAsync(args?.Value as string);
} }
} }
} }
/// <summary> /// <summary>
@ -178,8 +203,6 @@ public partial class Input<T> : InputBase<T>
#endregion #endregion
#region Style properties #region Style properties
/// <summary> /// <summary>
@ -209,25 +232,52 @@ public partial class Input<T> : InputBase<T>
[Parameter] public bool Clearable { get; set; } = false; [Parameter] public bool Clearable { get; set; } = false;
#region Wrapper class #region Wrapper class
/*[Parameter]
public string WrapperClass { get; set; } = string.Empty;*/
/*protected string WrapperClass => InputCssHelper.GetClassname(this,
() => HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder));*/
[Parameter] [Parameter]
public string WrapperClass { get; set; } = string.Empty; public string ClassList { get; set; } = string.Empty;
protected CssBuilder CompiledWrapperClass
protected string WrapperClassList
{ {
get get
{ {
return new CssBuilder("input") /*return new CssBuilder("input")
.AddClass($"input-{Variant.ToDescription()}")
.AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
.AddClass($"input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
.AddClass($"shrink", when: HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder))
.AddClass($"disabled", Disabled)
.AddClass($"input-error", HasErrors)
.AddClass($"ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone)
.AddClass(WrapperClass)
.Build();*/
var clasList = new CssBuilder("input")
.AddClass($"input-{Variant.ToDescription()}") .AddClass($"input-{Variant.ToDescription()}")
.AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) .AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) .AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
.AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined) .AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
.AddClass("shrink", when: HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder)) //.AddClass("shrink", when: () => HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder))
.AddClass("disabled", Disabled) .AddClass("disabled", Disabled)
.AddClass("input-error", HasErrors) .AddClass("input-error", HasErrors)
.AddClass("ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone) .AddClass("ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone)
.AddClass(WrapperClass); .AddClass(ClassList)
.Build();
return clasList;
} }
} }
/// <summary>
/// User styles, applied on top of the component's own classes and styles.
/// </summary>
[Parameter]
public string Style { get; set; }
#endregion #endregion
@ -235,8 +285,8 @@ public partial class Input<T> : InputBase<T>
#region Input field class #region Input field class
[Parameter] [Parameter]
public string Class { get; set; } = string.Empty; public string InputClass { get; set; } = string.Empty;
protected CssBuilder CompiledInputClass protected string InputClassList
{ {
get get
{ {
@ -245,7 +295,10 @@ public partial class Input<T> : InputBase<T>
.AddClass($"input-root-{Variant.ToDescription()}") .AddClass($"input-root-{Variant.ToDescription()}")
.AddClass($"input-root-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) .AddClass($"input-root-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) .AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
.AddClass(Class); .AddClass(InputClass)
.Build();
//return new CssBuilder().AddClass(InputCssHelper.GetInputClassname(this)).Build();
} }
} }
#endregion #endregion
@ -253,7 +306,7 @@ public partial class Input<T> : InputBase<T>
#region Adornment class #region Adornment class
[Parameter] [Parameter]
public string AdornmentClass { get; set; } = string.Empty; public string AdornmentClass { get; set; } = string.Empty;
protected CssBuilder CompiledAdornmentClass protected string AdornmentClassList
{ {
get get
{ {
@ -261,7 +314,10 @@ public partial class Input<T> : InputBase<T>
.AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None) .AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"text", !string.IsNullOrEmpty(AdornmentText)) .AddClass($"text", !string.IsNullOrEmpty(AdornmentText))
.AddClass($"input-root-filled-shrink", Variant == Variant.Filled) .AddClass($"input-root-filled-shrink", Variant == Variant.Filled)
.AddClass(AdornmentClass); .AddClass(AdornmentClass)
.Build();
//return new CssBuilder().AddClass(InputCssHelper.GetAdornmentClassname(this)).Build();
} }
} }
#endregion #endregion
@ -269,7 +325,7 @@ public partial class Input<T> : InputBase<T>
#region Clear icon class #region Clear icon class
[Parameter] [Parameter]
public string ClearButtonClass { get; set; } = string.Empty; public string ClearButtonClass { get; set; } = string.Empty;
protected CssBuilder CompiledClearButtonClassList protected string ClearButtonClassList
{ {
get get
{ {
@ -277,8 +333,9 @@ public partial class Input<T> : InputBase<T>
.AddClass("me-n1", Adornment == Adornment.End && HideSpinButtons == false) .AddClass("me-n1", Adornment == Adornment.End && HideSpinButtons == false)
.AddClass("icon-button-edge-end", Adornment == Adornment.End && HideSpinButtons == true) .AddClass("icon-button-edge-end", Adornment == Adornment.End && HideSpinButtons == true)
.AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false) .AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false)
.AddClass("icon-button-edge-margin-end", Adornment != Adornment.End && HideSpinButtons == true) .AddClass("micon-button-edge-margin-end", Adornment != Adornment.End && HideSpinButtons == true)
.AddClass(ClearButtonClass); .AddClass(ClearButtonClass)
.Build();
} }
} }
#endregion #endregion
@ -290,7 +347,7 @@ public partial class Input<T> : InputBase<T>
/// </summary> /// </summary>
[Parameter] [Parameter]
public string? HelperContainerClass { get; set; } public string? HelperContainerClass { get; set; }
protected CssBuilder CompiledHelperContainerClassList protected string HelperContainerClassList
{ {
get get
{ {
@ -298,7 +355,8 @@ public partial class Input<T> : InputBase<T>
.AddClass($"px-1", Variant == Variant.Filled) .AddClass($"px-1", Variant == Variant.Filled)
.AddClass($"px-2", Variant == Variant.Outlined) .AddClass($"px-2", Variant == Variant.Outlined)
.AddClass($"px-1", Variant == Variant.Text) .AddClass($"px-1", Variant == Variant.Text)
.AddClass(HelperContainerClass); .AddClass(HelperContainerClass)
.Build();
} }
} }
@ -314,7 +372,6 @@ public partial class Input<T> : InputBase<T>
#endregion #endregion
#region Content placeholders #region Content placeholders
/// <summary> /// <summary>
@ -336,6 +393,9 @@ public partial class Input<T> : InputBase<T>
private double _textChangeInterval; private double _textChangeInterval;
[Parameter]
public int Lines { get; set; } = 1;
/// <summary> /// <summary>
/// Interval to be awaited in MILLISECONDS before changing the Text value /// Interval to be awaited in MILLISECONDS before changing the Text value
@ -423,7 +483,6 @@ public partial class Input<T> : InputBase<T>
#endregion #endregion
#region Lifecycle events #region Lifecycle events
public override async Task SetParametersAsync(ParameterView parameters) public override async Task SetParametersAsync(ParameterView parameters)
{ {
@ -460,5 +519,4 @@ public partial class Input<T> : InputBase<T>
} }
#endregion #endregion
} }

@ -11,7 +11,7 @@
{ {
@if (AdornmentClick.HasDelegate) @if (AdornmentClick.HasDelegate)
{ {
<IconButton Icon="@Icon" Clicked="@AdornmentClick" Edge="@Edge" Size="@Size" Color="@Color" aria-label="@(!string.IsNullOrEmpty(AriaLabel) ? AriaLabel : "Icon Button")" tabindex="-1" /> <GlyphButton Glyph="@Icon" Clicked="@AdornmentClick" Edge="@Edge" Size="@Size" Color="@Color" aria-label="@(!string.IsNullOrEmpty(AriaLabel) ? AriaLabel : "Icon Button")" tabindex="-1" />
} }
else else
{ {

@ -17,7 +17,7 @@
<input @ref="_elementReferenceStart" @attributes="CustomAttributes" type="@InputTypeString" class="@InputClassname" @bind-value="@TextStart" @bind-value:event="@((ChangeTextImmediately ? "oninput" : "onchange"))" <input @ref="_elementReferenceStart" @attributes="CustomAttributes" type="@InputTypeString" class="@InputClassname" @bind-value="@TextStart" @bind-value:event="@((ChangeTextImmediately ? "oninput" : "onchange"))"
placeholder="@PlaceholderStart" disabled=@Disabled readonly="@ReadOnly" @onblur="@OnBlurred" @onkeydown="@InvokeKeyDown" @onkeypress="@InvokeKeyPress" @onkeyup="@InvokeKeyUp" inputmode="@InputMode.ToString()" pattern="@Pattern" /> placeholder="@PlaceholderStart" disabled=@Disabled readonly="@ReadOnly" @onblur="@OnBlurred" @onkeydown="@InvokeKeyDown" @onkeypress="@InvokeKeyPress" @onkeyup="@InvokeKeyUp" inputmode="@InputMode.ToString()" pattern="@Pattern" />
<Icon Class="range-input-separator mud-flip-x-rtl" Icon="@SeparatorIcon" Color="@ThemeColor.Default" /> <Icon Class="range-input-separator mud-flip-x-rtl" Glyph="@SeparatorIcon" Color="@ThemeColor.Default" />
<input @ref="_elementReferenceEnd" @attributes="CustomAttributes" type="@InputTypeString" class="@InputClassname" @bind-value="@TextEnd" @bind-value:event="@((ChangeTextImmediately ? "oninput" : "onchange"))" inputmode="@InputMode.ToString()" pattern="@Pattern" <input @ref="_elementReferenceEnd" @attributes="CustomAttributes" type="@InputTypeString" class="@InputClassname" @bind-value="@TextEnd" @bind-value:event="@((ChangeTextImmediately ? "oninput" : "onchange"))" inputmode="@InputMode.ToString()" pattern="@Pattern"
placeholder="@PlaceholderEnd" disabled=@Disabled readonly="@ReadOnly" @onblur="@OnBlurred" @onkeydown="@InvokeKeyDown" @onkeypress="@InvokeKeyPress" @onkeyup="@InvokeKeyUp" /> placeholder="@PlaceholderEnd" disabled=@Disabled readonly="@ReadOnly" @onblur="@OnBlurred" @onkeydown="@InvokeKeyDown" @onkeypress="@InvokeKeyPress" @onkeyup="@InvokeKeyUp" />

@ -6,6 +6,45 @@ namespace Connected.Components;
public partial class InputControl : UIComponent public partial class InputControl : UIComponent
{ {
#region Content
/// <summary>
/// Child content of component.
/// </summary>
[Parameter] public RenderFragment ChildContent { get; set; }
/// <summary>
/// Should be the Input
/// </summary>
[Parameter] public RenderFragment InputContent { get; set; }
/// <summary>
/// The ErrorText that will be displayed if Error true
/// </summary>
[Parameter] public string ErrorText { get; set; }
/// <summary>
/// The HelperText will be displayed below the text field.
/// </summary>
[Parameter] public string HelperText { get; set; }
/// <summary>
/// The current character counter, displayed below the text field.
/// </summary>
[Parameter] public string CounterText { get; set; }
/// <summary>
/// If string has value the label text will be displayed in the input, and scaled down at the top if the input has value.
/// </summary>
[Parameter] public string Label { get; set; }
/// <summary>
/// If string has value the label "for" attribute will be added.
/// </summary>
[Parameter] public string ForId { get; set; } = string.Empty;
#endregion
#region Styling
protected string Classname => protected string Classname =>
new CssBuilder("input-control") new CssBuilder("input-control")
.AddClass("input-required", when: () => Required) .AddClass("input-required", when: () => Required)
@ -27,77 +66,49 @@ public partial class InputControl : UIComponent
.AddClass("input-error", Error) .AddClass("input-error", Error)
.Build(); .Build();
/// <summary>
/// Child content of component.
/// </summary>
[Parameter] public RenderFragment ChildContent { get; set; }
/// <summary>
/// Should be the Input
/// </summary>
[Parameter] public RenderFragment InputContent { get; set; }
/// <summary> /// <summary>
/// Will adjust vertical spacing. /// Will adjust vertical spacing.
/// </summary> /// </summary>
[Parameter] public Margin Margin { get; set; } = Margin.None; [Parameter] public Margin Margin { get; set; } = Margin.None;
/// <summary> /// <summary>
/// If true, will apply mud-input-required class to the output div /// If true, the input will take up the full width of its container.
/// </summary>
[Parameter] public bool Required { get; set; }
/// <summary>
/// If true, the label will be displayed in an error state.
/// </summary> /// </summary>
[Parameter] public bool Error { get; set; } [Parameter] public bool FullWidth { get; set; }
/// <summary>
/// The ErrorText that will be displayed if Error true
/// </summary>
[Parameter] public string ErrorText { get; set; }
/// <summary>
/// The ErrorId that will be used by aria-describedby if Error true
/// </summary>
[Parameter] public string ErrorId { get; set; }
/// <summary> /// <summary>
/// The HelperText will be displayed below the text field. /// Variant can be Text, Filled or Outlined.
/// </summary> /// </summary>
[Parameter] public string HelperText { get; set; } [Parameter] public Variant Variant { get; set; } = Variant.Text;
/// <summary> /// <summary>
/// If true, the helper text will only be visible on focus. /// If true, the input element will be disabled.
/// </summary> /// </summary>
[Parameter] public bool HelperTextOnFocus { get; set; } [Parameter] public bool Disabled { get; set; }
#endregion
/// <summary>
/// The current character counter, displayed below the text field.
/// </summary>
[Parameter] public string CounterText { get; set; }
#region Behavior
/// <summary> /// <summary>
/// If true, the input will take up the full width of its container. /// If true, the label will be displayed in an error state.
/// </summary> /// </summary>
[Parameter] public bool FullWidth { get; set; } [Parameter] public bool Error { get; set; }
/// <summary> /// <summary>
/// If string has value the label text will be displayed in the input, and scaled down at the top if the input has value. /// If true, will apply mud-input-required class to the output div
/// </summary> /// </summary>
[Parameter] public string Label { get; set; } [Parameter] public bool Required { get; set; }
/// <summary> /// <summary>
/// Variant can be Text, Filled or Outlined. /// If true, the helper text will only be visible on focus.
/// </summary> /// </summary>
[Parameter] public Variant Variant { get; set; } = Variant.Text; [Parameter] public bool HelperTextOnFocus { get; set; }
/// <summary> /// <summary>
/// If true, the input element will be disabled. /// The ErrorId that will be used by aria-describedby if Error true
/// </summary> /// </summary>
[Parameter] public bool Disabled { get; set; } [Parameter] public string ErrorId { get; set; }
#endregion
/// <summary>
/// If string has value the label "for" attribute will be added.
/// </summary>
[Parameter] public string ForId { get; set; } = string.Empty;
} }

@ -4,6 +4,7 @@ namespace Connected.Components;
public partial class Layout : DrawerContainer public partial class Layout : DrawerContainer
{ {
#region Styling
protected override CssBuilder CompiledClassList protected override CssBuilder CompiledClassList
{ {
get get
@ -22,8 +23,14 @@ public partial class Layout : DrawerContainer
} }
} }
#endregion
#region Lifecycle
public Layout() public Layout()
{ {
Fixed = true; Fixed = true;
} }
#endregion
} }

@ -8,18 +8,21 @@ namespace Connected.Components;
public partial class Link : UIComponent public partial class Link : UIComponent
{ {
protected string Classname =>
new CssBuilder("typography mud-link")
.AddClass($"{Color.ToDescription()}-text")
.AddClass($"link-underline-{Underline.ToDescription()}")
.AddClass($"typography-{Typo.ToDescription()}")
// When Href is empty, link's hover cursor is text "I beam" even when Clicked has a delegate.
// To change this for more expected look change hover cursor to a pointer:
.AddClass("cursor-pointer", Href == default && OnClick.HasDelegate && !Disabled)
.AddClass($"link-disabled", Disabled)
.AddClass(AdditionalClassList)
.Build();
#region Events
/// <summary>
/// Link click event.
/// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
protected async Task OnClickHandler(MouseEventArgs ev)
{
if (Disabled) return;
await OnClick.InvokeAsync(ev);
}
#endregion
#region Content
private Dictionary<string, object> Attributes private Dictionary<string, object> Attributes
{ {
get => Disabled ? CustomAttributes : new Dictionary<string, object>(CustomAttributes) get => Disabled ? CustomAttributes : new Dictionary<string, object>(CustomAttributes)
@ -28,59 +31,61 @@ public partial class Link : UIComponent
{ "target", Target } { "target", Target }
}; };
} }
/// <summary> /// <summary>
/// The color of the component. It supports the theme colors. /// The URL, which is the actual link.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Link.Appearance)] [Category(CategoryTypes.Link.Behavior)]
public ThemeColor Color { get; set; } = ThemeColor.Primary; public string Href { get; set; }
/// <summary> /// <summary>
/// Typography variant to use. /// The target attribute specifies where to open the link, if Link is specified. Possible values: _blank | _self | _parent | _top | <i>framename</i>
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Link.Appearance)] [Category(CategoryTypes.Link.Behavior)]
public Typo Typo { get; set; } = Typo.body1; public string Target { get; set; }
#endregion
/// <summary> #region Styling
/// Controls when the link should have an underline. protected string Classname =>
/// </summary> new CssBuilder("typography mud-link")
[Parameter] .AddClass($"{Color.ToDescription()}-text")
[Category(CategoryTypes.Link.Appearance)] .AddClass($"link-underline-{Underline.ToDescription()}")
public Underline Underline { get; set; } = Underline.Hover; .AddClass($"typography-{Typo.ToDescription()}")
// When Href is empty, link's hover cursor is text "I beam" even when Clicked has a delegate.
// To change this for more expected look change hover cursor to a pointer:
.AddClass("cursor-pointer", Href == default && OnClick.HasDelegate && !Disabled)
.AddClass($"link-disabled", Disabled)
.AddClass(AdditionalClassList)
.Build();
/// <summary> /// <summary>
/// The URL, which is the actual link. /// Child content of component.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Link.Behavior)] [Category(CategoryTypes.Link.Behavior)]
public string Href { get; set; } public RenderFragment ChildContent { get; set; }
/// <summary> /// <summary>
/// The target attribute specifies where to open the link, if Link is specified. Possible values: _blank | _self | _parent | _top | <i>framename</i> /// The color of the component. It supports the theme colors.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Link.Behavior)] [Category(CategoryTypes.Link.Appearance)]
public string Target { get; set; } public ThemeColor Color { get; set; } = ThemeColor.Primary;
/// <summary> /// <summary>
/// Link click event. /// Typography variant to use.
/// </summary> /// </summary>
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; } [Parameter]
[Category(CategoryTypes.Link.Appearance)]
protected async Task OnClickHandler(MouseEventArgs ev) public Typo Typo { get; set; } = Typo.body1;
{
if (Disabled) return;
await OnClick.InvokeAsync(ev);
}
/// <summary> /// <summary>
/// Child content of component. /// Controls when the link should have an underline.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Link.Behavior)] [Category(CategoryTypes.Link.Appearance)]
public RenderFragment ChildContent { get; set; } public Underline Underline { get; set; } = Underline.Hover;
/// <summary> /// <summary>
/// If true, the navlink will be disabled. /// If true, the navlink will be disabled.
@ -88,4 +93,7 @@ public partial class Link : UIComponent
[Parameter] [Parameter]
[Category(CategoryTypes.Link.Behavior)] [Category(CategoryTypes.Link.Behavior)]
public bool Disabled { get; set; } public bool Disabled { get; set; }
#endregion
} }

@ -6,62 +6,90 @@ namespace Connected.Components;
public partial class List : UIComponent, IDisposable public partial class List : UIComponent, IDisposable
{ {
protected string Classname => #region Variables
new CssBuilder("list")
.AddClass("list-padding", !DisablePadding)
.AddClass(AdditionalClassList)
.Build();
[CascadingParameter] protected List ParentList { get; set; } [CascadingParameter] protected List ParentList { get; set; }
/// <summary> private HashSet<ListItem> _items = new();
/// The color of the selected List Item. private HashSet<List> _childLists = new();
/// </summary> private ListItem _selectedItem;
[Parameter] private object _selectedValue;
[Category(CategoryTypes.List.Appearance)]
public ThemeColor Color { get; set; } = ThemeColor.Primary;
/// <summary> internal bool CanSelect { get; private set; }
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment ChildContent { get; set; }
/// <summary> internal event Action ParametersChanged;
/// Set true to make the list items clickable. This is also the precondition for list selection to work. #endregion
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Selecting)]
public bool Clickable { get; set; }
/// <summary> #region Events
/// If true, vertical padding will be removed from the list.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public bool DisablePadding { get; set; }
/// <summary> /// <summary>
/// If true, compact vertical padding will be applied to all list items. /// Called whenever the selection changed
/// </summary> /// </summary>
[Parameter] [Parameter] public EventCallback<ListItem> SelectedItemChanged { get; set; }
[Category(CategoryTypes.List.Appearance)]
public bool Dense { get; set; }
/// <summary> /// <summary>
/// If true, the left and right padding is removed on all list items. /// Called whenever the selection changed
/// </summary> /// </summary>
[Parameter] [Parameter] public EventCallback<object> SelectedValueChanged { get; set; }
[Category(CategoryTypes.List.Appearance)] internal void Register(ListItem item)
public bool DisableGutters { get; set; } {
_items.Add(item);
if (CanSelect && SelectedValue != null && object.Equals(item.Value, SelectedValue))
{
item.SetSelected(true);
_selectedItem = item;
SelectedItemChanged.InvokeAsync(item);
}
}
internal void Unregister(ListItem item)
{
_items.Remove(item);
}
internal void Register(List child)
{
_childLists.Add(child);
}
internal void Unregister(List child)
{
_childLists.Remove(child);
}
internal void SetSelectedValue(object value, bool force = false)
{
if ((!CanSelect || !Clickable) && !force)
return;
if (object.Equals(_selectedValue, value))
return;
_selectedValue = value;
SelectedValueChanged.InvokeAsync(value).AndForget();
_selectedItem = null; // <-- for now, we'll see which item matches the value below
foreach (var listItem in _items.ToArray())
{
var isSelected = value != null && object.Equals(value, listItem.Value);
listItem.SetSelected(isSelected);
if (isSelected)
_selectedItem = listItem;
}
foreach (var childList in _childLists.ToArray())
{
childList.SetSelectedValue(value);
if (childList.SelectedItem != null)
_selectedItem = childList.SelectedItem;
}
SelectedItemChanged.InvokeAsync(_selectedItem).AndForget();
ParentList?.SetSelectedValue(value);
}
#endregion
#region Content
/// <summary> /// <summary>
/// If true, will disable the list item if it has onclick. /// Child content of component.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.List.Behavior)] [Category(CategoryTypes.List.Behavior)]
public bool Disabled { get; set; } public RenderFragment ChildContent { get; set; }
/// <summary> /// <summary>
/// The current selected list item. /// The current selected list item.
@ -80,11 +108,6 @@ public partial class List : UIComponent, IDisposable
} }
} }
/// <summary>
/// Called whenever the selection changed
/// </summary>
[Parameter] public EventCallback<ListItem> SelectedItemChanged { get; set; }
/// <summary> /// <summary>
/// The current selected value. /// The current selected value.
/// Note: make the list Clickable for item selection to work. /// Note: make the list Clickable for item selection to work.
@ -99,12 +122,63 @@ public partial class List : UIComponent, IDisposable
SetSelectedValue(value, force: true); SetSelectedValue(value, force: true);
} }
} }
#endregion
#region Styling
protected string Classname =>
new CssBuilder("list")
.AddClass("list-padding", !DisablePadding)
.AddClass(AdditionalClassList)
.Build();
/// <summary> /// <summary>
/// Called whenever the selection changed /// The color of the selected List Item.
/// </summary> /// </summary>
[Parameter] public EventCallback<object> SelectedValueChanged { get; set; } [Parameter]
[Category(CategoryTypes.List.Appearance)]
public ThemeColor Color { get; set; } = ThemeColor.Primary;
/// <summary>
/// If true, vertical padding will be removed from the list.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public bool DisablePadding { get; set; }
/// <summary>
/// If true, compact vertical padding will be applied to all list items.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public bool Dense { get; set; }
/// <summary>
/// If true, the left and right padding is removed on all list items.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public bool DisableGutters { get; set; }
#endregion
#region Behavior
/// <summary>
/// If true, will disable the list item if it has onclick.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public bool Disabled { get; set; }
/// <summary>
/// Set true to make the list items clickable. This is also the precondition for list selection to work.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Selecting)]
public bool Clickable { get; set; }
#endregion
#region Lifecycle
protected override void OnInitialized() protected override void OnInitialized()
{ {
if (ParentList != null) if (ParentList != null)
@ -118,77 +192,18 @@ public partial class List : UIComponent, IDisposable
} }
} }
internal event Action ParametersChanged;
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
base.OnParametersSet(); base.OnParametersSet();
ParametersChanged?.Invoke(); ParametersChanged?.Invoke();
} }
private HashSet<ListItem> _items = new();
private HashSet<List> _childLists = new();
private ListItem _selectedItem;
private object _selectedValue;
internal void Register(ListItem item)
{
_items.Add(item);
if (CanSelect && SelectedValue != null && object.Equals(item.Value, SelectedValue))
{
item.SetSelected(true);
_selectedItem = item;
SelectedItemChanged.InvokeAsync(item);
}
}
internal void Unregister(ListItem item)
{
_items.Remove(item);
}
internal void Register(List child)
{
_childLists.Add(child);
}
internal void Unregister(List child)
{
_childLists.Remove(child);
}
internal void SetSelectedValue(object value, bool force = false)
{
if ((!CanSelect || !Clickable) && !force)
return;
if (object.Equals(_selectedValue, value))
return;
_selectedValue = value;
SelectedValueChanged.InvokeAsync(value).AndForget();
_selectedItem = null; // <-- for now, we'll see which item matches the value below
foreach (var listItem in _items.ToArray())
{
var isSelected = value != null && object.Equals(value, listItem.Value);
listItem.SetSelected(isSelected);
if (isSelected)
_selectedItem = listItem;
}
foreach (var childList in _childLists.ToArray())
{
childList.SetSelectedValue(value);
if (childList.SelectedItem != null)
_selectedItem = childList.SelectedItem;
}
SelectedItemChanged.InvokeAsync(_selectedItem).AndForget();
ParentList?.SetSelectedValue(value);
}
internal bool CanSelect { get; private set; }
public void Dispose() public void Dispose()
{ {
ParametersChanged = null; ParametersChanged = null;
ParentList?.Unregister(this); ParentList?.Unregister(this);
} }
#endregion
} }

@ -6,14 +6,14 @@
{ {
<div class="list-item-avatar"> <div class="list-item-avatar">
<Avatar Class="@AvatarClass"> <Avatar Class="@AvatarClass">
<Icon Icon="@Avatar" Color="@IconColor" Size="@IconSize" /> <Icon Glyph="@Avatar" Color="@IconColor" Size="@IconSize" />
</Avatar> </Avatar>
</div> </div>
} }
else if (!string.IsNullOrWhiteSpace(Icon)) else if (!string.IsNullOrWhiteSpace(Icon))
{ {
<div class="list-item-icon"> <div class="list-item-icon">
<Icon Icon="@Icon" Color="@IconColor" Size="@IconSize" /> <Icon Glyph="@Icon" Color="@IconColor" Size="@IconSize" />
</div> </div>
} }
<div class="list-item-text @(Inset? "list-item-text-inset" : "")"> <div class="list-item-text @(Inset? "list-item-text-inset" : "")">
@ -30,7 +30,7 @@
</div> </div>
@if (NestedList != null) @if (NestedList != null)
{ {
<Icon Icon="@($"{(_expanded ? ExpandLessIcon : ExpandMoreIcon)}")" Size="@IconSize" Color="@AdornmentColor" /> <Icon Glyph="@($"{(_expanded ? ExpandLessIcon : ExpandMoreIcon)}")" Size="@IconSize" Color="@AdornmentColor" />
} }
</div> </div>
@if (NestedList != null) @if (NestedList != null)

@ -9,23 +9,95 @@ namespace Connected.Components;
public partial class ListItem : UIComponent, IDisposable public partial class ListItem : UIComponent, IDisposable
{ {
protected string Classname => #region Variables
new CssBuilder("list-item")
.AddClass("list-item-dense", (Dense ?? List?.Dense) ?? false)
.AddClass("list-item-gutters", !DisableGutters && !(List?.DisableGutters == true))
.AddClass("list-item-clickable", List?.Clickable)
.AddClass("ripple", List?.Clickable == true && !DisableRipple && !Disabled)
.AddClass($"selected-item mud-{List?.Color.ToDescription()}-text mud-{List?.Color.ToDescription()}-hover", _selected && !Disabled)
.AddClass("list-item-disabled", Disabled)
.AddClass(AdditionalClassList)
.Build();
[Inject] protected NavigationManager UriHelper { get; set; } [Inject] protected NavigationManager UriHelper { get; set; }
[CascadingParameter] protected List List { get; set; } [CascadingParameter] protected List List { get; set; }
private bool _onClickHandlerPreventDefault = false; private bool _onClickHandlerPreventDefault = false;
private bool _disabled;
private bool _expanded;
private Typo _textTypo;
private bool _selected;
#endregion
#region Events
[Parameter]
public EventCallback<bool> ExpandedChanged { get; set; }
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public bool OnClickHandlerPreventDefault
{
get => _onClickHandlerPreventDefault;
set => _onClickHandlerPreventDefault = value;
}
/// <summary>
/// List click event.
/// </summary>
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
protected void OnClickHandler(MouseEventArgs ev)
{
if (Disabled)
return;
if (!_onClickHandlerPreventDefault)
{
if (NestedList != null)
{
Expanded = !Expanded;
}
else if (Href != null)
{
List?.SetSelectedValue(this.Value);
OnClick.InvokeAsync(ev);
UriHelper.NavigateTo(Href, ForceLoad);
}
else
{
List?.SetSelectedValue(this.Value);
OnClick.InvokeAsync(ev);
if (Command?.CanExecute(CommandParameter) ?? false)
{
Command.Execute(CommandParameter);
}
}
}
else
{
OnClick.InvokeAsync(ev);
}
}
private void OnListParametersChanged()
{
if ((Dense ?? List?.Dense) ?? false)
{
_textTypo = Typo.body2;
}
else if (!((Dense ?? List?.Dense) ?? false))
{
_textTypo = Typo.body1;
}
StateHasChanged();
}
internal void SetSelected(bool selected)
{
if (Disabled)
return;
if (_selected == selected)
return;
_selected = selected;
StateHasChanged();
}
#endregion
#region Content
/// <summary> /// <summary>
/// The text to display /// The text to display
/// </summary> /// </summary>
@ -45,18 +117,63 @@ public partial class ListItem : UIComponent, IDisposable
public string Avatar { get; set; } public string Avatar { get; set; }
/// <summary> /// <summary>
/// Link to a URL when clicked. /// Glyph to use if set.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.List.ClickAction)] [Category(CategoryTypes.List.Behavior)]
public string Href { get; set; } public string Icon { get; set; }
/// <summary> /// <summary>
/// If true, force browser to redirect outside component router-space. /// Custom expand less icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Expanding)]
public string ExpandLessIcon { get; set; } = Icons.Material.Filled.ExpandLess;
/// <summary>
/// Custom expand more icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Expanding)]
public string ExpandMoreIcon { get; set; } = Icons.Material.Filled.ExpandMore;
/// <summary>
/// Display content of this list item. If set, this overrides Text
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// Add child list items here to create a nested list.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment NestedList { get; set; }
#endregion
#region Styling
protected string Classname =>
new CssBuilder("list-item")
.AddClass("list-item-dense", (Dense ?? List?.Dense) ?? false)
.AddClass("list-item-gutters", !DisableGutters && !(List?.DisableGutters == true))
.AddClass("list-item-clickable", List?.Clickable)
.AddClass("ripple", List?.Clickable == true && !DisableRipple && !Disabled)
.AddClass($"selected-item mud-{List?.Color.ToDescription()}-text mud-{List?.Color.ToDescription()}-hover", _selected && !Disabled)
.AddClass("list-item-disabled", Disabled)
.AddClass(AdditionalClassList)
.Build();
/// <summary>
/// Link to a URL when clicked.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.List.ClickAction)] [Category(CategoryTypes.List.ClickAction)]
public bool ForceLoad { get; set; } public string Href { get; set; }
/// <summary> /// <summary>
/// Avatar CSS Class to apply if Avatar is set. /// Avatar CSS Class to apply if Avatar is set.
@ -65,7 +182,7 @@ public partial class ListItem : UIComponent, IDisposable
[Category(CategoryTypes.List.Appearance)] [Category(CategoryTypes.List.Appearance)]
public string AvatarClass { get; set; } public string AvatarClass { get; set; }
private bool _disabled;
/// <summary> /// <summary>
/// If true, will disable the list item if it has onclick. /// If true, will disable the list item if it has onclick.
/// The value can be overridden by the parent list. /// The value can be overridden by the parent list.
@ -85,13 +202,6 @@ public partial class ListItem : UIComponent, IDisposable
[Category(CategoryTypes.List.Appearance)] [Category(CategoryTypes.List.Appearance)]
public bool DisableRipple { get; set; } public bool DisableRipple { get; set; }
/// <summary>
/// Glyph to use if set.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public string Icon { get; set; }
/// <summary> /// <summary>
/// The color of the icon. /// The color of the icon.
/// </summary> /// </summary>
@ -113,20 +223,6 @@ public partial class ListItem : UIComponent, IDisposable
[Category(CategoryTypes.List.Expanding)] [Category(CategoryTypes.List.Expanding)]
public ThemeColor AdornmentColor { get; set; } = ThemeColor.Default; public ThemeColor AdornmentColor { get; set; } = ThemeColor.Default;
/// <summary>
/// Custom expand less icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Expanding)]
public string ExpandLessIcon { get; set; } = Icons.Material.Filled.ExpandLess;
/// <summary>
/// Custom expand more icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Expanding)]
public string ExpandMoreIcon { get; set; } = Icons.Material.Filled.ExpandMore;
/// <summary> /// <summary>
/// If true, the List Subheader will be indented. /// If true, the List Subheader will be indented.
/// </summary> /// </summary>
@ -147,6 +243,15 @@ public partial class ListItem : UIComponent, IDisposable
[Parameter] [Parameter]
[Category(CategoryTypes.List.Appearance)] [Category(CategoryTypes.List.Appearance)]
public bool DisableGutters { get; set; } public bool DisableGutters { get; set; }
#endregion
#region Behavior
/// <summary>
/// If true, force browser to redirect outside component router-space.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.ClickAction)]
public bool ForceLoad { get; set; }
/// <summary> /// <summary>
/// Expand or collapse nested list. Two-way bindable. Note: if you directly set this to /// Expand or collapse nested list. Two-way bindable. Note: if you directly set this to
@ -166,11 +271,6 @@ public partial class ListItem : UIComponent, IDisposable
} }
} }
private bool _expanded;
[Parameter]
public EventCallback<bool> ExpandedChanged { get; set; }
/// <summary> /// <summary>
/// If true, expands the nested list on first display /// If true, expands the nested list on first display
/// </summary> /// </summary>
@ -191,66 +291,9 @@ public partial class ListItem : UIComponent, IDisposable
[Parameter] [Parameter]
[Category(CategoryTypes.List.ClickAction)] [Category(CategoryTypes.List.ClickAction)]
public ICommand Command { get; set; } public ICommand Command { get; set; }
#endregion
/// <summary> #region Lifecycle
/// Display content of this list item. If set, this overrides Text
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment ChildContent { get; set; }
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public bool OnClickHandlerPreventDefault
{
get => _onClickHandlerPreventDefault;
set => _onClickHandlerPreventDefault = value;
}
/// <summary>
/// Add child list items here to create a nested list.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment NestedList { get; set; }
/// <summary>
/// List click event.
/// </summary>
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
protected void OnClickHandler(MouseEventArgs ev)
{
if (Disabled)
return;
if (!_onClickHandlerPreventDefault)
{
if (NestedList != null)
{
Expanded = !Expanded;
}
else if (Href != null)
{
List?.SetSelectedValue(this.Value);
OnClick.InvokeAsync(ev);
UriHelper.NavigateTo(Href, ForceLoad);
}
else
{
List?.SetSelectedValue(this.Value);
OnClick.InvokeAsync(ev);
if (Command?.CanExecute(CommandParameter) ?? false)
{
Command.Execute(CommandParameter);
}
}
}
else
{
OnClick.InvokeAsync(ev);
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -263,32 +306,6 @@ public partial class ListItem : UIComponent, IDisposable
} }
} }
private Typo _textTypo;
private void OnListParametersChanged()
{
if ((Dense ?? List?.Dense) ?? false)
{
_textTypo = Typo.body2;
}
else if (!((Dense ?? List?.Dense) ?? false))
{
_textTypo = Typo.body1;
}
StateHasChanged();
}
private bool _selected;
internal void SetSelected(bool selected)
{
if (Disabled)
return;
if (_selected == selected)
return;
_selected = selected;
StateHasChanged();
}
public void Dispose() public void Dispose()
{ {
try try
@ -301,4 +318,7 @@ public partial class ListItem : UIComponent, IDisposable
catch (Exception) { /*ignore*/ } catch (Exception) { /*ignore*/ }
} }
#endregion
} }

@ -6,6 +6,7 @@ namespace Connected.Components;
public partial class ListSubheader : UIComponent public partial class ListSubheader : UIComponent
{ {
#region Styling
protected string Classname => protected string Classname =>
new CssBuilder("list-subheader") new CssBuilder("list-subheader")
.AddClass("list-subheader-gutters", !DisableGutters) .AddClass("list-subheader-gutters", !DisableGutters)
@ -13,10 +14,6 @@ public partial class ListSubheader : UIComponent
.AddClass(AdditionalClassList) .AddClass(AdditionalClassList)
.Build(); .Build();
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment ChildContent { get; set; }
[Parameter] [Parameter]
[Category(CategoryTypes.List.Appearance)] [Category(CategoryTypes.List.Appearance)]
public bool DisableGutters { get; set; } public bool DisableGutters { get; set; }
@ -24,4 +21,13 @@ public partial class ListSubheader : UIComponent
[Parameter] [Parameter]
[Category(CategoryTypes.List.Appearance)] [Category(CategoryTypes.List.Appearance)]
public bool Inset { get; set; } public bool Inset { get; set; }
#endregion
#region Content
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public RenderFragment ChildContent { get; set; }
#endregion
} }

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components;
namespace Connected.Components; namespace Connected.Components;
public partial class MainContent public partial class MainContent
{ {
#region Styling
private CssBuilder CompiledClassList private CssBuilder CompiledClassList
{ {
get get
@ -13,12 +14,16 @@ public partial class MainContent
} }
} }
[Parameter]
public RenderFragment ChildContent { get; set; }
/// <summary> /// <summary>
/// A space separated list of class names, added on top of the default class list. /// A space separated list of class names, added on top of the default class list.
/// </summary> /// </summary>
[Parameter] [Parameter]
public string? ClassList { get; set; } public string? ClassList { get; set; }
#endregion
#region Content
[Parameter]
public RenderFragment ChildContent { get; set; }
#endregion
} }

@ -54,9 +54,9 @@
@if (_showClearable && !Disabled) @if (_showClearable && !Disabled)
{ {
<IconButton Class="@ClearButtonClassname" <GlyphButton Class="@ClearButtonClassname"
Color="@ThemeColor.Default" Color="@ThemeColor.Default"
Icon="@ClearIcon" Glyph="@ClearIcon"
Size="@Size.Small" Size="@Size.Small"
Clicked="@HandleClearButton" /> Clicked="@HandleClearButton" />
} }

@ -14,425 +14,437 @@ namespace Connected.Components;
public partial class Mask : InputBase<string>, IDisposable public partial class Mask : InputBase<string>, IDisposable
{ {
public Mask() #region Variables
{ private ElementReference _elementReference;
TextUpdateSuppression = false; private ElementReference _elementReference1;
} private IJsEvent _jsEvent;
private IKeyInterceptor _keyInterceptor;
protected string Classname =>
new CssBuilder("input") [Inject] private IKeyInterceptorFactory _keyInterceptorFactory { get; set; }
.AddClass($"input-{Variant.ToDescription()}")
.AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) [Inject] private IJsEventFactory _jsEventFactory { get; set; }
.AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) [Inject] private IJsApiService _jsApiService { get; set; }
.AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
.AddClass("shrink", private string _elementId = "mask_" + Guid.NewGuid().ToString().Substring(0, 8);
when: () => !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start ||
!string.IsNullOrWhiteSpace(Placeholder)) private IMask _mask = new PatternMask("** **-** **");
.AddClass("disabled", Disabled)
.AddClass("input-error", HasErrors) private bool _showClearable;
.AddClass("ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone)
.AddClass(AdditionalClassList) private bool _updating;
.Build(); #endregion
protected string InputClassname => #region Events
new CssBuilder("input-slot") private void UpdateClearable(object value)
.AddClass("input-root") {
.AddClass($"input-root-{Variant.ToDescription()}") var showClearable = Clearable && !string.IsNullOrWhiteSpace(Text);
.AddClass($"input-root-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) if (_showClearable != showClearable)
.AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) _showClearable = showClearable;
.AddClass(AdditionalClassList) }
.Build();
/// <summary>
protected string AdornmentClassname => /// Button click event for clear button. Called after text and value has been cleared.
new CssBuilder("input-adornment") /// </summary>
.AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None) [Parameter]
.AddClass($"text", !string.IsNullOrEmpty(AdornmentText)) [Category(CategoryTypes.FormComponent.ListAppearance)]
.AddClass($"input-root-filled-shrink", Variant == Variant.Filled) public EventCallback<MouseEventArgs> OnClearButtonClick { get; set; }
.AddClass(AdditionalClassList) private async void HandleKeyDownInternally(KeyboardEventArgs args)
.Build(); {
await HandleKeyDown(args);
protected string ClearButtonClassname => }
new CssBuilder()
// .AddClass("me-n1", Adornment == Adornment.End && HideSpinButtons == false) protected internal async Task HandleKeyDown(KeyboardEventArgs e)
.AddClass("icon-button-edge-end", Adornment == Adornment.End) {
// .AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false) try
.AddClass("icon-button-edge-margin-end", Adornment != Adornment.End) {
.Build(); if ((e.CtrlKey && e.Key != "Backspace") || e.AltKey || ReadOnly)
return;
switch (e.Key)
private ElementReference _elementReference; {
private ElementReference _elementReference1; case "Backspace":
private IJsEvent _jsEvent; if (e.CtrlKey)
private IKeyInterceptor _keyInterceptor; {
MaskKind.Clear();
[Inject] private IKeyInterceptorFactory _keyInterceptorFactory { get; set; } await Update();
return;
[Inject] private IJsEventFactory _jsEventFactory { get; set; } }
[Inject] private IJsApiService _jsApiService { get; set; } MaskKind.Backspace();
await Update();
private string _elementId = "mask_" + Guid.NewGuid().ToString().Substring(0, 8); return;
case "Delete":
private IMask _mask = new PatternMask("** **-** **"); MaskKind.Delete();
await Update();
/// <summary> return;
/// ChildContent will only be displayed if InputType.Hidden and if its not null. Required for Select }
/// </summary>
[Parameter] if (Regex.IsMatch(e.Key, @"^.$"))
[Category(CategoryTypes.General.Appearance)] {
public RenderFragment ChildContent { get; set; } MaskKind.Insert(e.Key);
await Update();
/// <summary> }
/// Provide a masking object. Built-in masks are PatternMask, MultiMask, RegexMask and BlockMask }
/// </summary> finally
[Parameter] {
[Category(CategoryTypes.General.Data)] // call user callback
public IMask MaskKind await OnKeyDown.InvokeAsync(e);
{ }
get => _mask; }
set => SetMask(value);
} private async Task Update()
{
/// <summary> var caret = MaskKind.CaretPos;
/// Type of the input element. It should be a valid HTML5 input type. var selection = MaskKind.Selection;
/// </summary> var text = MaskKind.Text;
[Parameter] var cleanText = MaskKind.GetCleanText();
[Category(CategoryTypes.FormComponent.ListAppearance)] _updating = true;
public InputType InputType { get; set; } = InputType.Text; try
{
/// <summary> await base.SetTextAsync(text, updateValue: false);
/// Show clear button. if (Clearable)
/// </summary> UpdateClearable(Text);
[Parameter] var v = Converter.ConvertBack(cleanText);
[Category(CategoryTypes.FormComponent.ListBehavior)] Value = v;
public bool Clearable { get; set; } = false; await ValueChanged.InvokeAsync(v);
SetCaretPosition(caret, selection);
private bool _showClearable; }
finally
private void UpdateClearable(object value) {
{ _updating = false;
var showClearable = Clearable && !string.IsNullOrWhiteSpace(Text); }
if (_showClearable != showClearable) }
_showClearable = showClearable;
} internal async void HandleClearButton(MouseEventArgs e)
{
/// <summary> MaskKind.Clear();
/// Button click event for clear button. Called after text and value has been cleared. await Update();
/// </summary> await _elementReference.FocusAsync();
[Parameter] await OnClearButtonClick.InvokeAsync(e);
[Category(CategoryTypes.FormComponent.ListAppearance)] }
public EventCallback<MouseEventArgs> OnClearButtonClick { get; set; }
protected override async Task UpdateTextPropertyAsync(bool updateValue)
/// <summary> {
/// Custom clear icon. // allow this only via changes from the outside
/// </summary> if (_updating)
[Parameter] return;
[Category(CategoryTypes.General.Appearance)] var text = Converter.Convert(Value);
public string ClearIcon { get; set; } = Icons.Material.Filled.Clear; var cleanText = MaskKind.GetCleanText();
if (cleanText == text || string.IsNullOrEmpty(cleanText) && string.IsNullOrEmpty(text))
protected override async Task OnInitializedAsync() return;
{ var maskText = MaskKind.Text;
if (Text != MaskKind.Text) MaskKind.SetText(text);
await SetTextAsync(MaskKind.Text, updateValue: false); if (maskText == MaskKind.Text)
await base.OnInitializedAsync(); return; // no change, stop update loop
} await Update();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{ protected override async Task UpdateValuePropertyAsync(bool updateText)
if (firstRender) {
{ // allow this only via changes from the outside
_jsEvent = _jsEventFactory.Create(); if (_updating)
return;
await _jsEvent.Connect(_elementId, var text = Text;
new JsEventOptions if (MaskKind.Text == text)
{ return;
//EnableLogging = true, var maskText = MaskKind.Text;
TargetClass = "input-slot", MaskKind.SetText(text);
TagName = "INPUT" if (maskText == MaskKind.Text)
}); return; // no change, stop update loop
_jsEvent.CaretPositionChanged += OnCaretPositionChanged; await Update();
_jsEvent.Paste += OnPaste; }
_jsEvent.Select += OnSelect;
internal override InputType GetInputType() => InputType;
_keyInterceptor = _keyInterceptorFactory.Create();
private string GetCounterText() => Counter == null
await _keyInterceptor.Connect(_elementId, new KeyInterceptorOptions() ? string.Empty
{ : (Counter == 0
//EnableLogging = true, ? (string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}")
TargetClass = "input-slot", : ((string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}") + $" / {Counter}"));
Keys =
{ /// <summary>
new KeyOptions /// Clear the text field.
{ /// </summary>
Key = " ", PreventDown = "key+none" /// <returns></returns>
}, //prevent scrolling page, toggle open/close public Task Clear()
{
MaskKind.Clear();
return Update();
}
public override ValueTask FocusAsync()
{
return _elementReference.FocusAsync();
}
public override ValueTask SelectAsync()
{
return _elementReference.SelectAsync();
}
public override ValueTask SelectRangeAsync(int pos1, int pos2)
{
return _elementReference.SelectRangeAsync(pos1, pos2);
}
internal void OnCopy()
{
var text = Text;
if (MaskKind.Selection != null)
{
(_, text, _) = BaseMask.SplitSelection(text, MaskKind.Selection.Value);
}
_jsApiService.CopyToClipboardAsync(text);
}
internal async void OnPaste(string text)
{
if (text == null || ReadOnly)
return;
MaskKind.Insert(text);
await Update();
}
public void OnSelect(int start, int end)
{
MaskKind.Selection = _selection = (start, end);
}
internal void OnFocused(FocusEventArgs obj)
{
_isFocused = true;
}
protected internal override void OnBlurred(FocusEventArgs obj)
{
base.OnBlurred(obj);
_isFocused = false;
}
private int _caret;
private (int, int)? _selection;
private void SetCaretPosition(int caret, (int, int)? selection = null, bool render = true)
{
if (!_isFocused)
return;
_caret = caret;
if (caret == 0)
_caret = 0;
_selection = selection;
if (selection == null)
{
_elementReference.SelectRangeAsync(caret, caret).AndForget();
}
else
{
var sel = selection.Value;
_elementReference.SelectRangeAsync(sel.Item1, sel.Item2).AndForget();
}
}
// from JS event
internal void OnCaretPositionChanged(int pos)
{
if (MaskKind.Selection != null)
{
// do not clear selection if pos change is at selection border
var sel = MaskKind.Selection.Value;
if (pos == sel.Item1 || pos == sel.Item2)
return;
}
if (pos == MaskKind.CaretPos)
return;
MaskKind.Selection = null;
MaskKind.CaretPos = pos;
}
private void SetMask(IMask other)
{
if (_mask == null || other == null || _mask?.GetType() != other?.GetType())
{
_mask = other;
if (_mask == null)
_mask = new PatternMask("null ********"); // warn the user that the mask parameter is missing
return;
}
// set new mask properties without loosing state
_mask.UpdateFrom(other);
}
private async void OnCut(ClipboardEventArgs obj)
{
if (ReadOnly)
return;
if (_selection != null)
MaskKind.Delete();
await Update();
}
#endregion
#region Content
/// <summary>
/// Custom clear icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.General.Appearance)]
public string ClearIcon { get; set; } = Icons.Material.Filled.Clear;
/// <summary>
/// ChildContent will only be displayed if InputType.Hidden and if its not null. Required for Select
/// </summary>
[Parameter]
[Category(CategoryTypes.General.Appearance)]
public RenderFragment ChildContent { get; set; }
#endregion
#region Styling
/// <summary>
/// Provide a masking object. Built-in masks are PatternMask, MultiMask, RegexMask and BlockMask
/// </summary>
[Parameter]
[Category(CategoryTypes.General.Data)]
public IMask MaskKind
{
get => _mask;
set => SetMask(value);
}
protected string Classname =>
new CssBuilder("input")
.AddClass($"input-{Variant.ToDescription()}")
.AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
.AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
.AddClass("shrink",
when: () => !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start ||
!string.IsNullOrWhiteSpace(Placeholder))
.AddClass("disabled", Disabled)
.AddClass("input-error", HasErrors)
.AddClass("ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone)
.AddClass(AdditionalClassList)
.Build();
protected string InputClassname =>
new CssBuilder("input-slot")
.AddClass("input-root")
.AddClass($"input-root-{Variant.ToDescription()}")
.AddClass($"input-root-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
.AddClass(AdditionalClassList)
.Build();
protected string AdornmentClassname =>
new CssBuilder("input-adornment")
.AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None)
.AddClass($"text", !string.IsNullOrEmpty(AdornmentText))
.AddClass($"input-root-filled-shrink", Variant == Variant.Filled)
.AddClass(AdditionalClassList)
.Build();
protected string ClearButtonClassname =>
new CssBuilder()
// .AddClass("me-n1", Adornment == Adornment.End && HideSpinButtons == false)
.AddClass("icon-button-edge-end", Adornment == Adornment.End)
// .AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false)
.AddClass("icon-button-edge-margin-end", Adornment != Adornment.End)
.Build();
#endregion
#region Behavior
/// <summary>
/// Show clear button.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.ListBehavior)]
public bool Clearable { get; set; } = false;
/// <summary>
/// Type of the input element. It should be a valid HTML5 input type.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.ListAppearance)]
public InputType InputType { get; set; } = InputType.Text;
#endregion
#region Lifecycle
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing == true)
{
_jsEvent?.Dispose();
if (_keyInterceptor != null)
{
_keyInterceptor.KeyDown -= HandleKeyDownInternally;
_keyInterceptor.Dispose();
}
_keyInterceptor?.Dispose();
}
}
public Mask()
{
TextUpdateSuppression = false;
}
protected override async Task OnInitializedAsync()
{
if (Text != MaskKind.Text)
await SetTextAsync(MaskKind.Text, updateValue: false);
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsEvent = _jsEventFactory.Create();
await _jsEvent.Connect(_elementId,
new JsEventOptions
{
//EnableLogging = true,
TargetClass = "input-slot",
TagName = "INPUT"
});
_jsEvent.CaretPositionChanged += OnCaretPositionChanged;
_jsEvent.Paste += OnPaste;
_jsEvent.Select += OnSelect;
_keyInterceptor = _keyInterceptorFactory.Create();
await _keyInterceptor.Connect(_elementId, new KeyInterceptorOptions()
{
//EnableLogging = true,
TargetClass = "input-slot",
Keys =
{
new KeyOptions
{
Key = " ", PreventDown = "key+none"
}, //prevent scrolling page, toggle open/close
new KeyOptions { Key = "ArrowUp", PreventDown = "key+none" }, // prevent scrolling page new KeyOptions { Key = "ArrowUp", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = "ArrowDown", PreventDown = "key+none" }, // prevent scrolling page new KeyOptions { Key = "ArrowDown", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = "PageUp", PreventDown = "key+none" }, // prevent scrolling page new KeyOptions { Key = "PageUp", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = "PageDown", PreventDown = "key+none" }, // prevent scrolling page new KeyOptions { Key = "PageDown", PreventDown = "key+none" }, // prevent scrolling page
new KeyOptions { Key = @"/^.$/", PreventDown = "key+none|key+shift" }, new KeyOptions { Key = @"/^.$/", PreventDown = "key+none|key+shift" },
new KeyOptions { Key = "/./", SubscribeDown = true }, new KeyOptions { Key = "/./", SubscribeDown = true },
new KeyOptions { Key = "Backspace", PreventDown = "key+none" }, new KeyOptions { Key = "Backspace", PreventDown = "key+none" },
new KeyOptions { Key = "Delete", PreventDown = "key+none" }, new KeyOptions { Key = "Delete", PreventDown = "key+none" },
}, },
}); });
_keyInterceptor.KeyDown += HandleKeyDownInternally; _keyInterceptor.KeyDown += HandleKeyDownInternally;
} }
if (_isFocused && MaskKind.Selection == null) if (_isFocused && MaskKind.Selection == null)
SetCaretPosition(MaskKind.CaretPos, _selection, render: false); SetCaretPosition(MaskKind.CaretPos, _selection, render: false);
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
#endregion
private async void HandleKeyDownInternally(KeyboardEventArgs args)
{
await HandleKeyDown(args);
}
protected internal async Task HandleKeyDown(KeyboardEventArgs e)
{
try
{
if ((e.CtrlKey && e.Key != "Backspace") || e.AltKey || ReadOnly)
return;
switch (e.Key)
{
case "Backspace":
if (e.CtrlKey)
{
MaskKind.Clear();
await Update();
return;
}
MaskKind.Backspace();
await Update();
return;
case "Delete":
MaskKind.Delete();
await Update();
return;
}
if (Regex.IsMatch(e.Key, @"^.$"))
{
MaskKind.Insert(e.Key);
await Update();
}
}
finally
{
// call user callback
await OnKeyDown.InvokeAsync(e);
}
}
private bool _updating;
private async Task Update()
{
var caret = MaskKind.CaretPos;
var selection = MaskKind.Selection;
var text = MaskKind.Text;
var cleanText = MaskKind.GetCleanText();
_updating = true;
try
{
await base.SetTextAsync(text, updateValue: false);
if (Clearable)
UpdateClearable(Text);
var v = Converter.ConvertBack(cleanText);
Value = v;
await ValueChanged.InvokeAsync(v);
SetCaretPosition(caret, selection);
}
finally
{
_updating = false;
}
}
internal async void HandleClearButton(MouseEventArgs e)
{
MaskKind.Clear();
await Update();
await _elementReference.FocusAsync();
await OnClearButtonClick.InvokeAsync(e);
}
protected override async Task UpdateTextPropertyAsync(bool updateValue)
{
// allow this only via changes from the outside
if (_updating)
return;
var text = Converter.Convert(Value);
var cleanText = MaskKind.GetCleanText();
if (cleanText == text || string.IsNullOrEmpty(cleanText) && string.IsNullOrEmpty(text))
return;
var maskText = MaskKind.Text;
MaskKind.SetText(text);
if (maskText == MaskKind.Text)
return; // no change, stop update loop
await Update();
}
protected override async Task UpdateValuePropertyAsync(bool updateText)
{
// allow this only via changes from the outside
if (_updating)
return;
var text = Text;
if (MaskKind.Text == text)
return;
var maskText = MaskKind.Text;
MaskKind.SetText(text);
if (maskText == MaskKind.Text)
return; // no change, stop update loop
await Update();
}
internal override InputType GetInputType() => InputType;
private string GetCounterText() => Counter == null
? string.Empty
: (Counter == 0
? (string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}")
: ((string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}") + $" / {Counter}"));
/// <summary>
/// Clear the text field.
/// </summary>
/// <returns></returns>
public Task Clear()
{
MaskKind.Clear();
return Update();
}
public override ValueTask FocusAsync()
{
return _elementReference.FocusAsync();
}
public override ValueTask SelectAsync()
{
return _elementReference.SelectAsync();
}
public override ValueTask SelectRangeAsync(int pos1, int pos2)
{
return _elementReference.SelectRangeAsync(pos1, pos2);
}
internal void OnCopy()
{
var text = Text;
if (MaskKind.Selection != null)
{
(_, text, _) = BaseMask.SplitSelection(text, MaskKind.Selection.Value);
}
_jsApiService.CopyToClipboardAsync(text);
}
internal async void OnPaste(string text)
{
if (text == null || ReadOnly)
return;
MaskKind.Insert(text);
await Update();
}
public void OnSelect(int start, int end)
{
MaskKind.Selection = _selection = (start, end);
}
internal void OnFocused(FocusEventArgs obj)
{
_isFocused = true;
}
protected internal override void OnBlurred(FocusEventArgs obj)
{
base.OnBlurred(obj);
_isFocused = false;
}
private int _caret;
private (int, int)? _selection;
private void SetCaretPosition(int caret, (int, int)? selection = null, bool render = true)
{
if (!_isFocused)
return;
_caret = caret;
if (caret == 0)
_caret = 0;
_selection = selection;
if (selection == null)
{
_elementReference.SelectRangeAsync(caret, caret).AndForget();
}
else
{
var sel = selection.Value;
_elementReference.SelectRangeAsync(sel.Item1, sel.Item2).AndForget();
}
}
// from JS event
internal void OnCaretPositionChanged(int pos)
{
if (MaskKind.Selection != null)
{
// do not clear selection if pos change is at selection border
var sel = MaskKind.Selection.Value;
if (pos == sel.Item1 || pos == sel.Item2)
return;
}
if (pos == MaskKind.CaretPos)
return;
MaskKind.Selection = null;
MaskKind.CaretPos = pos;
}
private void SetMask(IMask other)
{
if (_mask == null || other == null || _mask?.GetType() != other?.GetType())
{
_mask = other;
if (_mask == null)
_mask = new PatternMask("null ********"); // warn the user that the mask parameter is missing
return;
}
// set new mask properties without loosing state
_mask.UpdateFrom(other);
}
private async void OnCut(ClipboardEventArgs obj)
{
if (ReadOnly)
return;
if (_selection != null)
MaskKind.Delete();
await Update();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing == true)
{
_jsEvent?.Dispose();
if (_keyInterceptor != null)
{
_keyInterceptor.KeyDown -= HandleKeyDownInternally;
_keyInterceptor.Dispose();
}
_keyInterceptor?.Dispose();
}
}
} }

@ -33,7 +33,7 @@
} }
else else
{ {
<IconButton Variant="@Variant" Icon="@Icon" Color="@Color" Size="@Size" Disabled="@Disabled" DisableRipple="@DisableRipple" DisableElevation="@DisableElevation" Clicked="@ToggleMenu" @ontouchend="@(ActivationEvent == MouseEvent.RightClick ? ToggleMenuTouch : null)" @oncontextmenu="@(ActivationEvent==MouseEvent.RightClick ? ToggleMenu : null)" /> <GlyphButton Variant="@Variant" Glyph="@Icon" Color="@Color" Size="@Size" Disabled="@Disabled" DisableRipple="@DisableRipple" DisableElevation="@DisableElevation" Clicked="@ToggleMenu" @ontouchend="@(ActivationEvent == MouseEvent.RightClick ? ToggleMenuTouch : null)" @oncontextmenu="@(ActivationEvent==MouseEvent.RightClick ? ToggleMenu : null)" />
} }
@* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@ @* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@
<Popover Open="@_isOpen" Class="@PopoverClass" MaxHeight="@MaxHeight" AnchorOrigin="@AnchorOrigin" TransformOrigin="@TransformOrigin" RelativeWidth="@FullWidth" Style="@PopoverStyle"> <Popover Open="@_isOpen" Class="@PopoverClass" MaxHeight="@MaxHeight" AnchorOrigin="@AnchorOrigin" TransformOrigin="@TransformOrigin" RelativeWidth="@FullWidth" Style="@PopoverStyle">

@ -9,36 +9,115 @@ namespace Connected.Components;
public partial class Menu : UIComponent, IActivatable public partial class Menu : UIComponent, IActivatable
{ {
protected string Classname => #region Variables
new CssBuilder("menu")
.AddClass(AdditionalClassList)
.Build();
protected string ActivatorClassname =>
new CssBuilder("menu-activator")
.AddClass("disabled", Disabled)
.Build();
private bool _isOpen; private bool _isOpen;
private bool _isMouseOver = false; private bool _isMouseOver = false;
[Parameter] public string PopoverStyle { get; set; }
[Category(CategoryTypes.Menu.Behavior)] #endregion
public string Label { get; set; }
#region Events
/// <summary> /// <summary>
/// User class names for the list, separated by space /// Specify the activation event when ActivatorContent is set
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.Behavior)]
public string ListClass { get; set; } public MouseEvent ActivationEvent { get; set; } = MouseEvent.LeftClick;
public void CloseMenu()
{
_isOpen = false;
_isMouseOver = false;
PopoverStyle = null;
StateHasChanged();
}
public void OpenMenu(EventArgs args)
{
if (Disabled)
return;
if (PositionAtCursor) SetPopoverStyle((MouseEventArgs)args);
_isOpen = true;
StateHasChanged();
}
// Sets the popover style ONLY when there is an activator
private void SetPopoverStyle(MouseEventArgs args)
{
AnchorOrigin = Origin.TopLeft;
PopoverStyle = $"margin-top: {args?.OffsetY.ToPx()}; margin-left: {args?.OffsetX.ToPx()};";
}
public void ToggleMenu(MouseEventArgs args)
{
if (Disabled)
return;
if (ActivationEvent == MouseEvent.LeftClick && args.Button != 0 && !_isOpen)
return;
if (ActivationEvent == MouseEvent.RightClick && args.Button != 2 && !_isOpen)
return;
if (_isOpen)
CloseMenu();
else
OpenMenu(args);
}
public void ToggleMenuTouch(TouchEventArgs args)
{
if (Disabled)
{
return;
}
if (_isOpen)
{
CloseMenu();
}
else
{
OpenMenu(args);
}
}
/// <summary> /// <summary>
/// User class names for the popover, separated by space /// Implementation of IActivatable.Activate, toggles the menu.
/// </summary> /// </summary>
/// <param name="activator"></param>
/// <param name="args"></param>
public void Activate(object activator, MouseEventArgs args)
{
ToggleMenu(args);
}
public void MouseEnter(MouseEventArgs args)
{
_isMouseOver = true;
if (ActivationEvent == MouseEvent.MouseOver)
{
OpenMenu(args);
}
}
public async void MouseLeave()
{
_isMouseOver = false;
await Task.Delay(100);
if (ActivationEvent == MouseEvent.MouseOver && _isMouseOver == false)
{
CloseMenu();
}
}
#endregion
#region Content
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.Behavior)]
public string PopoverClass { get; set; } public string Label { get; set; }
/// <summary> /// <summary>
/// Glyph to use if set will turn the button into a MudIconButton. /// Glyph to use if set will turn the button into a MudIconButton.
@ -47,13 +126,6 @@ public partial class Menu : UIComponent, IActivatable
[Category(CategoryTypes.Menu.Behavior)] [Category(CategoryTypes.Menu.Behavior)]
public string Icon { get; set; } public string Icon { get; set; }
/// <summary>
/// The color of the icon. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.Appearance)]
public ThemeColor IconColor { get; set; } = ThemeColor.Inherit;
/// <summary> /// <summary>
/// Glyph placed before the text if set. /// Glyph placed before the text if set.
/// </summary> /// </summary>
@ -69,118 +141,115 @@ public partial class Menu : UIComponent, IActivatable
public string EndIcon { get; set; } public string EndIcon { get; set; }
/// <summary> /// <summary>
/// The color of the button. It supports the theme colors. /// Place a MudButton, a MudIconButton or any other component capable of acting as an activator. This will
/// override the standard button and all parameters which concern it.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.Appearance)] [Category(CategoryTypes.Menu.Behavior)]
public ThemeColor Color { get; set; } = ThemeColor.Default; public RenderFragment ActivatorContent { get; set; }
/// <summary> /// <summary>
/// The button Size of the component. /// Add menu items here
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.Appearance)] [Category(CategoryTypes.Menu.PopupBehavior)]
public Size Size { get; set; } = Size.Medium; public RenderFragment ChildContent { get; set; }
#endregion
#region Styling
/// <summary> /// <summary>
/// The button variant to use. /// If true, the select menu will open either before or after the input depending on the direction.
/// </summary> /// </summary>
[Parameter] [ExcludeFromCodeCoverage]
[Category(CategoryTypes.Menu.Appearance)] [Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
public Variant Variant { get; set; } = Variant.Text; [Parameter] public bool OffsetY { get; set; }
/// <summary> /// <summary>
/// If true, compact vertical padding will be applied to all menu items. /// If true, the select menu will open either above or bellow the input depending on the direction.
/// </summary> /// </summary>
[Parameter] [ExcludeFromCodeCoverage]
[Category(CategoryTypes.Menu.PopupAppearance)] [Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
public bool Dense { get; set; } [Parameter] public bool OffsetX { get; set; }
protected string Classname =>
new CssBuilder("menu")
.AddClass(AdditionalClassList)
.Build();
protected string ActivatorClassname =>
new CssBuilder("menu-activator")
.AddClass("disabled", Disabled)
.Build();
/// <summary> /// <summary>
/// If true, the list menu will be same width as the parent. /// User class names for the list, separated by space
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.PopupAppearance)]
public bool FullWidth { get; set; } public string ListClass { get; set; }
/// <summary> /// <summary>
/// Sets the maxheight the menu can have when open. /// User class names for the popover, separated by space
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.PopupAppearance)]
public int? MaxHeight { get; set; } public string PopoverClass { get; set; }
/// <summary> /// <summary>
/// If true, instead of positioning the menu at the left upper corner, position at the exact cursor location. /// The color of the icon. It supports the theme colors.
/// This makes sense for larger activators
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupBehavior)] [Category(CategoryTypes.Menu.Appearance)]
public bool PositionAtCursor { get; set; } public ThemeColor IconColor { get; set; } = ThemeColor.Inherit;
/// <summary> /// <summary>
/// If true, instead of positioning the menu at the left upper corner, position at the exact cursor location. /// The color of the button. It supports the theme colors.
/// This makes sense for larger activators
/// </summary> /// </summary>
[Obsolete("Use PositionAtCursor instead.", true)]
[Parameter] [Parameter]
public bool PositionAtCurser [Category(CategoryTypes.Menu.Appearance)]
{ public ThemeColor Color { get; set; } = ThemeColor.Default;
get => PositionAtCursor;
set => PositionAtCursor = value;
}
/// <summary> /// <summary>
/// Place a MudButton, a MudIconButton or any other component capable of acting as an activator. This will /// The button Size of the component.
/// override the standard button and all parameters which concern it.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.Behavior)] [Category(CategoryTypes.Menu.Appearance)]
public RenderFragment ActivatorContent { get; set; } public Size Size { get; set; } = Size.Medium;
/// <summary> /// <summary>
/// Specify the activation event when ActivatorContent is set /// The button variant to use.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.Behavior)] [Category(CategoryTypes.Menu.Appearance)]
public MouseEvent ActivationEvent { get; set; } = MouseEvent.LeftClick; public Variant Variant { get; set; } = Variant.Text;
/// <summary> /// <summary>
/// Set the anchor origin point to determen where the popover will open from. /// If true, compact vertical padding will be applied to all menu items.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.PopupAppearance)]
public Origin AnchorOrigin { get; set; } = Origin.TopLeft; public bool Dense { get; set; }
/// <summary> /// <summary>
/// Sets the transform origin point for the popover. /// If true, the list menu will be same width as the parent.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.PopupAppearance)]
public Origin TransformOrigin { get; set; } = Origin.TopLeft; public bool FullWidth { get; set; }
/// <summary>
/// Sets the direction the select menu will start from relative to its parent.
/// </summary>
[ExcludeFromCodeCoverage]
[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
[Parameter] public Direction Direction { get; set; } = Direction.Bottom;
/// <summary>
/// If true, the select menu will open either before or after the input depending on the direction.
/// </summary>
[ExcludeFromCodeCoverage]
[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
[Parameter] public bool OffsetY { get; set; }
/// <summary> /// <summary>
/// If true, the select menu will open either above or bellow the input depending on the direction. /// Sets the maxheight the menu can have when open.
/// </summary> /// </summary>
[ExcludeFromCodeCoverage] [Parameter]
[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)] [Category(CategoryTypes.Menu.PopupAppearance)]
[Parameter] public bool OffsetX { get; set; } public int? MaxHeight { get; set; }
#endregion
#region Behavior
/// <summary> /// <summary>
/// Set to true if you want to prevent page from scrolling when the menu is open /// Set to true if you want to prevent page from scrolling when the menu is open
/// </summary> /// </summary>
@ -209,127 +278,35 @@ public partial class Menu : UIComponent, IActivatable
[Category(CategoryTypes.Menu.Appearance)] [Category(CategoryTypes.Menu.Appearance)]
public bool DisableElevation { get; set; } public bool DisableElevation { get; set; }
#region Obsolete members from previous MudButtonBase inherited structure
[ExcludeFromCodeCoverage]
[Obsolete("Linking is not supported. MudMenu is not a MudBaseButton anymore.", true)]
[Parameter] public string Link { get; set; }
[ExcludeFromCodeCoverage]
[Obsolete("Linking is not supported. MudMenu is not a MudBaseButton anymore.", true)]
[Parameter] public string Target { get; set; }
[ExcludeFromCodeCoverage]
[Obsolete("MudMenu is not a MudBaseButton anymore.", true)]
[Parameter] public string HtmlTag { get; set; } = "button";
[ExcludeFromCodeCoverage]
[Obsolete("MudMenu is not a MudBaseButton anymore.", true)]
[Parameter] public ButtonType ButtonType { get; set; }
[ExcludeFromCodeCoverage]
[Obsolete("MudMenu is not a MudBaseButton anymore.", true)]
[Parameter] public ICommand Command { get; set; }
[ExcludeFromCodeCoverage]
[Obsolete("MudMenu is not a MudBaseButton anymore.", true)]
[Parameter] public object CommandParameter { get; set; }
#endregion
/// <summary> /// <summary>
/// Add menu items here /// If true, instead of positioning the menu at the left upper corner, position at the exact cursor location.
/// This makes sense for larger activators
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupBehavior)] [Category(CategoryTypes.Menu.PopupBehavior)]
public RenderFragment ChildContent { get; set; } public bool PositionAtCursor { get; set; }
public string PopoverStyle { get; set; }
public void CloseMenu()
{
_isOpen = false;
_isMouseOver = false;
PopoverStyle = null;
StateHasChanged();
}
public void OpenMenu(EventArgs args)
{
if (Disabled)
return;
if (PositionAtCursor) SetPopoverStyle((MouseEventArgs)args);
_isOpen = true;
StateHasChanged();
}
// Sets the popover style ONLY when there is an activator
private void SetPopoverStyle(MouseEventArgs args)
{
AnchorOrigin = Origin.TopLeft;
PopoverStyle = $"margin-top: {args?.OffsetY.ToPx()}; margin-left: {args?.OffsetX.ToPx()};";
}
public void ToggleMenu(MouseEventArgs args)
{
if (Disabled)
return;
if (ActivationEvent == MouseEvent.LeftClick && args.Button != 0 && !_isOpen)
return;
if (ActivationEvent == MouseEvent.RightClick && args.Button != 2 && !_isOpen)
return;
if (_isOpen)
CloseMenu();
else
OpenMenu(args);
}
public void ToggleMenuTouch(TouchEventArgs args)
{
if (Disabled)
{
return;
}
if (_isOpen)
{
CloseMenu();
}
else
{
OpenMenu(args);
}
}
/// <summary> /// <summary>
/// Implementation of IActivatable.Activate, toggles the menu. /// Set the anchor origin point to determen where the popover will open from.
/// </summary> /// </summary>
/// <param name="activator"></param> [Parameter]
/// <param name="args"></param> [Category(CategoryTypes.Menu.PopupAppearance)]
public void Activate(object activator, MouseEventArgs args) public Origin AnchorOrigin { get; set; } = Origin.TopLeft;
{
ToggleMenu(args);
}
public void MouseEnter(MouseEventArgs args)
{
_isMouseOver = true;
if (ActivationEvent == MouseEvent.MouseOver)
{
OpenMenu(args);
}
}
public async void MouseLeave() /// <summary>
{ /// Sets the transform origin point for the popover.
_isMouseOver = false; /// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public Origin TransformOrigin { get; set; } = Origin.TopLeft;
await Task.Delay(100); /// <summary>
/// Sets the direction the select menu will start from relative to its parent.
/// </summary>
[ExcludeFromCodeCoverage]
[Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)]
[Parameter] public Direction Direction { get; set; } = Direction.Bottom;
#endregion
if (ActivationEvent == MouseEvent.MouseOver && _isMouseOver == false)
{
CloseMenu();
}
}
} }

@ -7,53 +7,15 @@ namespace Connected.Components;
public partial class MenuItem : UIComponent public partial class MenuItem : UIComponent
{ {
#region Variables
[CascadingParameter] public Menu Menu { get; set; } [CascadingParameter] public Menu Menu { get; set; }
[Parameter][Category(CategoryTypes.Menu.Behavior)] public RenderFragment ChildContent { get; set; } [Parameter][Category(CategoryTypes.Menu.Behavior)] public RenderFragment ChildContent { get; set; }
[Parameter][Category(CategoryTypes.Menu.Behavior)] public bool Disabled { get; set; } [Parameter][Category(CategoryTypes.Menu.Behavior)] public bool Disabled { get; set; }
[Inject] public NavigationManager UriHelper { get; set; } [Inject] public NavigationManager UriHelper { get; set; }
[Inject] public IJsApiService JsApiService { get; set; } [Inject] public IJsApiService JsApiService { get; set; }
#endregion
/// <summary> #region Events
/// If set to a URL, clicking the button will open the referenced document. Use Target to specify where (Obsolete replaced by Href)
/// </summary>
[Obsolete("Use Href Instead.", false)]
[Parameter]
[Category(CategoryTypes.Menu.ClickAction)]
public string Link { get => Href; set => Href = value; }
/// <summary>
/// If set to a URL, clicking the button will open the referenced document. Use Target to specify where
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.ClickAction)]
public string Href { get; set; }
/// <summary>
/// Glyph to be used for this menu entry
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public string Icon { get; set; }
/// <summary>
/// The color of the icon. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public ThemeColor IconColor { get; set; } = ThemeColor.Inherit;
/// <summary>
/// The Glyph Size.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public Size IconSize { get; set; } = Size.Medium;
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public string Target { get; set; }
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public bool ForceLoad { get; set; }
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public ICommand Command { get; set; }
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public object CommandParameter { get; set; }
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; } [Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter] public EventCallback<TouchEventArgs> OnTouch { get; set; } [Parameter] public EventCallback<TouchEventArgs> OnTouch { get; set; }
@ -102,4 +64,53 @@ public partial class MenuItem : UIComponent
} }
} }
} }
#endregion
#region Content
/// <summary>
/// If set to a URL, clicking the button will open the referenced document. Use Target to specify where (Obsolete replaced by Href)
/// </summary>
[Obsolete("Use Href Instead.", false)]
[Parameter]
[Category(CategoryTypes.Menu.ClickAction)]
public string Link { get => Href; set => Href = value; }
/// <summary>
/// If set to a URL, clicking the button will open the referenced document. Use Target to specify where
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.ClickAction)]
public string Href { get; set; }
/// <summary>
/// Glyph to be used for this menu entry
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public string Icon { get; set; }
#endregion
#region Styling
/// <summary>
/// The color of the icon. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public ThemeColor IconColor { get; set; } = ThemeColor.Inherit;
/// <summary>
/// The Glyph Size.
/// </summary>
[Parameter]
[Category(CategoryTypes.List.Appearance)]
public Size IconSize { get; set; } = Size.Medium;
#endregion
#region Behavior
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public string Target { get; set; }
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public bool ForceLoad { get; set; }
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public ICommand Command { get; set; }
[Parameter][Category(CategoryTypes.Menu.ClickAction)] public object CommandParameter { get; set; }
#endregion
} }

@ -6,17 +6,93 @@ namespace Connected.Components;
public partial class MessageBox : UIComponent public partial class MessageBox : UIComponent
{ {
#region Variables
[Inject] private IDialogService DialogService { get; set; } [Inject] private IDialogService DialogService { get; set; }
[CascadingParameter] private DialogInstance DialogInstance { get; set; } [CascadingParameter] private DialogInstance DialogInstance { get; set; }
private bool _isVisible;
private bool IsInline => DialogInstance == null;
private IDialogReference _reference;
private ActivatableCallback _yesCallback, _cancelCallback, _noCallback;
#endregion
#region Events
/// <summary> /// <summary>
/// The message box title. If null or empty, title will be hidden /// Fired when the yes button is clicked
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.MessageBox.Behavior)] public EventCallback<bool> OnYes { get; set; }
public string Title { get; set; }
/// <summary>
/// Fired when the no button is clicked
/// </summary>
[Parameter]
public EventCallback<bool> OnNo { get; set; }
/// <summary>
/// Fired when the cancel button is clicked or the msg box was closed via the X
/// </summary>
[Parameter]
public EventCallback<bool> OnCancel { get; set; }
/// <summary>
/// Raised when the inline dialog's display status changes.
/// </summary>
[Parameter]
public EventCallback<bool> IsVisibleChanged { get; set; }
public async Task<bool?> Show(DialogOptions options = null)
{
if (DialogService == null)
return null;
var parameters = new DialogParameters()
{
[nameof(Title)] = Title,
[nameof(TitleContent)] = TitleContent,
[nameof(Message)] = Message,
[nameof(MarkupMessage)] = MarkupMessage,
[nameof(MessageContent)] = MessageContent,
[nameof(CancelText)] = CancelText,
[nameof(CancelButton)] = CancelButton,
[nameof(NoText)] = NoText,
[nameof(NoButton)] = NoButton,
[nameof(YesText)] = YesText,
[nameof(YesButton)] = YesButton,
};
_reference = await DialogService.ShowAsync<MessageBox>(parameters: parameters, options: options, title: Title);
var result = await _reference.Result;
if (result.Cancelled || result.Data is not bool data)
return null;
return data;
}
public void Close()
{
_reference?.Close();
}
private void OnYesActivated(object arg1, MouseEventArgs arg2) => OnYesClicked();
private void OnNoActivated(object arg1, MouseEventArgs arg2) => OnNoClicked();
private void OnCancelActivated(object arg1, MouseEventArgs arg2) => OnCancelClicked();
private void OnYesClicked() => DialogInstance.Close(DialogResult.Ok(true));
private void OnNoClicked() => DialogInstance.Close(DialogResult.Ok(false));
private void OnCancelClicked() => DialogInstance.Close(DialogResult.Cancel());
private void HandleKeyDown(KeyboardEventArgs args)
{
if (args.Key == "Escape")
{
OnCancelClicked();
}
}
#endregion
#region Content
/// <summary> /// <summary>
/// Define the message box title as a renderfragment (overrides GlyphTitle) /// Define the message box title as a renderfragment (overrides GlyphTitle)
/// </summary> /// </summary>
@ -25,26 +101,32 @@ public partial class MessageBox : UIComponent
public RenderFragment TitleContent { get; set; } public RenderFragment TitleContent { get; set; }
/// <summary> /// <summary>
/// The message box message as string. /// Define the message box body as a renderfragment (overrides Message)
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.MessageBox.Behavior)] [Category(CategoryTypes.MessageBox.Behavior)]
public string Message { get; set; } public RenderFragment MessageContent { get; set; }
/// <summary> /// <summary>
/// The message box message as markup string. /// The message box title. If null or empty, title will be hidden
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.MessageBox.Behavior)] [Category(CategoryTypes.MessageBox.Behavior)]
public MarkupString MarkupMessage { get; set; } public string Title { get; set; }
/// <summary> /// <summary>
/// Define the message box body as a renderfragment (overrides Message) /// The message box message as string.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.MessageBox.Behavior)] [Category(CategoryTypes.MessageBox.Behavior)]
public RenderFragment MessageContent { get; set; } public string Message { get; set; }
/// <summary>
/// The message box message as markup string.
/// </summary>
[Parameter]
[Category(CategoryTypes.MessageBox.Behavior)]
public MarkupString MarkupMessage { get; set; }
/// <summary> /// <summary>
/// Text of the cancel button. Leave null to hide the button. /// Text of the cancel button. Leave null to hide the button.
@ -90,25 +172,9 @@ public partial class MessageBox : UIComponent
[Parameter] [Parameter]
[Category(CategoryTypes.MessageBox.Behavior)] [Category(CategoryTypes.MessageBox.Behavior)]
public RenderFragment YesButton { get; set; } public RenderFragment YesButton { get; set; }
#endregion
/// <summary> #region Behavior
/// Fired when the yes button is clicked
/// </summary>
[Parameter]
public EventCallback<bool> OnYes { get; set; }
/// <summary>
/// Fired when the no button is clicked
/// </summary>
[Parameter]
public EventCallback<bool> OnNo { get; set; }
/// <summary>
/// Fired when the cancel button is clicked or the msg box was closed via the X
/// </summary>
[Parameter]
public EventCallback<bool> OnCancel { get; set; }
/// <summary> /// <summary>
/// Bind this two-way to show and close an inlined message box. Has no effect on opened msg boxes /// Bind this two-way to show and close an inlined message box. Has no effect on opened msg boxes
/// </summary> /// </summary>
@ -133,50 +199,9 @@ public partial class MessageBox : UIComponent
IsVisibleChanged.InvokeAsync(value); IsVisibleChanged.InvokeAsync(value);
} }
} }
#endregion
private bool _isVisible; #region Lifecycle
private bool IsInline => DialogInstance == null;
private IDialogReference _reference;
/// <summary>
/// Raised when the inline dialog's display status changes.
/// </summary>
[Parameter]
public EventCallback<bool> IsVisibleChanged { get; set; }
public async Task<bool?> Show(DialogOptions options = null)
{
if (DialogService == null)
return null;
var parameters = new DialogParameters()
{
[nameof(Title)] = Title,
[nameof(TitleContent)] = TitleContent,
[nameof(Message)] = Message,
[nameof(MarkupMessage)] = MarkupMessage,
[nameof(MessageContent)] = MessageContent,
[nameof(CancelText)] = CancelText,
[nameof(CancelButton)] = CancelButton,
[nameof(NoText)] = NoText,
[nameof(NoButton)] = NoButton,
[nameof(YesText)] = YesText,
[nameof(YesButton)] = YesButton,
};
_reference = await DialogService.ShowAsync<MessageBox>(parameters: parameters, options: options, title: Title);
var result = await _reference.Result;
if (result.Cancelled || result.Data is not bool data)
return null;
return data;
}
public void Close()
{
_reference?.Close();
}
private ActivatableCallback _yesCallback, _cancelCallback, _noCallback;
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
@ -187,25 +212,6 @@ public partial class MessageBox : UIComponent
if (CancelButton != null) if (CancelButton != null)
_cancelCallback = new ActivatableCallback() { ActivateCallback = OnCancelActivated }; _cancelCallback = new ActivatableCallback() { ActivateCallback = OnCancelActivated };
} }
#endregion
private void OnYesActivated(object arg1, MouseEventArgs arg2) => OnYesClicked();
private void OnNoActivated(object arg1, MouseEventArgs arg2) => OnNoClicked();
private void OnCancelActivated(object arg1, MouseEventArgs arg2) => OnCancelClicked();
private void OnYesClicked() => DialogInstance.Close(DialogResult.Ok(true));
private void OnNoClicked() => DialogInstance.Close(DialogResult.Ok(false));
private void OnCancelClicked() => DialogInstance.Close(DialogResult.Cancel());
private void HandleKeyDown(KeyboardEventArgs args)
{
if (args.Key == "Escape")
{
OnCancelClicked();
}
}
} }

@ -5,14 +5,14 @@
<button @onclick="ExpandedToggle" tabindex="0" class="@ButtonClassname"> <button @onclick="ExpandedToggle" tabindex="0" class="@ButtonClassname">
@if (!String.IsNullOrEmpty(Icon)) @if (!String.IsNullOrEmpty(Icon))
{ {
<Icon Icon="@Icon" Color="@IconColor" Class="@IconClassname" /> <Icon Glyph="@Icon" Color="@IconColor" Class="@IconClassname" />
} }
<div Class="nav-link-text"> <div Class="nav-link-text">
@Title @Title
</div> </div>
@if (!HideExpandIcon) @if (!HideExpandIcon)
{ {
<Icon Icon="@ExpandIcon" class="@ExpandIconClassname" /> <Icon Glyph="@ExpandIcon" class="@ExpandIconClassname" />
} }
</button> </button>
<Collapse Expanded="@Expanded" MaxHeight="@MaxHeight" Class="navgroup-collapse"> <Collapse Expanded="@Expanded" MaxHeight="@MaxHeight" Class="navgroup-collapse">

@ -6,111 +6,123 @@ namespace Connected.Components;
public partial class NavGroup : UIComponent public partial class NavGroup : UIComponent
{ {
protected string Classname => #region Variables
new CssBuilder("nav-group") private bool _expanded;
.AddClass(AdditionalClassList) #endregion
.AddClass($"nav-group-disabled", Disabled)
.Build(); #region Events
protected void ExpandedToggle()
protected string ButtonClassname => {
new CssBuilder("nav-link") _expanded = !Expanded;
.AddClass($"ripple", !DisableRipple) ExpandedChanged.InvokeAsync(_expanded);
.AddClass("expanded", Expanded) }
.Build(); [Parameter] public EventCallback<bool> ExpandedChanged { get; set; }
#endregion
protected string IconClassname =>
new CssBuilder("nav-link-icon") #region Content
.AddClass($"nav-link-icon-default", IconColor == ThemeColor.Default) [Parameter]
.Build(); [Category(CategoryTypes.NavMenu.Behavior)]
public string Title { get; set; }
protected string ExpandIconClassname =>
new CssBuilder("nav-link-expand-icon") /// <summary>
.AddClass($"transform", Expanded && !Disabled) /// Glyph to use if set.
.AddClass($"transform-disabled", Expanded && Disabled) /// </summary>
.Build(); [Parameter]
[Category(CategoryTypes.NavMenu.Behavior)]
[Parameter] public string Icon { get; set; }
[Category(CategoryTypes.NavMenu.Behavior)]
public string Title { get; set; } /// <summary>
/// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color.
/// <summary> /// </summary>
/// Glyph to use if set. [Parameter]
/// </summary> [Category(CategoryTypes.NavMenu.Appearance)]
[Parameter] public ThemeColor IconColor { get; set; } = ThemeColor.Default;
[Category(CategoryTypes.NavMenu.Behavior)]
public string Icon { get; set; } /// <summary>
/// If set, overrides the default expand icon.
/// <summary> /// </summary>
/// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color. [Parameter]
/// </summary> [Category(CategoryTypes.NavMenu.Appearance)]
[Parameter] public string ExpandIcon { get; set; } = @Icons.Filled.ArrowDropDown;
[Category(CategoryTypes.NavMenu.Appearance)] #endregion
public ThemeColor IconColor { get; set; } = ThemeColor.Default;
#region Styling
/// <summary> protected string Classname =>
/// If true, the button will be disabled. new CssBuilder("nav-group")
/// </summary> .AddClass(AdditionalClassList)
[Parameter] .AddClass($"nav-group-disabled", Disabled)
[Category(CategoryTypes.NavMenu.Behavior)] .Build();
public bool Disabled { get; set; }
protected string ButtonClassname =>
/// <summary> new CssBuilder("nav-link")
/// If true, disables ripple effect. .AddClass($"ripple", !DisableRipple)
/// </summary> .AddClass("expanded", Expanded)
[Parameter] .Build();
[Category(CategoryTypes.NavMenu.Appearance)]
public bool DisableRipple { get; set; } protected string IconClassname =>
new CssBuilder("nav-link-icon")
private bool _expanded; .AddClass($"nav-link-icon-default", IconColor == ThemeColor.Default)
/// <summary> .Build();
/// If true, expands the nav group, otherwise collapse it.
/// Two-way bindable protected string ExpandIconClassname =>
/// </summary> new CssBuilder("nav-link-expand-icon")
[Parameter] .AddClass($"transform", Expanded && !Disabled)
[Category(CategoryTypes.NavMenu.Behavior)] .AddClass($"transform-disabled", Expanded && Disabled)
public bool Expanded .Build();
{
get => _expanded; /// <summary>
set /// If true, hides expand-icon at the end of the NavGroup.
{ /// </summary>
if (_expanded == value) [Parameter]
return; [Category(CategoryTypes.NavMenu.Appearance)]
public bool HideExpandIcon { get; set; }
_expanded = value;
ExpandedChanged.InvokeAsync(_expanded); /// <summary>
} /// Explicitly sets the height for the Collapse element to override the css default.
} /// </summary>
[Parameter]
[Parameter] public EventCallback<bool> ExpandedChanged { get; set; } [Category(CategoryTypes.NavMenu.Appearance)]
public int? MaxHeight { get; set; }
/// <summary>
/// If true, hides expand-icon at the end of the NavGroup. [Parameter]
/// </summary> [Category(CategoryTypes.NavMenu.Behavior)]
[Parameter] public RenderFragment ChildContent { get; set; }
[Category(CategoryTypes.NavMenu.Appearance)] #endregion
public bool HideExpandIcon { get; set; }
#region Behavior
/// <summary> /// <summary>
/// Explicitly sets the height for the Collapse element to override the css default. /// If true, the button will be disabled.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.NavMenu.Appearance)] [Category(CategoryTypes.NavMenu.Behavior)]
public int? MaxHeight { get; set; } public bool Disabled { get; set; }
/// <summary> /// <summary>
/// If set, overrides the default expand icon. /// If true, disables ripple effect.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.NavMenu.Appearance)] [Category(CategoryTypes.NavMenu.Appearance)]
public string ExpandIcon { get; set; } = @Icons.Filled.ArrowDropDown; public bool DisableRipple { get; set; }
[Parameter]
[Category(CategoryTypes.NavMenu.Behavior)] /// <summary>
public RenderFragment ChildContent { get; set; } /// If true, expands the nav group, otherwise collapse it.
/// Two-way bindable
protected void ExpandedToggle() /// </summary>
{ [Parameter]
_expanded = !Expanded; [Category(CategoryTypes.NavMenu.Behavior)]
ExpandedChanged.InvokeAsync(_expanded); public bool Expanded
} {
get => _expanded;
set
{
if (_expanded == value)
return;
_expanded = value;
ExpandedChanged.InvokeAsync(_expanded);
}
}
#endregion
} }

@ -12,7 +12,7 @@
ActiveClass="@ActiveClass"> ActiveClass="@ActiveClass">
@if (!string.IsNullOrEmpty(Icon)) @if (!string.IsNullOrEmpty(Icon))
{ {
<Icon Icon="@Icon" Color="@IconColor" Class="@IconClassname"/> <Icon Glyph="@Icon" Color="@IconColor" Class="@IconClassname" />
} }
<div class="nav-link-text"> <div class="nav-link-text">
@ChildContent @ChildContent
@ -26,7 +26,7 @@
ActiveClass="@ActiveClass"> ActiveClass="@ActiveClass">
@if (!string.IsNullOrEmpty(Icon)) @if (!string.IsNullOrEmpty(Icon))
{ {
<Icon Icon="@Icon" Color="@IconColor" Class="@IconClassname" /> <Icon Glyph="@Icon" Color="@IconColor" Class="@IconClassname" />
} }
<div Class="nav-link-text"> <div Class="nav-link-text">
@ChildContent @ChildContent

@ -7,6 +7,34 @@ namespace Connected.Components;
public partial class NavLink : SelectItemBase public partial class NavLink : SelectItemBase
{ {
#region Events
[CascadingParameter] INavigationEventReceiver NavigationEventReceiver { get; set; }
protected Task HandleNavigation()
{
if (!Disabled && NavigationEventReceiver != null)
{
return NavigationEventReceiver.OnNavigation();
}
return Task.CompletedTask;
}
#endregion
#region Content
/// <summary>
/// Glyph to use if set.
/// </summary>
[Parameter]
[Category(CategoryTypes.NavMenu.Behavior)]
public string Icon { get; set; }
[Parameter]
[Category(CategoryTypes.NavMenu.ClickAction)]
public string Target { get; set; }
#endregion
#region Styling
protected string Classname => protected string Classname =>
new CssBuilder("nav-item") new CssBuilder("nav-item")
.AddClass($"ripple", !DisableRipple && !Disabled) .AddClass($"ripple", !DisableRipple && !Disabled)
@ -23,23 +51,6 @@ public partial class NavLink : SelectItemBase
.AddClass($"nav-link-icon-default", IconColor == ThemeColor.Default) .AddClass($"nav-link-icon-default", IconColor == ThemeColor.Default)
.Build(); .Build();
protected Dictionary<string, object> Attributes
{
get => Disabled ? null : new Dictionary<string, object>()
{
{ "href", Href },
{ "target", Target },
{ "rel", !string.IsNullOrWhiteSpace(Target) ? "noopener noreferrer" : string.Empty }
};
}
/// <summary>
/// Glyph to use if set.
/// </summary>
[Parameter]
[Category(CategoryTypes.NavMenu.Behavior)]
public string Icon { get; set; }
/// <summary> /// <summary>
/// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color. /// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color.
/// </summary> /// </summary>
@ -47,13 +58,9 @@ public partial class NavLink : SelectItemBase
[Category(CategoryTypes.NavMenu.Appearance)] [Category(CategoryTypes.NavMenu.Appearance)]
public ThemeColor IconColor { get; set; } = ThemeColor.Default; public ThemeColor IconColor { get; set; } = ThemeColor.Default;
[Parameter]
[Category(CategoryTypes.NavMenu.Behavior)]
public NavLinkMatch Match { get; set; } = NavLinkMatch.Prefix;
[Parameter]
[Category(CategoryTypes.NavMenu.ClickAction)]
public string Target { get; set; }
/// <summary> /// <summary>
/// User class names when active, separated by space. /// User class names when active, separated by space.
@ -61,15 +68,21 @@ public partial class NavLink : SelectItemBase
[Parameter] [Parameter]
[Category(CategoryTypes.ComponentBase.Common)] [Category(CategoryTypes.ComponentBase.Common)]
public string ActiveClass { get; set; } = "active"; public string ActiveClass { get; set; } = "active";
#endregion
[CascadingParameter] INavigationEventReceiver NavigationEventReceiver { get; set; } #region Behavior
protected Dictionary<string, object> Attributes
protected Task HandleNavigation()
{ {
if (!Disabled && NavigationEventReceiver != null) get => Disabled ? null : new Dictionary<string, object>()
{ {
return NavigationEventReceiver.OnNavigation(); { "href", Href },
} { "target", Target },
return Task.CompletedTask; { "rel", !string.IsNullOrWhiteSpace(Target) ? "noopener noreferrer" : string.Empty }
};
} }
[Parameter]
[Category(CategoryTypes.NavMenu.Behavior)]
public NavLinkMatch Match { get; set; } = NavLinkMatch.Prefix;
#endregion
} }

@ -7,6 +7,13 @@ namespace Connected.Components;
public partial class NavMenu : UIComponent public partial class NavMenu : UIComponent
{ {
#region Content
[Category(CategoryTypes.NavMenu.Behavior)]
[Parameter] public RenderFragment ChildContent { get; set; }
#endregion
#region Styling
protected string Classname => protected string Classname =>
new CssBuilder("navmenu") new CssBuilder("navmenu")
.AddClass($"navmenu-{Color.ToDescription()}") .AddClass($"navmenu-{Color.ToDescription()}")
@ -17,9 +24,6 @@ public partial class NavMenu : UIComponent
.AddClass(AdditionalClassList) .AddClass(AdditionalClassList)
.Build(); .Build();
[Category(CategoryTypes.NavMenu.Behavior)]
[Parameter] public RenderFragment ChildContent { get; set; }
/// <summary> /// <summary>
/// The color of the active NavLink. /// The color of the active NavLink.
/// </summary> /// </summary>
@ -54,5 +58,6 @@ public partial class NavMenu : UIComponent
[Parameter] [Parameter]
[Category(CategoryTypes.NavMenu.Appearance)] [Category(CategoryTypes.NavMenu.Appearance)]
public bool Dense { get; set; } public bool Dense { get; set; }
#endregion
} }

@ -8,165 +8,199 @@ namespace Connected.Components;
public partial class Overlay : UIComponent, IDisposable public partial class Overlay : UIComponent, IDisposable
{ {
private bool _visible; #region Variables
private bool _visible;
protected string Classname => [Inject] public IScrollManager ScrollManager { get; set; }
new CssBuilder("overlay") #endregion
.AddClass("overlay-absolute", Absolute)
.AddClass(AdditionalClassList) #region Events
.Build(); /// <summary>
/// Fires when Visible changes
protected string ScrimClassname => /// </summary>
new CssBuilder("overlay-scrim") [Parameter]
.AddClass("overlay-dark", DarkBackground) public EventCallback<bool> VisibleChanged { get; set; }
.AddClass("overlay-light", LightBackground)
.Build(); /// <summary>
/// Fired when the overlay is clicked
protected string Styles => /// </summary>
new StyleBuilder() [Parameter]
.AddStyle("z-index", $"{ZIndex}", ZIndex != 5) public EventCallback<MouseEventArgs> OnClick { get; set; }
.Build(); protected internal void OnClickHandler(MouseEventArgs ev)
{
[Inject] public IScrollManager ScrollManager { get; set; } if (AutoClose)
Visible = false;
/// <summary> OnClick.InvokeAsync(ev);
/// Child content of the component. if (Command?.CanExecute(CommandParameter) ?? false)
/// </summary> {
[Parameter] Command.Execute(CommandParameter);
[Category(CategoryTypes.Overlay.Behavior)] }
public RenderFragment ChildContent { get; set; } }
/// <summary> //locks the scroll attaching a CSS class to the specified element, in this case the body
/// Fires when Visible changes void BlockScroll()
/// </summary> {
[Parameter] ScrollManager.LockScrollAsync("body", LockScrollClass);
public EventCallback<bool> VisibleChanged { get; set; } }
/// <summary> //removes the CSS class that prevented scrolling
/// If true overlay will be visible. Two-way bindable. void UnblockScroll()
/// </summary> {
[Parameter] ScrollManager.UnlockScrollAsync("body", LockScrollClass);
[Category(CategoryTypes.Overlay.Behavior)] }
public bool Visible #endregion
{
get => _visible; #region Content
set /// <summary>
{ /// Child content of the component.
if (_visible == value) /// </summary>
return; [Parameter]
_visible = value; [Category(CategoryTypes.Overlay.Behavior)]
VisibleChanged.InvokeAsync(_visible); public RenderFragment ChildContent { get; set; }
} #endregion
}
#region Styling
/// <summary> /// <summary>
/// If true overlay will set Visible false on click. /// If true overlay will be visible. Two-way bindable.
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Overlay.ClickAction)] [Category(CategoryTypes.Overlay.Behavior)]
public bool AutoClose { get; set; } public bool Visible
{
/// <summary> get => _visible;
/// If true (default), the Document.body element will not be able to scroll set
/// </summary> {
[Parameter] if (_visible == value)
[Category(CategoryTypes.Overlay.Behavior)] return;
public bool LockScroll { get; set; } = true; _visible = value;
VisibleChanged.InvokeAsync(_visible);
/// <summary> }
/// The css class that will be added to body if lockscroll is used. }
/// </summary> protected string Classname =>
[Parameter] new CssBuilder("overlay")
[Category(CategoryTypes.Overlay.Behavior)] .AddClass("overlay-absolute", Absolute)
public string LockScrollClass { get; set; } = "scroll-locked"; .AddClass(AdditionalClassList)
.Build();
/// <summary>
/// If true applys the themes dark overlay color. protected string ScrimClassname =>
/// </summary> new CssBuilder("overlay-scrim")
[Parameter] .AddClass("overlay-dark", DarkBackground)
[Category(CategoryTypes.Overlay.Behavior)] .AddClass("overlay-light", LightBackground)
public bool DarkBackground { get; set; } .Build();
/// <summary> protected string Styles =>
/// If true applys the themes light overlay color. new StyleBuilder()
/// </summary> .AddStyle("z-index", $"{ZIndex}", ZIndex != 5)
[Parameter] .Build();
[Category(CategoryTypes.Overlay.Behavior)]
public bool LightBackground { get; set; } /// <summary>
/// The css class that will be added to body if lockscroll is used.
/// <summary> /// </summary>
/// Glyph class names, separated by space [Parameter]
/// </summary> [Category(CategoryTypes.Overlay.Behavior)]
[Parameter] public string LockScrollClass { get; set; } = "scroll-locked";
[Category(CategoryTypes.Overlay.Behavior)]
public bool Absolute { get; set; } /// <summary>
/// If true applys the themes dark overlay color.
/// <summary> /// </summary>
/// Sets the z-index of the overlay. [Parameter]
/// </summary> [Category(CategoryTypes.Overlay.Behavior)]
[Parameter] public bool DarkBackground { get; set; }
[Category(CategoryTypes.Overlay.Behavior)]
public int ZIndex { get; set; } = 5; /// <summary>
/// If true applys the themes light overlay color.
/// <summary> /// </summary>
/// Command parameter. [Parameter]
/// </summary> [Category(CategoryTypes.Overlay.Behavior)]
[Parameter] public bool LightBackground { get; set; }
[Category(CategoryTypes.Overlay.ClickAction)]
public object CommandParameter { get; set; } /// <summary>
/// Glyph class names, separated by space
/// <summary> /// </summary>
/// Command executed when the user clicks on an element. [Parameter]
/// </summary> [Category(CategoryTypes.Overlay.Behavior)]
[Parameter] public bool Absolute { get; set; }
[Category(CategoryTypes.Overlay.ClickAction)]
public ICommand Command { get; set; } /// <summary>
/// Sets the z-index of the overlay.
/// <summary> /// </summary>
/// Fired when the overlay is clicked [Parameter]
/// </summary> [Category(CategoryTypes.Overlay.Behavior)]
[Parameter] public int ZIndex { get; set; } = 5;
public EventCallback<MouseEventArgs> OnClick { get; set; } #endregion
protected internal void OnClickHandler(MouseEventArgs ev)
{ #region Behavior
if (AutoClose) /// <summary>
Visible = false; /// If true overlay will set Visible false on click.
OnClick.InvokeAsync(ev); /// </summary>
if (Command?.CanExecute(CommandParameter) ?? false) [Parameter]
{ [Category(CategoryTypes.Overlay.ClickAction)]
Command.Execute(CommandParameter); public bool AutoClose { get; set; }
}
} /// <summary>
/// If true (default), the Document.body element will not be able to scroll
//if not visible or CSS `position:absolute`, don't lock scroll /// </summary>
protected override void OnAfterRender(bool firstTime) [Parameter]
{ [Category(CategoryTypes.Overlay.Behavior)]
if (!LockScroll || Absolute) public bool LockScroll { get; set; } = true;
return;
/// <summary>
if (Visible) /// Command parameter.
BlockScroll(); /// </summary>
else [Parameter]
UnblockScroll(); [Category(CategoryTypes.Overlay.ClickAction)]
public object CommandParameter { get; set; }
}
/// <summary>
//locks the scroll attaching a CSS class to the specified element, in this case the body /// Command executed when the user clicks on an element.
void BlockScroll() /// </summary>
{ [Parameter]
ScrollManager.LockScrollAsync("body", LockScrollClass); [Category(CategoryTypes.Overlay.ClickAction)]
} public ICommand Command { get; set; }
#endregion
//removes the CSS class that prevented scrolling
void UnblockScroll() #region Lifecycle
{ //if not visible or CSS `position:absolute`, don't lock scroll
ScrollManager.UnlockScrollAsync("body", LockScrollClass); protected override void OnAfterRender(bool firstTime)
} {
if (!LockScroll || Absolute)
//When disposing the overlay, remove the class that prevented scrolling return;
public void Dispose()
{ if (Visible)
UnblockScroll(); BlockScroll();
} else
UnblockScroll();
}
//When disposing the overlay, remove the class that prevented scrolling
public void Dispose()
{
UnblockScroll();
}
#endregion
} }

@ -9,164 +9,176 @@ namespace Connected.Components;
public partial class PageContentNavigation : IAsyncDisposable public partial class PageContentNavigation : IAsyncDisposable
{ {
private List<PageContentSection> _sections = new(); #region Variables
private IScrollSpy _scrollSpy; private List<PageContentSection> _sections = new();
private IScrollSpy _scrollSpy;
[Inject] IScrollSpyFactory ScrollSpyFactory { get; set; } [Inject] IScrollSpyFactory ScrollSpyFactory { get; set; }
/// <summary> private Dictionary<PageContentSection, PageContentSection> _parentMapper = new();
/// The displayed section within the MudPageContentNavigation #endregion
/// </summary>
public IEnumerable<PageContentSection> Sections => _sections.AsEnumerable(); #region Events
private Task OnNavLinkClick(string id)
/// <summary> {
/// The currently active session. null if there is no section selected SelectActiveSection(id);
/// </summary> return _scrollSpy.ScrollToSection(id);
public PageContentSection ActiveSection => _sections.FirstOrDefault(x => x.IsActive == true); }
/// <summary> private void ScrollSpy_ScrollSectionSectionCentered(object sender, ScrollSectionCenteredEventArgs e) =>
/// The text displayed about the section links. Defaults to "Conents" SelectActiveSection(e.Id);
/// </summary>
[Parameter] public string Headline { get; set; } = "Contents"; private void SelectActiveSection(string id)
{
/// <summary> if (string.IsNullOrEmpty(id))
/// The css selector used to identifify the HTML elements that should be observed for viewport changes {
/// </summary> return;
[Parameter] public string SectionClassSelector { get; set; } = string.Empty; }
/// <summary> var activelink = _sections.FirstOrDefault(x => x.Id == id);
/// If there are mutliple levels, this can specified to make a mapping between a level class likw "second-level" and the level in the hierarchy if (activelink == null)
/// </summary> {
[Parameter] public IDictionary<string, int> HierarchyMapper { get; set; } = new Dictionary<string, int>(); return;
}
/// <summary>
/// If there are multiple levels, this property controls they visibility of them. _sections.ToList().ForEach(item => item.Deactive());
/// </summary> activelink.Activate();
[Parameter] public ContentNavigationExpandBehaviour ExpandBehaviour { get; set; } = ContentNavigationExpandBehaviour.Always;
StateHasChanged();
/// <summary> }
/// If this option is true the first added section will become active when there is no other indication of an active session. Default value is false
/// </summary> private string GetNavLinkClass(PageContentSection section) => new CssBuilder("page-content-navigation-navlink")
[Parameter] public bool ActivateFirstSectionAsDefault { get; set; } = false; .AddClass("active", section.IsActive)
.AddClass($"navigation-level-{section.Level}")
private Task OnNavLinkClick(string id) .Build();
{
SelectActiveSection(id); private string GetPanelClass() => new CssBuilder("page-content-navigation").AddClass(AdditionalClassList).Build();
return _scrollSpy.ScrollToSection(id);
} /// <summary>
/// Scrolls to a section based on the fragment of the uri. If there is no fragment, no scroll will occured
private void ScrollSpy_ScrollSectionSectionCentered(object sender, ScrollSectionCenteredEventArgs e) => /// </summary>
SelectActiveSection(e.Id); /// <param name="uri">The uri containing the fragment to scroll</param>
/// <returns>A task that completes when the viewport has scrolled</returns>
private void SelectActiveSection(string id) public Task ScrollToSection(Uri uri) => _scrollSpy.ScrollToSection(uri);
{
if (string.IsNullOrEmpty(id)) /// <summary>
{ /// Add a section to the content navigation
return; /// </summary>
} /// <param name="sectionName">name of the section will be displayed in the navigation</param>
/// <param name="sectionId">id of the section. It will be appending to the current url, if the section becomes active</param>
var activelink = _sections.FirstOrDefault(x => x.Id == id); /// <param name="forceUpdate">If true, StateHasChanged is called, forcing a rerender of the component</param>
if (activelink == null) public void AddSection(string sectionName, string sectionId, bool forceUpdate) => AddSection(new(sectionName, sectionId), forceUpdate);
{
return;
}
/// <summary>
_sections.ToList().ForEach(item => item.Deactive()); /// Add a section to the content navigation
activelink.Activate(); /// </summary>
/// <param name="section">The section that needs to be added</param>
StateHasChanged(); /// <param name="forceUpdate">If true, StateHasChanged is called, forcing a rerender of the component</param>
} public void AddSection(PageContentSection section, bool forceUpdate)
{
private string GetNavLinkClass(PageContentSection section) => new CssBuilder("page-content-navigation-navlink") _sections.Add(section);
.AddClass("active", section.IsActive)
.AddClass($"navigation-level-{section.Level}") int diffRootLevel = 1_000_000;
.Build(); int counter = 0;
foreach (var item in _sections.Where(x => x.Parent == null))
private string GetPanelClass() => new CssBuilder("page-content-navigation").AddClass(AdditionalClassList).Build(); {
item.SetLevelStructure(counter, diffRootLevel);
/// <summary> counter += diffRootLevel;
/// Scrolls to a section based on the fragment of the uri. If there is no fragment, no scroll will occured }
/// </summary>
/// <param name="uri">The uri containing the fragment to scroll</param> if (section.Id == _scrollSpy.CenteredSection)
/// <returns>A task that completes when the viewport has scrolled</returns> {
public Task ScrollToSection(Uri uri) => _scrollSpy.ScrollToSection(uri); section.Activate();
}
/// <summary> else if (_sections.Count == 1 && ActivateFirstSectionAsDefault == true)
/// Add a section to the content navigation {
/// </summary> section.Activate();
/// <param name="sectionName">name of the section will be displayed in the navigation</param> _scrollSpy.SetSectionAsActive(section.Id).AndForget();
/// <param name="sectionId">id of the section. It will be appending to the current url, if the section becomes active</param> }
/// <param name="forceUpdate">If true, StateHasChanged is called, forcing a rerender of the component</param>
public void AddSection(string sectionName, string sectionId, bool forceUpdate) => AddSection(new(sectionName, sectionId), forceUpdate); if (forceUpdate == true)
{
private Dictionary<PageContentSection, PageContentSection> _parentMapper = new(); StateHasChanged();
}
/// <summary> }
/// Add a section to the content navigation
/// </summary>
/// <param name="section">The section that needs to be added</param> /// <summary>
/// <param name="forceUpdate">If true, StateHasChanged is called, forcing a rerender of the component</param> /// Rerender the component
public void AddSection(PageContentSection section, bool forceUpdate) /// </summary>
{ public void Update() => StateHasChanged();
_sections.Add(section); #endregion
int diffRootLevel = 1_000_000; #region Content
int counter = 0; /// <summary>
foreach (var item in _sections.Where(x => x.Parent == null)) /// The displayed section within the MudPageContentNavigation
{ /// </summary>
item.SetLevelStructure(counter, diffRootLevel); public IEnumerable<PageContentSection> Sections => _sections.AsEnumerable();
counter += diffRootLevel;
} /// <summary>
/// The currently active session. null if there is no section selected
if (section.Id == _scrollSpy.CenteredSection) /// </summary>
{ public PageContentSection ActiveSection => _sections.FirstOrDefault(x => x.IsActive == true);
section.Activate();
} /// <summary>
else if (_sections.Count == 1 && ActivateFirstSectionAsDefault == true) /// The text displayed about the section links. Defaults to "Conents"
{ /// </summary>
section.Activate(); [Parameter] public string Headline { get; set; } = "Contents";
_scrollSpy.SetSectionAsActive(section.Id).AndForget(); #endregion
}
#region Styling
if (forceUpdate == true) /// <summary>
{ /// The css selector used to identifify the HTML elements that should be observed for viewport changes
StateHasChanged(); /// </summary>
} [Parameter] public string SectionClassSelector { get; set; } = string.Empty;
} #endregion
#region Behavior
/// <summary> /// <summary>
/// Rerender the component /// If there are mutliple levels, this can specified to make a mapping between a level class likw "second-level" and the level in the hierarchy
/// </summary> /// </summary>
public void Update() => StateHasChanged(); [Parameter] public IDictionary<string, int> HierarchyMapper { get; set; } = new Dictionary<string, int>();
protected override void OnInitialized() /// <summary>
{ /// If there are multiple levels, this property controls they visibility of them.
_scrollSpy = ScrollSpyFactory.Create(); /// </summary>
} [Parameter] public ContentNavigationExpandBehaviour ExpandBehaviour { get; set; } = ContentNavigationExpandBehaviour.Always;
/// <summary>
protected override async Task OnAfterRenderAsync(bool firstRender) /// If this option is true the first added section will become active when there is no other indication of an active session. Default value is false
{ /// </summary>
if (firstRender) [Parameter] public bool ActivateFirstSectionAsDefault { get; set; } = false;
{ #endregion
_scrollSpy.ScrollSectionSectionCentered += ScrollSpy_ScrollSectionSectionCentered; #region Lifecycle
protected override void OnInitialized()
if (string.IsNullOrEmpty(SectionClassSelector) == false) {
{ _scrollSpy = ScrollSpyFactory.Create();
await _scrollSpy.StartSpying(SectionClassSelector); }
} protected override async Task OnAfterRenderAsync(bool firstRender)
{
SelectActiveSection(_scrollSpy.CenteredSection); if (firstRender)
} {
}
_scrollSpy.ScrollSectionSectionCentered += ScrollSpy_ScrollSectionSectionCentered;
public ValueTask DisposeAsync()
{ if (string.IsNullOrEmpty(SectionClassSelector) == false)
if (_scrollSpy == null) { return ValueTask.CompletedTask; } {
await _scrollSpy.StartSpying(SectionClassSelector);
_scrollSpy.ScrollSectionSectionCentered -= ScrollSpy_ScrollSectionSectionCentered; }
return _scrollSpy.DisposeAsync();
} SelectActiveSection(_scrollSpy.CenteredSection);
}
}
public ValueTask DisposeAsync()
{
if (_scrollSpy == null) { return ValueTask.CompletedTask; }
_scrollSpy.ScrollSectionSectionCentered -= ScrollSpy_ScrollSectionSectionCentered;
return _scrollSpy.DisposeAsync();
}
#endregion
} }

@ -6,13 +6,13 @@
@if (ShowFirstButton) @if (ShowFirstButton)
{ {
<li class="@ItemClassname"> <li class="@ItemClassname">
<IconButton Icon="@FirstIcon" Size="@Size" Variant="@Variant" Disabled="@(Selected == 1 || Disabled)" Clicked="@(() => OnClickControlButton(Page.First))" aria-label="First page"></IconButton> <GlyphButton Glyph="@FirstIcon" Size="@Size" Variant="@Variant" Disabled="@(Selected == 1 || Disabled)" Clicked="@(() => OnClickControlButton(Page.First))" aria-label="First page"></GlyphButton>
</li> </li>
} }
@if (ShowPreviousButton) @if (ShowPreviousButton)
{ {
<li class="@ItemClassname"> <li class="@ItemClassname">
<IconButton Icon="@BeforeIcon" Size="@Size" Variant="@Variant" Disabled="@(Selected == 1 || Disabled)" Clicked="@(() => OnClickControlButton(Page.Previous))" aria-label="Previous page"></IconButton> <GlyphButton Glyph="@BeforeIcon" Size="@Size" Variant="@Variant" Disabled="@(Selected == 1 || Disabled)" Clicked="@(() => OnClickControlButton(Page.Previous))" aria-label="Previous page"></GlyphButton>
</li> </li>
} }
@foreach (var state in GeneratePagination()) @foreach (var state in GeneratePagination())
@ -39,13 +39,13 @@
@if (ShowNextButton) @if (ShowNextButton)
{ {
<li class="@ItemClassname"> <li class="@ItemClassname">
<IconButton Icon="@NextIcon" Variant="@Variant" Size="@Size" Disabled="@(Selected == Count || Disabled)" Clicked="@(() => OnClickControlButton(Page.Next))" aria-label="Next page"></IconButton> <GlyphButton Glyph="@NextIcon" Variant="@Variant" Size="@Size" Disabled="@(Selected == Count || Disabled)" Clicked="@(() => OnClickControlButton(Page.Next))" aria-label="Next page"></GlyphButton>
</li> </li>
} }
@if (ShowLastButton) @if (ShowLastButton)
{ {
<li class="@ItemClassname"> <li class="@ItemClassname">
<IconButton Icon="@LastIcon" Variant="@Variant" Size="@Size" Disabled="@(Selected == Count || Disabled)" Clicked="@(() => OnClickControlButton(Page.Last))" aria-label="Last page"></IconButton> <GlyphButton Glyph="@LastIcon" Variant="@Variant" Size="@Size" Disabled="@(Selected == Count || Disabled)" Clicked="@(() => OnClickControlButton(Page.Last))" aria-label="Last page"></GlyphButton>
</li> </li>
} }
</Element> </Element>

@ -10,91 +10,97 @@ namespace Connected.Components;
public partial class Paper : UIComponent public partial class Paper : UIComponent
{ {
protected string Classname => #region Content
new CssBuilder("paper") /// <summary>
.AddClass($"paper-outlined", Outlined) /// Child content of the component.
.AddClass($"paper-square", Square) /// </summary>
.AddClass($"elevation-{Elevation.ToString()}", !Outlined) [Parameter]
.AddClass(AdditionalClassList) [Category(CategoryTypes.Paper.Behavior)]
.Build(); public RenderFragment ChildContent { get; set; }
#endregion
protected string Stylename =>
new StyleBuilder() #region Styling
.AddStyle("height", $"{Height}", !String.IsNullOrEmpty(Height)) protected string Classname =>
.AddStyle("width", $"{Width}", !String.IsNullOrEmpty(Width)) new CssBuilder("paper")
.AddStyle("max-height", $"{MaxHeight}", !String.IsNullOrEmpty(MaxHeight)) .AddClass($"paper-outlined", Outlined)
.AddStyle("max-width", $"{MaxWidth}", !String.IsNullOrEmpty(MaxWidth)) .AddClass($"paper-square", Square)
.AddStyle("min-height", $"{MinHeight}", !String.IsNullOrEmpty(MinHeight)) .AddClass($"elevation-{Elevation.ToString()}", !Outlined)
.AddStyle("min-width", $"{MinWidth}", !String.IsNullOrEmpty(MinWidth)) .AddClass(AdditionalClassList)
.Build(); .Build();
/// <summary> protected string Stylename =>
/// The higher the number, the heavier the drop-shadow. new StyleBuilder()
/// </summary> .AddStyle("height", $"{Height}", !String.IsNullOrEmpty(Height))
[Parameter] .AddStyle("width", $"{Width}", !String.IsNullOrEmpty(Width))
[Category(CategoryTypes.Paper.Appearance)] .AddStyle("max-height", $"{MaxHeight}", !String.IsNullOrEmpty(MaxHeight))
public int Elevation { set; get; } = 1; .AddStyle("max-width", $"{MaxWidth}", !String.IsNullOrEmpty(MaxWidth))
.AddStyle("min-height", $"{MinHeight}", !String.IsNullOrEmpty(MinHeight))
/// <summary> .AddStyle("min-width", $"{MinWidth}", !String.IsNullOrEmpty(MinWidth))
/// If true, border-radius is set to 0. .Build();
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// The higher the number, the heavier the drop-shadow.
public bool Square { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// If true, card will be outlined. public int Elevation { set; get; } = 1;
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// If true, border-radius is set to 0.
public bool Outlined { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Height of the component. public bool Square { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// If true, card will be outlined.
public string Height { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Width of the component. public bool Outlined { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// Height of the component.
public string Width { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Max-Height of the component. public string Height { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// Width of the component.
public string MaxHeight { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Max-Width of the component. public string Width { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// Max-Height of the component.
public string MaxWidth { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Min-Height of the component. public string MaxHeight { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// Max-Width of the component.
public string MinHeight { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Min-Width of the component. public string MaxWidth { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Appearance)] /// Min-Height of the component.
public string MinWidth { get; set; } /// </summary>
[Parameter]
/// <summary> [Category(CategoryTypes.Paper.Appearance)]
/// Child content of the component. public string MinHeight { get; set; }
/// </summary>
[Parameter] /// <summary>
[Category(CategoryTypes.Paper.Behavior)] /// Min-Width of the component.
public RenderFragment ChildContent { get; set; } /// </summary>
[Parameter]
[Category(CategoryTypes.Paper.Appearance)]
public string MinWidth { get; set; }
#endregion
} }

@ -4,13 +4,13 @@
@if (ReadOnly) @if (ReadOnly)
{ {
<span class="@ClassName"> <span class="@ClassName">
<Icon Icon="@ItemIcon" Size="@Size"></Icon> <Icon Glyph="@ItemIcon" Size="@Size"></Icon>
</span> </span>
} }
else else
{ {
<span class="@ClassName" @onmouseover="HandleMouseOver" @onclick="HandleClick" @onmouseout="HandleMouseOut "> <span class="@ClassName" @onmouseover="HandleMouseOver" @onclick="HandleClick" @onmouseout="HandleMouseOut ">
<input class="rating-input" type="radio" tabindex="-1" value="@ItemValue" name="@Rating?.Name" disabled="@Disabled" checked="@(IsChecked)" @attributes="CustomAttributes" /> <input class="rating-input" type="radio" tabindex="-1" value="@ItemValue" name="@Rating?.Name" disabled="@Disabled" checked="@(IsChecked)" @attributes="CustomAttributes" />
<Icon Icon="@ItemIcon" Size="@Size"></Icon> <Icon Glyph="@ItemIcon" Size="@Size"></Icon>
</span> </span>
} }

@ -6,7 +6,7 @@
@if (!HideIcon) @if (!HideIcon)
{ {
<div class="snackbar-icon"> <div class="snackbar-icon">
<Icon Icon="@Icon" Color="@IconColor" Size="@IconSize" /> <Icon Glyph="@Icon" Color="@IconColor" Size="@IconSize" />
</div> </div>
} }
@ -28,7 +28,7 @@
@if (ShowCloseIcon) @if (ShowCloseIcon)
{ {
<IconButton Icon="@CloseIcon" Size="Size.Small" Class="ms-2" Clicked="CloseIconClicked" /> <GlyphButton Glyph="@CloseIcon" Size="Size.Small" Class="ms-2" Clicked="CloseIconClicked" />
} }
</div> </div>
} }

@ -11,7 +11,7 @@
<span class="switch-thumb d-flex align-center justify-center"> <span class="switch-thumb d-flex align-center justify-center">
@if (!string.IsNullOrEmpty(ThumbIcon)) @if (!string.IsNullOrEmpty(ThumbIcon))
{ {
<Icon Color="@ThumbIconColor" Icon="@ThumbIcon" Style=" height:16px; width:16px;" /> <Icon Color="@ThumbIconColor" Glyph="@ThumbIcon" Style=" height:16px; width:16px;" />
} }
</span> </span>
</span> </span>

@ -13,7 +13,7 @@
<div class="d-flex"> <div class="d-flex">
@if (GroupDefinition.Expandable) @if (GroupDefinition.Expandable)
{ {
<IconButton Class="table-row-expander" Icon="@(IsExpanded ? ExpandIcon : CollapseIcon)" Clicked="@(() => IsExpanded = !IsExpanded)" /> <GlyphButton Class="table-row-expander" Glyph="@(IsExpanded ? ExpandIcon : CollapseIcon)" Clicked="@(() => IsExpanded = !IsExpanded)" />
} }
else else
{ {

@ -34,10 +34,10 @@
@if (!HidePagination) @if (!HidePagination)
{ {
<div class="table-pagination-actions"> <div class="table-pagination-actions">
<IconButton Class="flip-x-rtl" Icon="@FirstIcon" Disabled="@BackButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.First))" aria-label="First page" /> <GlyphButton Class="flip-x-rtl" Glyph="@FirstIcon" Disabled="@BackButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.First))" aria-label="First page" />
<IconButton Class="flip-x-rtl" Icon="@BeforeIcon" Disabled="@BackButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.Previous))" aria-label="Previous page" /> <GlyphButton Class="flip-x-rtl" Glyph="@BeforeIcon" Disabled="@BackButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.Previous))" aria-label="Previous page" />
<IconButton Class="flip-x-rtl" Icon="@NextIcon" Disabled="@ForwardButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.Next))" aria-label="Next page" /> <GlyphButton Class="flip-x-rtl" Glyph="@NextIcon" Disabled="@ForwardButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.Next))" aria-label="Next page" />
<IconButton Class="flip-x-rtl" Icon="@LastIcon" Disabled="@ForwardButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.Last))" aria-label="Last page" /> <GlyphButton Class="flip-x-rtl" Glyph="@LastIcon" Disabled="@ForwardButtonsDisabled" Clicked="@(() => Table.NavigateTo(Page.Last))" aria-label="Last page" />
</div> </div>
} }
@if (HorizontalAlignment == HorizontalAlignment.Start || @if (HorizontalAlignment == HorizontalAlignment.Start ||

@ -12,11 +12,11 @@
{ {
@if (_direction != SortDirection.None) @if (_direction != SortDirection.None)
{ {
<Icon Icon="@SortIcon" Class="@GetSortIconClass()" /> <Icon Glyph="@SortIcon" Class="@GetSortIconClass()" />
} }
else else
{ {
<Icon Icon="@SortIcon" Class="table-sort-label-icon"/> <Icon Glyph="@SortIcon" Class="table-sort-label-icon" />
} }
} }
@if (AppendIcon) @if (AppendIcon)

@ -15,19 +15,19 @@
} }
else else
{ {
<IconButton Size="@Size.Small" Icon="@Icons.Outlined.Edit" Class="pa-0" Clicked="StartEditingItem" Disabled="Context.EditButtonDisabled(Item)" /> <GlyphButton Size="@Size.Small" Glyph="@Icons.Outlined.Edit" Class="pa-0" Clicked="StartEditingItem" Disabled="Context.EditButtonDisabled(Item)" />
} }
} }
else if (object.ReferenceEquals(Context?.Table._editingItem, Item) && (!Context?.Table.ReadOnly ?? false) && Context?.Table.ApplyButtonPosition.DisplayApplyButtonAtStart() == true) else if (object.ReferenceEquals(Context?.Table._editingItem, Item) && (!Context?.Table.ReadOnly ?? false) && Context?.Table.ApplyButtonPosition.DisplayApplyButtonAtStart() == true)
{ {
<div style="display: flex;"> <div style="display: flex;">
<Tooltip Text="@Context.Table.CommitEditTooltip"> <Tooltip Text="@Context.Table.CommitEditTooltip">
<IconButton Class="pa-0" Icon="@Context.Table.CommitEditIcon" Clicked="FinishEdit" Size="@Size.Small" Disabled="@(!(Context?.Table.Validator?.IsValid ?? false))" /> <GlyphButton Class="pa-0" Glyph="@Context.Table.CommitEditIcon" Clicked="FinishEdit" Size="@Size.Small" Disabled="@(!(Context?.Table.Validator?.IsValid ?? false))" />
</Tooltip> </Tooltip>
@if (Context.Table.CanCancelEdit) @if (Context.Table.CanCancelEdit)
{ {
<Tooltip Text="@Context.Table.CancelEditTooltip"> <Tooltip Text="@Context.Table.CancelEditTooltip">
<IconButton Class="pa-0 ml-4" Icon="@Context.Table.CancelEditIcon" Clicked="CancelEdit" Size="@Size.Small" /> <GlyphButton Class="pa-0 ml-4" Glyph="@Context.Table.CancelEditIcon" Clicked="CancelEdit" Size="@Size.Small" />
</Tooltip> </Tooltip>
} }
</div> </div>
@ -58,19 +58,19 @@
} }
else else
{ {
<IconButton Size="@Size.Small" Icon="@Icons.Outlined.Edit" Class="pa-0" Clicked="StartEditingItem" Disabled="Context.EditButtonDisabled(Item)" /> <GlyphButton Size="@Size.Small" Glyph="@Icons.Outlined.Edit" Class="pa-0" Clicked="StartEditingItem" Disabled="Context.EditButtonDisabled(Item)" />
} }
} }
else if (object.ReferenceEquals(Context?.Table._editingItem, Item) && (!Context?.Table.ReadOnly ?? false) && Context?.Table.ApplyButtonPosition.DisplayApplyButtonAtEnd() == true) else if (object.ReferenceEquals(Context?.Table._editingItem, Item) && (!Context?.Table.ReadOnly ?? false) && Context?.Table.ApplyButtonPosition.DisplayApplyButtonAtEnd() == true)
{ {
<div style="display: flex;"> <div style="display: flex;">
<Tooltip Text="@Context.Table.CommitEditTooltip"> <Tooltip Text="@Context.Table.CommitEditTooltip">
<IconButton Class="pa-0" Icon="@Context.Table.CommitEditIcon" Clicked="FinishEdit" Size="@Size.Small" Disabled="@(!(Context?.Table.Validator?.IsValid ?? false))" /> <GlyphButton Class="pa-0" Glyph="@Context.Table.CommitEditIcon" Clicked="FinishEdit" Size="@Size.Small" Disabled="@(!(Context?.Table.Validator?.IsValid ?? false))" />
</Tooltip> </Tooltip>
@if (Context.Table.CanCancelEdit) @if (Context.Table.CanCancelEdit)
{ {
<Tooltip Text="@Context.Table.CancelEditTooltip"> <Tooltip Text="@Context.Table.CancelEditTooltip">
<IconButton Class="pa-0 ml-4" Icon="@Context.Table.CancelEditIcon" Clicked="CancelEdit" Size="@Size.Small" /> <GlyphButton Class="pa-0 ml-4" Glyph="@Context.Table.CancelEditIcon" Clicked="CancelEdit" Size="@Size.Small" />
</Tooltip> </Tooltip>
} }
</div> </div>

@ -15,12 +15,12 @@
if (string.IsNullOrEmpty(AddIconToolTip) == false) if (string.IsNullOrEmpty(AddIconToolTip) == false)
{ {
<Tooltip Text="@AddIconToolTip"> <Tooltip Text="@AddIconToolTip">
<IconButton Icon="@AddTabIcon" Class="@AddIconClass" Style="@AddIconStyle" Clicked="@AddTab" /> <GlyphButton Glyph="@AddTabIcon" Class="@AddIconClass" Style="@AddIconStyle" Clicked="@AddTab" />
</Tooltip> </Tooltip>
} }
else else
{ {
<IconButton Icon="@AddTabIcon" Class="@AddIconClass" Style="@AddIconStyle" Clicked="@AddTab" /> <GlyphButton Glyph="@AddTabIcon" Class="@AddIconClass" Style="@AddIconStyle" Clicked="@AddTab" />
} }
} }
; ;
@ -30,12 +30,12 @@
if (string.IsNullOrEmpty(CloseIconToolTip) == false) if (string.IsNullOrEmpty(CloseIconToolTip) == false)
{ {
<Tooltip Text="@CloseIconToolTip"> <Tooltip Text="@CloseIconToolTip">
<IconButton Icon="@CloseTabIcon" Class="@CloseIconClass" Style="@CloseIconStyle" Clicked="EventCallback.Factory.Create<MouseEventArgs>(this, () => CloseTab.InvokeAsync(context))" /> <GlyphButton Glyph="@CloseTabIcon" Class="@CloseIconClass" Style="@CloseIconStyle" Clicked="EventCallback.Factory.Create<MouseEventArgs>(this, () => CloseTab.InvokeAsync(context))" />
</Tooltip> </Tooltip>
} }
else else
{ {
<IconButton Icon="@CloseTabIcon" Class="@CloseIconClass" Style="@CloseIconStyle" Clicked="EventCallback.Factory.Create<MouseEventArgs>(this, () => CloseTab.InvokeAsync(context))" /> <GlyphButton Glyph="@CloseTabIcon" Class="@CloseIconClass" Style="@CloseIconStyle" Clicked="EventCallback.Factory.Create<MouseEventArgs>(this, () => CloseTab.InvokeAsync(context))" />
} }
} }
; ;

@ -14,7 +14,7 @@
@if (_showScrollButtons) @if (_showScrollButtons)
{ {
<div class="tabs-scroll-button"> <div class="tabs-scroll-button">
<IconButton Icon="@_prevIcon" Color="@ScrollIconColor" Clicked="@((e) => ScrollPrev())" Disabled="@_prevButtonDisabled" /> <GlyphButton Glyph="@_prevIcon" Color="@ScrollIconColor" Clicked="@((e) => ScrollPrev())" Disabled="@_prevButtonDisabled" />
</div> </div>
} }
<div @ref="@_tabsContentSize" class="tabs-toolbar-content"> <div @ref="@_tabsContentSize" class="tabs-toolbar-content">
@ -52,7 +52,7 @@
@if (_showScrollButtons) @if (_showScrollButtons)
{ {
<div class="tabs-scroll-button"> <div class="tabs-scroll-button">
<IconButton Icon="@_nextIcon" Color="@ScrollIconColor" Clicked="@((e) => ScrollNext())" Disabled="@_nextButtonDisabled" /> <GlyphButton Glyph="@_nextIcon" Color="@ScrollIconColor" Clicked="@((e) => ScrollNext())" Disabled="@_nextButtonDisabled" />
</div> </div>
} }
@if (HeaderPosition == TabHeaderPosition.After && Header != null) @if (HeaderPosition == TabHeaderPosition.After && Header != null)
@ -91,11 +91,11 @@
} }
else if (String.IsNullOrEmpty(panel.Text) && !String.IsNullOrEmpty(panel.Icon)) else if (String.IsNullOrEmpty(panel.Text) && !String.IsNullOrEmpty(panel.Icon))
{ {
<Icon Icon="@panel.Icon" Color="@IconColor" /> <Icon Glyph="@panel.Icon" Color="@IconColor" />
} }
else if (!String.IsNullOrEmpty(panel.Text) && !String.IsNullOrEmpty(panel.Icon)) else if (!String.IsNullOrEmpty(panel.Text) && !String.IsNullOrEmpty(panel.Icon))
{ {
<Icon Icon="@panel.Icon" Color="@IconColor" Class="tab-icon-text" /> <Icon Glyph="@panel.Icon" Color="@IconColor" Class="tab-icon-text" />
@((MarkupString)panel.Text) @((MarkupString)panel.Text)
} }
@if (TabPanelHeaderPosition == TabHeaderPosition.After && TabPanelHeader != null) @if (TabPanelHeaderPosition == TabHeaderPosition.After && TabPanelHeader != null)

@ -20,7 +20,7 @@
@if (!string.IsNullOrEmpty(Icon)) @if (!string.IsNullOrEmpty(Icon))
{ {
<div class="treeview-item-icon"> <div class="treeview-item-icon">
<Icon Icon="@Icon" Color="@IconColor" /> <Icon Glyph="@Icon" Color="@IconColor" />
</div> </div>
} }
@ -38,7 +38,7 @@
@if (!string.IsNullOrEmpty(EndIcon)) @if (!string.IsNullOrEmpty(EndIcon))
{ {
<div class="treeview-item-icon"> <div class="treeview-item-icon">
<Icon Icon="@EndIcon" Color="@EndIconColor" /> <Icon Glyph="@EndIcon" Color="@EndIconColor" />
</div> </div>
} }
} }

@ -5,7 +5,7 @@
<div class="treeview-item-arrow"> <div class="treeview-item-arrow">
@if (Visible) @if (Visible)
{ {
<IconButton Clicked="@ToggleAsync" Icon="@(Loading ? LoadingIcon : ExpandedIcon)" Color="@(Loading ? LoadingIconColor : ExpandedIconColor)" Class="@Classname"></IconButton> <GlyphButton Clicked="@ToggleAsync" Glyph="@(Loading ? LoadingIcon : ExpandedIcon)" Color="@(Loading ? LoadingIconColor : ExpandedIconColor)" Class="@Classname"></GlyphButton>
} }
</div> </div>

@ -20,4 +20,16 @@ public static class Helper
return false; return false;
} }
} }
public static bool IsNumeric(string input)
{
try
{
var number = Double.Parse(input);
return true;
} catch
{
return false;
}
}
} }

Loading…
Cancel
Save