diff --git a/docs/Tutorials/Elements/Textbox.md b/docs/Tutorials/Elements/Textbox.md index a3e51260..a0f638a1 100644 --- a/docs/Tutorials/Elements/Textbox.md +++ b/docs/Tutorials/Elements/Textbox.md @@ -1,18 +1,28 @@ # Textbox -| Support | | -| ------- |-| -| Events | Yes | +| Support | | +| ------- | --- | +| Events | Yes | -A textbox element is a form input element; you can render a textbox, single and multiline, to your page using [`New-PodeWebTextbox`](../../../Functions/Elements/New-PodeWebTextbox). +A Textbox element is a form input element; you can render a Textbox, single and multiline, to your page using [`New-PodeWebTextbox`](../../../Functions/Elements/New-PodeWebTextbox). -A textbox by default is a normal plain single lined textbox, however you can customise its `-Type` to Email/Password/etc. To change the textbox to be a multiline textbox you can supply the `-Multiline` switch. +A Textbox by default is a normal single lined textbox, however you can customise its `-Type` to Email/Password/etc. To change the textbox to be a multiline textbox you can supply the `-Multiline` switch. -Textboxes also allow you to specify `-AutoComplete` values ([see here](#autocomplete)). +Supported types are: +* Text +* Email +* Password +* Number +* Date +* Time +* File +* DateTime + +Textboxes also allow you to specify `-AutoComplete` options, as ([described here](#autocomplete)). ## Single -A default textbox is just a simple single lined textbox. You can change the type to Email/Password/etc using the `-Type` parameter: +A default Textbox is just a simple single lined textbox. You can change the type to Email/Password/etc using the `-Type` parameter: ```powershell New-PodeWebCard -Content @( @@ -34,7 +44,7 @@ Which looks like below: ### AutoComplete -For a single textbox, you can supply autocomplete values via a scriptblock passed to `-AutoComplete`. This scriptblock should return an array of strings, and will be called once when the textbox is initially loaded: +For a single Textbox, you can supply autocomplete options via a scriptblock passed to `-AutoComplete`. This scriptblock should return an array of strings, and will be called once when the textbox is initially loaded: ```powershell New-PodeWebCard -Content @( @@ -52,9 +62,39 @@ Which looks like below: ![textbox_auto](../../../images/textbox_auto.png) +#### Delay + +You can delay when the autocomplete options are shown by supplying `-AutoCompleteMinLength`. By default this is `1`, and the value refers to the number of characters typed into the Textbox before the option are displayed. + +!!! note + The options will still be loaded on initial page load, but will not render until the specified number of characters. + +#### Dynamic + +To have the autocomplete scriptblock be invoked on every character, instead of once on page load, you supply `-AutoCompleteType Always`. The default is `Once`, and `Always` will invoke the scriptblock every time. + +To help with dynamic filtering, the current value entered into the Textbox is supplied as `$WebEvent.Data.Value` - this is only available when using the `Always` autocomplete type, and **not** in the default `Once` type. + +```powershell +New-PodeWebCard -Content @( + New-PodeWebForm -Name 'Example' -ScriptBlock { + $svcName = $WebEvent.Data['Service Name'] + } -Content @( + New-PodeWebTextbox -Name 'Service Name' -AutoCompleteType Always -AutoComplete { + return Get-Service | + Where-Object { $_.Name -ilike "*$($WebEvent.Data.Value)*" } | + Select-Object -ExpandProperty Name + } + ) +) +``` + +!!! tip + The `-AutoCompleteMinLength` parameter still work here, and the scriptblock will only be invoked after the specified number of characters. + ## Multiline -A mutlilined textbox can be displayed by passing `-Multiline`. You cannot change the type of this textbox, it will always allow freestyle text: +A multi-lined Textbox can be displayed by passing `-Multiline`. You cannot change the type of this Textbox as only `Text` types support multi-line: ```powershell New-PodeWebCard -Content @( diff --git a/examples/autocomplete.ps1 b/examples/autocomplete.ps1 new file mode 100644 index 00000000..01bb4d17 --- /dev/null +++ b/examples/autocomplete.ps1 @@ -0,0 +1,50 @@ +Import-Module Pode -MaximumVersion 2.99.99 -Force +Import-Module ..\src\Pode.Web.psd1 -Force + +Start-PodeServer -Threads 2 { + # add a simple endpoint + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set the use of templates, and set a login page + Initialize-PodeWebTemplates -Title 'Autocomplete' -Theme Dark + + # form with load once autocomplete + $onceInstant = New-PodeWebForm -Name 'OnceInstant' -ButtonType Submit -AsCard -ScriptBlock { + $WebEvent.Data.OnceInstantOutput | Show-PodeWebToast + } -Content @( + New-PodeWebTextbox -Name 'OnceInstantInput' -AutoComplete { + return @('One', 'Two', 'Three', 'Four', 'Five') + } + ) + + # form with load every time autocomplete + $alwaysInstant = New-PodeWebForm -Name 'AlwaysInstant' -ButtonType Submit -AsCard -ScriptBlock { + $WebEvent.Data.AlwaysInstantOutput | Show-PodeWebToast + } -Content @( + New-PodeWebTextbox -Name 'AlwaysInstantInput' -AutoCompleteType Always -AutoComplete { + return @('One', 'Two', 'Three', 'Four', 'Five') | Where-Object { $_ -imatch "^$($WebEvent.Data.Value)" } + } + ) + + # form with load once autocomplete on 3 char delay + $onceDelay = New-PodeWebForm -Name 'OnceDelay' -ButtonType Submit -AsCard -ScriptBlock { + $WebEvent.Data.OnceDelayOutput | Show-PodeWebToast + } -Content @( + New-PodeWebTextbox -Name 'OnceDelayInput' -AutoCompleteMinLength 3 -AutoComplete { + return @('One', 'Two', 'Three', 'Four', 'Five') + } + ) + + # form with load every time autocomplete on 3 char delay + $alwaysDelay = New-PodeWebForm -Name 'AlwaysDelay' -ButtonType Submit -AsCard -ScriptBlock { + $WebEvent.Data.AlwaysDelayOutput | Show-PodeWebToast + } -Content @( + New-PodeWebTextbox -Name 'AlwaysDelayInput' -AutoCompleteType Always -AutoCompleteMinLength 3 -AutoComplete { + return @('One', 'Two', 'Three', 'Four', 'Five') | Where-Object { $_ -imatch "^$($WebEvent.Data.Value)" } + } + ) + + # add forms to page + Add-PodeWebPage -Name 'Home' -Path '/' -Content $onceInstant, $alwaysInstant, $onceDelay, $alwaysDelay -Title 'Testing Autocomplete' -HomePage +} \ No newline at end of file diff --git a/src/Public/Actions.ps1 b/src/Public/Actions.ps1 index f6f5554c..47d0707a 100644 --- a/src/Public/Actions.ps1 +++ b/src/Public/Actions.ps1 @@ -662,7 +662,7 @@ function Clear-PodeWebTextbox { function Show-PodeWebToast { [CmdletBinding()] param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] $Message, @@ -680,17 +680,31 @@ function Show-PodeWebToast { $Icon = 'information' ) - if ($Duration -le 0) { - $Duration = 3000 + begin { + $items = @() } - Send-PodeWebAction -Value @{ - Operation = 'Show' - ObjectType = 'Toast' - Message = [System.Net.WebUtility]::HtmlEncode($Message) - Title = [System.Net.WebUtility]::HtmlEncode($Title) - Duration = $Duration - Icon = (Protect-PodeWebIconType -Icon $Icon -Element 'Toast') + process { + if (![string]::IsNullOrWhiteSpace($Message)) { + $items += $Message + } + } + + end { + if ($Duration -le 0) { + $Duration = 3000 + } + + foreach ($msg in $items) { + Send-PodeWebAction -Value @{ + Operation = 'Show' + ObjectType = 'Toast' + Message = [System.Net.WebUtility]::HtmlEncode($msg) + Title = [System.Net.WebUtility]::HtmlEncode($Title) + Duration = $Duration + Icon = (Protect-PodeWebIconType -Icon $Icon -Element 'Toast') + } + } } } diff --git a/src/Public/Elements.ps1 b/src/Public/Elements.ps1 index fc306255..cfb58763 100644 --- a/src/Public/Elements.ps1 +++ b/src/Public/Elements.ps1 @@ -59,6 +59,16 @@ function New-PodeWebTextbox { [scriptblock] $AutoComplete, + [Parameter(ParameterSetName = 'Single')] + [ValidateSet('Once', 'Always')] + [string] + $AutoCompleteType = 'Once', + + [Parameter(ParameterSetName = 'Single')] + [ValidateRange(1, [int]::MaxValue)] + [int] + $AutoCompleteMinLength = 1, + [Parameter()] [string[]] $EndpointName, @@ -143,7 +153,11 @@ function New-PodeWebTextbox { HelpText = [System.Net.WebUtility]::HtmlEncode($HelpText) ReadOnly = $ReadOnly.IsPresent Disabled = $Disabled.IsPresent - IsAutoComplete = ($null -ne $AutoComplete) + AutoComplete = @{ + Enabled = ($null -ne $AutoComplete) + Type = $AutoCompleteType.ToLowerInvariant() + MinLength = $AutoCompleteMinLength + } Value = $items Prepend = @{ Enabled = (![string]::IsNullOrWhiteSpace($PrependText) -or ![string]::IsNullOrWhiteSpace($PrependIcon)) diff --git a/src/Public/Pages.ps1 b/src/Public/Pages.ps1 index 318e107b..c474bfbb 100644 --- a/src/Public/Pages.ps1 +++ b/src/Public/Pages.ps1 @@ -153,6 +153,7 @@ function Set-PodeWebLoginPage { GrantType = $grantType IsSystem = $true ConnectionType = (Get-PodeWebConnectionType) + Features = (Get-PodeWebState -Name 'features') } # set auth system urls @@ -427,6 +428,7 @@ function Add-PodeWebPage { Users = @($AccessUsers) } ConnectionType = (Get-PodeWebConnectionType) + Features = (Get-PodeWebState -Name 'features') } # does the page need auth? diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 79fcb9a0..bd41b3af 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -92,6 +92,11 @@ function Initialize-PodeWebTemplates { Set-PodeWebState -Name 'custom-js' -Value @() Set-PodeWebState -Name 'conn-type' -Value $ConnectionType.ToLowerInvariant() + # setup default features for frontend parsing + Set-PodeWebState -Name 'features' -Value @{ + ParseDateTime = !(Test-PodeIsPSCore) + } + # themes Set-PodeWebState -Name 'theme' -Value $Theme.ToLowerInvariant() Set-PodeWebState -Name 'custom-themes' -Value @{ diff --git a/src/Templates/Public/scripts/default.js b/src/Templates/Public/scripts/default.js index 3709ef5a..e8490860 100644 --- a/src/Templates/Public/scripts/default.js +++ b/src/Templates/Public/scripts/default.js @@ -16,6 +16,7 @@ var tooltips = function() { }; tooltips(); +var FEATURES = {}; var pageLoaded = false; var contentLoaded = false; @@ -26,6 +27,9 @@ $(() => { } pageLoaded = true; + // load features from body attributes + loadFeatures(); + // check theme if (checkAutoTheme()) { return; @@ -47,6 +51,12 @@ $(() => { setupClientConnection(); }); +function loadFeatures() { + FEATURES = { + ParseDateTime: ($('body').attr('pode-parse-datetime') === 'True') + }; +} + function loadContent() { if (contentLoaded) { return; @@ -1209,6 +1219,18 @@ function getTimeString() { return (new Date()).toLocaleTimeString().split(':').slice(0, 2).join(':'); } +function convertDateTimeString(value) { + if (!value || typeof value !== 'string') { + return value; + } + + // find references to "/Date(...)/" and convert to datetime object + return value.replace(/\/Date\((\d+)\)\//g, function(match, timestamp) { + // return in YYYY-MM-DDTHH:mm:ss format - same as .NET's default JSON date format + return new Date(parseInt(timestamp)).toISOString().split('.')[0]; + }); +} + function actionHref(action) { if (!action) { return; diff --git a/src/Templates/Public/scripts/templates.js b/src/Templates/Public/scripts/templates.js index a34c5680..2e624091 100644 --- a/src/Templates/Public/scripts/templates.js +++ b/src/Templates/Public/scripts/templates.js @@ -887,6 +887,14 @@ class PodeElement { element.off(evt); } + sanitize(value) { + if (FEATURES.ParseDateTime) { + value = convertDateTimeString(value); + } + + return value; + } + spinner(show) { if (!this.hasSpinner || (!show && this.loading)) { return; @@ -3370,6 +3378,7 @@ class PodeTable extends PodeRefreshableElement { break; default: + console.log(data); this.updateTable(data, sender, opts); break; } @@ -3413,7 +3422,7 @@ class PodeTable extends PodeRefreshableElement { elements.push(...(renderResult.elements)); } else { - html = rowData; + html = this.sanitize(rowData); } row.find(`td[pode-column="${key}"]`).html(html); @@ -3540,10 +3549,10 @@ class PodeTable extends PodeRefreshableElement { }); } else if (item[key] != null) { - value += item[key]; + value += this.sanitize(item[key]); } else if (!item[key] && header.length > 0) { - value += header.attr('default-value'); + value += this.sanitize(header.attr('default-value')); } value += ``; @@ -3794,7 +3803,11 @@ class PodeTextbox extends PodeFormElement { constructor(data, sender, opts) { super(data, sender, opts); this.multiline = data.Multiline ?? false; - this.autoComplete = data.IsAutoComplete ?? false; + this.autoComplete = { + enabled: data.AutoComplete.Enabled ?? false, + type: data.AutoComplete.Type ?? 'once', + minLength: data.AutoComplete.MinLength ?? 1 + } } new(data, sender, opts) { @@ -3859,12 +3872,35 @@ class PodeTextbox extends PodeFormElement { var obj = this; - if (this.autoComplete) { - sendAjaxReq(`${this.url}/autocomplete`, null, null, false, null, null, { - customActionCallback: (res) => { - obj.element.autocomplete({ source: res.Values }); - } - }); + // bind autocomplete handlers + if (this.autoComplete.enabled) { + switch (this.autoComplete.type) { + // load autocomplete options once, and cache on the element + case 'once': + sendAjaxReq(`${this.url}/autocomplete`, null, null, false, null, null, { + customActionCallback: (res) => { + obj.element.autocomplete({ + source: convertToArray(res.Values), + minLength: obj.autoComplete.minLength + }); + } + }); + break; + + // load autocomplete options on every char press + case 'always': + this.element.autocomplete({ + source: function(request, response) { + sendAjaxReq(`${obj.url}/autocomplete`, `Value=${request.term}`, null, false, null, null, { + customActionCallback: (res) => { + response(convertToArray(res.Values)); + } + }); + }, + minLength: obj.autoComplete.minLength + }); + break; + } } } diff --git a/src/Templates/Views/index.pode b/src/Templates/Views/index.pode index 8a295dc7..1d32d1b2 100644 --- a/src/Templates/Views/index.pode +++ b/src/Templates/Views/index.pode @@ -12,6 +12,7 @@ $(if ($data.Sessions.Enabled) { "pode-session-tabs='$($data.Sessions.Tabs)'" }) $(if ($data.AppPath) { "pode-app-path='$($data.AppPath)'" }) pode-conn-type="$($data.Page.ConnectionType)" + pode-parse-datetime="$($data.Page.Features.ParseDateTime)" $(ConvertTo-PodeWebEvents -Events $data.Page.Events)> $( diff --git a/src/Templates/Views/login.pode b/src/Templates/Views/login.pode index c47220dc..8b92dad9 100644 --- a/src/Templates/Views/login.pode +++ b/src/Templates/Views/login.pode @@ -12,6 +12,7 @@ $(if ($data.Sessions.Enabled) { "pode-session-tabs='$($data.Sessions.Tabs)'" }) $(if ($data.AppPath) { "pode-app-path='$($data.AppPath)'" }) pode-conn-type="$($data.Page.ConnectionType)" + pode-parse-datetime="$($data.Page.Features.ParseDateTime)" style="background-image: url('$($data.Background.Image)'); background-repeat: no-repeat; background-size: cover" $(ConvertTo-PodeWebEvents -Events $data.Page.Events)>