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() {
|
async function createXSLTProcessor() {
|
||||||
return fetch("/MessageLog.xsl")
|
return fetch(new URL("/MessageLog.xsl"))
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.ok)
|
if (resp.ok)
|
||||||
return resp.text();
|
return resp.text();
|
||||||
else
|
else
|
||||||
throw new Error(resp.status + ' ' + resp.statusText);
|
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)
|
.then(importStylesheet)
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
throw new Error('Could not load XSL stylesheet', { 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>
|
<title>🔥 MSN Viewer</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link rel="stylesheet" href="./scss/index.scss" />
|
||||||
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>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -27,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="real-body" class="d-none">
|
<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">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">
|
<a class="navbar-brand" href="#">
|
||||||
🔥 MSN Viewer
|
🔥 MSN Viewer
|
||||||
|
@ -159,10 +155,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" src="index.ts"></script>
|
||||||
<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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 {
|
||||||
/* table-layout: fixed; */
|
/* 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