Public/Elements.ps1

function New-PodeWebTextbox {
    [CmdletBinding(DefaultParameterSetName = 'Single')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(ParameterSetName = 'Single')]
        [ValidateSet('Text', 'Email', 'Password', 'Number', 'Date', 'Time', 'File', 'DateTime')]
        [string]
        $Type = 'Text',

        [Parameter()]
        [string]
        $Placeholder,

        [Parameter(ParameterSetName = 'Multi')]
        [Alias('Height')]
        [int]
        $Size = 4,

        [Parameter()]
        [string]
        $Width = 100,

        [Parameter()]
        [string]
        $HelpText,

        [Parameter()]
        [string]
        $PrependText,

        [Parameter()]
        [string]
        $PrependIcon,

        [Parameter()]
        [string]
        $AppendText,

        [Parameter()]
        [string]
        $AppendIcon,

        [Parameter(ValueFromPipeline = $true)]
        [object[]]
        $Value,

        [Parameter(ParameterSetName = 'Single')]
        [scriptblock]
        $AutoComplete,

        [Parameter()]
        [string[]]
        $EndpointName,

        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $MaxLength = 524288,

        [Parameter(ParameterSetName = 'Multi')]
        [switch]
        $Multiline,

        [switch]
        $Preformat,

        [switch]
        $ReadOnly,

        [switch]
        $Disabled,

        [Parameter(ParameterSetName = 'Single')]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [switch]
        $Required,

        [switch]
        $AutoFocus,

        [switch]
        $DynamicLabel,

        [switch]
        $AsJson,

        [Parameter(ParameterSetName = 'Multi')]
        [switch]
        $JsonInline,

        [switch]
        $HideName
    )

    begin {
        $items = @()
    }

    process {
        $items += $Value
    }

    end {
        if (!$AsJson -and ($items.Length -gt 0)) {
            $items = ($items | Out-String).Trim()
        }

        $Id = Get-PodeWebElementId -Tag Textbox -Id $Id -Name $Name

        # constrain number of lines shown
        if ($Size -le 0) {
            $Size = 4
        }

        # build element
        $element = @{
            Operation        = 'New'
            ComponentType    = 'Element'
            ObjectType       = 'Textbox'
            Name             = $Name
            DisplayName      = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
            HideName         = $HideName.IsPresent
            ID               = $Id
            Type             = $Type
            Multiline        = $Multiline.IsPresent
            Placeholder      = $Placeholder
            Size             = $Size
            Width            = (ConvertTo-PodeWebSize -Value $Width -Default 'auto' -Type '%')
            Preformat        = $Preformat.IsPresent
            HelpText         = [System.Net.WebUtility]::HtmlEncode($HelpText)
            ReadOnly         = $ReadOnly.IsPresent
            Disabled         = $Disabled.IsPresent
            IsAutoComplete   = ($null -ne $AutoComplete)
            Value            = $items
            Prepend          = @{
                Enabled = (![string]::IsNullOrWhiteSpace($PrependText) -or ![string]::IsNullOrWhiteSpace($PrependIcon))
                Text    = $PrependText
                Icon    = $PrependIcon
            }
            Append           = @{
                Enabled = (![string]::IsNullOrWhiteSpace($AppendText) -or ![string]::IsNullOrWhiteSpace($AppendIcon))
                Text    = $AppendText
                Icon    = $AppendIcon
            }
            NoAuthentication = $NoAuthentication.IsPresent
            Required         = $Required.IsPresent
            AutoFocus        = $AutoFocus.IsPresent
            DynamicLabel     = $DynamicLabel.IsPresent
            MaxLength        = $MaxLength
            AsJson           = $AsJson.IsPresent
            JsonInline       = $JsonInline.IsPresent
        }

        # create autocomplete route
        $routePath = "/pode.web-dynamic/elements/textbox/$($Id)/autocomplete"
        if (($null -ne $AutoComplete) -and !(Test-PodeWebRoute -Path $routePath)) {
            # check for scoped vars
            $AutoComplete, $autoUsingVars = Convert-PodeScopedVariables -ScriptBlock $AutoComplete -PSSession $PSCmdlet.SessionState
            $autoLogic = @{
                ScriptBlock    = $AutoComplete
                UsingVariables = $autoUsingVars
            }

            $auth = $null
            if (!$NoAuthentication -and !$PageData.NoAuthentication) {
                $auth = (Get-PodeWebState -Name 'auth')
            }

            if (Test-PodeIsEmpty $EndpointName) {
                $EndpointName = Get-PodeWebState -Name 'endpoint-name'
            }

            $argList = @(
                $element,
                $ElementData,
                $autoLogic
            )

            Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
                param($Element, $Parent, $Logic)
                $global:ElementData = $Element
                $global:ParentData = $Parent
                Set-PodeWebMetadata

                Write-PodeJsonResponse -Value @{
                    Values = (Invoke-PodeWebScriptBlock -Logic $Logic)
                }

                $global:ElementData = $null
                $global:ParentData = $null
            }
        }

        return $element
    }
}

function New-PodeWebFileUpload {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string[]]
        $Accept = '*/*',

        [switch]
        $Required,

        [switch]
        $Multiple,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag File -Id $Id -Name $Name

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'File-Upload'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName      = $HideName.IsPresent
        ID            = $Id
        Accept        = ($Accept -join ',')
        NoEvents      = $true
        Required      = $Required.IsPresent
        Multiple      = $Multiple.IsPresent
    }
}

function New-PodeWebParagraph {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = 'Value')]
        [string]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'Content')]
        [hashtable[]]
        $Content,

        [Parameter()]
        [ValidateSet('Left', 'Right', 'Center')]
        [string]
        $Alignment = 'Left'
    )

    # ensure elements are correct
    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Paragraph can only contain other elements'
    }

    $Id = Get-PodeWebElementId -Tag Para -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Paragraph'
        ID            = $Id
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        Content       = $Content
        Alignment     = $Alignment.ToLowerInvariant()
        NoEvents      = $true
    }
}

function New-PodeWebCodeBlock {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Language = [string]::Empty,

        [switch]
        $Scrollable,

        [switch]
        $NoHighlight
    )

    # id
    $Id = Get-PodeWebElementId -Tag Codeblock -Id $Id

    # language
    if ($NoHighlight) {
        $Language = 'plaintext'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'CodeBlock'
        ID            = $Id
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        Language      = $Language.ToLowerInvariant()
        Scrollable    = $Scrollable.IsPresent
        NoEvents      = $true
    }
}

function New-PodeWebCode {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Value
    )

    $Id = Get-PodeWebElementId -Tag Code -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Code'
        ID            = $Id
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        NoEvents      = $true
    }
}

function New-PodeWebCheckbox {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(ParameterSetName = 'Multiple')]
        [string[]]
        $Options,

        [Parameter(ParameterSetName = 'Multiple')]
        [string[]]
        $DisplayOptions,

        [Parameter(ParameterSetName = 'Multiple')]
        [switch]
        $Inline,

        [switch]
        $AsSwitch,

        [switch]
        $Checked,

        [switch]
        $Disabled,

        [switch]
        $Required,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag Checkbox -Id $Id -Name $Name

    if (($null -eq $Options) -or ($Options.Length -eq 0)) {
        $Options = @('true')
    }

    return @{
        Operation      = 'New'
        ComponentType  = 'Element'
        ObjectType     = 'Checkbox'
        Name           = $Name
        DisplayName    = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName       = $HideName.IsPresent
        ID             = $Id
        Options        = @($Options)
        DisplayOptions = @(Protect-PodeWebValues -Value $DisplayOptions -Default $Options -EqualCount -Encode)
        Inline         = $Inline.IsPresent
        AsSwitch       = $AsSwitch.IsPresent
        Checked        = $Checked.IsPresent
        Disabled       = $Disabled.IsPresent
        Required       = $Required.IsPresent
    }
}

function New-PodeWebRadio {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string[]]
        $Options,

        [Parameter()]
        [string[]]
        $DisplayOptions,

        [switch]
        $Inline,

        [switch]
        $Disabled,

        [switch]
        $Required,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag Radio -Id $Id -Name $Name

    return @{
        Operation      = 'New'
        ComponentType  = 'Element'
        ObjectType     = 'Radio'
        Name           = $Name
        DisplayName    = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName       = $HideName.IsPresent
        ID             = $Id
        Options        = @($Options)
        DisplayOptions = @(Protect-PodeWebValues -Value $DisplayOptions -Default $Options -EqualCount -Encode)
        Inline         = $Inline.IsPresent
        Disabled       = $Disabled.IsPresent
        Required       = $Required.IsPresent
    }
}

