Convert frontend js to typescript

This commit is contained in:
Alice Gaudon 2020-09-23 13:30:48 +02:00
parent 5f2215f491
commit dcdc8dd704
17 changed files with 174 additions and 85 deletions

View File

@ -1,6 +1,6 @@
{ {
"bundles": { "bundles": {
"app": "js/app.js", "app": "ts/app.ts",
"layout": "sass/layout.scss", "layout": "sass/layout.scss",
"error": "sass/error.scss", "error": "sass/error.scss",
"logo": "img/logo.svg", "logo": "img/logo.svg",

View File

@ -1,12 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.copyable-text').forEach(el => {
const contentEl = el.querySelector('.content');
contentEl.addEventListener('click', () => {
window.getSelection().selectAllChildren(contentEl);
});
el.querySelector('.copy-button').addEventListener('click', () => {
window.getSelection().selectAllChildren(contentEl);
document.execCommand('copy');
});
});
});

View File

@ -1,17 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const menuButton = document.getElementById('menu-button');
const mainMenu = document.getElementById('main-menu');
menuButton.addEventListener('click', (e) => {
e.stopPropagation();
mainMenu.classList.toggle('open');
});
mainMenu.addEventListener('click', (e) => {
e.stopPropagation();
});
document.addEventListener('click', () => {
mainMenu.classList.remove('open');
});
});

View File

@ -1,30 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
window.updateTooltips = () => {
console.debug('Update tooltips');
const elements = document.querySelectorAll('.tip, .dropdown');
// Calculate max potential displacement
let max = 0;
elements.forEach(el => {
const box = el.getBoundingClientRect();
if (max < box.height) max = box.height;
});
// Prevent displacement
elements.forEach(el => {
if (!el.tooltipSetup) {
el.tooltipSetup = true;
const box = el.getBoundingClientRect();
if (box.bottom >= document.body.clientHeight - (max + 32)) {
el.classList.add('top');
}
}
});
};
window.addEventListener('popstate', () => {
updateTooltips();
});
window.requestAnimationFrame(() => {
updateTooltips();
});
});

View File

@ -0,0 +1,39 @@
export default class PersistentWebsocket {
private webSocket?: WebSocket;
public constructor(
protected readonly url: string,
private readonly handler: MessageHandler,
protected readonly reconnectOnClose: boolean = true,
) {
}
public run() {
this.webSocket = new WebSocket(this.url);
this.webSocket.addEventListener('open', (e) => {
console.debug('Websocket connected');
});
this.webSocket.addEventListener('message', (e) => {
this.handler(this.webSocket!, e);
});
this.webSocket.addEventListener('error', (e) => {
console.error('Websocket error', e);
});
this.webSocket.addEventListener('close', (e) => {
this.webSocket = undefined;
console.debug('Websocket closed', e.code, e.reason);
if (this.reconnectOnClose) {
setTimeout(() => this.run(), 1000);
}
});
}
public send(data: string) {
if (!this.webSocket) throw new Error('WebSocket not connected');
this.webSocket.send(data);
}
}
export type MessageHandler = (webSocket: WebSocket, e: MessageEvent) => void;

View File

@ -6,6 +6,5 @@ import './tooltips-and-dropdowns';
import './main_menu'; import './main_menu';
import './font-awesome'; import './font-awesome';
// css
import '../sass/app.scss'; import '../sass/app.scss';
console.log('Hello world!');

View File

@ -0,0 +1,15 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.copyable-text').forEach(el => {
const contentEl = el.querySelector('.content');
const selection = window.getSelection();
if (contentEl && selection) {
contentEl.addEventListener('click', () => {
selection.selectAllChildren(contentEl);
});
el.querySelector('.copy-button')?.addEventListener('click', () => {
selection.selectAllChildren(contentEl);
document.execCommand('copy');
});
}
});
});

View File

