2024-07-28 11:20:00 +02:00
using Microsoft.AspNetCore.Components ;
2026-10-27 19:57:04 +01:00
using Microsoft.AspNetCore.Components.Web ;
2024-07-28 11:20:00 +02:00
2024-09-15 21:14:56 +02:00
using Timer = System . Timers . Timer ;
2024-08-21 08:30:01 +02:00
namespace AIStudio.Components ;
2024-07-28 11:20:00 +02:00
2025-08-09 19:29:43 +02:00
public partial class ConfigurationText : ConfigurationBaseCore
2024-07-28 11:20:00 +02:00
{
/// <summary>
/// The text used for the textfield.
/// </summary>
[Parameter]
public Func < string > Text { get ; set ; } = () => string . Empty ;
/// <summary>
2024-09-15 21:14:56 +02:00
/// An action which is called when the text was changed.
2024-07-28 11:20:00 +02:00
/// </summary>
[Parameter]
public Action < string > TextUpdate { get ; set ; } = _ => { };
/// <summary>
2026-10-17 19:12:43 +02:00
/// The icon to display next to the textfield, when it has a single line.
2024-07-28 11:20:00 +02:00
/// </summary>
[Parameter]
public string Icon { get ; set ; } = Icons . Material . Filled . Info ;
2026-10-27 19:57:04 +01:00
/// <summary>
/// Removes the default bottom margin when the field is part of a compact group.
/// </summary>
[Parameter]
public bool Compact { get ; set ; }
/// <summary>
/// Wraps and grows with its text, including while the field is not focused, up to the lines of a
/// field for file patterns.
/// </summary>
/// <remarks>
/// Such a field shows the draft rather than the stored value. Otherwise, a reset which leaves the
/// stored value as it was would not reach the field, e.g., restoring the default while the field
/// shows a draft which the validation rejected.
/// </remarks>
[Parameter]
public bool ExpandToContent { get ; set ; }
2024-07-28 11:20:00 +02:00
/// <summary>
/// The color of the icon to use.
/// </summary>
[Parameter]
public Color IconColor { get ; set ; } = Color . Default ;
2024-09-15 21:14:56 +02:00
/// <summary>
/// How many lines should the textfield have?
/// </summary>
[Parameter]
public int NumLines { get ; set ; } = 1 ;
/// <summary>
/// What is the maximum number of lines?
/// </summary>
[Parameter]
public int MaxLines { get ; set ; } = 12 ;
2026-08-11 20:14:21 +02:00
/// <summary>
/// When configured, displays a button which restores this value.
/// </summary>
[Parameter]
public Func < string >? ResetValue { get ; set ; }
/// <summary>
/// The text displayed on the optional reset button.
/// </summary>
[Parameter]
public string ResetButtonText { get ; set ; } = string . Empty ;
2026-10-27 19:57:04 +01:00
/// <summary>
/// Uses an accessible reset icon inside the field.
/// </summary>
[Parameter]
public bool ResetButtonIconOnly { get ; set ; }
2026-08-11 20:14:21 +02:00
/// <summary>
/// Validates the configured text before it is stored.
/// </summary>
[Parameter]
public Func < string , string? >? Validation { get ; set ; }
2024-09-15 21:14:56 +02:00
private string internalText = string . Empty ;
2026-10-27 19:57:04 +01:00
private string? lastSourceText ;
2025-08-09 19:29:43 +02:00
private readonly Timer timer = new ( TimeSpan . FromMilliseconds ( 500 ))
2024-09-15 21:14:56 +02:00
{
AutoReset = false
};
2026-10-27 19:57:04 +01:00
protected string EditingText => this . internalText ;
2025-08-09 19:29:43 +02:00
#region Overrides of ConfigurationBase
/// <inheritdoc />
protected override bool Stretch => true ;
protected override Variant Variant => Variant . Outlined ;
2026-10-27 19:57:04 +01:00
protected override string MarginClass => this . Compact ? "mb-0" : base . MarginClass ;
2025-08-09 19:29:43 +02:00
protected override string Label => this . OptionDescription ;
2024-09-15 21:14:56 +02:00
protected override async Task OnInitializedAsync ()
{
2026-08-25 18:53:09 +02:00
this . timer . Elapsed += ( _ , _ ) => this . InvokeAsync ( async () => await this . OptionChanged ( this . internalText )). Observe ( $"{nameof(ConfigurationText)}: applying the changed text" );
2024-09-15 21:14:56 +02:00
await base . OnInitializedAsync ();
}
protected override async Task OnParametersSetAsync ()
{
2026-10-27 19:57:04 +01:00
var sourceText = this . Text ();
// Re-rendering the parent must not replace a draft which is waiting for its debounce.
// A genuinely changed setting, e.g. from a configuration plugin, still replaces it.
if ( this . IsDisabled || ! string . Equals ( sourceText , this . lastSourceText , StringComparison . Ordinal ))
{
this . internalText = sourceText ;
this . lastSourceText = sourceText ;
}
2024-09-15 21:14:56 +02:00
await base . OnParametersSetAsync ();
}
#endregion
2026-10-27 19:57:04 +01:00
private bool AutoGrow => this . ExpandToContent || this . NumLines > 1 ;
2024-09-15 21:14:56 +02:00
2026-10-27 19:57:04 +01:00
private int GetMaxLines => this . ExpandToContent ? FilePatternSelection . MAX_FIELD_LINES : this . AutoGrow ? this . MaxLines : 1 ;
private string InputClass => this . ExpandToContent ? "pattern-text-field" : string . Empty ;
2024-09-15 21:14:56 +02:00
2026-10-17 19:12:43 +02:00
//
// MudBlazor centers the icon vertically, so in a field of several lines it floats in the middle
// of the text. Such a field goes without:
//
private Adornment IconAdornment => this . NumLines == 1 ? Adornment . Start : Adornment . None ;
2026-10-27 19:57:04 +01:00
protected void InternalUpdate ( string text )
2024-09-15 21:14:56 +02:00
{
2026-10-27 19:57:04 +01:00
if ( this . IsDisabled )
return ;
2024-09-15 21:14:56 +02:00
this . timer . Stop ();
this . internalText = text ;
this . timer . Start ();
}
2026-08-11 20:14:21 +02:00
2026-10-27 19:57:04 +01:00
private EventCallback < MouseEventArgs > ResetAdornmentClick => this . ResetButtonIconOnly
? EventCallback . Factory . Create < MouseEventArgs >( this , this . ResetTextAsync )
: default ;
protected async Task ResetTextAsync ()
2026-08-11 20:14:21 +02:00
{
if ( this . ResetValue is null || this . IsDisabled )
return ;
2026-10-27 19:57:04 +01:00
await this . ApplyTextAsync ( this . ResetValue ());
}
/// <summary>
/// Applies an explicit value immediately, including a reset of part of a compound setting.
/// </summary>
protected async Task ApplyTextAsync ( string text )
{
if ( this . IsDisabled )
return ;
2026-08-11 20:14:21 +02:00
this . timer . Stop ();
2026-10-27 19:57:04 +01:00
this . internalText = text ;
2026-08-11 20:14:21 +02:00
await this . OptionChanged ( this . internalText );
}
2024-07-28 11:20:00 +02:00
2026-10-27 19:57:04 +01:00
protected async Task OptionChanged ( string updatedText )
2024-07-28 11:20:00 +02:00
{
2026-10-27 19:57:04 +01:00
if ( this . IsDisabled || this . Validation ?. Invoke ( updatedText ) is not null )
2026-08-11 20:14:21 +02:00
return ;
2024-07-28 11:20:00 +02:00
this . TextUpdate ( updatedText );
2026-10-27 19:57:04 +01:00
// A save may finish after another keystroke. Remember the value it applied before
// awaiting storage, so its configuration-change notification cannot erase that keystroke.
this . lastSourceText = this . Text ();
2024-07-28 11:20:00 +02:00
await this . SettingsManager . StoreSettings ();
await this . InformAboutChange ();
}
2025-08-15 21:16:51 +02:00
#region Overrides of MSGComponentBase
protected override void DisposeResources ()
{
try
{
this . timer . Stop ();
this . timer . Dispose ();
}
catch
{
// ignore
}
base . DisposeResources ();
}
#endregion
2024-07-28 11:20:00 +02:00
}