Public
Edited
Feb 1, 2023
1 fork
Importers
2 stars
Insert cell
Insert cell
helpToggle("div.container", data, true) // set last arg to false to have non-modal (non-blocking) help
Insert cell
Insert cell
Insert cell
Inputs.table(data)
Insert cell
typeof "dffdas"
Insert cell
//
// Creates a help layer on top of rootElement using the info in helpData to position
// help icons and textual explanations. If blocking = true, the layer is modal, i.e,
// interation with the underlying interface is blocked.
//
function createHelpLayer(rootElement, helpData, blocking = true) {
let [width, height] = [rootElement.clientWidth, rootElement.clientHeight];
let layer = html`<div class="helpLayer">`;
if (!blocking) layer.style.pointerEvents = "none";
Object.assign(layer.style, {
position: "absolute",
top: "0px",
left: "0px",
minWidth: `${width}px`,
minHeight: `${height}px`,
//border: "1px solid red",
zIndex: 1000,
background: "rgba(0,0,0,0)"
});

const helpGeometry = function (helpItem, layerGeom) {
const iconSize = 18;
const boxWidth = helpBoxWidth;
const sep = 5;
let [xoff, yoff] = [+helpItem.xoffset, +helpItem.yoffset];
let icon = {};
let box = measure(boxWidth, helpItem.text);
if (helpItem.corner.includes("N")) {
icon.y = yoff;
box.y = yoff + iconSize + sep;
} else if (helpItem.corner.includes("S")) {
icon.y = layerGeom.height + yoff - iconSize;
box.y = icon.y - sep - box.height;
} else {
icon.y = layerGeom.height / 2 + yoff - iconSize / 2;
box.y = icon.y + iconSize / 2 - box.height / 2;
}
if (helpItem.corner.includes("W")) {
icon.x = xoff;
box.x = icon.x + sep + iconSize;
} else if (helpItem.corner.includes("E")) {
icon.x = layerGeom.width + xoff - iconSize;
box.x = icon.x - sep - box.width;
} else {
icon.x = layerGeom.width / 2 + xoff - iconSize / 2;
box.x = icon.x - box.width / 2 + iconSize / 2;
if (!helpItem.corner.includes("S") && !helpItem.corner.includes("N")) {
icon.y -= box.height / 4 + iconSize / 2 + sep;
box.y += box.height / 4;
}
}
return { icon, box };
};

let helpBoxes = [];
const showHelpBox = (box) => {
for (let hb of helpBoxes) {
if (hb === box) {
hb.style.display = hb.style.display == "block" ? "none" : "block";
} else {
hb.style.display = "none";
}
}
};

for (let helpItem of helpData) {
const node = rootElement.querySelector(helpItem.selector);
if (!node) {
console.log("Can't find:" + helpItem.selector);
continue;
}
//const rect = node.BoundingClientRect();
const geom = {
x: node.offsetLeft,
y: node.offsetTop,
width: node.clientWidth,
height: node.clientHeight
};
if (geom.x == 0 && geom.y == 0 && geom.width == 0 && geom.height == 0) {
console.log("Element with null geometry:" + helpItem.selector);
continue;
}
const panel = html`<div class="helpPanel">`;
Object.assign(panel.style, {
position: "absolute",
top: `${geom.y}px`,
left: `${geom.x}px`,
minWidth: `${geom.width}px`,
minHeight: `${geom.height}px`,
pointerEvents: "none"
// border: "1px solid blue",
// background: "rgba(0,0,0,0.1)"
});
const helpGeom = helpGeometry(helpItem, geom);
const icon = helpIcon();
Object.assign(icon.style, {
position: "absolute",
top: `${helpGeom.icon.y}px`,
left: `${helpGeom.icon.x}px`,
pointerEvents: "auto"
});
const helpBox = htl.html`<div class=helpBox>${helpItem.text}`;
Object.assign(helpBox.style, {
display: "none",
position: "absolute",
pointerEvents: "none",
top: `${helpGeom.box.y}px`,
left: `${helpGeom.box.x}px`,
width: `${helpGeom.box.width}px`
});
helpBoxes.push(helpBox);
icon.onclick = () => showHelpBox(helpBox);
panel.append(icon, helpBox);

layer.append(panel);
}
return layer;
}
Insert cell
//
// Creates a help layer (see above) when the toggle is set and removes it when it is unset
//
function helpToggle(rootElement, helpData, blocking = true) {
const div = htl.html`<div>`;
div.value = false;
let helpLayer = null;
let display = () => {
div.innerHTML = "";
if (div.value) {
div.append(onToggleIcon());
const root =
typeof rootElement === "string"
? document.querySelector(rootElement)
: rootElement;
helpLayer = createHelpLayer(root, helpData);
root.append(helpLayer);
} else {
div.append(offToggleIcon());
if (helpLayer) helpLayer.remove();
}
};
div.onclick = () => {
div.value = !div.value;
display();
div.dispatchEvent(new CustomEvent("input"));
};
display();
return div;
}
Insert cell
## Icons
Insert cell
Insert cell
//
// Creates a toggle in the "on" position
//
onToggleIcon = () => htl.html`<svg width="40" height="21" viewBox="0 0 40 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20L30 20C35.52 20 40 15.52 40 10C40 4.48 35.52 -4.82574e-07 30 0L10 1.74846e-06C4.48 2.23103e-06 -4.82574e-07 4.48 0 10C4.82574e-07 15.52 4.48 20 10 20ZM30 4C33.32 4 36 6.68 36 10C36 13.32 33.32 16 30 16C26.68 16 24 13.32 24 10C24 6.68 26.68 4 30 4Z" fill="#E23C12"/>
<path d="M12.3906 9.83008V11.0654C12.3906 11.5667 12.3353 12.0078 12.2246 12.3887C12.1172 12.7663 11.9593 13.082 11.751 13.3359C11.5426 13.5898 11.292 13.7803 10.999 13.9072C10.7061 14.0342 10.3757 14.0977 10.0078 14.0977C9.653 14.0977 9.3291 14.0342 9.03613 13.9072C8.74317 13.7803 8.49089 13.5898 8.2793 13.3359C8.06771 13.082 7.90332 12.7663 7.78613 12.3887C7.6722 12.0078 7.61524 11.5667 7.61524 11.0654V9.83008C7.61524 9.32878 7.6722 8.88932 7.78613 8.51172C7.90007 8.13086 8.06283 7.81348 8.27442 7.55957C8.486 7.30566 8.73828 7.11523 9.03125 6.98828C9.32422 6.85807 9.64811 6.79297 10.0029 6.79297C10.3708 6.79297 10.7012 6.85807 10.9941 6.98828C11.2904 7.11523 11.541 7.30566 11.7461 7.55957C11.9544 7.81348 12.1139 8.13086 12.2246 8.51172C12.3353 8.88932 12.3906 9.32878 12.3906 9.83008ZM11.502 11.0654V9.82031C11.502 9.42969 11.4694 9.0944 11.4043 8.81445C11.3392 8.53125 11.2432 8.30013 11.1162 8.12109C10.9893 7.94206 10.833 7.81022 10.6475 7.72558C10.4619 7.63769 10.2471 7.59375 10.0029 7.59375C9.77181 7.59375 9.56348 7.63769 9.37793 7.72558C9.19564 7.81022 9.03939 7.94206 8.90918 8.12109C8.77897 8.30013 8.67806 8.53125 8.60645 8.81445C8.53809 9.0944 8.50391 9.42969 8.50391 9.82031V11.0654C8.50391 11.4593 8.53809 11.7979 8.60645 12.0811C8.67806 12.361 8.77897 12.5921 8.90918 12.7744C9.04264 12.9534 9.20215 13.0869 9.3877 13.1748C9.57324 13.2594 9.77995 13.3018 10.0078 13.3018C10.252 13.3018 10.4668 13.2594 10.6523 13.1748C10.8411 13.0869 10.9974 12.9534 11.1211 12.7744C11.248 12.5921 11.3424 12.361 11.4043 12.0811C11.4694 11.7979 11.502 11.4593 11.502 11.0654Z" fill="white"/>
<path d="M18.4307 6.89062V14H17.5273L14.6807 8.5166V14H13.7822V6.89062H14.6807L17.542 12.3887V6.89062H18.4307Z" fill="white"/>
</svg>

`
Insert cell
Insert cell
Insert cell
Insert cell
helpBoxStyle = html`<style>
.helpBox {
box-sizing: border-box;
border: 1px solid rgba(0,0,0,0.4);
border-radius: 4px;
box-shadow: 0px 0px 6px 0px #868789;
background: rgba(240,240,240,0.9);
padding: 5px;
font-family: Roboto condensed, Helvetica, sans-serif, Arial;
font-size: 10px;
user-select: none;
}
</style>`
Insert cell
helpBoxWidth = 250 // Pixels
Insert cell
measure(
helpBoxWidth,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
)
Insert cell
//
// Given a text which is to be displayed in a div box
// and the width of that box, returns the width and height of
// the box containing text
//
function measure(width, text) {
const dummyTextDiv = htl.html`<div class=helpBox >`;
Object.assign(dummyTextDiv.style, {
width: width + "px",
position: "absolute",
bottom: "-1000px"
});
dummyTextDiv.innerHTML = text;
document.body.append(dummyTextDiv);
Promises.delay(0);
let obj = dummyTextDiv.getBoundingClientRect();
dummyTextDiv.remove();
return { width: obj.width, height: obj.height };
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more