Zundler/zundler/assets/zundler_main.js
Adrian Vollmer 3eda0fb604 Change behavior of loading indicator
We don't show the loading indicator at all between clicks. Instead, just
append the new iframe. Delete the old iframe when the new one sent the
"ready" message. This way we prevent flashing of the white browser
background, which looks jarring when viewing dark mode pages.
2024-04-27 18:59:25 +02:00

314 lines
9.8 KiB
JavaScript

const iFrameId = 'zundler-iframe';
var retrieveFile = function(path) {
// console.log("Retrieving file: " + path);
var fileTree = window.globalContext.fileTree;
var file = fileTree[path];
if (!file) {
console.warn("File not found: " + path);
return "";
} else {
return file;
}
};
var setFavicon = function(href) {
if (!href) {return;}
var favicon = document.createElement("link");
favicon.setAttribute('rel', 'shortcut icon');
href = normalizePath(href);
const file = window.globalContext.fileTree[href];
if (!file) {return;}
if (file.mime_type == 'image/svg+xml') {
favicon.setAttribute('href', 'data:' + file.mime_type + ';charset=utf-8;base64,' + btoa(file.data));
favicon.setAttribute('type', file.mime_type);
} else {
if (file.base64encoded) {
favicon.setAttribute('href', 'data:' + file.mime_type + ';base64,' + file.data);
}
}
document.head.appendChild(favicon);
};
var createIframe = function(html) {
var iframe = document.createElement("iframe");
iframe.setAttribute('src', '#');
iframe.setAttribute('name', iFrameId);
iframe.setAttribute('id', iFrameId);
iframe.setAttribute("srcdoc", html);
return iframe;
}
function deepCopyExcept(obj, skipProps) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (!skipProps.includes(key)) {
result[key] = deepCopyExcept(obj[key], skipProps);
}
}
}
return result;
}
var prepare = function(html) {
function unicodeToBase64(string) {
const utf8EncodedString = unescape(encodeURIComponent(string));
return btoa(utf8EncodedString);
}
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
// Insert the global context into the iframe's DOM, but without the file
// tree or utils. They are not necessary; the iframe will message the
// parent document to retrieve files.
//
// Convert JSON object to b64 because it contain all kinds of
// problematic characters: `, ", ', &, </script>, ...
// atob is insufficient, because it only deals with ASCII - we have
// unicode
const gcTag = doc.createElement("script");
const strippedGC = deepCopyExcept(window.globalContext, ["fileTree", "utils"]);
var serializedGC = unicodeToBase64(JSON.stringify(strippedGC));
gcTag.textContent = `
function base64ToUnicode(base64String) {
const utf8EncodedString = atob(base64String);
return decodeURIComponent(escape(utf8EncodedString));
}
window.globalContext = JSON.parse(base64ToUnicode("${serializedGC}"));
`;
const commonTag = doc.createElement("script");
commonTag.textContent = window.globalContext.utils.zundler_common;
const injectPreTag = doc.createElement("script");
injectPreTag.textContent = window.globalContext.utils.inject_pre;
const injectPostTag = doc.createElement("script");
injectPostTag.textContent = window.globalContext.utils.inject_post;
doc.head.prepend(commonTag);
doc.head.prepend(gcTag);
doc.head.prepend(injectPreTag);
doc.body.append(injectPostTag);
embedJs(doc);
embedCss(doc);
embedImgs(doc);
fixLinks(doc);
fixForms(doc);
window.document.title = doc.title;
return doc.documentElement.outerHTML;
}
var embedImg = function(img) {
if (img.hasAttribute('src')) {
const src = img.getAttribute('src');
if (isVirtual(src)) {
var path = normalizePath(src);
const file = retrieveFile(path);
const mime_type = file.mime_type;
if (mime_type == 'image/svg+xml') {
img.setAttribute('src', "data:image/svg+xml;charset=utf-8;base64, " + btoa(file.data));
} else {
img.setAttribute('src', `data:${mime_type};base64, ${file.data}`);
}
};
};
};
var embedJs = function(doc) {
Array.from(doc.querySelectorAll("script")).forEach( oldScript => {
const newScript = doc.createElement("script");
Array.from(oldScript.attributes).forEach( attr => {
newScript.setAttribute(attr.name, attr.value);
});
try {
if (newScript.hasAttribute('src') && isVirtual(newScript.getAttribute('src'))) {
var src = newScript.getAttribute('src');
let [path, getParameters, anchor] = splitUrl(src);
path = normalizePath(path);
console.debug("Embed script: " + path);
var src = retrieveFile(path).data + ' \n//# sourceURL=' + path;
newScript.appendChild(doc.createTextNode(src));
newScript.removeAttribute('src');
oldScript.parentNode.replaceChild(newScript, oldScript);
}
} catch (e) {
// Make sure all scripts are loaded
console.error("Caught error in " + oldScript.getAttribute("src"), e);
}
});
}
var embedCss = function(doc) {
Array.from(doc.querySelectorAll("link")).forEach( link => {
if (link.getAttribute('rel') == 'stylesheet' && link.getAttribute("href")) {
const style = doc.createElement("style");
var href = link.getAttribute('href');
let [path, getParameters, anchor] = splitUrl(href);
path = normalizePath(path);
style.textContent = retrieveFile(path).data;
link.replaceWith(style);
};
});
};
var fixLinks = function(doc) {
Array.from(doc.querySelectorAll("a")).forEach( a => {
fixLink(a);
});
};
var fixForms = function(doc) {
Array.from(doc.querySelectorAll("form")).forEach( form => {
fixForm(form);
});
};
var embedImgs = function(doc) {
Array.from(doc.querySelectorAll("img")).forEach( img => {
embedImg(img);
});
};
var loadVirtualPage = (function (path, getParams, anchor) {
// fill the iframe with the new page
// return True if it worked
// return False if loading indicator should be removed right away
const file = window.globalContext.fileTree[path];
if (!file) {
console.error("File not found:", path, getParams, anchor);
return false;
}
const data = file.data;
window.globalContext.getParameters = getParams;
// libmagic doesn't properly recognize mimetype of HTMl files that start
// with empty lines. It thinks it's javascript. So we also consider the
// filename when determining the file type.
if (file.mime_type == 'text/html' || path.toLowerCase().endsWith(".html")) {
window.globalContext.current_path = path;
window.globalContext.anchor = anchor;
const html = prepare(data);
window.history.pushState({path, getParams, anchor}, '', '#');
var oldIframe = document.getElementById(iFrameId);
if (oldIframe) { oldIframe.setAttribute('id', "old-" + iFrameId); };
var iframe = createIframe(html);
window.document.body.append(iframe);
return true;
} else {
let blob = new Blob([data], {type: file.mime_type})
var url = URL.createObjectURL(blob)
var myWindow = window.open(url, "_blank");
return false;
}
});
window.onload = function() {
// Set up message listener
window.addEventListener("message", (evnt) => {
console.log("Received message in parent", evnt.data);
var iframe = document.getElementById(iFrameId);
if (evnt.data.action == 'ready') {
// iframe is ready
hideLoadingIndicator();
iframe.contentWindow.postMessage({
action: "scrollToAnchor",
}, "*");
var oldIframe = document.getElementById("old-" + iFrameId);
if (oldIframe) { oldIframe.remove() };
} else if (evnt.data.action == 'retrieveFile') {
const path = evnt.data.argument.path;
const file = retrieveFile(path);
iframe.contentWindow.postMessage({
action: "sendFile",
argument: {
path: path,
file: file,
},
}, "*");
} else if (evnt.data.action == 'showMenu') {
showMenu();
} else if (evnt.data.action == 'set_title') {
// iframe has finished loading and sent us its title
// parent sets the title and responds with the globalContext object
window.document.title = evnt.data.argument.title;
setFavicon(evnt.data.argument.favicon);
} else if (evnt.data.action == 'virtualClick') {
// user has clicked on a link in the iframe
// showLoadingIndicator();
var loaded = loadVirtualPage(
evnt.data.argument.path,
evnt.data.argument.getParameters,
evnt.data.argument.anchor,
);
if (!loaded) {
hideLoadingIndicator();
}
}
}, false);
// Set up history event listener
window.addEventListener("popstate", (evnt) => {
loadVirtualPage(evnt.state.path, evnt.state.get_params, evnt.state.anchor);
});
// Load first page
loadVirtualPage(window.globalContext.current_path, "", "");
}
var showLoadingIndicator = function() {
var loading = document.getElementById('loading-indicator');
loading.style.display = '';
}
var hideLoadingIndicator = function() {
var loading = document.getElementById('loading-indicator');
loading.style.display = 'none';
}
function showMenu() {
// TODO show the menu containing info and functions
}
document.addEventListener('keyup', function (event) {
if (event.key == "Z" && event.ctrlKey){
showMenu();
}
});