@cloudcannon/suite
Version:
A suite of gulp tools to manage static sites on CloudCannon
509 lines (478 loc) • 19.8 kB
HTML
<head>
<title>State</title>
<style>
body {
font-family: sans-serif;
background-color: #222;
margin: 8px 0;
}
#top-bar {
width: 100%;
text-align: center;
border-bottom: 1px solid black;
display: flex;
background-color: #eee;
color: #444;
}
#top-bar div {
flex: 1;
}
.top-bar-options {
display: flex;
align-items: center;
border-left: 1px solid black;
}
.top-bar-options > div {
padding: 0px 5px;
}
#toggle-button h1 {
font-size: 1.2em;
}
#container {
display: flex;
flex-direction: row;
text-align: left;
width: 100vw;
height: 100vh;
}
#container h3 {
font-size: 1em;
text-align: center;
}
#container h3:after {
content: "";
width: 0;
height: 0;
border-top: 5px solid #444;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
display: inline-block;
vertical-align: middle;
margin-left: 4px;
}
.list {
display: flex;
flex-direction: column;
min-width: 150px;
max-width: 300px;
padding: 0 10px;
border-right: 1px solid black;
background-color: #eee;
overflow-x: hidden;
overflow-y: scroll;
}
.list h2 {
font-size: 1em;
font-style: italic;
text-align: center;
}
.node {
display: block;
margin: 4px 0;
color: #444;
}
.node .count {
font-weight: bold;
}
.node.leaf {
color: #aaa;
}
.node.open, .node:hover:not(.leaf):not(.duplicate) {
background-color: #ddd;
}
.node.duplicate {
background-color: black;
color: white;
}
.hidden, .node.hiddenByFilter, .node.hiddenByCollapse, .node.hiddenUsed, .node.hiddenUnused {
display: none;
}
</style>
</head>
<body>
<div id="top-bar">
<div id="toggle-button" onclick="toggleMode()"></div>
<div class="option-buttons top-bar-options">
<div>
<label id="hide-unused-label" for="hide-unused">Hide Unused</label>
<input type="checkbox" name="hide-unused" id="toggle-hide-unused">
</div>
<div>
<label id="hide-used-label" for="hide-used">Hide Used</label>
<input type="checkbox" name="hide-used" id="toggle-hide-used">
</div>
<div>
<label for="acyclic-graph">Prevent loops</label>
<input type="checkbox" name="acyclic-graph" id="toggle-acyclic" checked>
</div>
</div>
<div class="sorting-buttons top-bar-options">
<div>
<label>Sort highest</label>
<input type="radio" name="sorting" class="sort-mode-button" value="sort-high" checked>
</div>
<div>
<label>Sort lowest</label>
<input type="radio" name="sorting" class="sort-mode-button" value="sort-low">
</div>
<div>
<label>Alphabetical</label>
<input type="radio" name="sorting" class="sort-mode-button" value="alphabetical">
</div>
</div>
</div>
<div id="container"></div>
<script>
var container = document.getElementById("container");
var button = document.getElementById("toggle-button");
var usedGraph = {};
var dependencyGraph = {};
var dependentsGraph = {};
var hideUsedToggle = document.getElementById("toggle-hide-used");
var hideUnusedToggle = document.getElementById("toggle-hide-unused");
var hideUsedLabel = document.getElementById("hide-used-label");
var hideUnusedLabel = document.getElementById("hide-unused-label");
var acyclicToggle = document.getElementById("toggle-acyclic");
var sortMode = "sort-high";
var dependencyList;
var dependentsList;
hideUsedToggle.addEventListener("change", function (event) {
if (event.target.checked) {
document.querySelectorAll("a.node").forEach(function(element){
if (parseInt(element.getAttribute("data-node-child-count")) > 0) {
element.classList.add("hiddenUsed");
}
});
} else {
document.querySelectorAll(".hiddenUsed").forEach(function(element){
element.classList.remove("hiddenUsed");
});
}
updateAllSublistNumbers();
});
hideUnusedToggle.addEventListener("change", function (event) {
if (event.target.checked) {
document.querySelectorAll("a.node").forEach(function(element){
if (parseInt(element.getAttribute("data-node-child-count")) < 1) {
element.classList.add("hiddenUnused");
}
});
} else {
document.querySelectorAll(".hiddenUnused").forEach(function(element){
element.classList.remove("hiddenUnused");
});
}
updateAllSublistNumbers();
});
document.querySelectorAll(".sort-mode-button").forEach(function(radio) {
radio.onclick = function () {
sortMode = radio.value;
// sort existing lists
var lists = [];
lists.push(document.querySelector(".list[data-list-num='0']"));
document.querySelectorAll(".sublist").forEach(sublist => lists.push(sublist));
lists.forEach(function(listElement) {
var array = []
listElement.querySelectorAll("a.node").forEach(function(node){
array.push(node);
node.parentNode.removeChild(node);
});
array.sort(function(a, b) {
return sort(
[a.getAttribute("data-filename"), a.getAttribute("data-node-child-count")],
[b.getAttribute("data-filename"), b.getAttribute("data-node-child-count")]
);
});
array.forEach(function(node) {
listElement.appendChild(node);
});
});
}
});
// start
fetchJSONFile("dependencies.json", function (dependencyData) {
dependencyGraph = dependencyData;
fetchJSONFile("dependents.json", function (dependentData) {
dependentsGraph = dependentData;
initFirstLists();
usedGraph = dependentsGraph;
toggleMode();
});
});
function initFirstLists () {
function makeList (graph) {
usedGraph = graph;
var list = document.createElement("div");
list.classList.add("list");
list.setAttribute("data-list-num", 0);
var filter = document.createElement("input");
filter.setAttribute("type", "text");
filter.classList.add("filter");
list.appendChild(filter);
filter.addEventListener("change", function(){filterByString(filter);} );
container.appendChild(list);
var array = Object.keys(graph).map(function(key) {
return [key, getNumChildren(key)];
});
array.sort(function(a, b) {
return sort(a, b);
});
for (var i = 0; i < array.length; i++) {
addNode(array[i][0], list);
}
if (graph !== usedGraph) {
list.classList.add("hidden");
}
return list;
}
dependencyList = makeList(dependencyGraph);
dependentsList = makeList(dependentsGraph);
}
function toggleMode () {
closeListsToTheRight(0);
if (usedGraph === dependencyGraph) {
usedGraph = dependentsGraph;
dependentsList.classList.remove("hidden");
dependencyList.classList.add("hidden");
button.innerHTML = "<h1>Toggle Direction ←</h1><p>(X is referenced by Y)</p>";
hideUnusedLabel.innerHTML = "Hide unused";
hideUsedLabel.innerHTML = "Hide used";
} else {
usedGraph = dependencyGraph;
dependentsList.classList.add("hidden");
dependencyList.classList.remove("hidden");
button.innerHTML = "<h1>Toggle Direction →</h1><p>(X references Y)</p>";
hideUnusedLabel.innerHTML = "Hide items with no references";
hideUsedLabel.innerHTML = "Hide items with references";
}
}
function updateAllSublistNumbers() {
document.querySelectorAll(".list").forEach(function (list) {
updateSublistNumbers(list);
});
}
function updateSublistNumbers(list) {
list.querySelectorAll(".sublist").forEach(function (element) {
let numChildren = [...element.children].filter(node => node.classList.contains("node") && !isHidden(node)).length;
element.querySelector("span.sublist-num").innerHTML = numChildren;
});
}
function filterByString (filter) {
closeListsToTheRight(filter.parentElement.getAttribute("data-list-num"));
if (!filter.value) {
filter.parentElement.querySelectorAll("a.node").forEach(function(element) {
element.classList.remove("hiddenByFilter");
});
} else {
filter.parentElement.querySelectorAll("a.node").forEach(function(element) {
if (element.getAttribute("data-filename").includes(filter.value)) {
element.classList.remove("hiddenByFilter");
} else {
element.classList.add("hiddenByFilter");
}
});
}
updateSublistNumbers(filter.closest(".list"));
}
function sortLowestFirst (a, b) {
return a[1] - b[1];
}
function sortHighestFirst (a, b) {
return b[1] - a[1];
}
function sortAlphabetical (a, b) {
if (a[0] < b[0]) {
return -1;
} else if (a[0] > b[0]) {
return 1;
} else {
return 0;
}
}
// a and b are [filename, numChildren]
function sort (a, b) {
switch (sortMode) {
case "sort-high":
return sortHighestFirst(a, b);
case "sort-low":
return sortLowestFirst(a, b);
case "alphabetical":
return sortAlphabetical(a, b);
default:
return sortHighestFirst(a, b);
}
}
function closeListsToTheRight (index) {
container.querySelectorAll(".list").forEach(function(element) {
if (element.getAttribute("data-list-num") > index) {
element.parentNode.removeChild(element);
}
});
}
function toggleSublist (listElement) {
const filterValue = listElement.parentElement.querySelector("input.filter").value;
if (listElement.classList.contains("sublist-closed")) {
listElement.classList.remove("sublist-closed");
listElement.querySelectorAll("a.node").forEach(function (element) {
if ((!filterValue || filterValue.length > 0) || element.getAttribute("data-filename").includes(filterValue)) {
element.classList.remove("hiddenByCollapse");
}
});
} else {
listElement.classList.add("sublist-closed");
listElement.querySelectorAll("a.node").forEach(function (element) {
element.classList.add("hiddenByCollapse");
});
}
}
function createSublist(listData, key, list) {
listData[key].sort(function(a, b) {
return sort([a, getNumChildren(a)], [b, getNumChildren(b)]);
});
var sublist = document.createElement("div");
sublist.classList.add("sublist");
list.appendChild(sublist);
var header = document.createElement("h3");
header.innerHTML = `${key} [<span class='sublist-num'>${listData[key].length}</span>]`;
header.onclick = function () {
toggleSublist(sublist);
}
sublist.appendChild(header);
for (var i = 0; i < listData[key].length; i++) {
addNode(listData[key][i], sublist);
}
}
function createNewList(filename, num) {
var listData = usedGraph[filename];
var list = document.createElement("div");
list.classList.add("list");
list.setAttribute("data-list-num", num);
var header = document.createElement("h2");
if (usedGraph === dependencyGraph) {
header.innerText = filename + " →";
} else {
header.innerText = "← " + filename;
}
list.appendChild(header);
var filter = document.createElement("input");
filter.setAttribute("type", "text");
filter.classList.add("filter");
list.appendChild(filter);
filter.addEventListener("change", function(){filterByString(filter);} );
container.appendChild(list);
for (var key in listData) {
if (listData[key].length > 0) {
createSublist(listData, key, list);
}
}
}
function getVisibleNodesWithFilename(filename) {
return [].slice.call(document.querySelectorAll("[data-filename='" + filename + "']")).filter(element => !isHidden(element));
}
function isLeaf(filename) {
var dataObj = usedGraph[filename];
for (var key in dataObj) {
if (dataObj[key] && dataObj[key].length > 0) {
return false;
}
}
return true;
}
function isHidden(element) {
return element.classList.contains("hiddenByFilter")
|| element.classList.contains("hiddenUsed")
|| element.classList.contains("hiddenUnused")
|| element.classList.contains("hiddenByCollapse")
|| element.closest(".list").classList.contains("hidden");
}
function getNumChildren(filename){
var dataObj = usedGraph[filename];
var count = 0;
for (var key in dataObj) {
count += dataObj[key].length;
}
return count;
}
function isRepeat(filename) {
var instances = getVisibleNodesWithFilename(filename);
return instances.length > 1;
}
function addNode(filename, parentElement) {
if (!parentElement) {
var children = container.querySelectorAll(".list");
parentElement = children[children.length - 1];
}
var numChildren = getNumChildren(filename);
var node = document.createElement("a");
node.classList.add("node");
node.setAttribute("data-filename", filename);
node.setAttribute("data-node-child-count", numChildren);
node.innerHTML = `<span class='count'>[${numChildren}]</span> ${filename} `;
parentElement.appendChild(node);
if (isLeaf(filename)) {
node.classList.add("leaf");
if (hideUnusedToggle.checked) {
node.classList.add("hiddenUnused");
}
} else {
if (hideUsedToggle.checked) {
node.classList.add("hiddenUsed");
}
if (isRepeat(filename)) {
node.onmouseover = function () {
document.querySelectorAll("[data-filename='" + filename + "']").forEach(function(element) {
element.classList.add("duplicate");
});
};
node.onmouseout = function () {
document.querySelectorAll("[data-filename='" + filename + "']").forEach(function(element) {
element.classList.remove("duplicate");
});
};
}
node.onclick = function() {
openNode(filename, parentElement);
node.classList.remove("duplicate");
}
}
}
function openNode (filename, parentElement) {
var firstNodeInstance;
if (acyclicToggle.checked) {
var nodeInstances = getVisibleNodesWithFilename(filename);
// find oldest ancestor with the same filename
nodeInstances.sort(function(a, b) {
parseInt(a.parentElement.getAttribute("data-list-num"))
- parseInt(b.parentElement.getAttribute("data-list-num"));
});
firstNodeInstance = nodeInstances[0];
} else {
firstNodeInstance = [].slice.call(parentElement.querySelectorAll("[data-filename='" + filename + "']"))[0];
}
var listNum = parseInt(firstNodeInstance.closest(".list").getAttribute("data-list-num"));
closeListsToTheRight(listNum);
createNewList(filename, listNum + 1);
firstNodeInstance.parentElement.querySelectorAll(".open").forEach(function(child) {
child.classList.remove("open");
});
document.querySelectorAll("[data-filename='" + filename + "']").forEach(function(element) {
element.classList.remove("duplicate");
});
firstNodeInstance.classList.add("open");
}
function fetchJSONFile(path, callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200 || httpRequest.status === 0) {
var data = JSON.parse(httpRequest.responseText);
if (callback) callback(data);
}
}
};
httpRequest.open('GET', path);
httpRequest.send();
}
</script>
</body>