You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

452 lines
15 KiB
JavaScript

if (typeof _sageInitialized === 'undefined') {
_sageInitialized = 1;
const _sage = {
visiblePluses: [], // all visible toggle carets
currentPlus: -1, // currently selected caret
selectText: function (element) {
const selection = window.getSelection(),
range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
},
each: function (selector, callback) {
Array.prototype.slice.call(document.querySelectorAll(selector), 0).forEach(callback)
},
hasClass: function (target, className = '_sage-show') {
if (!target.classList) {
return false;
}
return target.classList.contains(className);
},
addClass: function (target, className = '_sage-show') {
target.classList.add(className);
},
removeClass: function (target, className = '_sage-show') {
target.classList.remove(className);
return target;
},
next: function (element) {
do {
element = element.nextElementSibling;
} while (element && element.tagName !== 'DD');
return element;
},
toggle: function (element, hide) {
if (typeof hide === 'undefined') {
hide = _sage.hasClass(element);
}
if (hide) {
_sage.removeClass(element);
} else {
_sage.addClass(element);
}
// also open up child element if there's only one
let parent = _sage.next(element);
if (parent && parent.childNodes.length === 1) {
parent = parent.childNodes[0].childNodes[0]; // reuse variable cause I can
// parent is checked in case of empty <pre> when array("\n") is dumped
if (parent && _sage.hasClass(parent, '_sage-parent')) {
_sage.toggle(parent, hide)
}
}
},
toggleChildren: function (element, hide) {
const parent = _sage.next(element)
, nodes = parent.getElementsByClassName('_sage-parent');
let i = nodes.length;
if (typeof hide === 'undefined') {
hide = _sage.hasClass(element);
}
while (i--) {
_sage.toggle(nodes[i], hide);
}
_sage.toggle(element, hide);
},
toggleAll: function (show) {
const elements = document.getElementsByClassName('_sage-parent')
let i = elements.length
while (i--) {
if (show) {
_sage.addClass(elements[i]);
} else {
_sage.removeClass(elements[i]);
}
}
},
switchTab: function (target) {
let lis, el = target, index = 0;
_sage.removeClass(target.parentNode.getElementsByClassName('_sage-active-tab')[0], '_sage-active-tab');
target.className = '_sage-active-tab';
// take the index of clicked title tab and make the same n-th content tab visible
while (el = el.previousSibling) el.nodeType === 1 && index++;
lis = target.parentNode.nextSibling.childNodes;
for (let i = 0; i < lis.length; i++) {
lis[i].style.display = i === index ? 'block' : 'none';
}
},
isSibling: function (el) {
for (; ;) {
el = el.parentNode;
if (!el || _sage.hasClass(el, '_sage')) {
break;
}
}
return !!el;
},
fetchVisiblePluses: function () {
_sage.visiblePluses = [];
_sage.each('._sage nav, ._sage-tabs>li:not(._sage-active-tab)', function (el) {
if (el.offsetWidth !== 0 || el.offsetHeight !== 0) {
_sage.visiblePluses.push(el)
}
});
},
// some custom implementations screw up the JS when they see <head> or <meta charset>
// this method survives minification
tag: function (contents) {
return '<' + contents + '>';
},
openInNewWindow: function (_sageContainer) {
let newWindow;
if (newWindow = window.open()) {
newWindow.document.open();
newWindow.document.write(
_sage.tag('html')
+ _sage.tag('head')
+ '<title>Sage ☯ (' + new Date().toISOString() + ')</title>'
+ _sage.tag('meta charset="utf-8"')
+ document.getElementsByClassName('_sage-js')[0].outerHTML
+ document.getElementsByClassName('_sage-css')[0].outerHTML
+ _sage.tag('/head')
+ _sage.tag('body')
+ '<input style="width: 100%" placeholder="Take some notes!">'
+ '<div class="_sage">'
+ _sageContainer.parentNode.outerHTML
+ '</div>'
+ _sage.tag('/body')
);
newWindow.document.close();
}
},
sortTable: function (table, column, header) {
const tbody = table.tBodies[0];
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
const direction = (typeof header.sage_direction === 'undefined') ? 1 : header.sage_direction
header.sage_direction = -1 * direction;
[].slice.call(table.tBodies[0].rows)
.sort(function (a, b) {
return direction * collator.compare(a.cells[column].textContent, b.cells[column].textContent)
})
.forEach(function (el) {
tbody.appendChild(el);
});
},
keyCallBacks: {
cleanup: function (i) {
const focusedClass = '_sage-focused';
const prevElement = document.querySelector('.' + focusedClass);
prevElement && _sage.removeClass(prevElement, focusedClass);
if (i !== -1) {
const el = _sage.visiblePluses[i];
_sage.addClass(el, focusedClass);
const offsetTop = function (el) {
return el.offsetTop + (el.offsetParent ? offsetTop(el.offsetParent) : 0);
};
const top = offsetTop(el) - (window.innerHeight / 2);
window.scrollTo(0, top);
}
_sage.currentPlus = i;
},
moveCursor: function (up, i) {
// todo make the first VISIBLE plus active
if (up) {
if (--i < 0) {
i = _sage.visiblePluses.length - 1;
}
} else {
if (++i >= _sage.visiblePluses.length) {
i = 0;
}
}
_sage.keyCallBacks.cleanup(i);
return false;
}
}
};
window.addEventListener('click', function (e) {
let target = e.target
, tagName = target.tagName;
if (!_sage.isSibling(target)) {
return;
}
// auto-select name of variable
if (tagName === 'DFN') {
_sage.selectText(target);
target = target.parentNode;
} else if (tagName === 'VAR') { // stupid workaround for misc elements
target = target.parentNode; // to not stop event from further propagating
tagName = target.tagName;
} else if (tagName === 'TH') {
if (!e.ctrlKey) {
_sage.sortTable(target.parentNode.parentNode.parentNode, target.cellIndex, target)
}
return false;
}
// switch tabs
if (tagName === 'LI' && target.parentNode.className === '_sage-tabs') {
if (target.className !== '_sage-active-tab') {
_sage.switchTab(target);
if (_sage.currentPlus !== -1) {
_sage.fetchVisiblePluses();
}
}
return false;
}
// handle clicks on the navigation caret
if (tagName === 'NAV') {
// special case for nav in footer
if (target.parentNode.tagName === 'FOOTER') {
target = target.parentNode;
_sage.toggle(target)
} else {
// ensure doubleclick has different behaviour, see below
setTimeout(function () {
const timer = parseInt(target._sageTimer, 10);
if (timer > 0) {
target._sageTimer--;
} else {
_sage.toggleChildren(target.parentNode); // <dt>
if (_sage.currentPlus !== -1) {
_sage.fetchVisiblePluses();
}
}
}, 300);
}
e.stopPropagation();
return false;
} else if (_sage.hasClass(target, '_sage-parent')) {
_sage.toggle(target);
if (_sage.currentPlus !== -1) {
_sage.fetchVisiblePluses();
}
return false;
} else if (_sage.hasClass(target, '_sage-ide-link')) {
fetch(target.href);
return false;
} else if (_sage.hasClass(target, '_sage-popup-trigger')) {
let _sageContainer = target.parentNode;
if (_sageContainer.tagName === 'FOOTER') {
_sageContainer = _sageContainer.previousSibling;
} else {
while (_sageContainer && !_sage.hasClass(_sageContainer, '_sage-parent')) {
_sageContainer = _sageContainer.parentNode;
}
}
_sage.openInNewWindow(_sageContainer);
} else if (tagName === 'PRE' && e.detail === 3) { // triple click pre to select it all
_sage.selectText(target);
}
}, false);
window.addEventListener('dblclick', function (e) {
const target = e.target;
if (!_sage.isSibling(target)) {
return;
}
if (target.tagName === 'NAV') {
target._sageTimer = 2;
_sage.toggleAll(_sage.hasClass(target));
if (_sage.currentPlus !== -1) {
_sage.fetchVisiblePluses();
}
e.stopPropagation();
}
}, false);
// keyboard navigation
window.onkeydown = function (e) { // direct assignment is used to have priority over ex FAYT
// todo use e.key https://www.toptal.com/developers/keycode
const keyCode = e.keyCode;
let i = _sage.currentPlus;
// user pressed ctrl+f
if (keyCode === 70 && e.ctrlKey) {
_sage.toggleAll(true);
return;
}
// do nothing if alt/ctrl key is pressed or if we're actually typing somewhere
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.altKey || e.ctrlKey) {
return;
}
if (keyCode === 9) { // TAB jumps out of navigation
_sage.keyCallBacks.cleanup(-1);
return;
// todo 's' too
} else if (keyCode === 68) { // 'd' : toggles navigation on/off
if (i === -1) {
_sage.fetchVisiblePluses();
return _sage.keyCallBacks.moveCursor(false, i);
} else {
_sage.keyCallBacks.cleanup(-1);
return false;
}
} else {
if (i === -1) {
return;
}
if (keyCode === 38) { // ARROW UP : moves up
return _sage.keyCallBacks.moveCursor(true, i);
} else if (keyCode === 40) { // ARROW DOWN : down
return _sage.keyCallBacks.moveCursor(false, i);
}
}
let currentNav = _sage.visiblePluses[i];
if (currentNav.tagName === 'LI') { // we're on a trace tab
if (keyCode === 32 || keyCode === 13) { // SPACE/ENTER
_sage.switchTab(currentNav);
_sage.fetchVisiblePluses();
return _sage.keyCallBacks.moveCursor(true, i);
} else if (keyCode === 39) { // arrows
return _sage.keyCallBacks.moveCursor(false, i);
} else if (keyCode === 37) {
return _sage.keyCallBacks.moveCursor(true, i);
}
}
// we are on a regular/footer [+]
currentNav = currentNav.parentNode; // simple dump
if (currentNav.tagName === 'FOOTER') {
if (keyCode === 32 || keyCode === 13) { // SPACE/ENTER : toggles
_sage.toggle(currentNav);
return false;
}
}
if (keyCode === 32 || keyCode === 13) { // SPACE/ENTER : toggles
_sage.toggle(currentNav);
_sage.fetchVisiblePluses();
return false;
} else if (keyCode === 39 || keyCode === 37) { // ARROW LEFT/RIGHT : respectively hides/shows and traverses
const visible = _sage.hasClass(currentNav);
const hide = keyCode === 37;
if (visible) {
_sage.toggleChildren(currentNav, hide); // expand/collapse all children if immediate ones are showing
} else {
if (hide) { // LEFT
// traverse to parent and THEN hide
do {
currentNav = currentNav.parentNode
} while (currentNav && currentNav.tagName !== 'DD');
if (currentNav) {
currentNav = currentNav.previousElementSibling;
i = -1;
const parentPlus = currentNav.querySelector('nav');
while (parentPlus !== _sage.visiblePluses[++i]) {
}
_sage.keyCallBacks.cleanup(i)
} else { // we are at root
currentNav = _sage.visiblePluses[i].parentNode;
}
}
_sage.toggle(currentNav, hide);
}
_sage.fetchVisiblePluses();
return false;
}
};
window.addEventListener('load', function () { // colorize microtime results relative to others
const elements = Array.prototype.slice.call(document.querySelectorAll('._sage-microtime'), 0);
let min = Infinity
, max = -Infinity;
elements.forEach(function (el) {
const val = parseFloat(el.innerHTML);
if (min > val) {
min = val;
}
if (max < val) {
max = val;
}
});
elements.forEach(function (el) {
const val = parseFloat(el.innerHTML);
const ratio = 1 - (val - min) / (max - min);
el.style.background = 'hsl(' + Math.round(ratio * 120) + ',60%,70%)';
});
});
}
// debug purposes only, removed in minified source
function clg(i) {
if (!window.console) {
return;
}
const l = arguments.length;
let o = 0;
while (o < l) console.log(arguments[o++])
}