Published
Edited
Aug 10, 2020
1 fork
Importers
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
footlist = (footnotes, {heading=LANG.header, separator=true, title=true}={}, cellname='footnotes') => {
/* Builds footnote list as HTML from a data object and also returns an object (use with "viewof")
- Footnote data object schema:
[ {ref: <reference index as string>, text: <footnote content>}, … ]
- Specify the cellname so that footnote references can be linked properly to the list
*/
const genFootnoteHash = ref => parseInt(ref) ? '#fn'+ref : '#'+ref;
const list = footnotes.map(fn =>
md`<li id="${genFootnoteHash(fn.ref)}"><div>
<div><a class="backref" href="${'#'+cellname+'_'+fn.ref}" title="${LANG.gotoRef}">^ ${fn.ref}</a></div><div>${fn.text}</div>
</div></li>`);
const element = html`${separator ? '<hr/>' : ''}<div id="footlist">${title ? `<h5>${heading}</h5>` : ''}<ul>${list}</ul></div>`;
element.value = { cellname: cellname, list: footnotes };
element.querySelectorAll('a.backref').forEach(link => link.addEventListener('click', e => {
e.preventDefault(); // prevents default-behaviour of element
const id = e.target.getAttribute('href').split('#')[1];
const linkElem = document.getElementById(id);
linkElem.scrollIntoView();
}));
return element;
}
Insert cell
Insert cell
function* mdfn(strings, ...keys) {
/* Tagged template literal to easily set footnote references in markdown (replaces "md`…`") */
const footnotesObj = (keys[0] && isFootnoteObject(keys[0]) ) ? keys[0]
: (isFootnoteObject(footnotes) ? footnotes : undefined);
try {
if (footnotesObj === undefined || footnotesObj === null) throw 'Invalid footnotes object!';

const stringsOutput = strings.map(str => {
// [^X] will be replaced with the footnote reference link and preview
let newStr = str.replace(/\[\^([0-9a-z-]+?)\]/g, (match, p1) => footlink(p1, footnotesObj) );
newStr = newStr.replace(/(\[)(\%\^)([0-9a-z-]+?\])/g, `$1^$3`); // remove escape characters
return newStr;
});
const keysOutput = keys.map((key_,i) => i === 0 && isFootnoteObject(key_) ? '' : key_);

// make sure that the output is in the correct order and apply md template literal
const elem = md(stringsOutput, ...keysOutput);

yield elem;
// adjust previews/tooltips to remain visible and prevent clipping
return yield* previewCorrection(elem);
}
catch (e) {
throw new Error(e);
}
}
Insert cell
function* previewCorrection(pElem) {
/* Adjusts position and size of footnote preview/tooltip or cell height to make sure previews/tooltips are visible to the viewer and remain within cell boundaries to prevent clipping. */
const getDimensions = elem => ({elem: elem, dims: elem.getBoundingClientRect()});
let cell = getDimensions(pElem.parentNode);
let spacerElem = undefined;
// if the cell is too small in height, add a spacer as its last child to
// enlarge it whenever the user hovers over a footnote link
if (cell.dims.height < 120) { // (<- number needs to be adjusted if too small)
spacerElem = document.createElement('div');
spacerElem.classList.add('footnoteSpacer');
cell.elem.appendChild(spacerElem);
// temporarily set a large spacer height to correctly adjust preview position
spacerElem.style['height'] = `${200}px`;
yield pElem;
cell = getDimensions(cell.elem); // update cell dimensions
// add event handlers for footnote links to dynamically adjust cell height based
// on the height difference of their preview boxes
const linkElems = pElem.querySelectorAll('.footlink-container');
for (let linkElem of linkElems) {
linkElem.addEventListener('mouseenter', e => {
e.preventDefault();
if (spacerElem.classList.contains('out')) spacerElem.classList.remove('out');

// calculate y difference to adjust spacer height
const preview = getDimensions(e.target.querySelector('.footlink-preview'));
const p = getDimensions(e.target.parentNode);
const yDiff = (preview.dims.y + preview.dims.height) - (p.dims.y + p.dims.height);
if (yDiff > 0) spacerElem.style['height'] = `${yDiff}px`;
});
linkElem.addEventListener('mouseleave', e => {
e.preventDefault();
// add class for different transition property on mouseleave
spacerElem.classList.add('out');
spacerElem.style['height'] = '0';
});
}
}
const previewElems = pElem.querySelectorAll('.footlink-preview');
for (let previewElem of previewElems) {
let preview = getDimensions(previewElem);
// if preview is only a single line, constrain its width to content
if (preview.dims.height < 34) {
preview.elem.style['min-width'] = '0';
preview.elem.style['max-width'] = 'none';
preview.elem.style['white-space'] = 'nowrap';
preview = getDimensions(preview.elem);
}
// make sure that preview remains inside the bounds of the cell
const xDiff = cell.dims.x - preview.dims.x;
const yDiff = (preview.dims.y + preview.dims.height) - (cell.dims.y + cell.dims.height);
const widthDiff = (preview.dims.x + preview.dims.width) - (cell.dims.x + width);
if (xDiff > 0) preview.elem.style['margin-left'] = xDiff+'px';
if (yDiff > 0) preview.elem.style['margin-bottom'] = (yDiff+26)+'px';
if (widthDiff > 0) preview.elem.style['margin-left'] = '-'+widthDiff+'px';
}
// make spacer invisible by default
if (spacerElem !== undefined) spacerElem.style['height'] = '0';
yield pElem;
}
Insert cell
isFootnoteObject = obj => {
if (obj === undefined || obj === null) return false;
if (typeof(obj) !== 'object') return false;
if (!obj.list || Object.keys(obj.list[0])[0] !== 'ref' || Object.keys(obj.list[0])[1] !== 'text') return false;
return true;
}
Insert cell
Insert cell
footnoteCSS = html`<style>
.footlink {
vertical-align: baseline;
position: relative;
top: -0.4em;
}
sub.footlink {
top: 0.4em;
}
.footlink-container {
position: relative;
}

.footlink-preview {
visibility: hidden;
opacity: 0;

font-size: 0.8rem;
line-height: 1.3em;
font-style: normal;
font-weight: normal;
position: absolute;
z-index: 999;
bottom: 0%;
left: 50%;
transform: translateX(-50%) translateY(100%);
background: #111; color: white;
padding: 0.2rem 0.4rem;
border-radius: 0.2rem;
display: inline-block;
min-width: 18rem;
max-width: ${width/5}px;
transition: all 0.3s ease-in 0.2s;
}
.footlink-container:hover > .footlink-preview {
visibility: visible;
opacity: 1;
}

#footlist h5 {
font-style: italic;
}
#footlist ul {
list-style-type: none;
font-size: 0.8rem;
}
#footlist li > div {
display: flex;
}
#footlist li:first-child {
margin-top: 0;
}
#footlist li {
margin-top: 0.2rem;
}
#footlist li > div > div:first-child {
flex: 0 1 auto;
margin-right: 0.6rem;
}
#footlist li > div > div:last-child {
flex: 1 1 0%;
}

.footnoteSpacer {
transition: all 0.2s ease-in 0s;
width: 100%;
}
.footnoteSpacer.out {
transition: all 0.1s ease-out 0.4s;
}
</style>`
Insert cell
viewof footnotes = footlist([])
Insert cell
loc_EN = ({header: 'Footnotes', gotoRef: 'Go to footnote reference'})
Insert cell
loc_DE = ({header: 'Fußnoten', gotoRef: 'Gehe zu Fußnotenreferenz'})
Insert cell
// add more translations here if needed
Insert cell
LANG = {
switch(setLang) {
case 'de':
return loc_DE;
case 'en':
return loc_EN;
// new translation objects need to be wired here
default:
return loc_EN;
}
}
Insert cell
setLang = ''
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more