public/controls/New-UiLink.ps1
|
function New-UiLink { <# .SYNOPSIS Creates a clickable hyperlink that opens a URL or runs a custom action. .DESCRIPTION Creates a TextBlock styled as a hyperlink with underline and accent color. By default opens the URL in the system browser. Use -Action for custom behavior. .PARAMETER Text The display text for the link. Defaults to the URL if not specified. .PARAMETER Url The URL to open when clicked. Opens in default browser. .PARAMETER Action Custom scriptblock to run instead of opening a URL. Overrides -Url behavior. .PARAMETER NoUnderline Removes the underline decoration from the link text. .PARAMETER WPFProperties Hashtable of additional WPF properties to set on the control. .EXAMPLE New-UiLink -Url 'https://github.com' -Text 'Visit GitHub' .EXAMPLE New-UiLink -Text 'Open Settings' -Action { Show-SettingsDialog } #> [CmdletBinding()] param( [Parameter()] [string]$Text, [Parameter()] [string]$Url, [Parameter()] [scriptblock]$Action, [switch]$NoUnderline, [Parameter()] [hashtable]$WPFProperties ) # Grab session and theme context $session = Assert-UiSession -CallerName 'New-UiLink' $colors = Get-ThemeColors $parent = $session.CurrentParent $displayText = if ($Text) { $Text } else { $Url } # Can't create a link with nothing to show if (!$displayText) { throw "New-UiLink: Either -Text or -Url must be specified." } # Build the link with accent color and hand cursor $link = [System.Windows.Controls.TextBlock]@{ Text = $displayText FontFamily = [System.Windows.Media.FontFamily]::new('Segoe UI Variable, Segoe UI') FontSize = 13 Foreground = ConvertTo-UiBrush $colors.Accent Cursor = 'Hand' Margin = [System.Windows.Thickness]::new(4, 2, 4, 2) } # Apply underline unless explicitly disabled if (!$NoUnderline) { $link.TextDecorations = [System.Windows.TextDecorations]::Underline } # Capture action context at creation time (like New-UiButton does) $capturedVars = $null $capturedFuncs = $null $resolvedModules = $null if ($Action) { $ctxParams = @{ Action = $Action LinkedVariables = @() LinkedFunctions = @() LinkedModules = @() } $actionContext = Get-UiActionContext @ctxParams $capturedVars = $actionContext.CapturedVars $capturedFuncs = $actionContext.CapturedFuncs $resolvedModules = $actionContext.LinkedModules } # Store action data in Tag (same structure as New-UiButton) $link.Tag = @{ BrushTag = 'AccentBrush' Url = $Url Action = $Action CapturedVars = $capturedVars CapturedFuncs = $capturedFuncs LinkedModules = $resolvedModules } # Wire up click handler - runs action async or opens URL in browser $link.Add_MouseLeftButtonUp({ param($sender, $eventArgs) $data = $sender.Tag Write-Debug "New-UiLink: Click detected. Action=$($null -ne $data.Action), Url=$($data.Url)" if ($data.Action) { Write-Debug "New-UiLink: Executing custom action via AsyncExecutor" $executor = [PsUi.AsyncExecutor]::new() $executor.UiDispatcher = [System.Windows.Threading.Dispatcher]::CurrentDispatcher # Route async output so Write-Host/Write-Warning/errors are visible $executor.add_OnHost({ param($hostRecord) Write-Host $hostRecord.Message }) $executor.add_OnWarning({ param($warningMsg) Write-Warning $warningMsg }) $executor.add_OnError({ param($errorRecord) Write-Warning "Link action error: $($errorRecord.Message)" }) $executor.add_OnComplete({ Write-Debug "New-UiLink: Action completed" $executor.Dispose() }.GetNewClosure()) # Store executor in session for Stop-UiAsync cancellation $linkSession = [PsUi.SessionManager]::Current if ($linkSession) { $linkSession.ActiveExecutor = $executor Write-Debug "New-UiLink: Executor stored in session $($linkSession.SessionId)" } # Build variables dict with theme colors (same as New-UiButton) $currentThemeColors = Get-ThemeColors $varsWithTheme = if ($data.CapturedVars) { $data.CapturedVars.Clone() } else { @{} } if ($currentThemeColors) { $varsWithTheme['__WPFThemeColors'] = $currentThemeColors } $executor.ExecuteAsync( $data.Action, $null, $varsWithTheme, $data.CapturedFuncs, [string[]]@($data.LinkedModules | Where-Object { $_ }) ) Write-Debug "New-UiLink: ExecuteAsync called" } elseif ($data.Url) { # Only allow http/https schemes $urlToOpen = $data.Url Write-Debug "New-UiLink: Opening URL $urlToOpen" if ($urlToOpen -match '^https?://[^\s]+$') { Start-Process $urlToOpen } else { Write-Warning "New-UiLink: Blocked opening URL with invalid scheme. Only http/https URLs are allowed: $urlToOpen" } } }.GetNewClosure()) # Subtle opacity change on hover for visual feeback $link.Add_MouseEnter({ param($sender, $eventArgs) $sender.Opacity = 0.7 }) $link.Add_MouseLeave({ param($sender, $eventArgs) $sender.Opacity = 1.0 }) # Register for dynamic theme switching [PsUi.ThemeEngine]::RegisterElement($link) if ($WPFProperties) { Set-UiProperties -Control $link -Properties $WPFProperties } # Attach to parent container if ($parent -is [System.Windows.Controls.Panel]) { [void]$parent.Children.Add($link) } elseif ($parent -is [System.Windows.Controls.ItemsControl]) { [void]$parent.Items.Add($link) } elseif ($parent -is [System.Windows.Controls.ContentControl]) { $parent.Content = $link } } |