You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
254 lines
6.9 KiB
254 lines
6.9 KiB
/* eslint-env browser */
|
|
(function () {
|
|
// Space optimisations
|
|
var doc = document;
|
|
var find = doc.querySelector.bind(doc);
|
|
var create = doc.createElement.bind(doc);
|
|
|
|
// Run callback when DOM is ready
|
|
function domReady(cb) {
|
|
// Run now if DOM has already loaded as we're loading async
|
|
if (['interactive', 'complete'].indexOf(doc.readyState) >= 0) {
|
|
cb();
|
|
|
|
// Otherwise wait for DOM
|
|
} else {
|
|
doc.addEventListener('DOMContentLoaded', cb);
|
|
}
|
|
}
|
|
|
|
// Feature detection
|
|
var supports = {
|
|
test: function (features) {
|
|
var self = this;
|
|
if (!features || features.length < 1) {
|
|
return false;
|
|
}
|
|
return features.every(function (feature) {
|
|
return self.tests[feature];
|
|
});
|
|
},
|
|
tests: {
|
|
localStorage: (function () {
|
|
try {
|
|
localStorage.setItem('test', 'test');
|
|
localStorage.removeItem('test');
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
})(),
|
|
inlineSVG: (function () {
|
|
var div = create('div');
|
|
div.innerHTML = '<svg/>';
|
|
return (
|
|
typeof SVGRect !== 'undefined' &&
|
|
div.firstChild &&
|
|
div.firstChild.namespaceURI
|
|
) === 'http://www.w3.org/2000/svg';
|
|
})(),
|
|
querySelector: typeof doc.querySelector === 'function',
|
|
classList: (function () {
|
|
var div = create('div');
|
|
div.innerHTML = '<svg/>';
|
|
return 'classList' in div.firstChild;
|
|
})(),
|
|
serviceWorker: 'serviceWorker' in navigator
|
|
}
|
|
};
|
|
|
|
// Favourite nodes
|
|
var favouriteNodes = {
|
|
|
|
// Key used in localStorage
|
|
storageKey: 'heartedNodes',
|
|
|
|
// Url to heart SVG
|
|
heartPath: '/assets/heart.svg',
|
|
|
|
// Class added to heart SVG element when active
|
|
activeClass: 'hearted',
|
|
|
|
// Gets current node hash
|
|
getCurrentNode: function () {
|
|
var node = /^\/node\/([a-zA-Z0-9]+)/.exec(window.location.pathname);
|
|
return node ? node[1] : node;
|
|
},
|
|
|
|
// Gets current node title
|
|
getCurrentNodeTitle: function () {
|
|
return find('h2.node-title .name').innerText;
|
|
},
|
|
|
|
// Gets hearted nodes
|
|
getHeartedNodes: function () {
|
|
return JSON.parse(localStorage.getItem(favouriteNodes.storageKey)) || {};
|
|
},
|
|
|
|
// Saves hearted nodes
|
|
saveHeartedNodes: function (heartedNodes) {
|
|
return localStorage.setItem(favouriteNodes.storageKey, JSON.stringify(heartedNodes));
|
|
},
|
|
|
|
// Checks if node is hearted
|
|
isHearted: function (node) {
|
|
return typeof favouriteNodes.getHeartedNodes()[node] !== 'undefined';
|
|
},
|
|
|
|
// Heart node
|
|
heart: function (node) {
|
|
var heartedNodes = favouriteNodes.getHeartedNodes();
|
|
heartedNodes[node] = favouriteNodes.getCurrentNodeTitle();
|
|
favouriteNodes.saveHeartedNodes(heartedNodes);
|
|
favouriteNodes.updateHeartedNodesList();
|
|
return heartedNodes;
|
|
},
|
|
|
|
// Unheart node
|
|
unHeart: function (node) {
|
|
var heartedNodes = favouriteNodes.getHeartedNodes();
|
|
delete heartedNodes[node];
|
|
favouriteNodes.saveHeartedNodes(heartedNodes);
|
|
favouriteNodes.updateHeartedNodesList();
|
|
return heartedNodes;
|
|
},
|
|
|
|
// Get list of hearted nodes
|
|
updateHeartedNodesList: function () {
|
|
var menu = find('.menu');
|
|
if (!menu) {
|
|
return false;
|
|
}
|
|
var menuHTML = '';
|
|
var heartedNodes = favouriteNodes.getHeartedNodes();
|
|
var nodeHashes = Object.keys(heartedNodes);
|
|
if (nodeHashes.length > 0) {
|
|
menuHTML += '<ul>';
|
|
nodeHashes.forEach(function (node) {
|
|
menuHTML += '<li><a href="/node/' + node + '">' + heartedNodes[node] + '</a></li>';
|
|
});
|
|
menuHTML += '</ul>';
|
|
} else {
|
|
menuHTML += '<div class="empty">Click the heart next to a node\'s title on it\'s own page to save it here for easy access :)</div>';
|
|
}
|
|
menu.innerHTML = menuHTML;
|
|
return menu.innerHTML;
|
|
},
|
|
|
|
// Load SVG, run callback when loaded
|
|
loadSVG: function (cb) {
|
|
// Get heart SVG
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', favouriteNodes.heartPath);
|
|
xhr.addEventListener('load', function () {
|
|
cb(xhr.responseText);
|
|
});
|
|
xhr.send();
|
|
},
|
|
|
|
// Initiate node favouriting
|
|
init: function () {
|
|
// Start loading heart SVG before DOM
|
|
favouriteNodes.loadSVG(function (svg) {
|
|
// Create heart SVG elem
|
|
var div = create('div');
|
|
div.innerHTML = svg;
|
|
var heartEl = div.firstChild;
|
|
|
|
// Show heart as active if we've already hearted this node
|
|
var node = favouriteNodes.getCurrentNode();
|
|
if (favouriteNodes.isHearted(node)) {
|
|
heartEl.classList.add(favouriteNodes.activeClass);
|
|
}
|
|
|
|
// Add click handler
|
|
heartEl.addEventListener('click', function () {
|
|
// Heart/unheart node
|
|
var node = favouriteNodes.getCurrentNode();
|
|
if (favouriteNodes.isHearted(node)) {
|
|
heartEl.classList.remove(favouriteNodes.activeClass);
|
|
favouriteNodes.unHeart(node);
|
|
} else {
|
|
heartEl.classList.add(favouriteNodes.activeClass);
|
|
favouriteNodes.heart(node);
|
|
}
|
|
});
|
|
|
|
// Then inject into DOM when it's ready
|
|
domReady(function () {
|
|
var headerHeight = find('.title').offsetHeight;
|
|
var headerBoxShadow = 3;
|
|
|
|
// Heart
|
|
var titleEl = find('h2.node-title');
|
|
if (titleEl) {
|
|
titleEl.insertBefore(heartEl, titleEl.firstChild);
|
|
}
|
|
|
|
// Menu button
|
|
var menuButton = create('div');
|
|
menuButton.classList.add('menu-button');
|
|
menuButton.style.height = headerHeight + 'px';
|
|
menuButton.innerHTML = svg;
|
|
menuButton.addEventListener('click', function () {
|
|
favouriteNodes.updateHeartedNodesList();
|
|
find('.menu').classList.toggle('active');
|
|
});
|
|
find('header .wrapper').appendChild(menuButton);
|
|
|
|
// Menu
|
|
var menu = create('div');
|
|
menu.classList.add('menu');
|
|
menu.style.top = (headerHeight + headerBoxShadow) + 'px';
|
|
menu.style.height = 'calc(100% - ' + (headerHeight + headerBoxShadow) + 'px)';
|
|
document.body.appendChild(menu);
|
|
favouriteNodes.updateHeartedNodesList();
|
|
});
|
|
});
|
|
|
|
// If current node is hearted
|
|
var node = favouriteNodes.getCurrentNode();
|
|
if (favouriteNodes.isHearted(node)) {
|
|
// Heart it again so we get the new name if it's updated
|
|
favouriteNodes.heart(node);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Service worker
|
|
if (supports.test(['serviceWorker', 'querySelector', 'classList'])) {
|
|
// Register service worker
|
|
navigator.serviceWorker.register('/sw.js');
|
|
|
|
// Show cache message on stale pages
|
|
domReady(function () {
|
|
if (window.cacheDate) {
|
|
var offlineMessage = create('div');
|
|
offlineMessage.classList.add('cache-message');
|
|
offlineMessage.innerText = '*There seems to be an issue connecting to the server. This data is cached from ' + window.cacheDate;
|
|
var main = find('main');
|
|
if (main) {
|
|
doc.body.classList.add('no-connection');
|
|
main.insertBefore(offlineMessage, main.firstChild);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Init favourite nodes
|
|
if (supports.test(['localStorage', 'inlineSVG', 'querySelector', 'classList'])) {
|
|
favouriteNodes.init();
|
|
}
|
|
|
|
// Add ios class to body on iOS devices
|
|
if (supports.test(['classList'])) {
|
|
domReady(function () {
|
|
if (
|
|
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
|
|
!window.MSStream
|
|
) {
|
|
doc.body.classList.add('ios');
|
|
}
|
|
});
|
|
}
|
|
})();
|
|
|