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(ex.message); 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 (
{props.filename}
); } 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(); }); modal.show(); } window.addEventListener('DOMContentLoaded', async () => { await initEvents(); showLoadingIndicator(false); showApplication(true); });