Hey all,
I'm getting more into the webdev side of Perspective — working on a dynamic HTML table element with custom CSS styling using the advanced stylesheet — but I'm running into an issue when attempting to inject <script>
elements into the DOM using a Markdown component's source property.
I can directly inject HTML and CSS into a view with this property, which I’m doing to generate a custom table; however, when I attempt to add in a <script>
element and assign an onclick
action to the <th>
element to call a function within the <script>
element to sort the table in asc/desc order, it seemingly gets ignored by the server parsing logic. I’m assuming this is intentional to avoid any sort of script injection via an input field from the UI side of the project - correct me if my understanding is wrong here.
Anyone have any tips?
Current function to populate the Markdown component's source property:
def build_html_table(inventory, percentages, forecast):
html = """
<div class="psc-analysis-card-wrapper">
<div class="psc-analysis-card-header">
<div class="psc-analysis-title">
<span class="psc-analysis-bar-icon"></span>
<svg viewBox="0 0 24 24" width="2vh" height="2vh" xmlns="http://www.w3.org/2000/svg">
<path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" />
<polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" />
<polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" />
</svg>
Inventory Analysis
</div>
</div>
<div class="psc-analysis-content">
<div class="psc-analysis-overflow">
<table id="inventoryAnalysisTable" class="psc-analysis-table">
<thead class="psc-analysis-table-header">
<tr class="psc-analysis-table-header-row">
<th class="psc-align-text-left psc-padding sortable-header" onclick="sortTable(0)" style="cursor: pointer;">
Item ID
<span class="sort-indicator">↕</span>
</th>
<th class="psc-align-text-right psc-padding sortable-header" onclick="sortTable(1)" style="cursor: pointer;">
Quantity Used
<span class="sort-indicator">↕</span>
</th>
<th class="psc-align-text-right psc-padding sortable-header" onclick="sortTable(2)" style="cursor: pointer;">
Usage Rate (%)
<span class="sort-indicator">↕</span>
</th>
<th class="psc-align-text-right psc-padding sortable-header" onclick="sortTable(3)" style="cursor: pointer;">
12-Week Forecast
<span class="sort-indicator">↕</span>
</th>
</tr>
</thead>
<tbody class="psc-analysis-table-body">
"""
for product_line in ("PL01", "PL02"):
for category in ("cat_a", "cat_b", "cat_c"):
for item_id in sorted(inventory[product_line][category].keys()):
used = inventory[product_line][category][item_id]
rate = percentages[product_line][category][item_id]
fcast = forecast[product_line][category][item_id]
badge_class = (
"psc-analysis-badge lowpriority"
if rate < 4.2
else ("psc-analysis-badge mediumpriority" if rate < 8.4 else "psc-analysis-badge highpriority")
)
html += """
<tr class="psc-light-bottom-border psc-hover-bg psc-duration-200">
<td class="psc-analysis-table-cell psc-align-text-left psc-bold-font" data-sort="{0}">
<span class="psc-analysis-badge">{0}</span> <span style="opacity:0.6;">({1}-{2})</span>
</td>
<td class="psc-analysis-table-cell psc-align-text-right psc-semibold-font" data-sort="{3}">{3}</td>
<td class="psc-analysis-table-cell psc-align-text-right psc-bold-font" data-sort="{5}">
<span class="{4}">{5:.1f}%</span>
</td>
<td class="psc-analysis-table-cell psc-align-text-right psc-bold-font" data-sort="{6}">{6}</td>
</tr>
""".format(item_id, product_line, category.upper(), used, badge_class, rate, fcast)
html += """
</tbody>
</table>
</div>
</div>
</div>
<style>
.sortable-header {
position: relative;
user-select: none;
}
.sortable-header:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.sort-indicator {
font-size: 0.8em;
margin-left: 5px;
opacity: 0.5;
}
.sort-indicator.asc::after {
content: '↑';
opacity: 1;
}
.sort-indicator.desc::after {
content: '↓';
opacity: 1;
}
</style>
<script>
function sortTable(columnIndex) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("inventoryAnalysisTable");
switching = true;
dir = "asc";
var indicators = table.querySelectorAll('.sort-indicator');
indicators.forEach(function(indicator) {
indicator.className = 'sort-indicator';
});
var currentIndicator = table.rows[0].cells[columnIndex].querySelector('.sort-indicator');
while (switching) {
switching = false;
rows = table.rows;
for (i = 1; i < (rows.length - 1); i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName("TD")[columnIndex];
y = rows[i + 1].getElementsByTagName("TD")[columnIndex];
var xValue = x.getAttribute('data-sort') || x.textContent || x.innerText;
var yValue = y.getAttribute('data-sort') || y.textContent || y.innerText;
if (columnIndex === 1 || columnIndex === 2 || columnIndex === 3) {
xValue = parseFloat(xValue) || 0;
yValue = parseFloat(yValue) || 0;
} else {
xValue = xValue.toLowerCase();
yValue = yValue.toLowerCase();
}
if (dir == "asc") {
if (xValue > yValue) {
shouldSwitch = true;
break;
}
} else if (dir == "desc") {
if (xValue < yValue) {
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount++;
} else {
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
currentIndicator.className = 'sort-indicator ' + dir;
}
</script>
"""
return html