function New-PodeWebSelect {
    [CmdletBinding(DefaultParameterSetName = 'Options')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(ParameterSetName = 'Options')]
        [string[]]
        $Options,

        [Parameter(ParameterSetName = 'Options')]
        [string[]]
        $DisplayOptions,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $SelectedValue,

        [Parameter()]
        [int]
        $Size = 4,

        [Parameter()]
        [string]
        $PrependText,

        [Parameter()]
        [string]
        $PrependIcon,

        [Parameter()]
        [string]
        $AppendText,

        [Parameter()]
        [string]
        $AppendIcon,

        [switch]
        $Multiple,

        [switch]
        $Required,

        [switch]
        $Disabled,

        [switch]
        $HideName
    )

    if (!$Multiple.IsPresent -and $SelectedValue.Length -ge 2) {
        throw 'Multiple selected values require -Multiple switch'
    }

    $Id = Get-PodeWebElementId -Tag Select -Id $Id -Name $Name

    if ($Size -le 0) {
        $Size = 4
    }

    $element = @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Select'
        Name             = $Name
        DisplayName      = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName         = $HideName.IsPresent
        ID               = $Id
        Options          = @($Options)
        DisplayOptions   = @(Protect-PodeWebValues -Value $DisplayOptions -Default $Options -EqualCount -Encode)
        IsDynamic        = ($null -ne $ScriptBlock)
        SelectedValue    = $SelectedValue
        Multiple         = $Multiple.IsPresent
        Size             = $Size
        Prepend          = @{
            Enabled = (![string]::IsNullOrWhiteSpace($PrependText) -or ![string]::IsNullOrWhiteSpace($PrependIcon))
            Text    = $PrependText
            Icon    = $PrependIcon
        }
        Append           = @{
            Enabled = (![string]::IsNullOrWhiteSpace($AppendText) -or ![string]::IsNullOrWhiteSpace($AppendIcon))
            Text    = $AppendText
            Icon    = $AppendIcon
        }
        NoAuthentication = $NoAuthentication.IsPresent
        Required         = $Required.IsPresent
        Disabled         = $Disabled.IsPresent
    }

    $routePath = "/pode.web-dynamic/elements/select/$($Id)/options"
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = @(Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data)

            $wrapped = $null
            if (Test-PodeWebActionsAsync) {
                if ($result.Length -gt 0) {
                    if ($null -eq $result[0]) {
                        $result = @()
                    }

                    $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
                }
            }
            else {
                if ($null -eq $result) {
                    $result = @()
                }

                $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
            }

            if ($result.Length -gt 0) {
                $result = ($result | Update-PodeWebSelect -Id $ElementData.ID)
            }

            $result = Join-PodeWebDynamicOutput -Wrapped $wrapped -Output $result

            if (($null -ne $result) -and ($result.Length -gt 0)) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    return $element
}

function New-PodeWebRange {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [int]
        $Value = 0,

        [Parameter()]
        [int]
        $Min = 0,

        [Parameter()]
        [int]
        $Max = 100,

        [switch]
        $Disabled,

        [switch]
        $ShowValue,

        [switch]
        $Required,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag Range -Id $Id -Name $Name

    if ($Value -lt $Min) {
        $Value = $Min
    }

    if ($Value -gt $Max) {
        $Value = $Max
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Range'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName      = $HideName.IsPresent
        ID            = $Id
        Value         = $Value
        Min           = $Min
        Max           = $Max
        Disabled      = $Disabled.IsPresent
        ShowValue     = $ShowValue.IsPresent
        Required      = $Required.IsPresent
    }
}

function New-PodeWebProgress {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [int]
        $Value = 0,

        [Parameter()]
        [int]
        $Min = 0,

        [Parameter()]
        [int]
        $Max = 100,

        [Parameter()]
        [ValidateSet('Blue', 'Grey', 'Green', 'Red', 'Yellow', 'Cyan', 'Light', 'Dark')]
        [string]
        $Colour = 'Blue',

        [switch]
        $ShowValue,

        [switch]
        $Striped,

        [switch]
        $Animated,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag Progress -Id $Id -Name $Name

    if ($Value -lt $Min) {
        $Value = $Min
    }

    if ($Value -gt $Max) {
        $Value = $Max
    }

    $percentage = 0
    if ($Value -gt 0) {
        $percentage = ($Value / $Max) * 100.0
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Progress'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID            = $Id
        Value         = $Value
        Min           = $Min
        Max           = $Max
        Percentage    = $percentage
        ShowValue     = $ShowValue.IsPresent
        Striped       = ($Striped.IsPresent -or $Animated.IsPresent)
        Animated      = $Animated.IsPresent
        Colour        = $Colour
        HideName      = $HideName.IsPresent
    }
}

function New-PodeWebImage {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Source,

        [Parameter()]
        [Alias('Alt')]
        [string]
        $Title,

        [Parameter()]
        [ValidateSet('Left', 'Right', 'Center')]
        [string]
        $Alignment = 'Left',

        [Parameter()]
        [string]
        $Height = 0,

        [Parameter()]
        [string]
        $Width = 0
    )

    $Id = Get-PodeWebElementId -Tag Img -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Image'
        ID            = $Id
        Source        = (Add-PodeWebAppPath -Url $Source)
        Title         = $Title
        Alignment     = $Alignment.ToLowerInvariant()
        Height        = (ConvertTo-PodeWebSize -Value $Height -Default 'auto' -Type 'px')
        Width         = (ConvertTo-PodeWebSize -Value $Width -Default 'auto' -Type 'px')
    }
}

function New-PodeWebHeader {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [ValidateSet(1, 2, 3, 4, 5, 6)]
        [int]
        $Size,

        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Secondary,

        [Parameter()]
        [object]
        $Icon
    )

    $Id = Get-PodeWebElementId -Tag Header -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Header'
        ID            = $Id
        Size          = $Size
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        Secondary     = [System.Net.WebUtility]::HtmlEncode($Secondary)
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'Header')
        NoEvents      = $true
    }
}

function New-PodeWebQuote {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [ValidateSet('Left', 'Right', 'Center')]
        [string]
        $Alignment,

        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Source
    )

    $Id = Get-PodeWebElementId -Tag Quote -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Quote'
        ID            = $Id
        Alignment     = $Alignment.ToLowerInvariant()
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        Source        = [System.Net.WebUtility]::HtmlEncode($Source)
        NoEvents      = $true
    }
}

function New-PodeWebList {
    [CmdletBinding(DefaultParameterSetName = 'Values')]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = 'Items')]
        [hashtable[]]
        $Items,

        [Parameter(Mandatory = $true, ParameterSetName = 'Values')]
        [string[]]
        $Values,

        [switch]
        $Numbered
    )

    if (!(Test-PodeWebContent -Content $Items -ComponentType Element -ObjectType ListItem)) {
        throw 'Lists can only contain ListItem elements, or raw Values'
    }

    $Id = Get-PodeWebElementId -Tag List -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'List'
        ID            = $Id
        Values        = @(foreach ($value in $Values) {
                [System.Net.WebUtility]::HtmlEncode($value)
            })
        Items         = $Items
        Numbered      = $Numbered.IsPresent
        NoEvents      = $true
    }
}

function New-PodeWebListItem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A ListItem can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'List-Item'
        ID            = (Get-PodeWebElementId -Tag ListItem)
        Content       = $Content
        NoEvents      = $true
    }
}

function New-PodeWebLink {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [Parameter(Mandatory = $true)]
        [string]
        $Value,

        [switch]
        $NewTab,

        [switch]
        $Disabled
    )

    $Id = Get-PodeWebElementId -Tag A -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Link'
        ID            = $Id
        Url           = (Add-PodeWebAppPath -Url $Url)
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        NewTab        = $NewTab.IsPresent
        Disabled      = $Disabled.IsPresent
    }
}

function New-PodeWebText {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [ValidateSet('Normal', 'Underlined', 'StrikeThrough', 'Deleted', 'Inserted', 'Italics', 'Bold', 'Small')]
        [string]
        $Style = 'Normal',

        [Parameter(ParameterSetName = 'Paragraph')]
        [ValidateSet('Left', 'Right', 'Center')]
        [string]
        $Alignment = 'Left',

        [Parameter()]
        [string]
        $Pronunciation,

        [Parameter(ParameterSetName = 'Paragraph')]
        [switch]
        $InParagraph
    )

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Text'
        ID            = (Get-PodeWebElementId -Tag Txt -Id $Id)
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        Pronunciation = [System.Net.WebUtility]::HtmlEncode($Pronunciation)
        Style         = $Style
        InParagraph   = $InParagraph.IsPresent
        Alignment     = $Alignment.ToLowerInvariant()
        NoEvents      = $true
    }
}

function New-PodeWebLine {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id
    )

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Line'
        ID            = (Get-PodeWebElementId -Tag Line -Id $Id)
        NoEvents      = $true
    }
}

function New-PodeWebHidden {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $Value
    )

    $Id = Get-PodeWebElementId -Tag Hidden -Id $Id -Name $Name

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Hidden'
        Name          = $Name
        ID            = $Id
        Value         = $Value
        NoEvents      = $true
    }
}

function New-PodeWebCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $HelpText,

        [Parameter()]
        [string]
        $PrependText,

        [Parameter()]
        [string]
        $PrependIcon,

        [Parameter()]
        [string]
        $AppendText,

        [Parameter()]
        [string]
        $AppendIcon,

        [Parameter()]
        [string]
        $DisplayUsername,

        [Parameter()]
        [string]
        $DisplayPassword,

        [Parameter()]
        [ValidateSet('Username', 'Password')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Type = @('Username', 'Password'),

        [switch]
        $ReadOnly,

        [switch]
        $Required,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag Cred -Id $Id -Name $Name

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Credential'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName      = $HideName.IsPresent
        ID            = $Id
        HelpText      = [System.Net.WebUtility]::HtmlEncode($HelpText)
        ReadOnly      = $ReadOnly.IsPresent
        Prepend       = @{
            Enabled = (![string]::IsNullOrWhiteSpace($PrependText) -or ![string]::IsNullOrWhiteSpace($PrependIcon))
            Text    = $PrependText
            Icon    = $PrependIcon
        }
        Append        = @{
            Enabled = (![string]::IsNullOrWhiteSpace($AppendText) -or ![string]::IsNullOrWhiteSpace($AppendIcon))
            Text    = $AppendText
            Icon    = $AppendIcon
        }
        Placeholders  = @{
            Username = (Protect-PodeWebValue -Value $DisplayUsername -Default 'Username' -Encode)
            Password = (Protect-PodeWebValue -Value $DisplayPassword -Default 'Password' -Encode)
        }
        Type          = @($Type)
        Required      = $Required.IsPresent
    }
}

function New-PodeWebDateTime {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $HelpText,

        [Parameter()]
        [string]
        $PrependText,

        [Parameter()]
        [string]
        $PrependIcon,

        [Parameter()]
        [string]
        $AppendText,

        [Parameter()]
        [string]
        $AppendIcon,

        [Parameter()]
        [string]
        $DisplayDate,

        [Parameter()]
        [string]
        $DisplayTime,

        [Parameter()]
        [ValidateSet('Date', 'Time')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Type = @('Date', 'Time'),

        [Parameter()]
        [string]
        $DateValue,

        [Parameter()]
        [string]
        $TimeValue,

        [switch]
        $ReadOnly,

        [switch]
        $Required,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag DateTime -Id $Id -Name $Name

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'DateTime'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName      = $HideName.IsPresent
        ID            = $Id
        HelpText      = [System.Net.WebUtility]::HtmlEncode($HelpText)
        ReadOnly      = $ReadOnly.IsPresent
        Prepend       = @{
            Enabled = (![string]::IsNullOrWhiteSpace($PrependText) -or ![string]::IsNullOrWhiteSpace($PrependIcon))
            Text    = $PrependText
            Icon    = $PrependIcon
        }
        Append        = @{
            Enabled = (![string]::IsNullOrWhiteSpace($AppendText) -or ![string]::IsNullOrWhiteSpace($AppendIcon))
            Text    = $AppendText
            Icon    = $AppendIcon
        }
        Placeholders  = @{
            Date = (Protect-PodeWebValue -Value $DisplayDate -Default 'Date' -Encode)
            Time = (Protect-PodeWebValue -Value $DisplayTime -Default 'Time' -Encode)
        }
        Type          = @($Type)
        Required      = $Required.IsPresent
        Values        = @{
            Date = (Protect-PodeWebValue -Value $DateValue -Default '' -Encode)
            Time = (Protect-PodeWebValue -Value $TimeValue -Default '' -Encode)
        }
    }
}

function New-PodeWebMinMax {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $HelpText,

        [Parameter()]
        [double]
        $MinValue = 0,

        [Parameter()]
        [double]
        $MaxValue = 0,

        [Parameter()]
        [string]
        $PrependText,

        [Parameter()]
        [string]
        $PrependIcon,

        [Parameter()]
        [string]
        $AppendText,

        [Parameter()]
        [string]
        $AppendIcon,

        [Parameter()]
        [string]
        $DisplayMin,

        [Parameter()]
        [string]
        $DisplayMax,

        [Parameter()]
        [ValidateSet('Min', 'Max')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Type = @('Min', 'Max'),

        [switch]
        $ReadOnly,

        [switch]
        $Required,

        [switch]
        $HideName
    )

    $Id = Get-PodeWebElementId -Tag MinMax -Id $Id -Name $Name

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'MinMax'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        HideName      = $HideName.IsPresent
        ID            = $Id
        Values        = @{
            Min = $MinValue
            Max = $MaxValue
        }
        HelpText      = [System.Net.WebUtility]::HtmlEncode($HelpText)
        ReadOnly      = $ReadOnly.IsPresent
        Prepend       = @{
            Enabled = (![string]::IsNullOrWhiteSpace($PrependText) -or ![string]::IsNullOrWhiteSpace($PrependIcon))
            Text    = $PrependText
            Icon    = $PrependIcon
        }
        Append        = @{
            Enabled = (![string]::IsNullOrWhiteSpace($AppendText) -or ![string]::IsNullOrWhiteSpace($AppendIcon))
            Text    = $AppendText
            Icon    = $AppendIcon
        }
        Placeholders  = @{
            Min = (Protect-PodeWebValue -Value $DisplayMin -Default 'Minimum' -Encode)
            Max = (Protect-PodeWebValue -Value $DisplayMax -Default 'Maximum' -Encode)
        }
        Type          = @($Type)
        Required      = $Required.IsPresent
    }
}

function New-PodeWebRaw {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Value
    )

    $Id = Get-PodeWebElementId -Tag Raw -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Raw'
        ID            = $Id
        Value         = $Value
        NoEvents      = $true
    }
}

function New-PodeWebButtonGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [ValidateSet('Horizontal', 'Vertical')]
        [string]
        $Direction = 'Horizontal',

        [Parameter()]
        [ValidateSet('Normal', 'Small', 'Large')]
        [string]
        $Size = 'Normal',

        [Parameter()]
        [hashtable[]]
        $Buttons
    )

    if (!(Test-PodeWebContent -Content $Buttons -ComponentType Element -ObjectType Button)) {
        throw 'A Button Group can only contain Buttons'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Button-Group'
        ID            = (Get-PodeWebElementId -Tag ButtonGroup -Id $Id)
        Buttons       = $Buttons
        Direction     = $Direction
        NoEvents      = $true
    }
}

function New-PodeWebButton {
    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $ClickName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [string]
        $DataValue,

        [Parameter()]
        [object]
        $Icon,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [object[]]
        $ArgumentList,

        [Parameter(Mandatory = $true, ParameterSetName = 'Url')]
        [string]
        $Url,

        [Parameter()]
        [ValidateSet('Blue', 'Grey', 'Green', 'Red', 'Yellow', 'Cyan', 'Light', 'Dark')]
        [string]
        $Colour = 'Blue',

        [Parameter()]
        [ValidateSet('Normal', 'Small', 'Large')]
        [string]
        $Size = 'Normal',

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [switch]
        $IconOnly,

        [switch]
        $NewLine,

        [Parameter(ParameterSetName = 'Url')]
        [switch]
        $NewTab,

        [switch]
        $Outline,

        [switch]
        $Disabled,

        [switch]
        $FullWidth
    )

    $Id = Get-PodeWebElementId -Tag Btn -Id $Id -Name $Name

    $element = @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Button'
        Name             = $Name
        DisplayName      = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ClickName        = [System.Net.WebUtility]::HtmlEncode($ClickName)
        ID               = $Id
        DataValue        = $DataValue
        Icon             = (Protect-PodeWebIconType -Icon $Icon -Element 'Button')
        Url              = (Add-PodeWebAppPath -Url $Url)
        IsDynamic        = ($null -ne $ScriptBlock)
        IconOnly         = $IconOnly.IsPresent
        Colour           = $Colour
        Outline          = $Outline.IsPresent
        Size             = $Size
        FullWidth        = $FullWidth.IsPresent
        NewLine          = $NewLine.IsPresent
        NewTab           = $NewTab.IsPresent
        NoEvents         = $true
        NoAuthentication = $NoAuthentication.IsPresent
        Disabled         = $Disabled.IsPresent
    }

    $routePath = "/pode.web-dynamic/elements/button/$($Id)/click"
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

            if (($null -ne $result) -and !$WebEvent.Response.Headers.ContainsKey('Content-Disposition')) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    return $element
}

function New-PodeWebAlert {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [ValidateSet('Note', 'Tip', 'Important', 'Info', 'Warning', 'Error', 'Success')]
        [string]
        $Type = 'Note',

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter(Mandatory = $true, ParameterSetName = 'Value')]
        [string]
        $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'Content')]
        [hashtable[]]
        $Content
    )

    # ensure content are correct
    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'An Alert can only contain other elements'
    }

    $Id = Get-PodeWebElementId -Tag Alert -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Alert'
        ID            = $Id
        Type          = $Type
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Type -Encode)
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
        Content       = $Content
    }
}

