From dcdc8dd7040641e62844b12955aa32224ed1c4ce Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Wed, 23 Sep 2020 13:30:48 +0200 Subject: [PATCH] Convert frontend js to typescript --- assets/config.json | 2 +- assets/js/copyable_text.js | 12 ------ assets/js/main_menu.js | 17 -------- assets/js/tooltips-and-dropdowns.js | 30 -------------- assets/ts/PersistentWebSocket.ts | 39 +++++++++++++++++++ assets/{js/app.js => ts/app.ts} | 3 +- assets/ts/copyable_text.ts | 15 +++++++ .../external_links.ts} | 2 +- .../font-awesome.js => ts/font-awesome.ts} | 0 assets/{js/forms.js => ts/forms.ts} | 36 +++++++++-------- assets/ts/main_menu.ts | 21 ++++++++++ .../message_icons.js => ts/message_icons.ts} | 15 ++++--- assets/ts/tooltips-and-dropdowns.ts | 31 +++++++++++++++ package.json | 2 + tsconfig.frontend.json | 18 +++++++++ tsconfig.json | 3 +- webpack.config.js | 13 +++++++ 17 files changed, 174 insertions(+), 85 deletions(-) delete mode 100644 assets/js/copyable_text.js delete mode 100644 assets/js/main_menu.js delete mode 100644 assets/js/tooltips-and-dropdowns.js create mode 100644 assets/ts/PersistentWebSocket.ts rename assets/{js/app.js => ts/app.ts} (87%) create mode 100644 assets/ts/copyable_text.ts rename assets/{js/external_links.js => ts/external_links.ts} (98%) rename assets/{js/font-awesome.js => ts/font-awesome.ts} (100%) rename assets/{js/forms.js => ts/forms.ts} (52%) create mode 100644 assets/ts/main_menu.ts rename assets/{js/message_icons.js => ts/message_icons.ts} (60%) create mode 100644 assets/ts/tooltips-and-dropdowns.ts create mode 100644 tsconfig.frontend.json diff --git a/assets/config.json b/assets/config.json index 5c18915..6bcf54c 100644 --- a/assets/config.json +++ b/assets/config.json @@ -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", diff --git a/assets/js/copyable_text.js b/assets/js/copyable_text.js deleted file mode 100644 index 6501a1a..0000000 --- a/assets/js/copyable_text.js +++ /dev/null @@ -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'); - }); - }); -}); \ No newline at end of file diff --git a/assets/js/main_menu.js b/assets/js/main_menu.js deleted file mode 100644 index f2a89da..0000000 --- a/assets/js/main_menu.js +++ /dev/null @@ -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'); - }); -}); \ No newline at end of file diff --git a/assets/js/tooltips-and-dropdowns.js b/assets/js/tooltips-and-dropdowns.js deleted file mode 100644 index 3fa6e5b..0000000 --- a/assets/js/tooltips-and-dropdowns.js +++ /dev/null @@ -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(); - }); -}); \ No newline at end of file diff --git a/assets/ts/PersistentWebSocket.ts b/assets/ts/PersistentWebSocket.ts new file mode 100644 index 0000000..b53f948 --- /dev/null +++ b/assets/ts/PersistentWebSocket.ts @@ -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; diff --git a/assets/js/app.js b/assets/ts/app.ts similarity index 87% rename from assets/js/app.js rename to assets/ts/app.ts index 48eba69..8594552 100644 --- a/assets/js/app.js +++ b/assets/ts/app.ts @@ -6,6 +6,5 @@ import './tooltips-and-dropdowns'; import './main_menu'; import './font-awesome'; +// css import '../sass/app.scss'; - -console.log('Hello world!'); \ No newline at end of file diff --git a/assets/ts/copyable_text.ts b/assets/ts/copyable_text.ts new file mode 100644 index 0000000..60d58ab --- /dev/null +++ b/assets/ts/copyable_text.ts @@ -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'); + }); + } + }); +}); diff --git a/assets/js/external_links.js b/assets/ts/external_links.ts similarity index 98% rename from assets/js/external_links.js rename to assets/ts/external_links.ts index d72b537..bc9ba26 100644 --- a/assets/js/external_links.js +++ b/assets/ts/external_links.ts @@ -8,4 +8,4 @@ document.addEventListener('DOMContentLoaded', () => { }); feather.replace(); -}); \ No newline at end of file +}); diff --git a/assets/js/font-awesome.js b/assets/ts/font-awesome.ts similarity index 100% rename from assets/js/font-awesome.js rename to assets/ts/font-awesome.ts diff --git a/assets/js/forms.js b/assets/ts/forms.ts similarity index 52% rename from assets/js/forms.js rename to assets/ts/forms.ts index a65d2f9..46cd108 100644 --- a/assets/js/forms.js +++ b/assets/ts/forms.ts @@ -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('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 = ` ${messages[fieldName].message}`; } } -} \ No newline at end of file +} diff --git a/assets/ts/main_menu.ts b/assets/ts/main_menu.ts new file mode 100644 index 0000000..4e19e40 --- /dev/null +++ b/assets/ts/main_menu.ts @@ -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'); + }); + } +}); diff --git a/assets/js/message_icons.js b/assets/ts/message_icons.ts similarity index 60% rename from assets/js/message_icons.js rename to assets/ts/message_icons.ts index b625f55..b737de3 100644 --- a/assets/js/message_icons.js +++ b/assets/ts/message_icons.ts @@ -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('.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(); -}); \ No newline at end of file +}); diff --git a/assets/ts/tooltips-and-dropdowns.ts b/assets/ts/tooltips-and-dropdowns.ts new file mode 100644 index 0000000..e66c732 --- /dev/null +++ b/assets/ts/tooltips-and-dropdowns.ts @@ -0,0 +1,31 @@ +export function updateTooltips() { + console.debug('Updating 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.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(); + }); +}); diff --git a/package.json b/package.json index 500a667..3ffa1b4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tsconfig.frontend.json b/tsconfig.frontend.json new file mode 100644 index 0000000..4b5a3f5 --- /dev/null +++ b/tsconfig.frontend.json @@ -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/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 18f1bee..19d375e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "target": "ES6", "strict": true, "lib": [ - "es2020" + "es2020", + "DOM" ], "typeRoots": [ "./node_modules/@types" diff --git a/webpack.config.js b/webpack.config.js index 478541b..655dee0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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',