Migrate to Parcel and Typescript.
This commit is contained in:
parent
10c31561fd
commit
4a761fb031
|
@ -0,0 +1,4 @@
|
|||
.parcel-cache/
|
||||
dist/
|
||||
node_modules/
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@parcel/config-default",
|
||||
"transformers": {
|
||||
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
|
||||
},
|
||||
"validators": {
|
||||
"*.{ts,tsx}": ["@parcel/validator-typescript"]
|
||||
}
|
||||
}
|
7
index.js
7
index.js
|
@ -75,14 +75,17 @@ function importStylesheet(xsl) {
|
|||
}
|
||||
|
||||
async function createXSLTProcessor() {
|
||||
return fetch("/MessageLog.xsl")
|
||||
return fetch(new URL("/MessageLog.xsl"))
|
||||
.then(resp => {
|
||||
if (resp.ok)
|
||||
return resp.text();
|
||||
else
|
||||
throw new Error(resp.status + ' ' + resp.statusText);
|
||||
})
|
||||
.then(str => new DOMParser().parseFromString(str, "text/xml"))
|
||||
.then(str => {
|
||||
console.log(str);
|
||||
return new DOMParser().parseFromString(str, "text/xml");
|
||||
})
|
||||
.then(importStylesheet)
|
||||
.catch(cause => {
|
||||
throw new Error('Could not load XSL stylesheet', { cause });
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true,
|
||||
"checkJs": true,
|
||||
"jsx": "react",
|
||||
"lib": [ "dom", "es2017" ]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "msn-viewer",
|
||||
"source": "src/index.html",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"scripts": {
|
||||
"start": "parcel",
|
||||
"build": "parcel build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-inline-string": "2.8.2",
|
||||
"@parcel/transformer-sass": "2.8.2",
|
||||
"@parcel/transformer-typescript-tsc": "^2.8.2",
|
||||
"@parcel/validator-typescript": "^2.8.2",
|
||||
"parcel": "^2.8.2",
|
||||
"typescript": ">=3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@types/bootstrap": "^5.2.3",
|
||||
"bootstrap": "^5.2"
|
||||
}
|
||||
}
|
|
@ -4,11 +4,7 @@
|
|||
<title>🔥 MSN Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta charset="UTF-8" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
<link rel="stylesheet" href="./scss/index.scss" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -27,7 +23,7 @@
|
|||
</div>
|
||||
|
||||
<div id="real-body" class="d-none">
|
||||
<nav id="main-navbar" class="navbar px-0 sticky-top navbar-expand-lg navbar-light bg-body-tertiary">
|
||||
<nav id="main-navbar" class="navbar px-0 sticky-top navbar-expand-lg bg-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">
|
||||
🔥 MSN Viewer
|
||||
|
@ -159,10 +155,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="module" src="index.ts"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,163 @@
|
|||
import * as bootstrap from 'bootstrap';
|
||||
import xsl from 'bundle-text:./MessgeLog.xsl';
|
||||
function showApplication(toggle) {
|
||||
if (toggle) {
|
||||
document.getElementById('real-body').classList.remove('d-none');
|
||||
}
|
||||
else {
|
||||
document.getElementById('real-body').classList.add('d-none');
|
||||
}
|
||||
}
|
||||
function showLoadingIndicator(toggle) {
|
||||
if (toggle) {
|
||||
document.getElementById('loading').classList.remove('d-none');
|
||||
}
|
||||
else {
|
||||
document.getElementById('loading').classList.add('d-none');
|
||||
}
|
||||
}
|
||||
function displayError(querySelector, category, ex) {
|
||||
console.error(ex);
|
||||
const errorDiv = document.querySelector(querySelector);
|
||||
const errorHeading = errorDiv.querySelector('.alert-heading');
|
||||
let message = ex.message;
|
||||
if (ex.hasOwnProperty('cause')) {
|
||||
if (ex.cause.hasOwnProperty('message')) {
|
||||
// @ts-ignore
|
||||
message += ': ' + ex.cause.message;
|
||||
}
|
||||
else {
|
||||
message += ex.cause;
|
||||
}
|
||||
}
|
||||
//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 = message;
|
||||
errorDiv.appendChild(errorSpan);
|
||||
errorHeading.innerText = category;
|
||||
errorDiv.classList.remove('d-none');
|
||||
}
|
||||
function displayFatalError(category, ex) {
|
||||
showApplication(false);
|
||||
showLoadingIndicator(false);
|
||||
displayError('#fatal-error', category, ex);
|
||||
}
|
||||
function displayNonFatalError(category, ex) {
|
||||
showApplication(true);
|
||||
showLoadingIndicator(false);
|
||||
displayError('#display-backup-error', category, ex);
|
||||
}
|
||||
function hideErrors() {
|
||||
document
|
||||
.querySelectorAll('#fatal-error, #display-backup-error')
|
||||
.forEach(el => el.classList.add('d-none'));
|
||||
}
|
||||
function importStylesheet(xsl) {
|
||||
//Firefox does not seem to report XML parsing errors into the
|
||||
//exception tree. So we catch here, log, and return some friendly
|
||||
//error.
|
||||
try {
|
||||
const xsltProcessor = new XSLTProcessor();
|
||||
xsltProcessor.importStylesheet(xsl);
|
||||
return xsltProcessor;
|
||||
}
|
||||
catch (cause) {
|
||||
console.error(cause);
|
||||
throw new Error('Stylesheet parsing failed', { cause });
|
||||
}
|
||||
}
|
||||
async function createXSLTProcessor() {
|
||||
const doc = new DOMParser().parseFromString(xsl, "text/xml");
|
||||
return Promise.resolve(importStylesheet(doc));
|
||||
// return fetch(new URL("./MessageLog.xsl", import.meta.url))
|
||||
// .then(resp => {
|
||||
// if (resp.ok)
|
||||
// return resp.text();
|
||||
// else
|
||||
// throw new Error(resp.status + ' ' + resp.statusText);
|
||||
// })
|
||||
// .then(str => new DOMParser().parseFromString(str, "text/xml"))
|
||||
// .then(importStylesheet)
|
||||
// .catch(cause => {
|
||||
// throw new Error('Could not load XSL stylesheet', { cause });
|
||||
// });
|
||||
}
|
||||
function removeChildren(element) {
|
||||
while (element.hasChildNodes()) {
|
||||
element.removeChild(element.lastChild);
|
||||
}
|
||||
}
|
||||
function checkOverflow(elem) {
|
||||
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));
|
||||
document.querySelectorAll('.message-content div').forEach(div => {
|
||||
if (checkOverflow(div)) {
|
||||
div.parentElement.classList.add('overflow-icon');
|
||||
}
|
||||
});
|
||||
}
|
||||
function displayBackup(xsltProcessor, file) {
|
||||
hideErrors();
|
||||
//document.getElementById('intro-card').classList.add('d-none');
|
||||
const chatDisplay = document.getElementById("chat-display");
|
||||
removeChildren(chatDisplay);
|
||||
file.text().then(xmlText => {
|
||||
document.getElementById('currently-viewing').innerText = file.name;
|
||||
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);
|
||||
chatDisplay.appendChild(fragment);
|
||||
processFragment();
|
||||
}
|
||||
catch (e) {
|
||||
displayNonFatalError('Could not load backup', e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const err = new Error(errorNode.firstChild.nodeValue);
|
||||
displayNonFatalError('Could not load backup', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
function initEvents(xsltProcessor) {
|
||||
const selectFileButtons = document.querySelectorAll("#backup-file-button, #backup-file-button-card");
|
||||
const fileSelector = document.getElementById("backup-file");
|
||||
selectFileButtons.forEach(button => button.addEventListener("click", e => {
|
||||
fileSelector.click();
|
||||
e.preventDefault();
|
||||
}));
|
||||
fileSelector.addEventListener("change", async (e) => {
|
||||
const target = e.currentTarget;
|
||||
const file = target.files[0];
|
||||
displayBackup(xsltProcessor, file);
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth <= 500) {
|
||||
document.querySelector('#chat-display table').classList.add('table-sm');
|
||||
}
|
||||
else {
|
||||
document.querySelector('#chat-display table').classList.remove('table-sm');
|
||||
}
|
||||
});
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
createXSLTProcessor()
|
||||
.then(initEvents)
|
||||
.then(_ => {
|
||||
showLoadingIndicator(false);
|
||||
showApplication(true);
|
||||
})
|
||||
.catch(ex => displayFatalError('Initialiation Failed', ex));
|
||||
});
|
|
@ -0,0 +1,171 @@
|
|||
import * as bootstrap from 'bootstrap';
|
||||
import xsl from 'bundle-text:./MessageLog.xsl';
|
||||
import * as ui from './ui';
|
||||
|
||||
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 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 importStylesheet(xsl: Node) {
|
||||
//Firefox does not seem to report XML parsing errors into the
|
||||
//exception tree. So we catch here, log, and return some friendly
|
||||
//error.
|
||||
try {
|
||||
const xsltProcessor = new XSLTProcessor();
|
||||
xsltProcessor.importStylesheet(xsl);
|
||||
return xsltProcessor;
|
||||
} catch (cause) {
|
||||
console.error(cause);
|
||||
throw new Error('Stylesheet parsing failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function createXSLTProcessor() {
|
||||
const doc = new DOMParser().parseFromString(xsl, "text/xml");
|
||||
return Promise.resolve(importStylesheet(doc));
|
||||
// return fetch(new URL("./MessageLog.xsl", import.meta.url))
|
||||
// .then(resp => {
|
||||
// if (resp.ok)
|
||||
// return resp.text();
|
||||
// else
|
||||
// throw new Error(resp.status + ' ' + resp.statusText);
|
||||
// })
|
||||
// .then(str => new DOMParser().parseFromString(str, "text/xml"))
|
||||
// .then(importStylesheet)
|
||||
// .catch(cause => {
|
||||
// throw new Error('Could not load XSL stylesheet', { cause });
|
||||
// });
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
document.querySelectorAll('.message-content div').forEach(div => {
|
||||
if (checkOverflow(div as HTMLElement)) {
|
||||
div.parentElement?.classList.add('overflow-icon');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayBackup(xsltProcessor: XSLTProcessor, file: File) {
|
||||
hideErrors();
|
||||
|
||||
removeChildren(ui.chatDisplay);
|
||||
|
||||
file.text().then(xmlText => {
|
||||
ui.currentlyViewing.innerText = file.name;
|
||||
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);
|
||||
processFragment();
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error) {
|
||||
displayNonFatalError('Could not load backup', e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const err = new Error(errorNode.firstChild?.nodeValue || '');
|
||||
displayNonFatalError('Could not load backup', err);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function initEvents(xsltProcessor: XSLTProcessor) {
|
||||
ui.fileSelector.addEventListener("change", async (e) => {
|
||||
const target = e.currentTarget as HTMLInputElement | null;
|
||||
const file = target?.files?.[0];
|
||||
if (file) {
|
||||
displayBackup(xsltProcessor, file);
|
||||
} else {
|
||||
displayNonFatalError('Could not load file', new Error('No file found.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
createXSLTProcessor()
|
||||
.then(initEvents)
|
||||
.then(_ => {
|
||||
showLoadingIndicator(false);
|
||||
showApplication(true);
|
||||
})
|
||||
.catch(ex => displayFatalError('Initialiation Failed', ex));
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
// Enable typescript completion for bundle-text inlining.
|
||||
declare module 'bundle-text:*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
table {
|
||||
/* table-layout: fixed; */
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"lib": [ "dom", "es6" ],
|
||||
"jsxImportSource": "preact",
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
//All of these can ignore maybe-null errors, as the script runs at the bottom of the page.
|
||||
//So DOM is guaranteed to exist.
|
||||
export const realBody: HTMLElement = document.getElementById('real-body')!;
|
||||
export const loadingIndicator: HTMLElement = document.getElementById('loading')!;
|
||||
export const fatalError: HTMLElement = document.getElementById('fatal-error')!;
|
||||
export const nonFatalError: HTMLElement = document.getElementById('display-backup-error')!;
|
||||
export const chatDisplay: HTMLElement = document.getElementById('chat-display')!;
|
||||
export const fileSelector: HTMLInputElement = document.getElementById('backup-file')! as HTMLInputElement;
|
||||
export const currentlyViewing: HTMLElement = document.getElementById('currently-viewing')!;
|
Loading…
Reference in New Issue