diff --git a/Examples/Example-Carousel/Example-Carousel03.ps1 b/Examples/Example-Carousel/Example-Carousel03.ps1 new file mode 100644 index 00000000..ba70e07a --- /dev/null +++ b/Examples/Example-Carousel/Example-Carousel03.ps1 @@ -0,0 +1,68 @@ +Import-Module .\PSWriteHTML.psd1 -Force + +# Minimal demo showing carousels + charts + tables inside tabs +# Charts/tables in the non-active tab should render correctly on first show + +$Data = Get-Process | + Select-Object -First 12 Name, Id, CPU + +New-HTML -Name 'Tabs + Carousel' -FilePath "$PSScriptRoot\Example-Carousel03.html" -Online -Show { + New-HTMLTab -Name 'Tab 1' -Heading 'First' { + New-HTMLSection -HeaderText 'Visible Tab' -Invisible { + New-HTMLCarousel { + New-CarouselSlide { + New-HTMLChart { + New-ChartPie -Name 'A' -Value 10 + New-ChartPie -Name 'B' -Value 20 + New-ChartPie -Name 'C' -Value 30 + } -Title 'Pie - Tab 1' + } + New-CarouselSlide { + New-HTMLText -Text 'Second slide (Tab 1)' + } + } + } + New-HTMLSection -HeaderText 'Table' -Invisible { + New-HTMLTable -DataTable $Data -HideFooter + } + } + + New-HTMLTab -Name 'Tab 2' -Heading 'Second' { + # This tab is initially hidden; content should reflow on tab activation + New-HTMLSection -HeaderText 'Hidden Tab (initial)' -Invisible { + New-HTMLCarousel { + New-CarouselSlide { + New-HTMLChart { + New-ChartPie -Name 'X' -Value 30 + New-ChartPie -Name 'Y' -Value 40 + New-ChartPie -Name 'Z' -Value 50 + } -Title 'Pie - Tab 2' + } + New-CarouselSlide { + New-HTMLText -Text 'Second slide (Tab 2)' + } + } + } + New-HTMLSection -HeaderText 'Table in hidden Tab' -Invisible { + New-HTMLTable -DataTable $Data -HideFooter + } + + # Collapsible section to verify reflow on show/hide + New-HTMLSection -HeaderText 'Collapsible Carousel' -CanCollapse -Collapsed { + New-HTMLCarousel { + New-CarouselSlide { + New-HTMLChart { + New-ChartLegend -LegendPosition bottom -HorizontalAlign right + New-ChartBarOptions -Distributed + New-ChartBar -Name 'One' -Value 10 + New-ChartBar -Name 'Two' -Value 20 + New-ChartBar -Name 'Three' -Value 15 + } -Title 'Bar - Collapsible' + } + New-CarouselSlide { + New-HTMLText -Text 'Collapsed initially; open to reflow.' + } + } + } + } +} diff --git a/Examples/Example-TableColumnHighlight/Example-SearchBuilder.ps1 b/Examples/Example-TableColumnHighlight/Example-SearchBuilder.ps1 new file mode 100644 index 00000000..557676b8 --- /dev/null +++ b/Examples/Example-TableColumnHighlight/Example-SearchBuilder.ps1 @@ -0,0 +1,48 @@ +Import-Module .\PSWriteHTML.psd1 -Force + +$ProcessesAll = Get-Process | Select-Object -First 3 #-Property Name, Id, StartTime + +New-HTML -TitleText 'Title' -Online -FilePath $PSScriptRoot\Example-SearchBuilder.html -ShowHTML { + #New-HTMLTableStyle -BackgroundColor Blue -Type RowSelected + #New-HTMLTableStyle -BackgroundColor Yellow -Type RowHover + #New-HTMLTableStyle -BackgroundColor Yellow -Type RowHoverSelected + + New-HTMLSection -HeaderText 'Search Builder 1' { + New-HTMLTable -DataTable $ProcessesAll -SearchBuilder -Buttons excelHtml5, copyHtml5, csvHtml5 { + #New-HTMLTableContent -ColumnName 'PriorityClass' -BackgroundColor Salmon + New-HTMLTableContent -ColumnName 'HandleCount' -BackGroundColor Salmon + New-HTMLTableCondition -ColumnName 'Product' -BackgroundColor Salmon -Value '1Password' -ChildRowFill Both + New-HTMLTableCondition -ColumnName 'Name' -BackgroundColor AirForceBlue -Value '1password' + } + New-HTMLTable -DataTable $ProcessesAll -SearchBuilder -Buttons excelHtml5, copyHtml5, csvHtml5 { + #New-HTMLTableContent -ColumnName 'PriorityClass' -BackgroundColor Salmon + New-HTMLTableContent -ColumnName 'HandleCount' -BackGroundColor Salmon + New-HTMLTableCondition -ColumnName 'Product' -BackgroundColor Salmon -Value '1Password' + New-HTMLTableCondition -ColumnName 'Name' -BackgroundColor AirForceBlue -Value '1password' + + New-HTMLTableCondition -ColumnName 'VM' -BackgroundColor Alizarin + } -DataStore JavaScript + } + # New-HTMLSection -HeaderText 'Search Builder as button' { + # New-HTMLTable -DataTable $ProcessesAll + # } + # # This won't really work - button + searchBuilder + # New-HTMLSection -HeaderText 'Search Builder + button' { + # # Search Builder will be disabled, button will work + # New-HTMLTable -DataTable $ProcessesAll -SearchBuilder + # } +} + +# New-HTML -TitleText 'Title' -FilePath $PSScriptRoot\Example-SearchBuilder2.html { +# New-HTMLSection -HeaderText 'Search Builder 1' { +# New-HTMLTable -DataTable $ProcessesAll -Filtering -ScrollX -SearchBuilder -Buttons excelHtml5, copyHtml5, csvHtml5 +# } +# New-HTMLSection -HeaderText 'Search Builder as button' { +# New-HTMLTable -DataTable $ProcessesAll +# } +# # This won't really work - button + searchBuilder +# New-HTMLSection -HeaderText 'Search Builder + button' { +# # Search Builder will be disabled, button will work +# New-HTMLTable -DataTable $ProcessesAll -SearchBuilder +# } +# } -ShowHTML \ No newline at end of file diff --git a/Private/Parameters.Configuration.ps1 b/Private/Parameters.Configuration.ps1 index e173b192..366004e8 100644 --- a/Private/Parameters.Configuration.ps1 +++ b/Private/Parameters.Configuration.ps1 @@ -15,7 +15,7 @@ param( ) - $ConfigurationURL = 'https://cdn.jsdelivr.net/gh/evotecit/cdn@0.0.30' + $ConfigurationURL = 'https://cdn.jsdelivr.net/gh/evotecit/cdn@0.0.31' $Configuration = [ordered] @{ Features = [ordered] @{ Inject = @{ @@ -959,14 +959,17 @@ Email = $false } DataTablesConditions = @{ - Comment = 'DataTables Conditions' - FooterAlways = @{ - JS = @( - "$PSScriptRoot\..\Resources\JS\dataTables.conditions.js" + Comment = 'DataTables Conditions' + Header = @{ + JsLink = @( + "https://cdn.jsdelivr.net/npm/@evotecit/htmlextensions@0.1.2/dist/datatables.columnHighlighter.js" + ) + JS = @( + "$PSScriptRoot\..\Resources\JS\dataTables.columnHighlighter.js" ) } - Default = $true - Email = $false + Default = $true + Email = $false } DataTablesColReorder = @{ Comment = 'DataTables ColReorder Features' diff --git a/Private/Tables/Convert-TableConditionsToHighlighterRules.ps1 b/Private/Tables/Convert-TableConditionsToHighlighterRules.ps1 new file mode 100644 index 00000000..0dd582eb --- /dev/null +++ b/Private/Tables/Convert-TableConditionsToHighlighterRules.ps1 @@ -0,0 +1,163 @@ +function Convert-TableConditionsToHighlighterRules { + [CmdletBinding()] + param( + [Parameter(Mandatory)][System.Collections.IEnumerable] $ConditionalFormatting, + [Parameter(Mandatory)][string[]] $Header + ) + + # Build rules with idiomatic array comprehensions for clarity and simplicity + [array] $rules = @( + foreach ($Entry in $ConditionalFormatting) { + if (-not $Entry) { continue } + + $ct = $Entry.ConditionType + + if ($ct -eq 'Condition') { + # Single condition or unconditional highlight + $isUnconditional = ($Entry.PSObject.Properties.Name -notcontains 'Value') -or ($null -eq $Entry.Value) + if (-not $isUnconditional) { + $cond = New-TablePluginCondition -Condition $Entry + } + + # Determine target names + $targetNames = if ($Entry.Row) { + $Header + } elseif ($Entry.HighlightHeaders) { + $Entry.HighlightHeaders + } else { + @($Entry.Name) + } + + # Build targets + $fill = $null + if ($Entry.ChildRowFill) { + if ($Entry.ChildRowFill -eq 'Parent') { $fill = 'parent' } + elseif ($Entry.ChildRowFill -eq 'Both') { $fill = 'both' } + } + [array] $targets = @( + foreach ($n in $targetNames) { + $t = [ordered]@{ + column = $n + css = $Entry.Style + } + if ($fill) { $t['fill'] = $fill } + $t + } + ) + + # Build failTargets, if any + $failTargets = $null + if ($Entry.FailStyle.Keys.Count -gt 0) { + $failTargets = @( + foreach ($n in $targetNames) { + $ft = [ordered]@{ + column = $n + css = $Entry.FailStyle + } + # Do not propagate fill to fail targets by default + $ft + } + ) + } + + if ($isUnconditional) { + $rule = [ordered]@{ + conditionsContainer = @() # unconditional + targets = $targets + } + } else { + $rule = [ordered]@{ + conditionsContainer = @( + [ordered]@{ + logic = 'AND' + conditions = @($cond) + } + ) + targets = $targets + } + } + if ($failTargets) { + $rule['failTargets'] = $failTargets + } + + [pscustomobject]$rule + + } elseif ($ct -eq 'ConditionGroup') { + # Grouped conditions + [array] $conditions = @( + foreach ($Nested in $Entry.Conditions) { + if ($Nested.Type -eq 'TableCondition') { + New-TablePluginCondition -Condition $Nested.Output + } + } + ) + $groupUnconditional = ($conditions.Count -eq 0) + + # Determine targets + $targetNames = if ($Entry.Row) { + $Header + } elseif ($Entry.HighlightHeaders) { + $Entry.HighlightHeaders + } else { + @( + foreach ($Nested in $Entry.Conditions) { + if ($Nested.Type -eq 'TableCondition') { $Nested.Output.Name } + } + ) + } + + $fill = $null + if ($Entry.ChildRowFill) { + if ($Entry.ChildRowFill -eq 'Parent') { $fill = 'parent' } + elseif ($Entry.ChildRowFill -eq 'Both') { $fill = 'both' } + } + [array] $targets = @( + foreach ($n in $targetNames) { + $t = [ordered]@{ + column = $n + css = $Entry.Style + } + if ($fill) { $t['fill'] = $fill } + $t + } + ) + + $failTargets = $null + if ($Entry.FailStyle.Keys.Count -gt 0) { + $failTargets = @( + foreach ($n in $targetNames) { + [ordered]@{ + column = $n + css = $Entry.FailStyle + } + } + ) + } + + if ($groupUnconditional) { + $rule = [ordered]@{ + conditionsContainer = @() # unconditional + targets = $targets + } + } else { + $rule = [ordered]@{ + conditionsContainer = @( + [ordered]@{ + logic = $Entry.Logic + conditions = $conditions + } + ) + targets = $targets + } + } + if ($failTargets) { + $rule['failTargets'] = $failTargets + } + + [pscustomobject]$rule + } + } + ) + + return ,$rules +} diff --git a/Private/Tables/New-TablePluginCondition.ps1 b/Private/Tables/New-TablePluginCondition.ps1 new file mode 100644 index 00000000..2298867e --- /dev/null +++ b/Private/Tables/New-TablePluginCondition.ps1 @@ -0,0 +1,52 @@ +function New-TablePluginCondition { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject] $Condition + ) + + # Map PSWriteHTML condition to plugin condition schema + $plugin = [ordered]@{ + columnName = $Condition.Name + operator = ($Condition.Operator | ForEach-Object { $_.ToLower() }) + type = ($Condition.Type | ForEach-Object { $_.ToLower() }) + value = $Condition.Value + caseSensitive = $Condition.CaseSensitive + dateTimeFormat = $Condition.DateTimeFormat + reverseCondition = $Condition.ReverseCondition + } + + if ($plugin.type -eq 'date' -and $null -ne $Condition.Value) { + if ($Condition.Value -is [datetime]) { + [void]$plugin.Remove('value') + $plugin['valueDate'] = [ordered]@{ + year = $Condition.Value.Year + month = $Condition.Value.Month + day = $Condition.Value.Day + hours = $Condition.Value.Hour + minutes = $Condition.Value.Minute + seconds = $Condition.Value.Second + } + } elseif ($Condition.Value -is [System.Collections.IEnumerable]) { + [array] $dates = @( + foreach ($dv in $Condition.Value) { + if ($dv -is [datetime]) { + [ordered]@{ + year = $dv.Year + month = $dv.Month + day = $dv.Day + hours = $dv.Hour + minutes = $dv.Minute + seconds = $dv.Second + } + } + } + ) + if ($dates.Count -gt 0) { + [void]$plugin.Remove('value') + $plugin['valueDate'] = $dates + } + } + } + + [pscustomobject]$plugin +} diff --git a/Public/New-HTMLTable.ps1 b/Public/New-HTMLTable.ps1 index ec64681f..505c0e58 100644 --- a/Public/New-HTMLTable.ps1 +++ b/Public/New-HTMLTable.ps1 @@ -1265,7 +1265,20 @@ $Options.autoWidth = $false } - $Options = $Options | ConvertTo-Json -Depth 6 #ConvertTo-JsonLiteral -Depth 6 -AdvancedReplace @{ '.' = '\.'; '$' = '\$' } + # Prefer plugin-based conditional formatting: serialize rules into columnHighlighter + + if ($ConditionalFormatting -and $ConditionalFormatting.Count -gt 0) { + $rules = Convert-TableConditionsToHighlighterRules -ConditionalFormatting $ConditionalFormatting -Header $HeaderNames + if ($rules -and $rules.Count -gt 0) { + if ($Options.Contains('createdRow')) { + $Options.Remove('createdRow') + } + $Options['columnHighlighter'] = [ordered]@{ rules = $rules } + } + } + + + $Options = $Options | ConvertTo-Json -Depth 8 #ConvertTo-JsonLiteral -Depth 6 -AdvancedReplace @{ '.' = '\.'; '$' = '\$' } # cleans up $Options for ImmediatelyShowHiddenDetails # Since it's JavaScript inside we're basically removing double quotes from JSON in favor of no quotes at all @@ -1339,8 +1352,10 @@ $Table = $null } - # Process Conditional Formatting. Ugly JS building - $Options = New-TableConditionalFormatting -Options $Options -ConditionalFormatting $ConditionalFormatting -Header $HeaderNames -DataStore $DataStore + # Process Conditional Formatting using rowCallback only if plugin rules are not present + # if ($Options -notmatch '"columnHighlighter"\s*:\s*\{') { + # $Options = New-TableConditionalFormatting -Options $Options -ConditionalFormatting $ConditionalFormatting -Header $HeaderNames -DataStore $DataStore + # } # Process Row Grouping. Ugly JS building if ($RowGroupingColumnID -ne -1) { $Options = Convert-TableRowGrouping -Options $Options -RowGroupingColumnID $RowGroupingColumnID @@ -1501,4 +1516,4 @@ } $AfterTable } -} \ No newline at end of file +} diff --git a/Public/New-TableCondition.ps1 b/Public/New-TableCondition.ps1 index 89cc5b6f..87e02c4e 100644 --- a/Public/New-TableCondition.ps1 +++ b/Public/New-TableCondition.ps1 @@ -115,12 +115,14 @@ function New-TableCondition { [string[]] $HighlightHeaders, [alias('Type')][ValidateSet('number', 'string', 'bool', 'date')][string] $ComparisonType = 'string', [ValidateSet('lt', 'le', 'eq', 'ge', 'gt', 'ne', 'contains', 'like', 'notlike', 'notcontains', 'between', 'betweenInclusive', 'in', 'notin')][string] $Operator = 'eq', - [parameter(Mandatory)][Object] $Value, + [Object] $Value, [switch] $Row, [switch] $Inline, [switch] $CaseSensitive, [string] $DateTimeFormat, [switch] $ReverseCondition, + # Child row fill for responsive lists + [ValidateSet('Parent','Both')][string] $ChildRowFill, # Style for PASS [string]$Color, [string]$BackgroundColor, @@ -192,6 +194,7 @@ function New-TableCondition { CaseSensitive = $CaseSensitive.IsPresent DateTimeFormat = $DateTimeFormat ReverseCondition = $ReverseCondition.IsPresent + ChildRowFill = $ChildRowFill } [PSCustomObject] @{ Type = if ($Inline) { 'TableConditionInline' } else { 'TableCondition' } @@ -202,4 +205,4 @@ function New-TableCondition { Register-ArgumentCompleter -CommandName New-TableCondition -ParameterName Color -ScriptBlock $Script:ScriptBlockColors Register-ArgumentCompleter -CommandName New-TableCondition -ParameterName BackgroundColor -ScriptBlock $Script:ScriptBlockColors Register-ArgumentCompleter -CommandName New-TableCondition -ParameterName FailColor -ScriptBlock $Script:ScriptBlockColors -Register-ArgumentCompleter -CommandName New-TableCondition -ParameterName FailBackgroundColor -ScriptBlock $Script:ScriptBlockColors \ No newline at end of file +Register-ArgumentCompleter -CommandName New-TableCondition -ParameterName FailBackgroundColor -ScriptBlock $Script:ScriptBlockColors diff --git a/Public/New-TableConditionGroup.ps1 b/Public/New-TableConditionGroup.ps1 index 9985f4d6..f2645d12 100644 --- a/Public/New-TableConditionGroup.ps1 +++ b/Public/New-TableConditionGroup.ps1 @@ -107,6 +107,8 @@ [string[]] $HighlightHeaders, [switch] $Row, [switch] $Inline, + # Child row fill for responsive lists + [ValidateSet('Parent','Both')][string] $ChildRowFill, # Style for PASS [string]$Color, [string]$BackgroundColor, @@ -174,6 +176,7 @@ Logic = $Logic HighlightHeaders = $HighlightHeaders DateTimeFormat = $DateTimeFormat + ChildRowFill = $ChildRowFill } [PSCustomObject] @{ Type = if ($Inline) { 'TableConditionGroupInline' } else { 'TableConditionGroup' } @@ -185,4 +188,4 @@ Register-ArgumentCompleter -CommandName New-TableConditionGroup -ParameterName Color -ScriptBlock $Script:ScriptBlockColors Register-ArgumentCompleter -CommandName New-TableConditionGroup -ParameterName BackgroundColor -ScriptBlock $Script:ScriptBlockColors Register-ArgumentCompleter -CommandName New-TableConditionGroup -ParameterName FailColor -ScriptBlock $Script:ScriptBlockColors -Register-ArgumentCompleter -CommandName New-TableConditionGroup -ParameterName FailBackgroundColor -ScriptBlock $Script:ScriptBlockColors \ No newline at end of file +Register-ArgumentCompleter -CommandName New-TableConditionGroup -ParameterName FailBackgroundColor -ScriptBlock $Script:ScriptBlockColors diff --git a/Resources/JS/HideSection.js b/Resources/JS/HideSection.js index 9422adee..a0180802 100644 --- a/Resources/JS/HideSection.js +++ b/Resources/JS/HideSection.js @@ -7,25 +7,13 @@ function show(obj) { var flexDirection = window.getComputedStyle(topSectionDiv).getPropertyValue("flex-direction"); if (flexDirection == 'column') { //console.log('flexDirection 1' + flexDirection) - } else{ + } else { document.getElementById(obj).parentNode.classList.add('sectionShow'); document.getElementById(obj).parentNode.classList.remove('sectionHide'); //console.log('flexDirection 2' + flexDirection) } - // resize tables within section - try { - var table = document.getElementById(obj).querySelectorAll('table'); - table.forEach(resizeTable) - } catch (e) { - console.log('No datatables available.'); - } - // redraw calendars within section - try { - var calendar = document.getElementById(obj).querySelectorAll('div[id^="Calendar-"]'); - calendar.forEach(redrawCalendar) - } catch (e) { - console.log('No calendars available.'); - } + // reflow/redraw objects within this section (tables, charts, carousels, calendars, diagrams) + try { findObjectsToRedraw(obj); } catch (e) { } } function hide(obj) { @@ -38,7 +26,7 @@ function hide(obj) { var flexDirection = window.getComputedStyle(topSectionDiv).getPropertyValue("flex-direction"); if (flexDirection == 'column') { //console.log('flexDirection 1' + flexDirection) - } else{ + } else { document.getElementById(obj).parentNode.classList.remove('sectionShow'); document.getElementById(obj).parentNode.classList.add('sectionHide'); //console.log('flexDirection 2' + flexDirection) diff --git a/Resources/JS/dataTables.columnHighlighter.js b/Resources/JS/dataTables.columnHighlighter.js new file mode 100644 index 00000000..4e5d6679 --- /dev/null +++ b/Resources/JS/dataTables.columnHighlighter.js @@ -0,0 +1,234 @@ +// DataTables Column Highlighter (standalone) +// Combines conditional engine and responsive child-row + visible cell styling +// Usage: +// $('#table').DataTable({ +// columnHighlighter: { rules: [ /* see README.md */ ] } +// }); +/* + DataTables Column Highlighter v1.0.0 + (c) 2025 EvotecIT | MIT + https://github.com/EvotecIT/HTMLExtensions +*/ +(function(){ + if (typeof window === 'undefined') { return; } + + function isEmptyOrSpaces(str) { return !str || str.trim() === ''; } + + function dataTablesCheckCondition(condition, data) { + var columnName = condition['columnName']; + var reverseCondition = condition['reverseCondition']; + var columnId = condition['columnId']; + var operator = (condition['operator'] || '').toLowerCase(); + var columnValue; + if ((condition['dataStore'] || '').toLowerCase() != 'html') { + var columnExists = false; + Object.getOwnPropertyNames(data).forEach(function (val) { + if (val.toLowerCase() == columnName.toLowerCase()) { + columnName = val; columnExists = true; return; + } + }); + if (!columnExists) { return false; } + columnValue = data[columnName]; + } else { + if (columnId == -1) { return false; } + columnValue = data[columnId]; + } + var conditionValue = condition['value']; + if (condition['type'] == 'bool') { + columnValue = String(columnValue).toLowerCase(); + conditionValue = String(conditionValue).toLowerCase(); + } else if (condition['type'] == 'string') { + if (!condition['caseSensitive']) { + columnValue = String(columnValue).toLowerCase(); + conditionValue = String(conditionValue).toLowerCase(); + } + } else if (condition['type'] == 'number') { + if (Array.isArray(conditionValue)) { + var tmp = []; + for (var i = 0; i < conditionValue.length; i++) { + if (!isEmptyOrSpaces(String(conditionValue[i]))) { tmp.push(Number(conditionValue[i])); } + else { tmp.push(undefined); } + } + conditionValue = tmp; + if (!isEmptyOrSpaces(String(columnValue))) { columnValue = Number(columnValue); } + else { columnValue = undefined; } + } else { + if (!isEmptyOrSpaces(String(conditionValue))) { conditionValue = Number(conditionValue); } + else { conditionValue = undefined; } + if (!isEmptyOrSpaces(String(columnValue))) { columnValue = Number(columnValue); } + else { columnValue = undefined; } + } + } else if (condition['type'] == 'date') { + if (Array.isArray(condition['valueDate'])) { + var tmp2 = []; + for (var j = 0; j < condition['valueDate'].length; j++) { + var valueDate = condition['valueDate'][j]; + tmp2.push(new Date(valueDate.year, valueDate.month - 1, valueDate.day, valueDate.hours, valueDate.minutes, valueDate.seconds)); + } + conditionValue = tmp2; + } else { + var vd = condition['valueDate']; + conditionValue = new Date(vd.year, vd.month - 1, vd.day, vd.hours, vd.minutes, vd.seconds); + } + var momentConversion = (typeof moment !== 'undefined') ? moment(columnValue, condition['dateTimeFormat']) : columnValue; + columnValue = new Date(momentConversion); + } + var left, right; if (reverseCondition) { left = conditionValue; right = columnValue; } else { left = columnValue; right = conditionValue; } + if (operator == 'eq') { return left == right; } + else if (operator == 'ne') { return left != right; } + else if (operator == 'gt') { return left > right; } + else if (operator == 'lt') { return left < right; } + else if (operator == 'le') { return left <= right; } + else if (operator == 'ge') { return left >= right; } + else if (operator == 'in') { return Array.isArray(right) && right.indexOf(left) != -1; } + else if (operator == 'notin') { return Array.isArray(right) && right.indexOf(left) == -1; } + else if (operator == 'contains' || operator == 'like') { var rx = new RegExp(right); return rx.test(left); } + else if (operator == 'notcontains' || operator == 'notlike') { var rx2 = new RegExp(right); return !rx2.test(left); } + else if (operator == 'betweeninclusive') { return Array.isArray(right) && left >= right[0] && left <= right[1]; } + else if (operator == 'between') { return Array.isArray(right) && left > right[0] && left < right[1]; } + return false; + } + + function getHeaderNames(tableId) { + try { + var names = []; var $ths = jQuery('#' + tableId + ' thead th'); + $ths.each(function(){ names.push(jQuery(this).text().trim()); }); return names; + } catch (e) { return []; } + } + function indexToHeaderName(tableId, index) { + var headers = getHeaderNames(tableId); if (index >= 0 && index < headers.length) { return headers[index]; } return null; + } + function headerNameToIndex(tableId, name) { + var headers = getHeaderNames(tableId); if (!name) return -1; var lname = ('' + name).toLowerCase(); + for (var i = 0; i < headers.length; i++) { if (('' + headers[i]).toLowerCase() === lname) return i; } return -1; + } + + window.DataTablesColumnHighlighter = { + configurations: {}, + detectStore: function(table){ + try { if (table && table.rows) { var d = table.rows({ page: 'current' }).data(); var f = d && d.length ? d[0] : null; if (Array.isArray(f)) return 'HTML'; if (f && typeof f === 'object') return 'JavaScript'; } } catch(e) {} + return 'HTML'; + }, + normalizeCondition: function(c, headers, store){ + if (!c) return c; try { + if (!c.dataStore) c.dataStore = store; + if ((c.columnId === undefined || c.columnId === -1) && c.columnName) { + var lname = ('' + c.columnName).toLowerCase(); for (var h = 0; h < headers.length; h++) { if ((headers[h] + '').toLowerCase() === lname) { c.columnId = h; break; } } + } + if ((!c.columnName || c.columnName === '') && (typeof c.columnId === 'number')) { if (c.columnId >= 0 && c.columnId < headers.length) { c.columnName = headers[c.columnId]; } } + } catch(e) {} + return c; + }, + normalizeRules: function(tableId, config, table){ + try { + var headers = getHeaderNames(tableId); var store = this.detectStore(table); + for (var i = 0; i < config.length; i++) { + var rule = config[i]; if (rule && rule.conditionsContainer) { + for (var r = 0; r < rule.conditionsContainer.length; r++) { + var container = rule.conditionsContainer[r]; if (container && container.conditions) { + for (var k = 0; k < container.conditions.length; k++) { container.conditions[k] = this.normalizeCondition(container.conditions[k], headers, store); } + } + } + } + } + } catch(e) {} + return config; + }, + normalizeRowData: function(rowData) { + try { if (Array.isArray(rowData)) { return rowData.map(function(v){ try { return jQuery('

' + v + '

').text().trim(); } catch(e) { return ('' + v).trim(); } }); } } catch(e) {} + return rowData; + }, + init: function(tableId, config, table) { + var normalized = this.normalizeRules(tableId, Array.isArray(config) ? config : [config], table); + this.configurations[tableId] = { config: normalized, table: table }; + this.setupEventHandlers(tableId, table); + }, + setupEventHandlers: function(tableId, table) { + var self = this; + if (table && table.on) { + table.on('responsive-display', function (e, datatable, row, showHide) { + try { if (showHide) { var tr = row.node(); var $tr = jQuery(tr); var data = row.data(); self.applyHighlighting(tableId, $tr, data); } } catch (err) { } + }); + table.on('draw.dt', function(){ + try { table.rows({ page: 'current' }).every(function(){ var tr = this.node(); var $tr = jQuery(tr); var data = this.data(); self.applyHighlighting(tableId, $tr, data); }); } catch (err) { } + }); + setTimeout(function(){ try { table.rows({ page: 'current' }).every(function(){ var tr = this.node(); var $tr = jQuery(tr); var data = this.data(); self.applyHighlighting(tableId, $tr, data); }); } catch (err) { } }, 0); + } + if (!(table && table.on)) { + jQuery('#' + tableId + ' tbody').on('click', 'td.dtr-control, td.dt-control, .dtr-control, .dt-control', function () { + var $tr = jQuery(this).closest('tr'); setTimeout(function() { var childRow = $tr.next('tr.child'); if (childRow.length > 0) { var rowData = []; $tr.find('td').each(function(){ var $td = jQuery(this); if (!$td.hasClass('dtr-control') && !$td.hasClass('dt-control')) { rowData.push($td.text().trim()); } }); self.applyHighlighting(tableId, $tr, rowData); } }, 100); + }); + } + }, + applyHighlighting: function(tableId, $parentRow, rowData) { + var cfg = this.configurations[tableId]; if (!cfg) { return; } + var childRow = $parentRow.next('tr.child'); var norm = this.normalizeRowData(rowData); + var styleMap = {}; + function pushStyle(col, style, isPass) { if (!col) return; if (!styleMap[col]) styleMap[col] = { pass: [], fail: [] }; (isPass ? styleMap[col].pass : styleMap[col].fail).push(style || {}); } + function normalizeTarget(t) { return { column: t.column, backgroundColor: t.backgroundColor, textColor: t.textColor, css: t.css, highlightParent: t.highlightParent }; } + function mergedStyle(entry) { var src = entry.pass.length > 0 ? entry.pass : entry.fail; if (src.length === 0) return null; var out = {}; for (var i=0;i0){ DataTablesColumnHighlighter.applyColumnStyling(childRow, target); } DataTablesColumnHighlighter.applyVisibleCellStyling(tableId, $parentRow, target); } + }, + applyColumnStyling: function(childRow, target) { + childRow.find('.dtr-title').each(function() { var $elem = jQuery(this); var text = $elem.text().trim(); if (text === target.column) { var $dataElem = $elem.siblings('.dtr-data').length > 0 ? $elem.siblings('.dtr-data') : $elem.next(); if (target.backgroundColor) { $dataElem.css('background-color', target.backgroundColor); } if (target.textColor) { $dataElem.css('color', target.textColor); } if (target.css) { $dataElem.css(target.css); } if (target.highlightParent) { $elem.parent().css('background-color', target.backgroundColor); if (target.textColor) { $elem.parent().css('color', target.textColor); } } } }); + }, + applyVisibleCellStyling: function(tableId, $parentRow, target) { + try { var idx = (typeof target.column === 'number') ? target.column : headerNameToIndex(tableId, target.column); if (idx < 0) return; var $cells = $parentRow.find('td'); if ($cells.length === 0) return; var $cell = $cells.eq(idx); if ($cell && $cell.length > 0) { if (target.backgroundColor) { $cell.css('background-color', target.backgroundColor); } if (target.textColor) { $cell.css('color', target.textColor); } if (target.css) { $cell.css(target.css); } } } catch (e) { } + }, + createConfig: function(conditions, targets) { return [{ condition: conditions, targets: Array.isArray(targets) ? targets : [targets] }]; } + }; + + window.setupChildRowConditionalFormatting = function(tableId, conditionsContainer, highlightColumn, css, failCss, table) { + try { + var headers = getHeaderNames(tableId); + var detectStore = function(){ try { if (table && table.rows) { var d = table.rows({ page: 'current' }).data(); var first = d && d.length ? d[0] : null; if (Array.isArray(first)) return 'HTML'; if (first && typeof first === 'object') return 'JavaScript'; } } catch(e) {} return 'HTML'; }; + var dataStoreType = detectStore(); + try { + for (var i = 0; i < conditionsContainer.length; i++) { + var container = conditionsContainer[i]; if (!container || !container.conditions) continue; + for (var k = 0; k < container.conditions.length; k++) { + var c = container.conditions[k] || {}; + if (!c.dataStore) { c.dataStore = dataStoreType; } + if ((c.columnId === undefined || c.columnId === -1) && c.columnName) { var lname = ('' + c.columnName).toLowerCase(); for (var h = 0; h < headers.length; h++) { if ((headers[h] + '').toLowerCase() === lname) { c.columnId = h; break; } } } + if ((!c.columnName || c.columnName === '') && (typeof c.columnId === 'number')) { if (c.columnId >= 0 && c.columnId < headers.length) { c.columnName = headers[c.columnId]; } } + container.conditions[k] = c; + } + } + } catch (e) { } + var targets = []; + if (Array.isArray(highlightColumn)) { + for (var i = 0; i < highlightColumn.length; i++) { + var entry = highlightColumn[i]; var header = null; if (typeof entry === 'number') { header = indexToHeaderName(tableId, entry); } else if (typeof entry === 'string') { header = entry; } else if (entry && typeof entry === 'object' && entry.column) { header = entry.column; } + if (header !== null && header !== undefined) { var t = { column: header }; if (css) { if (css['background-color']) t.backgroundColor = css['background-color']; if (css['color']) t.textColor = css['color']; t.css = css; } targets.push(t); } + } + } + var newRule = { conditionsContainer: conditionsContainer, targets: targets }; + if (window.DataTablesColumnHighlighter && window.DataTablesColumnHighlighter.configurations && window.DataTablesColumnHighlighter.configurations[tableId]) { + try { window.DataTablesColumnHighlighter.configurations[tableId].config.push(newRule); var tbl = table || window.DataTablesColumnHighlighter.configurations[tableId].table; if (tbl && tbl.rows) { tbl.rows({ page: 'current' }).every(function(){ var tr = this.node(); var $tr = jQuery(tr); var data = this.data(); window.DataTablesColumnHighlighter.applyHighlighting(tableId, $tr, data); }); } } catch (e) { window.DataTablesColumnHighlighter.init(tableId, [newRule], table || window.DataTablesColumnHighlighter.configurations[tableId].table); } + } else { window.DataTablesColumnHighlighter.init(tableId, [newRule], table); } + } catch (e) { } + }; + + window.setupColumnHighlighting = function(tableId, config, table) { DataTablesColumnHighlighter.init(tableId, config, table); }; + + function autoInitFromSettings(settings) { + try { + var api = new jQuery.fn.dataTable.Api(settings); + var tableId = settings && settings.nTable ? settings.nTable.getAttribute('id') : null; if (!tableId) return; + if (window.DataTablesColumnHighlighter && window.DataTablesColumnHighlighter.configurations && window.DataTablesColumnHighlighter.configurations[tableId]) { return; } + var oInit = settings.oInit || {}; var ch = oInit.columnHighlighter; if (!ch) return; + var rules = ch.rules || ch.config || []; if (!rules || (Array.isArray(rules) && rules.length === 0)) return; + window.DataTablesColumnHighlighter.init(tableId, rules, api); + try { api.rows({ page: 'current' }).every(function(){ var tr = this.node(); var $tr = jQuery(tr); var data = this.data(); window.DataTablesColumnHighlighter.applyHighlighting(tableId, $tr, data); }); } catch(err) { } + } catch(err) { } + } + try { jQuery(document).on('preInit.dt', function(e, settings){ autoInitFromSettings(settings); }); jQuery(document).on('init.dt', function(e, settings){ autoInitFromSettings(settings); }); } catch(e) { } + try { jQuery(function(){ try { if (!jQuery.fn.dataTable) return; var doScan = function(){ try { var apis = jQuery.fn.dataTable.tables({ api: true }); apis.every(function(){ try { autoInitFromSettings(this.settings()[0]); } catch(err) { } }); return apis && apis.count && apis.count() > 0; } catch(err) { return false; } }; doScan(); var tries = 0; var timer = setInterval(function(){ var found = doScan(); tries++; if (found || tries > 40) { clearInterval(timer); } }, 50); } catch(err) { } }); } catch(e) { } +})(); diff --git a/Resources/JS/dataTables.conditions.js b/Resources/JS/dataTables.conditions.js deleted file mode 100644 index cbde940f..00000000 --- a/Resources/JS/dataTables.conditions.js +++ /dev/null @@ -1,238 +0,0 @@ -function isEmptyOrSpaces(str) { - return !str || str.trim() === ''; -} -function dataTablesCheckCondition(condition, data) { - var columnName = condition['columnName']; - var reverseCondition = condition['reverseCondition']; - var columnId = condition['columnId']; - var operator = condition['operator'].toLowerCase(); - if (condition['dataStore'].toLowerCase() != 'html') { - var columnExists = false; - // we need to find whether the column name exists or not, and to make sure we know the column name exact case (it's case sensitive) just in case user provided it wrong - Object.getOwnPropertyNames(data).forEach( - function (val) { - if (val.toLowerCase() == columnName.toLowerCase()) { - columnName = val; - columnExists = true; - return - } - } - ); - if (!columnExists) { - return false; - } - var columnValue = data[columnName]; - } else { - // check if columnid is set - if it's not set it means the column doesn't exists so we dont' proceed - if (columnId == -1) { - return false; - } - var columnValue = data[columnId]; - } - var conditionValue = condition['value']; - - //console.log('before: ' + conditionValue + ' || ' + columnValue + ' type: ' + condition['type']); - if (condition['type'] == 'bool') { - columnValue = columnValue.toString().toLowerCase(); - conditionValue = conditionValue.toString().toLowerCase(); - } else if (condition['type'] == 'string') { - if (!condition['caseSensitive']) { - columnValue = columnValue.toString().toLowerCase(); - conditionValue = conditionValue.toString().toLowerCase(); - } - } else if (condition['type'] == 'number') { - if (Array.isArray(conditionValue)) { - // this will be used for between, betweenInclusive - // if its an array we need to make sure to convert conditionValue within an array - var conditionValueTemporary = []; - - for (var i = 0; i < conditionValue.length; i++) { - //for (let value of conditionValue) { - if (!isEmptyOrSpaces(conditionValue[i].toString())) { - conditionValueTemporary.push(Number(conditionValue[i])); - } else { - conditionValueTemporary.push(undefined); - } - } - conditionValue = conditionValueTemporary; - if (!isEmptyOrSpaces(columnValue.toString())) { - columnValue = Number(columnValue); - } else { - columnValue = undefined; - } - } else { - // This logic is to get rid of empty string which is normally treated as 0 - if (!isEmptyOrSpaces(conditionValue.toString())) { - conditionValue = Number(conditionValue); - } else { - conditionValue = undefined; - } - if (!isEmptyOrSpaces(columnValue.toString())) { - columnValue = Number(columnValue); - } else { - columnValue = undefined; - } - } - } else if (condition['type'] == 'date') { - if (Array.isArray(condition['valueDate'])) { - var conditionValueTemporary = []; - for (var i = 0; i < condition['valueDate'].length; i++) { - //for (let value of condition['valueDate']) { - var valueDate = condition['valueDate'][i]; - conditionValueTemporary.push(new Date(valueDate.year, valueDate.month - 1, valueDate.day, valueDate.hours, valueDate.minutes, valueDate.seconds)); - } - conditionValue = conditionValueTemporary; - } else { - // bring conditionValue to proper date - var valueDate = condition['valueDate']; - conditionValue = new Date(valueDate.year, valueDate.month - 1, valueDate.day, valueDate.hours, valueDate.minutes, valueDate.seconds); - } - // bring columnValue based on dateTimeFormat provided - var momentConversion = moment(columnValue, condition['dateTimeFormat']); - columnValue = new Date(momentConversion); - } - - if (reverseCondition) { - var sideLeft = conditionValue; - var sideRight = columnValue; - } else { - var sideLeft = columnValue; - var sideRight = conditionValue; - } - //console.log('after: ' + conditionValue + ' || ' + columnValue); - if (operator == 'eq') { - if (sideLeft == sideRight) { - return true; - } - } else if (operator == 'ne') { - if (sideLeft != sideRight) { - return true; - } - } else if (operator == 'gt') { - if (sideLeft > sideRight) { - return true; - } - } else if (operator == 'lt') { - if (sideLeft < sideRight) { - return true; - } - } else if (operator == 'le') { - if (sideLeft <= sideRight) { - return true; - } - } else if (operator == 'ge') { - if (sideLeft >= sideRight) { - return true; - } - } else if (operator == 'in') { - if (sideRight.indexOf(sideLeft) != -1) { - return true; - } - } else if (operator == 'notin') { - if (sideRight.indexOf(sideLeft) == -1) { - return true; - } - } else if (operator == 'contains' || operator == 'like') { - //var compareValue = conditionValue.replace('*', '.*') - var regex = new RegExp(sideRight); - if (regex.test(sideLeft)) { - return true; - } - } else if (operator == 'notcontains' || operator == 'notlike') { - //var compareValue = conditionValue.replace('*', '.*') - var regex = new RegExp(sideRight) - if (!regex.test(sideLeft)) { - return true; - } - } else if (operator == 'betweeninclusive') { - if (Array.isArray(sideRight) && sideLeft >= sideRight[0] && sideLeft <= sideRight[1]) { - return true; - } - } else if (operator == 'between') { - if (Array.isArray(sideRight) && sideLeft > sideRight[0] && sideLeft < sideRight[1]) { - return true; - } - } - return false; -} -function dataTablesConditionalFormatting(row, data, conditionsContainer, highlightColumn, css, failCss) { - var conditionsMatch = []; - var found = false; - for (var i = 0; i < conditionsContainer.length; i++) { - var container = conditionsContainer[i]; - for (var k = 0; k < container['conditions'].length; k++) { - var condition = container['conditions'][k]; - conditionsMatch.push( - dataTablesCheckCondition(condition, data) - ); - } - if (container['logic'] == 'AND') { - // if (conditionsMatch.every(value => value === true)) { - // found = true; - // } - - for (var a = 0; a < conditionsMatch.length; a++) { - if (conditionsMatch[a] !== true) { - found = false; - break; - } else { - found = true; - } - } - - - } else if (container['logic'] == 'OR') { - //if (conditionsMatch.some(value => value === true)) { - // found = true; - //} - - for (var a = 0; a < conditionsMatch.length; a++) { - if (conditionsMatch[a] === true) { - found = true; - break; - } - } - - } else if (container['logic'] == 'NONE') { - // if (conditionsMatch.every(value => value != true)) { - // found = true; - //} - - for (var a = 0; a < conditionsMatch.length; a++) { - if (conditionsMatch[a] !== false) { - found = false; - break; - } else { - found = true; - } - } - } - } - if (found) { - if (highlightColumn == null) { - $('td', row).css(css); - } else { - for (var a = 0; a < highlightColumn.length; a++) { - var column = highlightColumn[a]; - //for (let column of highlightColumn) { - $("td:eq(" + column + ")", row).css(css); - - //if (data.Type == "group") { - // $('td:eq(6)', row).html('A'); - //} - } - } - } else { - if (failCss) { - if (highlightColumn == null) { - $('td', row).css(failCss); - } else { - for (var a = 0; a < highlightColumn.length; a++) { - var column = highlightColumn[a]; - //for (let column of highlightColumn) { - $("td:eq(" + column + ")", row).css(failCss); - } - } - } - } -} \ No newline at end of file diff --git a/Resources/JS/redrawObjects.js b/Resources/JS/redrawObjects.js index cd16bbad..dd2bdd2d 100644 --- a/Resources/JS/redrawObjects.js +++ b/Resources/JS/redrawObjects.js @@ -4,21 +4,37 @@ console.log('Fitting calendar with id ' + calendar.id); } function resizeTable(table) { - if (table.id) { - if ($.fn.DataTable.isDataTable("#" + table.id)) { - try { - $("#" + table.id).DataTable().columns.adjust().responsive.recalc(); - console.log('Fitting table with id ' + table.id); - } catch (e) { - try { - $("#" + table.id).DataTable().columns.adjust(); - } catch (e) { - console.log('Failed to fit table with id ' + table.id); + if (!table || !table.id) return; + // Skip hidden tables + if (table.offsetParent === null) return; + + if ($.fn.DataTable.isDataTable("#" + table.id)) { + try { + var dt = $("#" + table.id).DataTable(); + var opts = (window.pswhRedrawOptions || {}); + var recordsTotal = 0; + try { recordsTotal = dt.page().info().recordsTotal || 0; } catch (e) { } + var largeThreshold = typeof opts.dataTablesLargeThreshold === 'number' ? opts.dataTablesLargeThreshold : 20000; + var skipResponsive = opts.skipDataTablesResponsive === true || recordsTotal >= largeThreshold; + var skipAll = opts.skipDataTables === true; + + if (!skipAll) { + if (!skipResponsive) { + dt.columns.adjust().responsive.recalc(); + } else { + dt.columns.adjust(); } + console.log('Fitting table with id ' + table.id + (skipResponsive ? ' (light)' : '')); + } + } catch (e) { + try { + $("#" + table.id).DataTable().columns.adjust(); + } catch (e2) { + console.log('Failed to fit table with id ' + table.id); } - } else { - console.log('Skipping fitting table id ' + table.id); } + } else { + console.log('Skipping fitting table id ' + table.id); } } function redrawDiagram(diagram) { @@ -39,32 +55,96 @@ function redrawFixedHeaderFooter() { } } } -function findObjectsToRedraw(id) { - // redrawTables +function redrawApexCharts(container) { try { - var table = document.getElementById(id).querySelectorAll('table.dataTables'); //.querySelectorAll('table[id^="DT-"]'); - table.forEach(resizeTable); + // Reflow visible ApexCharts within the container + var charts = container.querySelectorAll('.apexcharts-canvas'); + charts.forEach(function (el) { + var chart = el.__apexcharts__; + if (chart && el.offsetParent !== null) { + try { + if (!(window.pswhRedrawOptions && window.pswhRedrawOptions.skipApexCharts === true)) { + chart.windowResizeHandler(); + } + console.log('Resized ApexChart for element ' + (el.id || '[no-id]')); + } catch (e) { + console.log('ApexChart resize failed for element ' + (el.id || '[no-id]')); + } + } + }); } catch (e) { - console.log('No datatables available.'); + console.log('No ApexCharts available.'); } - // redrawCalendar +} +function refreshKinetoCarousels(container) { try { - var calendar = document.getElementById(id).querySelectorAll('div.calendarFullCalendar'); - calendar.forEach(redrawCalendar); + if (window.Kineto) { + // Refresh each Kineto carousel within the container + var carousels = container.querySelectorAll('div.carousel'); + carousels.forEach(function (c) { + try { + // Kineto exposes a static refresh API that accepts a container element + if (!(window.pswhRedrawOptions && window.pswhRedrawOptions.skipCarousels === true)) { + window.Kineto.refresh(c); + } + console.log('Refreshed Kineto carousel for element ' + (c.id || '[no-id]')); + } catch (e) { + console.log('Kineto refresh failed for element ' + (c.id || '[no-id]')); + } + }); + } } catch (e) { - console.log('No calendars available.'); + console.log('No Kineto carousels available.'); } - // redrawDiagram +} +function findObjectsToRedraw(id) { try { - var diagram = document.getElementById(id).querySelectorAll('div.diagramObject'); - diagram.forEach(redrawDiagram); + var root = document.getElementById(id) || document; + + // redrawTables + try { + var table = root.querySelectorAll('table.dataTables'); //.querySelectorAll('table[id^="DT-"]'); + table.forEach(resizeTable); + } catch (e) { + console.log('No datatables available.'); + } + + // redrawCalendar + try { + var calendar = root.querySelectorAll('div.calendarFullCalendar'); + calendar.forEach(redrawCalendar); + } catch (e) { + console.log('No calendars available.'); + } + + // redrawDiagram + try { + var diagram = root.querySelectorAll('div.diagramObject'); + diagram.forEach(redrawDiagram); + } catch (e) { + console.log('No diagrams available.'); + } + + // Resize ApexCharts in the visible container + redrawApexCharts(root); + + // Refresh Kineto carousels in the visible container + refreshKinetoCarousels(root); + + // finds all tables with fixed headers and footers and forces them to check if they are visible or not and hide or show accordingly + try { + redrawFixedHeaderFooter(); + } catch (e) { + console.log('No datatables fixed header/footer available.'); + } } catch (e) { - console.log('No diagrams available.'); + console.log('Redraw failed for id ' + id); } - // finds all tables with fixed headers and footers and forces them to check if they are visible or not and hide or show accordingly + + // As a safety net, dispatch a window resize to notify components try { - redrawFixedHeaderFooter(); - } catch (e) { - console.log('No datatables fixed header/footer available.'); - } -} \ No newline at end of file + if (!(window.pswhRedrawOptions && window.pswhRedrawOptions.skipWindowResizeEvent === true)) { + window.dispatchEvent(new Event('resize')); + } + } catch (e) { } +} diff --git a/Resources/JS/tabbisAdditional.js b/Resources/JS/tabbisAdditional.js index 14a24379..81a6a769 100644 --- a/Resources/JS/tabbisAdditional.js +++ b/Resources/JS/tabbisAdditional.js @@ -4,13 +4,22 @@ var tabs = tabbis.init({ tabActive: "active", paneActive: "active", callback: function (tab, pane) { - // console.log("TAB id:" + tab.id + pane.id + tableid); - // this makes sure to refresh tables on given tab change to make sure they have buttons and everything - // which tab has which table - findObjectsToRedraw(tab.id + "-Content"); + // Ensure redraw runs after pane becomes visible + try { + var id = (tab && tab.id ? (tab.id + "-Content") : (pane && pane.id ? pane.id : null)); + if (id) { + setTimeout(function () { + try { findObjectsToRedraw(id); } catch (e) {} + try { document.dispatchEvent(new CustomEvent('pswh:tab:shown', { detail: { id: id } })); } catch (e) {} + }, 80); + } + } catch (e) {} } }); -// in theory should take care of removing local storage for tabbis -// some errors occurs if the local storage is not cleaned after a while -window.addEventListener("unload", tabbis.remove, false); +// Safely clear tab memory on unload (if supported) +try { + if (tabbis && typeof tabbis.reset === 'function') { + window.addEventListener("unload", tabbis.reset, false); + } +} catch (e) {}