@ -1,9 +1,10 @@
// For labels to update their state (css selectors based on the value attribute) /*
document.addEventListener('DOMContentLoaded', () => { * For labels to update their state (css selectors based on the value attribute)
window.updateInputs = () => { */
document.querySelectorAll('input, textarea').forEach(el => { export function updateInputs() {
if (!el.inputSetup) { document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('input, textarea').forEach(el => {
el.inputSetup = true; if (!el.dataset.inputSetup) {
el.dataset.inputSetup = 'true';
if (el.type !== 'checkbox') { if (el.type !== 'checkbox') {
el.setAttribute('value', el.value); el.setAttribute('value', el.value);
el.addEventListener('change', () => { el.addEventListener('change', () => {
@ -12,14 +13,17 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
}); });
}; }
document.addEventListener('DOMContentLoaded', () => {
updateInputs(); updateInputs();
}); });
window.applyFormMessages = function (formElement, messages) { export function applyFormMessages(formElement: HTMLFormElement, messages: { [p: string]: any }) {
for (const fieldName of Object.keys(messages)) { for (const fieldName of Object.keys(messages)) {
const field = formElement.querySelector('#field-' + fieldName); const field = formElement.querySelector('#field-' + fieldName);
if (!field) continue;
let parent = field.parentElement; let parent = field.parentElement;
while (parent && !parent.classList.contains('form-field')) parent = parent.parentElement; while (parent && !parent.classList.contains('form-field')) parent = parent.parentElement;
@ -28,7 +32,7 @@ window.applyFormMessages = function (formElement, messages) {
if (!err) { if (!err) {
err = document.createElement('div'); err = document.createElement('div');
err.classList.add('error'); err.classList.add('error');
parent.insertBefore(err, parent.querySelector('.hint') || parent); parent?.insertBefore(err, parent.querySelector('.hint') || parent);
} }
err.innerHTML = `<i data-feather="x-circle"></i> ${messages[fieldName].message}`; err.innerHTML = `<i data-feather="x-circle"></i> ${messages[fieldName].message}`;
} }

21
assets/ts/main_menu.ts Normal file
View File

@ -0,0 +1,21 @@
document.addEventListener('DOMContentLoaded', () => {
const menuButton = document.getElementById('menu-button');
const mainMenu = document.getElementById('main-menu');
if (menuButton) {
menuButton.addEventListener('click', (e) => {
e.stopPropagation();
mainMenu?.classList.toggle('open');
});
}
if (mainMenu) {
mainMenu.addEventListener('click', (e) => {
e.stopPropagation();
});
document.addEventListener('click', () => {
mainMenu.classList.remove('open');
});
}
});

View File

@ -1,19 +1,24 @@
import feather from "feather-icons"; import feather from "feather-icons";
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const messageTypeToIcon = { const messageTypeToIcon: { [p: string]: string } = {
info: 'info', info: 'info',
success: 'check', success: 'check',
warning: 'alert-triangle', warning: 'alert-triangle',
error: 'x-circle', error: 'x-circle',
question: 'help-circle', question: 'help-circle',
}; };
document.querySelectorAll('.message').forEach(el => {
const type = el.dataset['type']; document.querySelectorAll<HTMLElement>('.message').forEach(el => {
const icon = el.querySelector('.icon'); const icon = el.querySelector('.icon');
const type = el.dataset['type'];
if (!icon || !type) return;
if (!messageTypeToIcon[type]) throw new Error(`No icon for type ${type}`);
const svgContainer = document.createElement('div'); const svgContainer = document.createElement('div');
svgContainer.innerHTML = feather.icons[messageTypeToIcon[type]].toSvg(); svgContainer.innerHTML = feather.icons[messageTypeToIcon[type]].toSvg();
el.insertBefore(svgContainer.firstChild, icon);
if (svgContainer.firstChild) el.insertBefore(svgContainer.firstChild, icon);
icon.remove(); icon.remove();
}); });

View File

@ -0,0 +1,31 @@
export function updateTooltips() {
console.debug('Updating tooltips');
const elements = document.querySelectorAll<HTMLElement>('.tip, .dropdown');
// Calculate max potential displacement
let max = 0;
elements.forEach(el => {
const box = el.getBoundingClientRect();
if (max < box.height) max = box.height;
});
// Prevent displacement
elements.forEach(el => {
if (!el.dataset.tooltipSetup) {
el.dataset.tooltipSetup = 'true';
const box = el.getBoundingClientRect();
if (box.bottom >= document.body.clientHeight - (max + 32)) {
el.classList.add('top');
}
}
});
}
document.addEventListener('DOMContentLoaded', () => {
window.addEventListener('popstate', () => {
updateTooltips();
});
window.requestAnimationFrame(() => {
updateTooltips();
});
});

View File

@ -20,6 +20,7 @@
"@types/config": "^0.0.36", "@types/config": "^0.0.36",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/express-session": "^1.17.0", "@types/express-session": "^1.17.0",
"@types/feather-icons": "^4.7.0",
"@types/formidable": "^1.0.31", "@types/formidable": "^1.0.31",
"@types/jest": "^26.0.4", "@types/jest": "^26.0.4",
"@types/mysql": "^2.15.15", "@types/mysql": "^2.15.15",
@ -44,6 +45,7 @@
"nodemon": "^2.0.3", "nodemon": "^2.0.3",
"sass-loader": "^10.0.1", "sass-loader": "^10.0.1",
"ts-jest": "^26.1.1", "ts-jest": "^26.1.1",
"ts-loader": "^8.0.4",
"typescript": "^4.0.2", "typescript": "^4.0.2",
"uglifyjs-webpack-plugin": "^2.2.0", "uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.43.0", "webpack": "^4.43.0",

18
tsconfig.frontend.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "public/js",
"target": "ES6",
"strict": true,
"lib": [
"es2020",
"DOM"
],
"typeRoots": [
"./node_modules/@types"
]
},
"include": [
"assets/ts/**/*"
]
}

View File

@ -6,7 +6,8 @@
"target": "ES6", "target": "ES6",
"strict": true, "strict": true,
"lib": [ "lib": [
"es2020" "es2020",
"DOM"
], ],
"typeRoots": [ "typeRoots": [
"./node_modules/@types" "./node_modules/@types"

View File

@ -48,6 +48,16 @@ const config = {
test: /\.(woff2?|eot|ttf|otf)$/i, test: /\.(woff2?|eot|ttf|otf)$/i,
use: 'file-loader?name=../fonts/[name].[ext]', use: 'file-loader?name=../fonts/[name].[ext]',
}, },
{
test: /\.tsx?$/i,
use: {
loader: 'ts-loader',
options: {
configFile: 'tsconfig.frontend.json',
}
},
exclude: '/node_modules/'
},
{ {
test: /\.(png|jpe?g|gif|svg)$/i, test: /\.(png|jpe?g|gif|svg)$/i,
use: [ use: [
@ -68,6 +78,9 @@ const config = {
} }
], ],
}, },
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [ plugins: [
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: '../css/[name].css', filename: '../css/[name].css',