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;i