Convert frontend js to typescript
This commit is contained in:
parent
5f2215f491
commit
dcdc8dd704
@ -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",
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
39
assets/ts/PersistentWebSocket.ts
Normal file
39
assets/ts/PersistentWebSocket.ts
Normal 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;
|
@ -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!');
|
|
15
assets/ts/copyable_text.ts
Normal file
15
assets/ts/copyable_text.ts
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -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
21
assets/ts/main_menu.ts
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
31
assets/ts/tooltips-and-dropdowns.ts
Normal file
31
assets/ts/tooltips-and-dropdowns.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
@ -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
18
tsconfig.frontend.json
Normal 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/**/*"
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user