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-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); });