function New-PodeWebIcon {
    [CmdletBinding(DefaultParameterSetName = 'Rotate')]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Colour = '',

        [Parameter()]
        [string]
        $Title = '',

        [Parameter(ParameterSetName = 'Flip')]
        [ValidateSet('Horizontal', 'Vertical')]
        [string]
        $Flip,

        [Parameter(ParameterSetName = 'Rotate')]
        [ValidateSet(0, 45, 90, 135, 180, 225, 270, 315)]
        [int]
        $Rotate = 0,

        [Parameter()]
        [ValidateSet(0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50)]
        [int]
        $Size = 0,

        [Parameter()]
        [hashtable]
        $ToggleIcon,

        [Parameter()]
        [hashtable]
        $HoverIcon,

        [switch]
        $Spin
    )

    # ensure icon presets are correct
    if (!(Test-PodeWebContent -Content $ToggleIcon -ComponentType Element -ObjectType 'Icon-Preset')) {
        throw 'The ToggleIcon for an Icon can only be an Icon-Preset element'
    }

    if (!(Test-PodeWebContent -Content $HoverIcon -ComponentType Element -ObjectType 'Icon-Preset')) {
        throw 'The HoverIcon for an Icon can only be an Icon-Preset element'
    }

    # generate an ID
    $Id = Get-PodeWebElementId -Tag Icon -Id $Id

    if (![string]::IsNullOrWhiteSpace($Colour)) {
        $Colour = $Colour.ToLowerInvariant()
    }

    $element = @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Icon'
        ID            = $Id
        Name          = $Name
        Colour        = $Colour
        Title         = $Title
        Flip          = $Flip
        Rotate        = $Rotate
        Size          = $Size
        Spin          = $Spin.IsPresent
    }

    $element.Icons = @{
        Toggle = (Protect-PodeWebIconPreset -Icon $element -Preset $ToggleIcon)
        Hover  = (Protect-PodeWebIconPreset -Icon $element -Preset $HoverIcon)
    }

    return $element
}

function New-PodeWebIconPreset {
    [CmdletBinding(DefaultParameterSetName = 'Rotate')]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Colour,

        [Parameter()]
        [string]
        $Title,

        [Parameter(ParameterSetName = 'Flip')]
        [ValidateSet('Horizontal', 'Vertical')]
        [string]
        $Flip,

        [Parameter(ParameterSetName = 'Rotate')]
        [ValidateSet(-1, 0, 45, 90, 135, 180, 225, 270, 315)]
        [int]
        $Rotate = -1,

        [Parameter()]
        [ValidateSet(-1, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50)]
        [int]
        $Size = -1,

        [switch]
        $Spin
    )

    if (![string]::IsNullOrWhiteSpace($Colour)) {
        $Colour = $Colour.ToLowerInvariant()
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Icon-Preset'
        Name          = $Name
        Colour        = $Colour
        Title         = $Title
        Flip          = $Flip
        Rotate        = $Rotate
        Size          = $Size
        Spin          = (Test-PodeWebParameter -Parameters $PSBoundParameters -Name 'Spin' -Value $Spin.IsPresent)
    }
}

function New-PodeWebSpinner {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Colour,

        [Parameter()]
        [string]
        $Title
    )

    if (![string]::IsNullOrWhiteSpace($Colour)) {
        $Colour = $Colour.ToLowerInvariant()
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Spinner'
        ID            = (Get-PodeWebElementId -Tag Spinner -Id $Id)
        Colour        = $Colour
        Title         = $Title
        NoEvents      = $true
    }
}

function New-PodeWebBadge {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [ValidateSet('Blue', 'Grey', 'Green', 'Red', 'Yellow', 'Cyan', 'Light', 'Dark')]
        [string]
        $Colour = 'Blue',

        [Parameter(Mandatory = $true)]
        [string]
        $Value
    )

    $Id = Get-PodeWebElementId -Tag Alert -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Badge'
        ID            = $Id
        Colour        = $Colour
        Value         = [System.Net.WebUtility]::HtmlEncode($Value)
    }
}

function New-PodeWebComment {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $AvatarUrl,

        [Parameter(Mandatory = $true)]
        [string]
        $Username,

        [Parameter(Mandatory = $true)]
        [string]
        $Message,

        [Parameter()]
        [DateTime]
        $TimeStamp
    )

    $Id = Get-PodeWebElementId -Tag Comment -Id $Id

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Comment'
        ID            = $Id
        AvatarUrl     = (Add-PodeWebAppPath -Url $AvatarUrl)
        Username      = [System.Net.WebUtility]::HtmlEncode($Username)
        Message       = [System.Net.WebUtility]::HtmlEncode($Message)
        TimeStamp     = $TimeStamp
        NoEvents      = $true
    }
}

function New-PodeWebChart {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Message,

        [Parameter(ParameterSetName = 'Data', ValueFromPipeline = $true)]
        $Data,

        [Parameter(Mandatory = $true, ParameterSetName = 'Dynamic')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [ValidateSet('line', 'pie', 'doughnut', 'bar')]
        [string]
        $Type = 'line',

        [Parameter()]
        [int]
        $MaxItems = 0,

        [Parameter()]
        [string]
        $Height = 0,

        [Parameter()]
        [string]
        $Width = 0,

        [Parameter(ParameterSetName = 'Dynamic')]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [int]
        $MinX = [int]::MinValue,

        [Parameter()]
        [int]
        $MaxX = [int]::MaxValue,

        [Parameter()]
        [int]
        $MinY = [int]::MinValue,

        [Parameter()]
        [int]
        $MaxY = [int]::MaxValue,

        [Parameter(ParameterSetName = 'Dynamic')]
        [int]
        $RefreshInterval = 60,

        [Parameter()]
        [string[]]
        $Colours,

        [switch]
        $Append,

        [switch]
        $TimeLabels,

        [Parameter(ParameterSetName = 'Dynamic')]
        [switch]
        $AutoRefresh,

        [Parameter(ParameterSetName = 'Dynamic')]
        [switch]
        $NoRefresh,

        [switch]
        $NoLegend,

        [switch]
        $AsCard
    )

    begin {
        $items = @()
    }

    process {
        if ($null -ne $Data) {
            if ($Data.Values -isnot [array]) {
                if ($Data.Values -is [hashtable]) {
                    $Data.Values = @($Data.Values)
                }
                else {
                    $Data.Values = @(@{
                            Key   = 'Default'
                            Value = $Data.Values
                        })
                }
            }

            $items += $Data
        }
    }

    end {
        $Id = Get-PodeWebElementId -Tag Chart -Id $Id -Name $Name

        if ($MaxItems -lt 0) {
            $MaxItems = 0
        }

        if ($RefreshInterval -le 0) {
            $RefreshInterval = 60
        }

        $null = Test-PodeWebColour -Colour $Colours -HexOnly

        $element = @{
            Operation        = 'New'
            ComponentType    = 'Element'
            ObjectType       = 'Chart'
            Name             = $Name
            DisplayName      = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
            ID               = $Id
            Message          = $Message
            ChartType        = $Type
            IsDynamic        = ($null -ne $ScriptBlock)
            Append           = $Append.IsPresent
            MaxItems         = $MaxItems
            Height           = (ConvertTo-PodeWebSize -Value $Height -Default 'auto' -Type 'px').ToLowerInvariant()
            Width            = (ConvertTo-PodeWebSize -Value $Width -Default '100%' -Type 'px').ToLowerInvariant()
            TimeLabels       = $TimeLabels.IsPresent
            AutoRefresh      = $AutoRefresh.IsPresent
            RefreshInterval  = ($RefreshInterval * 1000)
            NoRefresh        = $NoRefresh.IsPresent
            NoLegend         = $NoLegend.IsPresent
            Min              = @{
                X = $MinX
                Y = $MinY
            }
            Max              = @{
                X = $MaxX
                Y = $MaxY
            }
            NoEvents         = $true
            NoAuthentication = $NoAuthentication.IsPresent
            Colours          = $Colours
        }

        $routePath = "/pode.web-dynamic/elements/chart/$($Id)/data"
        if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
            # check for scoped vars
            $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
            $elementLogic = @{
                ScriptBlock    = $ScriptBlock
                UsingVariables = $usingVars
            }

            $auth = $null
            if (!$NoAuthentication -and !$PageData.NoAuthentication) {
                $auth = (Get-PodeWebState -Name 'auth')
            }

            if (Test-PodeIsEmpty $EndpointName) {
                $EndpointName = Get-PodeWebState -Name 'endpoint-name'
            }

            $argList = @(
                @{ Data = $ArgumentList },
                $element,
                $ElementData,
                $elementLogic
            )

            Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
                param($Data, $Element, $Parent, $Logic)
                $global:ElementData = $Element
                $global:ParentData = $Parent
                Set-PodeWebMetadata

                $result = @(Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data)

                $wrapped = $null
                if (Test-PodeWebActionsAsync) {
                    if ($result.Length -gt 0) {
                        if ($null -eq $result[0]) {
                            $result = @()
                        }

                        $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
                    }
                }
                else {
                    if ($null -eq $result) {
                        $result = @()
                    }

                    $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
                }

                if ($result.Length -gt 0) {
                    $result = ($result | Update-PodeWebChart -Id $ElementData.ID)
                }

                $result = Join-PodeWebDynamicOutput -Wrapped $wrapped -Output $result

                if (($null -ne $result) -and ($result.Length -gt 0)) {
                    Write-PodeJsonResponse -Value $result
                }

                $global:ElementData = $null
                $global:ParentData = $null
            }
        }

        $element['Data'] = $items

        if ($AsCard) {
            $element = New-PodeWebCard -Name $Name -DisplayName $DisplayName -Content $element
        }

        return $element
    }
}

