258 lines
7.9 KiB
TypeScript
258 lines
7.9 KiB
TypeScript
import * as bootstrap from 'bootstrap';
|
|
|
|
import xslContent from 'bundle-text:../xsl/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-lg-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
|
|
errorDiv.appendChild(<span>ex.message</span>);
|
|
|
|
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 })
|
|
.then(_ => toggleLoadPrevious(true))
|
|
.catch(err => console.error('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 toggleLoadPrevious(toggle: boolean) {
|
|
ui.loadPreviousButtons.forEach(btn => {
|
|
if (toggle) {
|
|
btn.classList.remove('disabled');
|
|
} else {
|
|
btn.classList.add('disabled');
|
|
bootstrap.Modal.getOrCreateInstance(ui.previousBackupsModal)?.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function initEvents() {
|
|
db.entries.count()
|
|
.then(count => {
|
|
if (count > 0) {
|
|
toggleLoadPrevious(true);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
//Something wrong with db access, disable buttons anyway.
|
|
console.error(err);
|
|
toggleLoadPrevious(false);
|
|
});
|
|
|
|
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.'));
|
|
}
|
|
});
|
|
|
|
// ui.previousBackupsList.addEventListener('click', , true);
|
|
}
|
|
|
|
async function deleteBackup(e: MouseEvent) {
|
|
const el = e.currentTarget as HTMLElement;
|
|
const anchor = el.closest('a.backup-entry') as HTMLAnchorElement | null;
|
|
const filename = anchor?.dataset['filename'];
|
|
if (filename) {
|
|
if (window.confirm('Do you want to remove ' + filename + ' from the backup list?')) {
|
|
try {
|
|
await db.entries.delete(filename);
|
|
const count = await db.entries.count();
|
|
|
|
if (count > 0) {
|
|
populateSavedBackups();
|
|
} else {
|
|
toggleLoadPrevious(false);
|
|
}
|
|
} catch (e) {
|
|
displayNonFatalError('Could not remove backup', e as Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadBackup(e: MouseEvent) {
|
|
const listItem = e.target as HTMLElement | null;
|
|
const filename = listItem?.dataset['filename'];
|
|
|
|
if (filename) {
|
|
const entry = await db.entries.get(filename);
|
|
if (entry) {
|
|
displayBackup(xsltProcessor, entry.filename, entry.backupData);
|
|
} else {
|
|
displayNonFatalError('Internal Error', new Error('Backup data disappeared?!'));
|
|
}
|
|
|
|
bootstrap.Modal.getInstance(ui.previousBackupsModal)?.hide();
|
|
}
|
|
}
|
|
|
|
function BackupListItem(props: { filename: string }) {
|
|
return (
|
|
<a href="#" class="backup-entry list-group-item list-group-item-action"
|
|
onClick={loadBackup} dataset={{ filename: props.filename }}>
|
|
<div class="backup-name pe-none">
|
|
{props.filename}
|
|
</div>
|
|
<div class="backup-delete" onClick={deleteBackup}>
|
|
<i class="bi bi-x-circle pe-none"></i>
|
|
</div>
|
|
</a>
|
|
);
|
|
}
|
|
|
|
async function populateSavedBackups() {
|
|
removeChildren(ui.previousBackupsList);
|
|
const modal = bootstrap.Modal.getOrCreateInstance(ui.previousBackupsModal);
|
|
const entries = await db.entries.orderBy('filename').toArray();
|
|
|
|
entries.forEach(entry => {
|
|
ui.previousBackupsList.appendChild(<BackupListItem filename={entry.filename} />);
|
|
});
|
|
|
|
modal.show();
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
await initEvents();
|
|
showLoadingIndicator(false);
|
|
showApplication(true);
|
|
});
|