I have stripped down my code to the minimum that does what I want and still shows the problem.
I have a dialog that has three parts: header, content and footer. The dialog content is further broken down into an inner content and inner footer.
The inner content will get it’s data from an ajax call. The data is a collapsable tree. The problem occurs when I click on a node to expand it the inner footer and dialog footer get pushed off the visible portion of the dialog.
The dialog is resizable and the moment I resize it even by as little as a pixel the layout corrects itself immediately. I have been at this for a couple of days now with no luck. I am open to any suggestions.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Testing Collapsable Trees</title>
<style>
ul.tree
{
margin-block-start: 0;
}
ul.tree li:hover
{
background-color: yellow;
}
ul.tree li
{
padding-top : 0.25em;
list-style-type: none;
position: relative;
}
ul.tree ul
{
display: none;
}
ul.tree ul.open
{
display: block;
}
ul.tree li::before
{
height: 1em;
padding: 0.1em;
font-size: 0.8em;
display: block;
position: absolute;
left: -1.3em;
top: 0.35em;
}
ul.tree li:has(+ul)
{
text-decoration : underline black double 1px;
}
ul.tree li:has(+ul)::before
{
content: '+';
}
ul.tree li:has(+ul.open)::before
{
content: '-';
}
.selectedNode
{
color : green;
}
dialog
{
overflow: hidden;
resize: both;
padding : 0;
margin: auto;
max-height: 25%;
}
dialog h3
{
margin : 0.25em 0;
}
dialog button
{
margin: 0.33em;
padding: 0.33em 0.5em;
}
.dlgbody
{
height: 100%;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas: 'header' 'main' 'footer';
}
.dlgheader
{
background-color: lightgreen;
display: flex;
justify-content: center;
border-bottom: 2px double black;
grid-area: header;
}
.dlgcontent
{
overflow: hidden;
background-color: white;
grid-area: main;
padding: 5px;
}
.dlgfooter
{
background-color: lightblue;
padding-right: 0.5em;
display: flex;
justify-content: flex-end;
border-top: 2px double black;
grid-area: footer;
}
.fmbody
{
height: 100%;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
grid-template-areas: 'fmmain' 'fmfooter';
}
.fmcontent
{
background-color: white;
grid-area: fmmain;
overflow: auto;
padding: 0px 5px 0px 5px;
}
.fmfooter
{
background-color: white;
border-top: 1px solid black;
grid-area: fmfooter;
}
</style>
<script>
class Tree
{
constructor(root)
{
this.root = document.createElement("ul");
this.root.classList.add("tree");
this.root.tabIndex = "-1";
this.root.addEventListener("click", evt => this.mouseHandler(evt));
}
toggleCollapse(evt)
{
let node = evt.target.nextElementSibling;
if(node == null || node.nodeName != "UL")
return;
if(node.classList.contains("open"))
{
node.classList.remove('open');
let opensubs = node.querySelectorAll(':scope .open');
for(let i = 0; i < opensubs.length; i++)
opensubs[i].classList.remove('open');
}
else
node.classList.add('open');
}
mouseHandler(evt)
{
this.toggleCollapse(evt);
let node = this.getSelected();
if(node != null)
node.classList.remove("selectedNode");
evt.target.classList.add("selectedNode");
this.root.dispatchEvent(new Event("selected"));
}
getRoot()
{
return this.root;
}
getSelected()
{
return this.root.querySelector(":scope .selectedNode");
}
}
class Dialog
{
constructor()
{
this.dialogDiv = document.createElement("dialog");
this.dlgHeader = document.createElement("div");
this.dlgContent = document.createElement("div");
this.dlgFooter = document.createElement("div");
}
init(opts)
{
let body = document.createElement("div");
body.classList.add("dlgbody");
// Build header
let title = document.createElement("h3");
title.innerHTML = opts.title;
this.dlgHeader.appendChild(title);
this.dlgHeader.classList.add("dlgheader");
body.appendChild(this.dlgHeader);
// build div for dialog content
this.dlgContent.classList.add("dlgcontent");
body.appendChild(this.dlgContent);
// build footer
let okbutton = document.createElement("button");
okbutton.textContent = "OK";
okbutton.onclick = (evt) => { this.dialogDiv.close("OK"); }
this.dlgFooter.appendChild(okbutton);
this.dlgFooter.classList.add("dlgfooter");
body.appendChild(this.dlgFooter);
this.dialogDiv.appendChild(body);
document.body.appendChild(this.dialogDiv);
}
show()
{
this.dialogDiv.returnValue = "";
this.dialogDiv.showModal();
}
getContentDiv()
{
return this.dlgContent;
}
getDialogRoot()
{
return this.dialogDiv;
}
}
class FileManager
{
constructor(opts)
{
this.uiList = document.createElement("div");
this.files = new Tree();
this.dialog = new Dialog();
this.dialog.init({title : opts.title});
this.uiList.classList.add("fmcontent");
this.uiList.appendChild(this.files.getRoot());
let uiFooter = document.createElement("div");
uiFooter.classList.add("fmfooter");
let label = document.createElement("h3");
label.textContent = "Content Footer";
uiFooter.appendChild(label);
let ui = document.createElement("div");
ui.classList.add("fmbody");
ui.appendChild(this.uiList);
ui.appendChild(uiFooter);
this.dialog.getContentDiv().appendChild(ui);
}
loadData()
{
let tree = this.files.getRoot();
let data = JSON.parse(testData);
this.build(tree, data);
}
showDialog()
{
this.dialog.show();
this.files.getRoot().focus();
if(this.files.getRoot().children.length == 0)
this.loadData();
}
build(tree, data)
{
for(let i = 0; i < data.length; i++)
{
if(i == 0)
{
if(data[i] == "")
continue;
let li = document.createElement("li");
li.textContent = data[i];
tree.appendChild(li);
let ul = document.createElement("ul");
tree.appendChild(ul);
tree = ul;
}
if(Array.isArray(data[i]))
this.build(tree, data[i]);
else if(i > 0)
{
let li = document.createElement("li");
li.textContent = data[i];
tree.appendChild(li);
}
}
}
}
var testData = '["", ["files", "subfile1", "subfile2", "subfile3", "subfile4", "subfile5", "subfile6"], ["images", "image 1"]]';
var fm = null;
function init()
{
fm = new FileManager({title : "Dialog Header"});
}
</script>
</head>
<body onload="init();">
<button onclick="javascript:fm.showDialog();">Dialog</button>
</body>
</html>