Last active
June 6, 2025 11:22
-
-
Save Podbrushkin/8945794a909da8176bbd517078a5e74a to your computer and use it in GitHub Desktop.
Format-HtmlTable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<link href="https://unpkg.com/[email protected]/dist/css/tabulator.min.css" rel="stylesheet"> | |
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/js/tabulator.min.js"></script> | |
<style> | |
a { | |
text-decoration: none; | |
} | |
a:hover { | |
text-decoration: underline; | |
} | |
a:visited { | |
text-decoration: underline; | |
} | |
.tag-filter-section { | |
margin: 10px 0; | |
padding: 10px; | |
border: 1px solid #ddd; | |
border-radius: 5px; | |
} | |
.tag-filter-header { | |
font-weight: bold; | |
margin-bottom: 5px; | |
} | |
.tag-filter-options { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 10px; | |
} | |
.tag-filter-option { | |
display: flex; | |
align-items: center; | |
white-space: nowrap; | |
} | |
.tag-filter-count { | |
margin-left: 5px; | |
color: #666; | |
font-size: 0.9em; | |
} | |
.tag-filter-select { | |
padding: 5px; | |
border-radius: 4px; | |
border: 1px solid #ccc; | |
} | |
.control-bar { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
margin-bottom: 10px; | |
} | |
.row-count { | |
margin-left: auto; | |
color: #666; | |
font-size: 0.9em; | |
} | |
</style> | |
</head> | |
<body> | |
<details> | |
<summary>Details</summary> | |
<pre>%DESCRIPTION%</pre> | |
</details> | |
<div id="tagFiltersContainer"></div> | |
<div class="control-bar"> | |
<select id="tagFilterSelect" class="tag-filter-select"> | |
<option value="">Tag & Filter</option> | |
</select> | |
<label for="fSearch">Search</label> | |
<input type="text" id="fSearch" name="fSearch"> | |
<label>%DATETIME%</label> | |
<div class="row-count" id="rowCount">0 rows</div> | |
</div> | |
<div id="example-table"></div> | |
<script> | |
// Columns to be interpreted as tags initially | |
var initialTagColumns = []; | |
var data = | |
//JSON_ARRAY_START | |
[{"path":"./col-table-depth-counts.html","tags":["catalogue-of-life","biology"],"numTag":[1,4,1],"ggl":"http://www.google.com/"},{"path":"./index.html","tags":[],"numTag":[4,2,4],"ggl":"http://www.google.com/"},{"path":"./table-gbif-taxon-10k-byDescendants.html","tags":["table","biology","gbif"],"numTag":[3,0,2],"ggl":"http://www.google.com/"},{"path":"./wikimediaCommons/index.html","tags":[],"numTag":[3,0,0],"ggl":"http://www.google.com/"},{"path":"./col-table-rank-counts.html","tags":["catalogue-of-life","biology"],"numTag":[1,1,2],"ggl":"http://www.google.com/"},{"path":"./iNatur-tree-pictures-nsk-noBirdsNoPlants-dpi27-565.svg","tags":["iNaturalist","biology"],"numTag":[1,2,1],"ggl":"http://www.google.com/"},{"path":"./gbif-tree-pictures-birds.pdf","tags":["gbif","biology"],"numTag":[2,0,4],"ggl":"http://www.google.com/"},{"path":"./wikimediaCommons/Russian_writers_depicted.html","tags":["table"],"numTag":[0,3,3],"ggl":"http://www.google.com/"},{"path":"./biology/graph2d_Ursidae_taxons.html","tags":["taxon","graph","biology"],"numTag":[4,1,1],"ggl":"http://www.google.com/"},{"path":"./biology/index.html","tags":["biology"],"numTag":[0,0,2],"ggl":"http://www.google.com/"},{"path":"./librivox/tableReadersLangsRus.html","tags":["table"],"numTag":[2,2,0],"ggl":"http://www.google.com/"},{"path":"./gbif-tree-pictures-noPlantsInsectsBirdsMammal-LR.pdf","tags":["gbif","biology"],"numTag":[3,4,0],"ggl":"http://www.google.com/"},{"path":"./biology/tableTaxonsPlantsKnown3k.html","tags":["table","taxon","biology"],"numTag":[0,3,1],"ggl":"http://www.google.com/"},{"path":"./iNatur-tree-pictures-nsk-birds-dpi27-225.svg","tags":["iNaturalist","biology"],"numTag":[1,3,3],"ggl":"http://www.google.com/"},{"path":"./librivox/tableAllSectionsRus.html","tags":["table"],"numTag":[4,3,1],"ggl":"http://www.google.com/"},{"path":"./col-tree-pictures-birds-TB-206.svg","tags":["catalogue-of-life","biology"],"numTag":[4,3,1],"ggl":"http://www.google.com/"},{"path":"./librivox/tableReadersLangsSeconds.html","tags":["table"],"numTag":[4,0,1],"ggl":"http://www.google.com/"},{"path":"./wikisource/index.html","tags":[],"numTag":[0,0,4],"ggl":"http://www.google.com/"},{"path":"./col-tree-pictures-noPlantsInsectsBirdsMammal-TB-259.svg","tags":["catalogue-of-life","biology"],"numTag":[1,0,4],"ggl":"http://www.google.com/"},{"path":"./biology/graph3d_Ursidae_taxons.html","tags":["taxon","graph","biology"],"numTag":[4,1,1],"ggl":"http://www.google.com/"}] | |
//JSON_ARRAY_END | |
; | |
// Track currently active tag filters | |
var activeTagFilters = {}; | |
function matchAny(data, filterParams) { | |
const regex = RegExp(filterParams.value, 'i'); | |
for (var key in data) { | |
if (regex.test(data[key])) return true; | |
} | |
return false; | |
} | |
function combinedFilter(data) { | |
// Apply search filter | |
const searchValue = document.getElementById("fSearch").value; | |
const searchMatch = searchValue ? matchAny(data, { value: searchValue }) : true; | |
if (!searchMatch) return false; | |
// Check tag filters - different groups use AND logic | |
for (const [column, selectedTags] of Object.entries(activeTagFilters)) { | |
if (selectedTags.size === 0) continue; | |
const cellValue = data[column]; | |
if (cellValue === undefined || cellValue === null) return false; | |
const values = Array.isArray(cellValue) ? cellValue : [cellValue]; | |
// Within same group, use AND logic - all selected tags must be present | |
let allTagsMatch = true; | |
for (const tag of selectedTags) { | |
if (!values.some(v => String(v) === tag)) { | |
allTagsMatch = false; | |
break; | |
} | |
} | |
if (!allTagsMatch) return false; | |
} | |
return true; | |
} | |
function updateRowCount() { | |
const visibleRows = table.getData(true).length; | |
const totalRows = table.getData().length; | |
document.getElementById("rowCount").textContent = `${visibleRows} of ${totalRows} rows`; | |
} | |
const input = document.getElementById("fSearch"); | |
input.addEventListener("keyup", function() { | |
table.setFilter(combinedFilter); | |
}); | |
function urlFormatter(cell, formatterParams, onRendered){ | |
return "<a href='" + cell.getValue() + "' target='_blank'>" + cell.getValue() + "</a>"; | |
} | |
function htmlFormatter(cell, formatterParams, onRendered) { | |
return cell.getValue(); | |
} | |
var table = new Tabulator("#example-table", { | |
height:"90vh", | |
layout:"fitColumns", | |
data: data, // Start with empty data | |
autoColumns: true, | |
}); | |
function applyColumnFormatters() { | |
let columns = table.getColumnDefinitions(); | |
let dataTmp = table.getData(); | |
for (let i = 0; i < columns.length; i++) { | |
let columnName = columns[i].field; | |
if (columnName === "url") { | |
columns[i].formatter = urlFormatter; | |
continue; | |
} | |
let sampleVal = dataTmp.map(x => x[columnName]).find(val => val !== null && val !== undefined && val !== ''); | |
if (typeof sampleVal === "string" && sampleVal.startsWith("http")) { | |
columns[i].formatter = urlFormatter; | |
} | |
if (typeof sampleVal === "string" && sampleVal.startsWith("<a href")) { | |
columns[i].formatter = htmlFormatter; | |
} | |
} | |
table.setColumns(columns); | |
} | |
function createTagFilter(column) { | |
// Remove existing filter for this column if it exists | |
const existingFilter = document.getElementById(`tag-filter-${column}`); | |
if (existingFilter) { | |
existingFilter.remove(); | |
delete activeTagFilters[column]; | |
table.setFilter(combinedFilter); | |
return; | |
} | |
// Get all unique values in the column | |
const uniqueValues = new Set(); | |
const allData = table.getData(); | |
allData.forEach(row => { | |
const value = row[column]; | |
if (value !== undefined && value !== null) { | |
if (Array.isArray(value)) { | |
value.forEach(v => uniqueValues.add(String(v))); | |
} else { | |
uniqueValues.add(String(value)); | |
} | |
} | |
}); | |
// Create filter UI | |
const filterSection = document.createElement('div'); | |
filterSection.className = 'tag-filter-section'; | |
filterSection.id = `tag-filter-${column}`; | |
const header = document.createElement('div'); | |
header.className = 'tag-filter-header'; | |
header.textContent = `Filter by ${column}:`; | |
filterSection.appendChild(header); | |
const optionsContainer = document.createElement('div'); | |
optionsContainer.className = 'tag-filter-options'; | |
const sortedValues = Array.from(uniqueValues).sort((a, b) => a.localeCompare(b)); | |
sortedValues.forEach(value => { | |
const option = document.createElement('div'); | |
option.className = 'tag-filter-option'; | |
const checkbox = document.createElement('input'); | |
checkbox.type = 'checkbox'; | |
checkbox.value = value; | |
checkbox.id = `tag-${column}-${value.replace(/\s+/g, '-')}`; | |
checkbox.addEventListener('change', () => { | |
if (!activeTagFilters[column]) { | |
activeTagFilters[column] = new Set(); | |
} | |
if (checkbox.checked) { | |
activeTagFilters[column].add(value); | |
} else { | |
activeTagFilters[column].delete(value); | |
if (activeTagFilters[column].size === 0) { | |
delete activeTagFilters[column]; | |
} | |
} | |
table.setFilter(combinedFilter); | |
}); | |
const label = document.createElement('label'); | |
label.htmlFor = checkbox.id; | |
const labelText = document.createElement('span'); | |
labelText.textContent = value; | |
const count = document.createElement('span'); | |
count.className = 'tag-filter-count'; | |
// Static count - doesn't update with filtering | |
count.textContent = `(${allData.filter(row => { | |
const cellValue = row[column]; | |
if (cellValue === undefined || cellValue === null) return false; | |
const values = Array.isArray(cellValue) ? cellValue : [cellValue]; | |
return values.some(v => String(v) === value); | |
}).length})`; | |
label.appendChild(labelText); | |
label.appendChild(count); | |
option.appendChild(checkbox); | |
option.appendChild(label); | |
optionsContainer.appendChild(option); | |
}); | |
filterSection.appendChild(optionsContainer); | |
document.getElementById('tagFiltersContainer').appendChild(filterSection); | |
// Initialize empty filter for this column | |
activeTagFilters[column] = new Set(); | |
} | |
// Initialize tag filter select | |
function initTagFilterSelect() { | |
const select = document.getElementById('tagFilterSelect'); | |
select.innerHTML = '<option value="">Tag & Filter</option>'; | |
const columns = table.getColumnDefinitions(); | |
columns.forEach(col => { | |
if (col.field) { | |
const option = document.createElement('option'); | |
option.value = col.field; | |
option.textContent = col.field; | |
select.appendChild(option); | |
} | |
}); | |
select.addEventListener('change', () => { | |
if (select.value) { | |
createTagFilter(select.value); | |
select.value = ""; // Reset the select | |
} | |
}); | |
} | |
// Initialize table | |
table.on("tableBuilt", function() { | |
applyColumnFormatters(); | |
initTagFilterSelect(); | |
initialTagColumns.forEach(column => { | |
if (table.getColumn(column)) { | |
createTagFilter(column); | |
} | |
}); | |
}); | |
// Update row count when data changes | |
table.on("dataFiltered", function(filters, rows){ | |
document.getElementById("rowCount").textContent = `${rows.length} of ${table.getData().length} rows`; | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Format-HtmlTable { | |
param ( | |
[Parameter(ValueFromPipeline = $true)] | |
[ValidateNotNullOrEmpty()] | |
$JsonOrObject, | |
[string]$Description, | |
$MergeLabelsAndUrls = $true, | |
[array]$GroupBy = @(), | |
[switch]$DataTree | |
) | |
begin { | |
$all = @() | |
} | |
process { | |
$all += $JsonOrObject | |
} | |
end { | |
$fullJson = $null | |
if ($all[0] -is [String]) { | |
$fullJson = $all -join ' ' | |
} else { | |
if ($MergeLabelsAndUrls) { | |
# merge urls and labels | |
$labelPropNames = $all | % {$_.psobject.properties} | ? name -Like *Label | % name | select -unique | |
#Write-Host $labelPropNames | |
$all = $all | ConvertTo-Json -depth 9 | ConvertFrom-Json -depth 9 | |
$all | % { | |
$obj = $_ | |
$labelPropNames | % { | |
$labelPropName = $_ | |
$urlPropName = $_.Replace('Label','') | |
$label = $obj.$labelPropName | |
$url = $obj.$urlPropName | |
if ($label) { | |
$obj.$urlPropName = "<a href='$url' target='_blank' style='color: black;'>$label</a>" | |
} | |
$obj.psobject.properties.remove($labelPropName) | |
} | |
} | |
} | |
$fullJson = $all | ConvertTo-Json -Depth 5 # | % {$_ -replace '": null\r?\n',""": """"`n"} | |
} | |
$templatePath = Join-Path $PSScriptRoot 'Format-HtmlTable.html' | |
$htmlContent = (cat $templatePath -raw).replace('%DESCRIPTION%',[System.Web.HttpUtility]::HtmlEncode($Description)) ` | |
-replace '%DATETIME%',(Get-Date -Format "yyyy-MM-dd") | |
$htmlContent = Replace-StringBetweenAnchors $htmlContent '//JSON_ARRAY_START\r' '\s*//JSON_ARRAY_END' $fullJson | |
if ($GroupBy) { | |
$rplcmnt = (,$GroupBy | ConvertTo-Json -Compress) | |
$htmlContent = $htmlContent -replace '(initialTagColumns = ).*?\n',"`$1$rplcmnt`n" | |
} | |
$filename = "table$(Get-Date -Format FileDateTime).html" | |
$outFile = $htmlContent | New-Item $filename | |
# Start $outFile | |
return $outFile | |
} | |
} | |
function Replace-StringBetweenAnchors ($inputString, $anchorLeft, $anchorRight, $replacementString) { | |
return ($inputString -replace "(?smi)(?<=$anchorLeft).*?(?=$anchorRight)", $replacementString) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment