msn-viewer/src/index.ts

205 lines
6.4 KiB
TypeScript

import * as bootstrap from 'bootstrap';
import xslContent from 'bundle-text:./MessageLog.xsl';
import * as ui from './ui';
import { db } from './db';
const xsltProcessor = createXSLTProcessor(xslContent)!;
function showApplication(toggle: boolean) {
if (toggle) {
ui.realBody.classList.remove('d-none');
} else {
ui.realBody.classList.add('d-none');
}
}
function showLoadingIndicator(toggle: boolean) {
if (toggle) {
ui.loadingIndicator.classList.remove('d-none');
} else {
ui.loadingIndicator.classList.add('d-none');
}
}
function showLoadedControls() {
ui.noFileLoadedControls.classList.add('d-none');
ui.noFileLoadedControls.classList.remove('d-md-block');
ui.fileLoadedControls.classList.remove('d-none');
}
function displayError(errorDiv: HTMLElement, category: string, ex: Error) {
console.error(ex);
const errorHeading = errorDiv.querySelector('.alert-heading') as HTMLElement | null;
//clear all error text, but not heading
const errorMessages = errorDiv.querySelectorAll('span');
errorMessages.forEach(message => errorDiv.removeChild(message));
//add new message and show the error alert
const errorSpan = document.createElement('span');
errorSpan.innerText = ex.message;
errorDiv.appendChild(errorSpan);
if (errorHeading) {
errorHeading.innerText = category;
}
errorDiv.classList.remove('d-none');
}
function displayFatalError(category: string, ex: Error) {
showApplication(false);
showLoadingIndicator(false);
displayError(ui.fatalError, category, ex);
}
function displayNonFatalError(category: string, ex: Error) {
showApplication(true);
showLoadingIndicator(false);
displayError(ui.nonFatalError, category, ex);
}
function hideErrors() {
document
.querySelectorAll('#fatal-error, #display-backup-error')
.forEach(el => el.classList.add('d-none'));
}
function createXSLTProcessor(xmlStylesheet: string): XSLTProcessor | undefined {
const doc = new DOMParser().parseFromString(xmlStylesheet, "text/xml");
try {
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(doc);
return xsltProcessor;
} catch (cause) {
console.error(cause);
displayFatalError('Initialiation Failed', new Error('Stylesheet parsing failed'));
}
}
function removeChildren(element: HTMLElement) {
while (element.hasChildNodes() && element.lastChild != null) {
element.removeChild(element.lastChild);
}
}
function checkOverflow(elem: HTMLElement) {
const elemHeight = elem.scrollHeight;
const parentHeight = elem.offsetHeight;
return elemHeight > parentHeight;
}
function processFragment() {
document
.querySelectorAll('[data-bs-toggle="popover"]')
.forEach(popover => new bootstrap.Popover(popover, { trigger: 'focus' }));
document.querySelectorAll('.message-content div').forEach(div => {
if (checkOverflow(div as HTMLElement)) {
div.parentElement?.classList.add('overflow-icon');
}
});
}
async function saveToDatabase(filename: string, xml: string) {
db.entries
.put({ filename, backupData: xml })
.catch(err => displayFatalError('Saving Failed', err));
}
function displayBackup(xsltProcessor: XSLTProcessor, filename: string, xmlText: string): boolean {
hideErrors();
removeChildren(ui.chatDisplay);
showLoadedControls();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
const errorNode = xmlDoc.querySelector('parsererror');
if (!errorNode) {
try {
const fragment = xsltProcessor.transformToFragment(xmlDoc, document);
ui.chatDisplay.appendChild(fragment);
ui.currentlyViewing.value = filename;
processFragment();
return true;
}
catch (e) {
if (e instanceof Error) {
displayNonFatalError('Could not load backup', e);
}
return false;
}
} else {
const err = new Error(errorNode.firstChild?.nodeValue || '');
displayNonFatalError('Could not load backup', err);
return false;
}
}
function initEvents() {
ui.loadPreviousButtons.forEach(button => button.addEventListener('click', async () => {
await populateSavedBackups();
}));
ui.viewNewButtons.forEach(button => button.addEventListener('click', e => {
ui.fileSelector.click();
e.preventDefault();
}));
ui.fileSelector.addEventListener("change", async (e) => {
const target = e.currentTarget as HTMLInputElement | null;
const file = target?.files?.[0];
if (file) {
const xmlText = await file.text();
if (displayBackup(xsltProcessor, file.name, xmlText)) {
await saveToDatabase(file.name, xmlText);
}
} else {
displayNonFatalError('Could not load file', new Error('No file found.'));
}
});
}
async function populateSavedBackups() {
removeChildren(ui.previousBackupsList);
const modal = new bootstrap.Modal('#previous-backups-modal');
const entries = await db.entries.orderBy('filename').toArray();
entries.forEach(entry => {
const listItem = document.createElement('a');
listItem.href = '#';
listItem.dataset['filename'] = entry.filename;
listItem.innerText = entry.filename;
listItem.classList.add('list-group-item', 'list-group-item-action');
listItem.addEventListener('click', async () => {
const reloaded = await db.entries.get(entry.filename);
if (reloaded) {
displayBackup(xsltProcessor, reloaded.filename, reloaded.backupData);
} else {
displayNonFatalError('Internal Error', new Error('Backup data disappeared?!'));
}
modal.hide();
});
ui.previousBackupsList.appendChild(listItem);
});
modal.show();
}
window.addEventListener('DOMContentLoaded', async () => {
window.addEventListener('resize', () => {
if (window.innerWidth <= 500) {
ui.chatDisplay.querySelector('table')?.classList.add('table-sm');
} else {
ui.chatDisplay.querySelector('table')?.classList.remove('table-sm');
}
});
initEvents();
showLoadingIndicator(false);
showApplication(true);
});