Skip to content

Commit eccf3f0

Browse files
committed
Add sticky first row and column to optimization results table
Sticky first column: CSS position:sticky on td:first-child. The column can use CSS because the horizontal scroll container (overflow-x:auto on .table_div) does not trap sticky:left anchoring. Sticky header row: JS fixed-clone overlay. CSS sticky:top cannot be used because the same overflow-x:auto scroll container also acts as a scroll boundary for the vertical axis, trapping sticky:top inside it. A position:fixed clone is used instead, syncing horizontal scroll via wrapper.scrollLeft. border-collapse:separate; border-spacing:0 ensures sticky cells retain their own borders (with collapse, borders are shared between adjacent cells and lost when a sticky cell creates its own stacking context).
1 parent 56a5e5f commit eccf3f0

4 files changed

Lines changed: 112 additions & 3 deletions

File tree

src/emhass/static/script.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//on page reload get saved data
99
window.onload = async function () {
1010
await loadBasicOrAdvanced();
11+
initStickyTables();
1112

1213
//add listener for basic and advanced html switch
1314
document
@@ -224,6 +225,74 @@ async function getTemplate() {
224225
TempScript.innerHTML = script.innerHTML;
225226
script.parentElement.appendChild(TempScript);
226227
}
228+
initStickyTables();
229+
}
230+
231+
function initStickyTables() {
232+
// Remove clones and listeners from any previous call
233+
document.querySelectorAll(".sticky-header-wrapper").forEach((el) => el.remove());
234+
document.querySelectorAll("table.mystyle").forEach((table) => {
235+
if (table._stickyScrollListener)
236+
window.removeEventListener("scroll", table._stickyScrollListener);
237+
if (table._stickyHScrollListener && table._stickyContainer)
238+
table._stickyContainer.removeEventListener("scroll", table._stickyHScrollListener);
239+
});
240+
241+
document.querySelectorAll("table.mystyle").forEach((table) => {
242+
const thead = table.querySelector("thead");
243+
if (!thead) return;
244+
const container = table.closest(".table_div");
245+
if (!container) return;
246+
247+
// Fixed wrapper clips the clone to the container's visible horizontal area
248+
const wrapper = document.createElement("div");
249+
wrapper.className = "sticky-header-wrapper";
250+
document.body.appendChild(wrapper);
251+
252+
// Clone table containing only the header
253+
const cloneTable = document.createElement("table");
254+
cloneTable.className = table.className;
255+
cloneTable.style.tableLayout = "fixed";
256+
cloneTable.appendChild(thead.cloneNode(true));
257+
wrapper.appendChild(cloneTable);
258+
259+
// Sync wrapper position and column widths with the live table
260+
const syncGeometry = () => {
261+
const r = container.getBoundingClientRect();
262+
wrapper.style.left = r.left + "px";
263+
wrapper.style.width = container.clientWidth + "px";
264+
cloneTable.style.width = table.getBoundingClientRect().width + "px";
265+
thead.querySelectorAll("th").forEach((th, i) => {
266+
const cloneTh = cloneTable.querySelectorAll("th")[i];
267+
if (cloneTh) cloneTh.style.width = th.getBoundingClientRect().width + "px";
268+
});
269+
};
270+
requestAnimationFrame(syncGeometry);
271+
window.addEventListener("resize", syncGeometry, { passive: true });
272+
273+
// Horizontal scroll: sync wrapper.scrollLeft — avoids transform stacking context,
274+
// so overflow:hidden clips correctly on both sides
275+
const syncHScroll = () => {
276+
wrapper.scrollLeft = container.scrollLeft;
277+
};
278+
container.addEventListener("scroll", syncHScroll, { passive: true });
279+
table._stickyHScrollListener = syncHScroll;
280+
table._stickyContainer = container;
281+
282+
// Vertical scroll: toggle visibility only — no per-frame transform update
283+
let shown = false;
284+
const updateVisibility = () => {
285+
const shouldShow =
286+
thead.getBoundingClientRect().top <= 0 &&
287+
table.getBoundingClientRect().bottom > 0;
288+
if (shouldShow !== shown) {
289+
shown = shouldShow;
290+
wrapper.style.display = shouldShow ? "block" : "none";
291+
}
292+
};
293+
window.addEventListener("scroll", updateVisibility, { passive: true });
294+
table._stickyScrollListener = updateVisibility;
295+
});
227296
}
228297

229298
//test localStorage support

src/emhass/static/style.css

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,10 +680,19 @@ button {
680680
.main-svg {
681681
font-size: 11pt;
682682
font-family: Arial, sans-serif;
683-
border-collapse: collapse;
683+
border-collapse: separate;
684+
border-spacing: 0;
684685
border-style: hidden;
685686
}
686687

688+
.mystyle td,
689+
.mystyle th {
690+
border-width: 0 1px 1px 0;
691+
}
692+
693+
.mystyle td:first-child,
694+
.mystyle thead th:first-child { border-left-width: 1px; }
695+
687696
.mystyle td {
688697
padding: 5px;
689698
}
@@ -707,6 +716,32 @@ th {
707716
cursor: pointer;
708717
}
709718

719+
/* Fixed clone rendered by JS for sticky header row */
720+
.sticky-header-wrapper {
721+
position: fixed;
722+
top: 0;
723+
overflow: hidden;
724+
z-index: 100;
725+
display: none;
726+
margin: 0;
727+
padding: 0;
728+
}
729+
730+
/* Sticky first column */
731+
.mystyle td:first-child,
732+
.mystyle thead th:first-child {
733+
position: sticky;
734+
left: 0;
735+
z-index: 1;
736+
}
737+
738+
/* Opaque background for sticky header row (original table + JS clone) */
739+
.mystyle thead th {
740+
background: #e1e1e1;
741+
border-top-width: 1px;
742+
z-index: 2;
743+
}
744+
710745
th:last-child {
711746
border-top-right-radius: 7px;
712747
}
@@ -1318,6 +1353,11 @@ p {
13181353
background-color: #3f3f3f;
13191354
}
13201355

1356+
.mystyle thead th { background: #282928; }
1357+
1358+
.mystyle td,
1359+
.mystyle th { border-color: #666; }
1360+
13211361
.modebar-group {
13221362
background-color: #0000 !important;
13231363
}

src/emhass/templates/configuration.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<head>
66
<title>EMHASS: Energy Management Optimization for Home Assistant</title>
77
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8-
<link rel="stylesheet" type="text/css" href="static/style.css?version=1">
8+
<link rel="stylesheet" type="text/css" href="static/style.css?version=2">
99
<link rel="icon" type="image/x-icon" href="static/img/emhass_logo_short.svg">
1010
<script src="static/configuration_script.js"></script>
1111
</head>

src/emhass/templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<head>
66
<meta charset="UTF-8"> <title>EMHASS: Energy Management Optimization for Home Assistant</title>
77
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8-
<link rel="stylesheet" type="text/css" href="static/style.css?version=1">
8+
<link rel="stylesheet" type="text/css" href="static/style.css?version=2">
99
<link rel="icon" type="image/x-icon" href="static/img/emhass_logo_short.svg">
1010
<script src="static/script.js"></script>
1111
</head>

0 commit comments

Comments
 (0)