function New-PodeWebCounterChart {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Counter,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [int]
        $MaxItems = 30,

        [Parameter()]
        [string]
        $Height = 0,

        [Parameter()]
        [string]
        $Width = 0,

        [Parameter()]
        [int]
        $MinX = [int]::MinValue,

        [Parameter()]
        [int]
        $MaxX = [int]::MaxValue,

        [Parameter()]
        [int]
        $MinY = [int]::MinValue,

        [Parameter()]
        [int]
        $MaxY = [int]::MaxValue,

        [Parameter()]
        [string[]]
        $Colours,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [switch]
        $NoLegend,

        [switch]
        $AsCard
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = Split-Path -Path $Counter -Leaf
    }

    if ($MaxItems -le 0) {
        $MaxItems = 30
    }

    New-PodeWebChart `
        -Name $Name `
        -DisplayName $DisplayName `
        -Type Line `
        -MaxItems $MaxItems `
        -Height $Height `
        -Width $Width `
        -ArgumentList $Counter `
        -Append `
        -TimeLabels `
        -AutoRefresh `
        -MinX $MinX `
        -MinY $MinY `
        -MaxX $MaxX `
        -MaxY $MaxY `
        -Colours $Colours `
        -NoAuthentication:$NoAuthentication `
        -AsCard:$AsCard `
        -NoLegend:$NoLegend `
        -ScriptBlock {
        param($counter)
        @{
            Values = ((Get-Counter -Counter $counter -SampleInterval 1 -MaxSamples 2).CounterSamples.CookedValue | Measure-Object -Average).Average
        }
    }
}

function New-PodeWebTable {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Message,

        [Parameter()]
        [string]
        $DataColumn,

        [Parameter()]
        [hashtable[]]
        $Columns,

        [Parameter(ParameterSetName = 'Data', ValueFromPipeline = $true)]
        $Data,

        [Parameter(ParameterSetName = 'Dynamic')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'Dynamic')]
        [object[]]
        $ArgumentList,

        [Parameter(ParameterSetName = 'Csv')]
        [string]
        $CsvFilePath,

        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [Alias('PageAmount')]
        [int]
        $PageSize = 20,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [scriptblock]
        $ClickScriptBlock,

        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [int]
        $RefreshInterval = 60,

        [switch]
        $Compact,

        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [switch]
        $Filter,

        [switch]
        $SimpleFilter,

        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [switch]
        $Sort,

        [switch]
        $SimpleSort,

        [switch]
        $Click,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [switch]
        $Paginate,

        [switch]
        $NoExport,

        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [switch]
        $NoRefresh,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [Parameter(ParameterSetName = 'Dynamic')]
        [Parameter(ParameterSetName = 'Csv')]
        [switch]
        $AutoRefresh,

        [switch]
        $AsCard
    )

    begin {
        $items = @()
    }

    process {
        if ($null -ne $Data) {
            $items += $Data
        }
    }

    end {
        $Id = Get-PodeWebElementId -Tag Table -Id $Id -Name $Name

        if (![string]::IsNullOrWhiteSpace($CsvFilePath) -and $CsvFilePath.StartsWith('.')) {
            $CsvFilePath = Join-PodeWebPath (Get-PodeServerPath) $CsvFilePath
        }

        if ($RefreshInterval -le 0) {
            $RefreshInterval = 60
        }

        $element = @{
            Operation        = 'New'
            ComponentType    = 'Element'
            ObjectType       = 'Table'
            Name             = $Name
            DisplayName      = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
            ID               = $Id
            DataColumn       = $DataColumn
            Columns          = $Columns
            Buttons          = @()
            Message          = $Message
            Compact          = $Compact.IsPresent
            Filter           = @{
                Enabled = ($Filter.IsPresent -or $SimpleFilter.IsPresent)
                Simple  = $SimpleFilter.IsPresent
            }
            Sort             = @{
                Enabled = ($Sort.IsPresent -or $SimpleSort.IsPresent)
                Simple  = $SimpleSort.IsPresent
            }
            Click            = ($Click.IsPresent -or ($null -ne $ClickScriptBlock))
            ClickIsDynamic   = ($null -ne $ClickScriptBlock)
            IsDynamic        = ($PSCmdlet.ParameterSetName -iin @('dynamic', 'csv'))
            NoExport         = $NoExport.IsPresent
            AutoRefresh      = $AutoRefresh.IsPresent
            RefreshInterval  = ($RefreshInterval * 1000)
            NoRefresh        = $NoRefresh.IsPresent
            NoAuthentication = $NoAuthentication.IsPresent
            Paging           = @{
                Enabled = $Paginate.IsPresent
                Size    = $PageSize
            }
            NoEvents         = $true
        }

        # auth an endpoint
        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        # main table data script
        $routePath = "/pode.web-dynamic/elements/table/$($Id)/data"
        $buildRoute = (($null -ne $ScriptBlock) -or ![string]::IsNullOrWhiteSpace($CsvFilePath))

        if ($buildRoute -and !(Test-PodeWebRoute -Path $routePath)) {
            # check for scoped vars
            $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
            $elementLogic = @{
                ScriptBlock    = $ScriptBlock
                UsingVariables = $usingVars
            }

            $argList = @(
                @{
                    Data    = $ArgumentList
                    CsvPath = $CsvFilePath
                },
                $element,
                $ElementData,
                $elementLogic
            )

            Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
                param($Data, $Element, $Parent, $Logic)
                $global:ElementData = $Element
                $global:ParentData = $Parent
                Set-PodeWebMetadata

                $csvFilePath = $Data.CsvPath
                if ([string]::IsNullOrWhiteSpace($csvFilePath)) {
                    $result = @(Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data)
                }
                else {
                    $result = Import-Csv -Path $csvFilePath

                    $filter = $WebEvent.Data['Filter']
                    if (![string]::IsNullOrWhiteSpace($filter)) {
                        $filter = "*$($filter)*"
                        $result = @($result | Where-Object { ($_.psobject.properties.value -ilike $filter).length -gt 0 })
                    }
                }

                $wrapped = $null
                if (Test-PodeWebActionsAsync) {
                    if ($result.Length -gt 0) {
                        if ($null -eq $result[0]) {
                            $result = @()
                        }

                        $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
                    }
                }
                else {
                    if ($null -eq $result) {
                        $result = @()
                    }

                    $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
                }

                if ($result.Length -gt 0) {
                    $paginate = $ElementData.Paging.Enabled
                    $result = ($result | Update-PodeWebTable -Id $ElementData.ID -Columns $ElementData.Columns -Paginate:$paginate)
                }

                $result = Join-PodeWebDynamicOutput -Wrapped $wrapped -Output $result

                if (($null -ne $result) -and ($result.Length -gt 0)) {
                    Write-PodeJsonResponse -Value $result
                }

                $global:ElementData = $null
                $global:ParentData = $null
            }
        }

        # table row click
        $clickPath = "$($routePath)/click"
        if (($null -ne $ClickScriptBlock) -and !(Test-PodeWebRoute -Path $clickPath)) {
            # check for scoped vars
            $ClickScriptBlock, $clickUsingVars = Convert-PodeScopedVariables -ScriptBlock $ClickScriptBlock -PSSession $PSCmdlet.SessionState
            $clickLogic = @{
                ScriptBlock    = $ClickScriptBlock
                UsingVariables = $clickUsingVars
            }

            $argList = @(
                @{ Data = $ArgumentList },
                $element,
                $ElementData,
                $clickLogic
            )

            Add-PodeRoute -Method Post -Path $clickPath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
                param($Data, $Element, $Parent, $Logic)
                $global:ElementData = $Element
                $global:ParentData = $Parent
                Set-PodeWebMetadata

                $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

                if (($null -ne $result) -and !$WebEvent.Response.Headers.ContainsKey('Content-Disposition')) {
                    Write-PodeJsonResponse -Value $result
                }

                $global:ElementData = $null
                $global:ParentData = $null
            }
        }

        $element['Data'] = $items

        if ($AsCard) {
            $element = New-PodeWebCard -Name $Name -DisplayName $DisplayName -Content $element
        }

        return $element
    }
}

function Initialize-PodeWebTableColumn {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Width = 0,

        [Parameter()]
        [ValidateSet('Left', 'Right', 'Center')]
        [string]
        $Alignment = 'Left',

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [object]
        $Icon,

        [Parameter()]
        [string]
        $Default,

        [switch]
        $Hide
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = $Key
    }

    return @{
        Key       = $Key
        Width     = (ConvertTo-PodeWebSize -Value $Width -Default 'auto' -Type '%')
        Alignment = $Alignment.ToLowerInvariant()
        Name      = $Name
        Icon      = (Protect-PodeWebIconType -Icon $Icon -Element 'Table Column')
        Default   = $Default
        Hide      = $Hide.IsPresent
    }
}

function Add-PodeWebTableButton {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [hashtable]
        $Table,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [object]
        $Icon,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [switch]
        $WithText
    )

    if ($Table.ObjectType -ieq 'card') {
        $Table = @($Table.Content | Where-Object { $_.ObjectType -ieq 'table' })[0]
    }

    $routePath = "/pode.web-dynamic/elements/table/$($Table.ID)/button/$($Name)"
    if (!(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$Table.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $Table,
            $ElementData,
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

            if (($null -ne $result) -and !$WebEvent.Response.Headers.ContainsKey('Content-Disposition')) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    $Table.Buttons += @{
        Name        = $Name
        DisplayName = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        Icon        = (Protect-PodeWebIconType -Icon $Icon -Element 'Table Button')
        IsDynamic   = ($null -ne $ScriptBlock)
        WithText    = $WithText.IsPresent
    }
}

function New-PodeWebCodeEditor {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Language = 'plaintext',

        [Parameter()]
        [ValidateSet('', 'Light', 'Dark', 'HighContrast')]
        [string]
        $Theme,

        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [scriptblock]
        $Upload,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [switch]
        $ReadOnly,

        [switch]
        $AsCard
    )

    $Id = Get-PodeWebElementId -Tag CodeEditor -Id $Id -Name $Name
    $uploadable = ($null -ne $Upload)

    $element = @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Code-Editor'
        Name             = $Name
        ID               = $Id
        Language         = $Language.ToLowerInvariant()
        Theme            = $Theme
        Value            = $Value
        ReadOnly         = $ReadOnly.IsPresent
        Uploadable       = $uploadable
        NoAuthentication = $NoAuthentication.IsPresent
    }

    # upload route
    $routePath = "/pode.web-dynamic/elements/code-editor/$($Id)/upload"
    if ($uploadable -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $Upload, $uploadUsingVars = Convert-PodeScopedVariables -ScriptBlock $Upload -PSSession $PSCmdlet.SessionState
        $uploadLogic = @{
            ScriptBlock    = $Upload
            UsingVariables = $uploadUsingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $uploadLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data
            if ($null -ne $result) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    if ($AsCard) {
        $element = New-PodeWebCard -Name $Name -Content $element
    }

    return $element
}

function New-PodeWebForm {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Message,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [ValidateSet('Get', 'Post')]
        [string]
        $Method = 'Post',

        [Parameter()]
        [string]
        $Action,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $SubmitText = 'Submit',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $ResetText = 'Reset',

        [Parameter()]
        [ValidateSet('None', 'Submit', 'Reset')]
        [string[]]
        $ButtonType = 'Submit',

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [switch]
        $AsCard
    )

    # ensure content are correct
    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Form can only contain other elements'
    }

    # generate ID
    $Id = Get-PodeWebElementId -Tag Form -Id $Id -Name $Name
    $routePath = "/pode.web-dynamic/elements/form/$($Id)/submit"

    $element = @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Form'
        Name             = $Name
        ID               = $Id
        Message          = $Message
        Content          = $Content
        NoHeader         = $NoHeader.IsPresent
        Method           = $Method
        Action           = (Protect-PodeWebValue -Value $Action -Default $routePath)
        NoEvents         = $true
        NoAuthentication = $NoAuthentication.IsPresent
        ButtonType       = $ButtonType.ToLowerInvariant()
        ResetText        = (Protect-PodeWebValue -Value $ResetText -Default 'Reset' -Encode)
        SubmitText       = (Protect-PodeWebValue -Value $SubmitText -Default 'Submit' -Encode)
    }

    if (!(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data
            if ($null -ne $result) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    if ($AsCard) {
        $element = New-PodeWebCard -Name $Name -Content $element
    }

    return $element
}

function New-PodeWebTimer {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [int]
        $Interval = 60,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication
    )

    # generate timer id
    $Id = Get-PodeWebElementId -Tag Timer -Id $Id -Name $Name

    # check for min interval
    if ($Interval -lt 10) {
        $Interval = 10
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    $element = @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Timer'
        Name             = $Name
        ID               = $Id
        Interval         = ($Interval * 1000)
        NoEvents         = $true
        NoAuthentication = $NoAuthentication.IsPresent
    }

    $elementLogic = @{
        ScriptBlock    = $ScriptBlock
        UsingVariables = $usingVars
    }

    $routePath = "/pode.web-dynamic/elements/timer/$($Id)/trigger"
    if (!(Test-PodeWebRoute -Path $routePath)) {
        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data
            if ($null -ne $result) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    $element = New-PodeWebContainer -Content $element -Hide
    return $element
}

function New-PodeWebTile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [object]
        $Icon,

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, ParameterSetName = 'Content')]
        [hashtable[]]
        $Content,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [scriptblock]
        $ClickScriptBlock,

        [Parameter()]
        [ValidateSet('Blue', 'Grey', 'Green', 'Red', 'Yellow', 'Cyan', 'Light', 'Dark')]
        [string]
        $Colour = 'Blue',

        [Parameter()]
        [int]
        $RefreshInterval = 60,

        [switch]
        $NoRefresh,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [Parameter()]
        [switch]
        $AutoRefresh,

        [switch]
        $NewLine
    )

    # ensure content are correct
    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Tile can only contain other elements'
    }

    $Id = Get-PodeWebElementId -Tag Tile -Id $Id -Name $Name

    if ($RefreshInterval -le 0) {
        $RefreshInterval = 60
    }

    $element = @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Tile'
        Name             = $Name
        DisplayName      = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID               = $Id
        Click            = ($null -ne $ClickScriptBlock)
        IsDynamic        = ($null -ne $ScriptBlock)
        Content          = $Content
        Icon             = (Protect-PodeWebIconType -Icon $Icon -Element 'Tile')
        Colour           = $Colour
        AutoRefresh      = $AutoRefresh.IsPresent
        RefreshInterval  = ($RefreshInterval * 1000)
        NoRefresh        = $NoRefresh.IsPresent
        NewLine          = $NewLine.IsPresent
        NoEvents         = $true
        NoAuthentication = $NoAuthentication.IsPresent
    }

    # auth an endpoint
    $auth = $null
    if (!$NoAuthentication -and !$PageData.NoAuthentication) {
        $auth = (Get-PodeWebState -Name 'auth')
    }

    if (Test-PodeIsEmpty $EndpointName) {
        $EndpointName = Get-PodeWebState -Name 'endpoint-name'
    }

    # main route to load tile value
    $routePath = "/pode.web-dynamic/elements/tile/$($Id)/data"
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = @(Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data)

            $wrapped = $null
            if (Test-PodeWebActionsAsync) {
                if ($result.Length -gt 0) {
                    if ($null -eq $result[0]) {
                        $result = @()
                    }

                    $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
                }
            }
            else {
                if ($null -eq $result) {
                    $result = @()
                }

                $wrapped, $result = Split-PodeWebDynamicOutput -Output $result
            }

            if ($result.Length -gt 0) {
                $result = ($result | Update-PodeWebTile -Id $ElementData.ID)
            }

            $result = Join-PodeWebDynamicOutput -Wrapped $wrapped -Output $result

            if (($null -ne $result) -and ($result.Length -gt 0)) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    # tile click route
    $clickPath = "$($routePath)/click"
    if (($null -ne $ClickScriptBlock) -and !(Test-PodeWebRoute -Path $clickPath)) {
        # check for scoped vars
        $ClickScriptBlock, $clickUsingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $clickLogic = @{
            ScriptBlock    = $ClickScriptBlock
            UsingVariables = $clickUsingVars
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $element,
            $ElementData,
            $clickLogic
        )

        Add-PodeRoute -Method Post -Path $clickPath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Element, $Parent, $Logic)
            $global:ElementData = $Element
            $global:ParentData = $Parent
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

            if (($null -ne $result) -and !$WebEvent.Response.Headers.ContainsKey('Content-Disposition')) {
                Write-PodeJsonResponse -Value $result
            }

            $global:ElementData = $null
            $global:ParentData = $null
        }
    }

    return $element
}

function New-PodeWebFileStream {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [Parameter()]
        [int]
        $Height = 20,

        [Parameter()]
        [int]
        $Interval = 10,

        [Parameter()]
        [object]
        $Icon,

        [switch]
        $NoHeader
    )

    $Id = Get-PodeWebElementId -Tag FileStream -Id $Id -Name $Name

    if ($Height -le 0) {
        $Height = 20
    }

    if ($Interval -le 0) {
        $Interval = 10
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'File-Stream'
        Name          = $Name
        ID            = $Id
        Height        = $Height
        Url           = (Add-PodeWebAppPath -Url $Url)
        Interval      = ($Interval * 1000)
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'File Stream')
        NoHeader      = $NoHeader.IsPresent
    }
}

function New-PodeWebIFrame {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [Parameter()]
        [string]
        $Title
    )

    if ([string]::IsNullOrWhiteSpace($Title)) {
        $Title = $Name
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'iFrame'
        Name          = $Name
        ID            = (Get-PodeWebElementId -Tag iFrame -Id $Id -Name $Name)
        Url           = (Add-PodeWebAppPath -Url $Url)
        Title         = $Title
        NoEvents      = $true
    }
}

function New-PodeWebAudio {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Source,

        [Parameter()]
        [hashtable[]]
        $Track,

        [Parameter()]
        [string]
        $NotSupportedText,

        [Parameter()]
        [string]
        $Width = 20,

        [switch]
        $Muted,

        [switch]
        $AutoPlay,

        [switch]
        $AutoBuffer,

        [switch]
        $Loop,

        [switch]
        $NoControls,

        [switch]
        $NoDownload
    )

    if (!(Test-PodeWebContent -Content $Source -ComponentType Element -ObjectType AudioSource)) {
        throw 'Audio sources can only contain AudioSource elements'
    }

    if (!(Test-PodeWebContent -Content $Track -ComponentType Element -ObjectType MediaTrack)) {
        throw 'Audio tracks can only contain MediaTrack elements'
    }

    return @{
        Operation        = 'New'
        ComponentType    = 'Element'
        ObjectType       = 'Audio'
        Name             = $Name
        ID               = (Get-PodeWebElementId -Tag Audio -Id $Id -Name $Name)
        Width            = (ConvertTo-PodeWebSize -Value $Width -Default 20 -Type '%')
        Sources          = $Source
        Tracks           = $Track
        NotSupportedText = (Protect-PodeWebValue -Value $NotSupportedText -Default 'Your browser does not support the audio element' -Encode)
        Muted            = $Muted.IsPresent
        AutoPlay         = $AutoPlay.IsPresent
        AutoBuffer       = $AutoBuffer.IsPresent
        Loop             = $Loop.IsPresent
        NoControls       = $NoControls.IsPresent
        NoDownload       = $NoDownload.IsPresent
    }
}

function New-PodeWebAudioSource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Url
    )

    $type = [string]::Empty

    switch (($Url -split '\.')[-1].ToLowerInvariant()) {
        'mp3' { $type = 'audio/mpeg' }
        'ogg' { $type = 'audio/ogg' }
        'wav' { $type = 'audio/wav' }
        default {
            throw "Audio source type unsupported: $($_)"
        }
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'AudioSource'
        Url           = (Add-PodeWebAppPath -Url $Url)
        Type          = $type
        NoEvents      = $true
    }
}

function New-PodeWebVideo {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]
        $Source,

        [Parameter()]
        [hashtable[]]
        $Track,

        [Parameter()]
        [string]
        $Thumbnail,

        [Parameter()]
        [string]
        $NotSupportedText,

        [Parameter()]
        [string]
        $Width = 20,

        [Parameter()]
        [string]
        $Height = 15,

        [switch]
        $Muted,

        [switch]
        $AutoPlay,

        [switch]
        $AutoBuffer,

        [switch]
        $Loop,

        [switch]
        $NoControls,

        [switch]
        $NoDownload,

        [switch]
        $NoPictureInPicture
    )

    if (!(Test-PodeWebContent -Content $Source -ComponentType Element -ObjectType VideoSource)) {
        throw 'Video sources can only contain VideoSource elements'
    }

    if (!(Test-PodeWebContent -Content $Track -ComponentType Element -ObjectType MediaTrack)) {
        throw 'Video tracks can only contain MediaTrack elements'
    }

    return @{
        Operation          = 'New'
        ComponentType      = 'Element'
        ObjectType         = 'Video'
        Name               = $Name
        ID                 = (Get-PodeWebElementId -Tag Video -Id $Id -Name $Name)
        Width              = (ConvertTo-PodeWebSize -Value $Width -Default 20 -Type '%')
        Height             = (ConvertTo-PodeWebSize -Value $Height -Default 15 -Type '%')
        Sources            = $Source
        Tracks             = $Track
        Thumbnail          = $Thumbnail
        NotSupportedText   = (Protect-PodeWebValue -Value $NotSupportedText -Default 'Your browser does not support the video element' -Encode)
        Muted              = $Muted.IsPresent
        AutoPlay           = $AutoPlay.IsPresent
        AutoBuffer         = $AutoBuffer.IsPresent
        Loop               = $Loop.IsPresent
        NoControls         = $NoControls.IsPresent
        NoDownload         = $NoDownload.IsPresent
        NoPictureInPicture = $NoPictureInPicture.IsPresent
    }
}

function New-PodeWebVideoSource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Url
    )

    $type = [string]::Empty

    switch (($Url -split '\.')[-1].ToLowerInvariant()) {
        'mp4' { $type = 'video/mp4' }
        'ogg' { $type = 'video/ogg' }
        'webm' { $type = 'video/webm' }
        default {
            throw "Video source type unsupported: $($_)"
        }
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'VideoSource'
        Url           = (Add-PodeWebAppPath -Url $Url)
        Type          = $type
        NoEvents      = $true
    }
}

function New-PodeWebMediaTrack {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [Parameter()]
        [string]
        $Language,

        [Parameter()]
        [string]
        $Title,

        [Parameter()]
        [ValidateSet('captions', 'chapters', 'descriptions', 'metadata', 'subtitles')]
        [string]
        $Type = 'subtitles',

        [switch]
        $Default
    )

    if (($Url -split '\.')[-1] -ine 'vtt') {
        throw 'Invalid media track file format supplied, expected a .vtt file'
    }

    if (($Type -ieq 'subtitles') -and [string]::IsNullOrWhiteSpace($Language)) {
        throw 'A language is required for subtitle tracks'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'MediaTrack'
        Url           = (Add-PodeWebAppPath -Url $Url)
        Language      = $Language
        Title         = $Title
        Type          = $Type.ToLowerInvariant()
        Default       = $Default.IsPresent
        NoEvents      = $true
    }
}

function Use-PodeWebElement {
    [CmdletBinding(DefaultParameterSetName = 'ID')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Element')]
        [hashtable]
        $Element,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ID')]
        [string]
        $Id
    )

    # element is an element?
    if (($null -ne $Element) -and ($Element.ComponentType -ine 'element')) {
        throw 'You can only reference another element'
    }

    # set element ID
    if ($null -ne $Element) {
        $Id = $Element.ID
    }

    return @{
        Operation     = 'Use'
        ComponentType = 'Element'
        ObjectType    = 'Element'
        Reference     = @{
            ID = $Id
        }
    }
}

function New-PodeWebGrid {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Cells,

        [Parameter()]
        [int]
        $Width = 0,

        [switch]
        $Vertical
    )

    if (!(Test-PodeWebContent -Content $Cells -ComponentType Element -ObjectType Cell)) {
        throw 'A Grid can only contain Cell elements'
    }

    if ($Vertical) {
        $Width = 1
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Grid'
        Cells         = $Cells
        Width         = $Width
        ID            = (Get-PodeWebElementId -Tag Grid -Id $Id)
        NoEvents      = $true
    }
}

function New-PodeWebCell {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter()]
        [string]
        $Width,

        [Parameter()]
        [ValidateSet('Left', 'Right', 'Center')]
        [string]
        $Alignment = 'Left'
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Cell can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Cell'
        Content       = $Content
        Width         = (Protect-PodeWebRange -Value $Width -Min 1 -Max 12)
        ID            = (Get-PodeWebElementId -Tag Cell -Id $Id)
        Alignment     = $Alignment.ToLowerInvariant()
        NoEvents      = $true
    }
}

function New-PodeWebTabs {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Tabs,

        [Parameter()]
        [int]
        $CycleInterval = 15,

        [switch]
        $Cycle
    )

    if (!(Test-PodeWebContent -Content $Tabs -ComponentType Element -ObjectType Tab)) {
        throw 'Tabs can only contain Tab elements'
    }

    if ($CycleInterval -lt 10) {
        $CycleInterval = 10
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Tabs'
        ID            = (Get-PodeWebElementId -Tag Tabs -Id $Id)
        Tabs          = $Tabs
        Cycle         = @{
            Enabled  = $Cycle.IsPresent
            Interval = ($CycleInterval * 1000)
        }
        NoEvents      = $true
    }
}

function New-PodeWebTab {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter()]
        [object]
        $Icon
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Tab can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Tab'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID            = (Get-PodeWebElementId -Tag Tab -Id $Id -Name $Name)
        Content       = $Content
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'Tab')
        NoEvents      = $true
    }
}

function New-PodeWebCard {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter()]
        [hashtable[]]
        $Buttons,

        [Parameter()]
        [object]
        $Icon,

        [switch]
        $NoTitle,

        [switch]
        $NoHide
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Card can only contain other elements'
    }

    if (!(Test-PodeWebContent -Content $Buttons -ComponentType Element -ObjectType Button, 'Button-Group')) {
        throw 'Card Buttons can only contain Buttons'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Card'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID            = (Get-PodeWebElementId -Tag Card -Id $Id -Name $Name)
        Content       = $Content
        Buttons       = $Buttons
        NoTitle       = $NoTitle.IsPresent
        NoHide        = $NoHide.IsPresent
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'Card')
        NoEvents      = $true
    }
}

function New-PodeWebContainer {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [switch]
        $NoBackground,

        [switch]
        $Hide
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Container can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Container'
        ID            = (Get-PodeWebElementId -Tag Container -Id $Id)
        Content       = $Content
        NoBackground  = $NoBackground.IsPresent
        Hide          = $Hide.IsPresent
        NoEvents      = $true
    }
}

function New-PodeWebModal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter()]
        [object]
        $Icon,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $SubmitText = 'Submit',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $CloseText = 'Close',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $ResetText = 'Reset',

        [Parameter()]
        [ValidateSet('Small', 'Medium', 'Large')]
        [string]
        $Size = 'Small',

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [ValidateSet('Get', 'Post')]
        [string]
        $Method = 'Post',

        [Parameter()]
        [string]
        $Action,

        [Parameter()]
        [ValidateSet('None', 'Submit', 'Reset', 'Close')]
        [string[]]
        $ButtonType = @('Close', 'Submit'),

        [switch]
        $AsForm,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Modal can only contain other elements'
    }

    # generate ID
    $Id = Get-PodeWebElementId -Tag Modal -Id $Id -Name $Name

    $routePath = "/pode.web-dynamic/elements/modal/$($Id)/submit"
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Logic)
            Set-PodeWebMetadata
            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

            if ($null -ne $result) {
                Write-PodeJsonResponse -Value $result
            }
        }
    }

    if ($null -eq $ScriptBlock) {
        $ButtonType = @('Close')
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Modal'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID            = $Id
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'Modal')
        Content       = $Content
        CloseText     = (Protect-PodeWebValue -Value $CloseText -Default 'Close' -Encode)
        SubmitText    = (Protect-PodeWebValue -Value $SubmitText -Default 'Submit' -Encode)
        ResetText     = (Protect-PodeWebValue -Value $ResetText -Default 'Reset' -Encode)
        Size          = $Size
        AsForm        = $AsForm.IsPresent
        ButtonType    = $ButtonType.ToLowerInvariant()
        Method        = $Method
        Action        = (Protect-PodeWebValue -Value $Action -Default $routePath)
        NoEvents      = $true
    }
}

function New-PodeWebHero {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Title,

        [Parameter(Mandatory = $true)]
        [string]
        $Message,

        [Parameter()]
        [hashtable[]]
        $Content
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Hero can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Hero'
        ID            = (Get-PodeWebElementId -Tag Hero -Id $Id)
        Title         = [System.Net.WebUtility]::HtmlEncode($Title)
        Message       = [System.Net.WebUtility]::HtmlEncode($Message)
        Content       = $Content
        NoEvents      = $true
    }
}

function New-PodeWebCarousel {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Slides
    )

    if (!(Test-PodeWebContent -Content $Slides -ComponentType Element -ObjectType Slide)) {
        throw 'A Carousel can only contain Slide elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Carousel'
        ID            = (Get-PodeWebElementId -Tag Carousel -Id $Id)
        Slides        = $Slides
        NoEvents      = $true
    }
}

function New-PodeWebSlide {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter()]
        [string]
        $Title,

        [Parameter()]
        [string]
        $Message
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Slide can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Slide'
        Content       = $Content
        ID            = (Get-PodeWebElementId -Tag Slide)
        Title         = [System.Net.WebUtility]::HtmlEncode($Title)
        Message       = [System.Net.WebUtility]::HtmlEncode($Message)
        NoEvents      = $true
    }
}

function New-PodeWebSteps {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Steps,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication
    )

    if (!(Test-PodeWebContent -Content $Steps -ComponentType Element -ObjectType Step)) {
        throw 'Steps can only contain Step elements'
    }

    # generate ID
    $Id = Get-PodeWebElementId -Tag Steps -Id $Id -Name $Name

    # add route
    $routePath = "/pode.web-dynamic/elements/steps/$($Id)/submit"
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Logic)
            Set-PodeWebMetadata
            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

            if ($null -ne $result) {
                Write-PodeJsonResponse -Value $result
            }
        }
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Steps'
        ID            = $Id
        Steps         = $Steps
        NoEvents      = $true
    }
}

function New-PodeWebStep {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [hashtable[]]
        $Content,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [object]
        $Icon,

        [Parameter()]
        [string[]]
        $EndpointName,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Step can only contain other elements'
    }

    # generate ID
    $Id = Get-PodeWebElementId -Tag Step -Name $Name

    # add route
    $routePath = "/pode.web-dynamic/elements/step/$($Id)/submit"
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $routePath)) {
        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $elementLogic = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }

        $auth = $null
        if (!$NoAuthentication -and !$PageData.NoAuthentication) {
            $auth = (Get-PodeWebState -Name 'auth')
        }

        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        $argList = @(
            @{ Data = $ArgumentList },
            $elementLogic
        )

        Add-PodeRoute -Method Post -Path $routePath -Authentication $auth -ArgumentList $argList -EndpointName $EndpointName -ScriptBlock {
            param($Data, $Logic)
            Set-PodeWebMetadata
            $result = Invoke-PodeWebScriptBlock -Logic $Logic -Arguments $Data.Data

            if ($null -ne $result) {
                Write-PodeJsonResponse -Value $result
            }
        }
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Step'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID            = $Id
        Content       = $Content
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'Step')
        IsDynamic     = ($null -ne $ScriptBlock)
        NoEvents      = $true
    }
}

function Set-PodeWebBreadcrumb {
    [CmdletBinding()]
    param(
        [Parameter()]
        [hashtable[]]
        $Items = @()
    )

    if (($null -eq $Items)) {
        $Items = @()
    }

    if (!(Test-PodeWebContent -Content $Items -ComponentType Element -ObjectType BreadcrumbItem)) {
        throw 'A Breadcrumb can only contain breadcrumb item elements'
    }

    $foundActive = $false
    foreach ($item in $Items) {
        if ($foundActive -and $item.Active) {
            throw 'Cannot have two active breadcrumb items'
        }

        $foundActive = $item.Active
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Breadcrumb'
        Items         = $Items
        NoEvents      = $true
    }
}

function New-PodeWebBreadcrumbItem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter(Mandatory = $true)]
        [string]
        $Url,

        [switch]
        $Active
    )

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Breadcrumb-Item'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        Url           = (Add-PodeWebAppPath -Url $Url)
        Active        = $Active.IsPresent
        NoEvents      = $true
    }
}

function New-PodeWebAccordion {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Bellows,

        [Parameter()]
        [int]
        $CycleInterval = 15,

        [Parameter()]
        [ValidateSet('Normal', 'Collapsed', 'Expanded')]
        [string]
        $Mode = 'Normal',

        [switch]
        $Cycle
    )

    if (!(Test-PodeWebContent -Content $Bellows -ComponentType Element -ObjectType Bellow)) {
        throw 'An Accordion can only contain Bellow elements'
    }

    if ($CycleInterval -lt 10) {
        $CycleInterval = 10
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Accordion'
        ID            = (Get-PodeWebElementId -Tag Accordion -Id $Id -Name $Name)
        Name          = $Name
        Bellows       = $Bellows
        Mode          = $Mode
        Cycle         = @{
            Enabled  = $Cycle.IsPresent
            Interval = ($CycleInterval * 1000)
        }
        NoEvents      = $true
    }
}

function New-PodeWebBellow {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [hashtable[]]
        $Content,

        [Parameter()]
        [object]
        $Icon
    )

    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Bellow can only contain other elements'
    }

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Bellow'
        Name          = $Name
        DisplayName   = (Protect-PodeWebValue -Value $DisplayName -Default $Name -Encode)
        ID            = (Get-PodeWebElementId -Tag Bellow -Id $Id -Name $Name)
        Content       = $Content
        Icon          = (Protect-PodeWebIconType -Icon $Icon -Element 'Bellow')
        NoEvents      = $true
    }
}

function New-PodeWebElementGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content,

        [Parameter()]
        [string]
        $SubmitButtonId
    )

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Element-Group'
        Content       = $Content
        ID            = (Get-PodeWebElementId -Tag 'ElementGroup' -Id $Id)
        SubmitId      = $SubmitButtonId
        NoEvents      = $true
    }
}

function New-PodeWebSpan {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [hashtable[]]
        $Content
    )

    return @{
        Operation     = 'New'
        ComponentType = 'Element'
        ObjectType    = 'Span'
        Content       = $Content
        ID            = (Get-PodeWebElementId -Tag 'Span' -Id $Id)
        NoEvents      = $true
    }
}