Browse Source

Convert frontend js to typescript

master
Alice Gaudon 5 months ago
parent
commit
dcdc8dd704
17 changed files with 174 additions and 85 deletions
  1. +1
    -1
      assets/config.json
  2. +0
    -12
      assets/js/copyable_text.js
  3. +0
    -17
      assets/js/main_menu.js
  4. +0
    -30
      assets/js/tooltips-and-dropdowns.js
  5. +39
    -0
      assets/ts/PersistentWebSocket.ts
  6. +1
    -2
      assets/ts/app.ts
  7. +15
    -0
      assets/ts/copyable_text.ts
  8. +1
    -1
      assets/ts/external_links.ts
  9. +0
    -0
      assets/ts/font-awesome.ts
  10. +20
    -16
      assets/ts/forms.ts
  11. +21
    -0
      assets/ts/main_menu.ts
  12. +10
    -5
      assets/ts/message_icons.ts
  13. +31
    -0
      assets/ts/tooltips-and-dropdowns.ts
  14. +2
    -0
      package.json
  15. +18
    -0
      tsconfig.frontend.json
  16. +2
    -1
      tsconfig.json
  17. +13
    -0
      webpack.config.js

+ 1
- 1
assets/config.json View File

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


+ 0
- 12
assets/js/copyable_text.js 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');
});
});
});

+ 0
- 17
assets/js/main_menu.js 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');
});
});

+ 0
- 30
assets/js/tooltips-and-dropdowns.js 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();
});
});

+ 39
- 0
assets/ts/PersistentWebSocket.ts 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;

assets/js/app.js → assets/ts/app.ts View File

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

+ 15
- 0
assets/ts/copyable_text.ts 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');
});
}
});
});

assets/js/external_links.js → assets/ts/external_links.ts View File

@ -8,4 +8,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
feather.replace();
});
});

assets/js/font-awesome.js → assets/ts/font-awesome.ts View File


assets/js/forms.js → assets/ts/forms.ts View File

@ -1,25 +1,29 @@
// For labels to update their state (css selectors based on the value attribute)
document.addEventListener('DOMContentLoaded', () => {
window.updateInputs = () => {
document.querySelectorAll('input, textarea').forEach(el => {
if (!el.inputSetup) {
el.inputSetup = true;
if (el.type !== 'checkbox') {
/*
* For labels to update their state (css selectors based on the value attribute)
*/
export function updateInputs() {
document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('input, textarea').forEach(el => {
if (!el.dataset.inputSetup) {
el.dataset.inputSetup = 'true';
if (el.type !== 'checkbox') {
el.setAttribute('value', el.value);
el.addEventListener('change', () => {
el.setAttribute('value', el.value);
el.addEventListener('change', () => {
el.setAttribute('value', el.value);
});
}
});
}
});
};
}
});
}
document.addEventListener('DOMContentLoaded', () => {
updateInputs();
});
window.applyFormMessages = function (formElement, messages) {
export function applyFormMessages(formElement: HTMLFormElement, messages: { [p: string]: any }) {
for (const fieldName of Object.keys(messages)) {
const field = formElement.querySelector('#field-' + fieldName);
if (!field) continue;
let parent = field.parentElement;
while (parent && !parent.classList.contains('form-field')) parent = parent.parentElement;
@ -28,9 +32,9 @@ window.applyFormMessages = function (formElement, messages) {
if (!err) {
err = document.createElement('div');
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}`;
}
}
}
}

+ 21
- 0
assets/ts/main_menu.ts 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');
});
}
});

assets/js/message_icons.js → assets/ts/message_icons.ts View File

@ -1,21 +1,26 @@
import feather from "feather-icons";
document.addEventListener('DOMContentLoaded', () => {
const messageTypeToIcon = {
const messageTypeToIcon: { [p: string]: string } = {
info: 'info',
success: 'check',
warning: 'alert-triangle',
error: 'x-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 type = el.dataset['type'];
if (!icon || !type) return;
if (!messageTypeToIcon[type]) throw new Error(`No icon for type ${type}`);
const svgContainer = document.createElement('div');
svgContainer.innerHTML = feather.icons[messageTypeToIcon[type]].toSvg();
el.insertBefore(svgContainer.firstChild, icon);
if (svgContainer.firstChild) el.insertBefore(svgContainer.firstChild, icon);
icon.remove();
});
feather.replace();
});
});

+ 31
- 0
assets/ts/tooltips-and-dropdowns.ts 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();
});
});

+ 2
- 0
package.json View File

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


+ 18
- 0
tsconfig.frontend.json 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/**/*"
]
}

+ 2
- 1
tsconfig.json View File

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


+ 13
- 0
webpack.config.js View File

@ -48,6 +48,16 @@ const config = {
test: /\.(woff2?|eot|ttf|otf)$/i,
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,
use: [
@ -68,6 +78,9 @@ const config = {
}
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
new MiniCssExtractPlugin({
filename: '../css/[name].css',


Loading…
Cancel
Save