From 400771f06c9f56e0c1936c0f2f2695bb7ddfb5da Mon Sep 17 00:00:00 2001 From: Niels Date: Fri, 22 Nov 2024 20:26:12 +0100 Subject: [PATCH 1/3] Added fileserver to host static files --- internal/javascript/iFrame.js | 2 +- setup/PlutoBoardNotebook.jl | 26 ++- setup/static/index.html | 8 +- .../javascript/{javascript.js => main.js} | 7 +- src/LoadHTML.jl | 148 ++++----------- src/PlutoBoard.jl | 3 + src/fileserver/FileServer.jl | 89 +++++++++ src/static/javascript/iFrame.js | 173 ++++++++++++++++++ src/static/javascript/interface.js | 71 +++++++ src/static/javascript/internal.js | 19 ++ src/static/javascript/loadHTML.js | 16 ++ src/static/javascript/main.js | 28 +++ src/static/javascript/settings.js | 126 +++++++++++++ src/static/javascript/toasts.js | 48 +++++ 14 files changed, 638 insertions(+), 126 deletions(-) rename setup/static/javascript/{javascript.js => main.js} (77%) create mode 100644 src/fileserver/FileServer.jl create mode 100644 src/static/javascript/iFrame.js create mode 100644 src/static/javascript/interface.js create mode 100644 src/static/javascript/internal.js create mode 100644 src/static/javascript/loadHTML.js create mode 100644 src/static/javascript/main.js create mode 100644 src/static/javascript/settings.js create mode 100644 src/static/javascript/toasts.js diff --git a/internal/javascript/iFrame.js b/internal/javascript/iFrame.js index 874adc2..e78bc6e 100644 --- a/internal/javascript/iFrame.js +++ b/internal/javascript/iFrame.js @@ -148,7 +148,7 @@ function placeIframe(targetCellID, destinationDiv) { } -function placeAlliFrames() { +export function placeAlliFrames() { //get all divs with class cell-div let cellDivs = document.querySelectorAll('.cell-div'); cellDivs.forEach(cellDiv => { diff --git a/setup/PlutoBoardNotebook.jl b/setup/PlutoBoardNotebook.jl index 38924d0..e4e6fb0 100644 --- a/setup/PlutoBoardNotebook.jl +++ b/setup/PlutoBoardNotebook.jl @@ -19,6 +19,9 @@ using PlutoBoard # ╔═╡ 96ff4362-fda0-4cae-9786-2dc29626479c using HTTP.WebSockets +# ╔═╡ fccb684b-4672-4822-a592-bb4df869c7b6 + + # ╔═╡ 64f17c2b-5f54-4df5-8d7d-d57f3b314b5b begin project_toml_path = joinpath(@__DIR__, "Project.toml") @@ -33,7 +36,7 @@ end PlutoBoard.load_scripts_and_links() # ╔═╡ 191eb887-6681-4f75-8192-240ca3fc5da2 -PlutoBoard.load_html_string_to_body() + # ╔═╡ 7d9362b1-c508-4cad-add2-4f62a6ad8409 PlutoBoard.load_js() @@ -78,6 +81,15 @@ function handle_julia_function_call(ws, parsed) end +# ╔═╡ 231a2a26-b3ec-4ad0-8335-db43a53f1a86 +begin + if PlutoBoard.global_fileserver !== nothing + schedule(PlutoBoard.global_fileserver, InterruptException(), error = true) + end + + PlutoBoard.global_fileserver = @async PlutoBoard.start_server(8085) +end + # ╔═╡ 147ed5fe-0133-4eef-96f2-afafe9385f27 begin if user_package.global_websocket !== nothing @@ -96,13 +108,14 @@ begin end end -# ╔═╡ ffa65a89-13b7-41b1-b1d5-95605c5ae39d -x = 1 +# ╔═╡ 2eac3d05-ae01-40c8-abfa-d55349f043f3 +x=1 -# ╔═╡ bbcda6f9-7ddf-4e5c-a43a-3804a9e51866 +# ╔═╡ 2e287869-a591-45ba-ac12-28c24fd9059a x # ╔═╡ Cell order: +# ╠═fccb684b-4672-4822-a592-bb4df869c7b6 # ╠═caff9170-f1e7-11ee-3e0a-7bed8d1d0dd4 # ╠═306bfe84-b28b-4f52-a427-ba6950ddead4 # ╠═0e8625f9-e2cb-4660-b355-cc62d35b252d @@ -113,6 +126,7 @@ x # ╠═191eb887-6681-4f75-8192-240ca3fc5da2 # ╠═7d9362b1-c508-4cad-add2-4f62a6ad8409 # ╠═a12112c1-58e7-473b-a8c2-d825d0f416d9 +# ╠═231a2a26-b3ec-4ad0-8335-db43a53f1a86 # ╠═147ed5fe-0133-4eef-96f2-afafe9385f27 -# ╠═ffa65a89-13b7-41b1-b1d5-95605c5ae39d -# ╠═bbcda6f9-7ddf-4e5c-a43a-3804a9e51866 +# ╠═2eac3d05-ae01-40c8-abfa-d55349f043f3 +# ╠═2e287869-a591-45ba-ac12-28c24fd9059a diff --git a/setup/static/index.html b/setup/static/index.html index 53c2fc9..301d6f9 100644 --- a/setup/static/index.html +++ b/setup/static/index.html @@ -70,7 +70,7 @@

+ onclick="addCell()">Add @@ -103,11 +103,11 @@

You can place Pluto cells wherever you want on the screen! Here they are just in a simple grid.

-
+
-
+
@@ -132,7 +132,7 @@
type="text" id="buttonInput" placeholder="Input number"> + id="calculateButton">Calculate
Output:
diff --git a/setup/static/javascript/javascript.js b/setup/static/javascript/main.js similarity index 77% rename from setup/static/javascript/javascript.js rename to setup/static/javascript/main.js index dc4dcfc..50ab163 100644 --- a/setup/static/javascript/javascript.js +++ b/setup/static/javascript/main.js @@ -1,5 +1,4 @@ -console.log("Hello from PlutoBoard.jl") - +import { callJuliaFunction } from "/internal/static/javascript/interface.js"; function calculateVeryHardStuff() { const input = document.getElementById("buttonInput"); @@ -20,4 +19,6 @@ function calculateVeryHardStuff() { outputP.innerHTML = `Cube of ${number} is ${r}`; } ) -} \ No newline at end of file +} + +document.getElementById("calculateButton").addEventListener("click", calculateVeryHardStuff); \ No newline at end of file diff --git a/src/LoadHTML.jl b/src/LoadHTML.jl index c087dc7..9132e14 100644 --- a/src/LoadHTML.jl +++ b/src/LoadHTML.jl @@ -50,66 +50,10 @@ end Returns a HypertextLiteral.Result object to call a javascript function to insert the html and css to the body. """ -function load_html_string_to_body() - #get filepath where this module is loaded - plutoboard_filepath = dirname(@__FILE__) - internal_css_path = joinpath(plutoboard_filepath, "static/css/internal.css") - always_load_css_path = joinpath(plutoboard_filepath, "static/css/alwaysLoad.css") - - - html_string = css_string = internal_css = always_css = vue_css = "" +function get_html() open(html_path) do file - html_string = read(file, String) - end - open(css_path) do file - css_string = read(file, String) - end - open(internal_css_path) do file - internal_css = read(file, String) - end - open(always_load_css_path) do file - always_css = read(file, String) - end - try - open("static/vue.css") do file - vue_css = read(file, String) - end - catch - end - - - if hide_notebook == true - css_string = """""" - else - css_string = """""" + return read(file, String) end - - - - return @htl(""" - - - - """) end """ @@ -118,70 +62,50 @@ end Returns a HypertextLiteral.Result object to load all javascript files. """ function load_js()::HypertextLiteral.Result - paths = [] - for path in ["""$(plutoboard_filepath)/$(config["paths"]["javascript_dir"])/""", "static/javascript/"] - for file in readdir(path) - if file == "Init.js" - continue - end - push!(paths, path * file) - - end - end - - println(paths) - - #add init.js to paths - #push!(paths, "MINDFulPluto/src/static/scripts/Init.js") - - - scripts = [] - for path in paths - push!(scripts, open_file(path)) - end - - return @htl(""" diff --git a/src/PlutoBoard.jl b/src/PlutoBoard.jl index a4af9f4..7a6fb28 100644 --- a/src/PlutoBoard.jl +++ b/src/PlutoBoard.jl @@ -13,6 +13,7 @@ include("Utilities.jl") include("JSCommands/HandleCommands.jl") include("../internal/javascript/Dummy.jl") include("EditCells.jl") +include("fileserver/FileServer.jl") const plutoboard_filepath = dirname(dirname(pathof(PlutoBoard))) const config = TOML.parsefile(plutoboard_filepath * "/config/config.toml") @@ -26,6 +27,8 @@ hide_notebook::Bool = true scripts_urls::Array{String} = [] stylesheet_urls::Array{String} = [] +global_fileserver = nothing + end export PlutoBoard diff --git a/src/fileserver/FileServer.jl b/src/fileserver/FileServer.jl new file mode 100644 index 0000000..7a48a4d --- /dev/null +++ b/src/fileserver/FileServer.jl @@ -0,0 +1,89 @@ +SERVE_DIR = joinpath(pwd(), "static") + +# Utility function to handle file serving +function serve_file(req::HTTP.Request) + # Extract the requested path + base_path = String(HTTP.unescapeuri(req.target))[2:end] + if split(base_path, "/")[1] == "internal" + base_path = joinpath(@__DIR__, "..", split(base_path, "/")[2:end]...) + end + + requested_path = joinpath(SERVE_DIR, base_path) + + @info "Requested file: $requested_path" + + headers = Dict( + "Access-Control-Allow-Origin" => "http://localhost:1234", + "Access-Control-Allow-Methods" => "GET, POST, PUT, DELETE", + "Access-Control-Allow-Headers" => "Content-Type", + "Content-Type" => get_mime_type(requested_path), + ) + + if isfile(requested_path) + content = read(requested_path) # Read file content + return HTTP.Response(200, headers, content) + elseif isdir(requested_path) + dir_contents = join("\n", readdir(requested_path)) + return HTTP.Response(200, headers, dir_contents) + else + return HTTP.Response(404, headers, "File not found") + end +end + +# Start the file server +function start_server(port::Int = 8080) + @info "Starting server on port $port to server $SERVE_DIR" + HTTP.serve("127.0.0.1", port) do req + # try + # serve_file(req) + # catch e + # @error "Error serving request: $e" + # return HTTP.Response(500, "Internal Server Error") + # end + serve_file(req) + end +end + +function get_mime_type(file::String) + if endswith(file, ".js") + return "application/javascript" + elseif endswith(file, ".css") + return "text/css" + elseif endswith(file, ".html") + return "text/html" + elseif endswith(file, ".json") + return "application/json" + elseif endswith(file, ".png") + return "image/png" + elseif endswith(file, ".jpg") || endswith(file, ".jpeg") + return "image/jpeg" + elseif endswith(file, ".gif") + return "image/gif" + elseif endswith(file, ".svg") + return "image/svg+xml" + elseif endswith(file, ".txt") + return "text/plain" + elseif endswith(file, ".xml") + return "application/xml" + elseif endswith(file, ".pdf") + return "application/pdf" + elseif endswith(file, ".woff") + return "font/woff" + elseif endswith(file, ".woff2") + return "font/woff2" + elseif endswith(file, ".otf") + return "font/otf" + elseif endswith(file, ".eot") + return "application/vnd.ms-fontobject" + elseif endswith(file, ".mp4") + return "video/mp4" + elseif endswith(file, ".webm") + return "video/webm" + elseif endswith(file, ".mp3") + return "audio/mp3" + elseif endswith(file, ".wav") + return "audio/wav" + else + return "application/octet-stream" + end +end diff --git a/src/static/javascript/iFrame.js b/src/static/javascript/iFrame.js new file mode 100644 index 0000000..abf7196 --- /dev/null +++ b/src/static/javascript/iFrame.js @@ -0,0 +1,173 @@ +export function placeIframe(targetCellID, destinationDiv) { + const iFrameID = `cell-iframe-${targetCellID}`; + const plutoBoardExportDivID = 'main-export'; + + let listener = setInterval(function () { + if (destinationDiv !== null) { + clearInterval(listener); + + + let notebook = document.querySelector('pluto-notebook'); + let notebookID = notebook.id; + + let div = destinationDiv; + //remove all iFrame children of div + div.querySelectorAll('iframe').forEach(iframe => { + iframe.remove(); + }); + + let iframe = document.createElement('iframe'); + iframe.id = iFrameID; + iframe.src = `http://localhost:1234/edit?id=${notebookID}`; + + if (window.location === window.parent.location) { + div.appendChild(iframe); + + + //wait until iframe is loaded + let interval = setInterval(function () { + if (document.querySelector(`#${iFrameID}`).contentDocument) { + clearInterval(interval); + console.log('iframe loaded'); + + let interval2 = setInterval(function () { + if (document.querySelector(`#${iFrameID}`).contentDocument.querySelector(`#${plutoBoardExportDivID}`)) { + clearInterval(interval2); + console.log('main-export loaded'); + + //find all style tags in iframe and remove them + let iframeDoc = document.querySelector(`#${iFrameID}`).contentDocument; + let styles = iframeDoc.querySelectorAll('style'); + //remove all styles without class attribute + styles.forEach(style => { + // if (!style.hasAttribute('class')) { + // style.remove(); + // } + //if stlye.innerHTML includes 'PlutoBoard.jl internal stylesheet' remove it + if (style.innerHTML.includes('PlutoBoard.jl internal stylesheet')) { + style.remove(); + } + + }); + + //get all pluto-cell elements and hide them + let cells = iframeDoc.querySelectorAll('pluto-cell'); + cells.forEach(cell => { + //check if id is equal to targetCellID + if (cell.id !== targetCellID) { + cell.style.display = 'none'; + } else { + cell.style.margin = '0.8vw'; + cell.style.padding = '2vw'; + } + }); + + //set body background to transparent + let body = iframeDoc.querySelector('body'); + body.style.backgroundColor = 'transparent'; + + //hide header + let header = iframeDoc.querySelector('header'); + header.style.display = 'none'; + + //hie footer + let footer = iframeDoc.querySelector('footer'); + footer.style.display = 'none'; + + //hide #main-export + let mainExport = iframeDoc.querySelector(`#${plutoBoardExportDivID}`); + mainExport.style.display = 'none'; + + //hide .not-iframe + let notIframe = iframeDoc.querySelectorAll('.not-iframe'); + notIframe.forEach(element => { + element.style.display = 'none'; + }); + + //hide #helpbox-wrapper + let helpbox = iframeDoc.querySelector('#helpbox-wrapper'); + helpbox.style.display = 'none'; + + //hide every child of body except body>div>pluto-editor>main>pluto-notebook + + //set main margin & padding to 0 + let main = iframeDoc.querySelector('main'); + main.style.margin = '0'; + main.style.padding = '0'; + main.style.display = 'contents'; + + //hide preamble + let preamble = iframeDoc.querySelector('preamble'); + preamble.style.display = 'none'; + + //hide all add_cell buttons + let addCellButtons = iframeDoc.querySelectorAll('.add_cell'); + addCellButtons.forEach(button => { + button.style.display = 'none'; + }); + + //hide .cm-gutters + let cmGutters = iframeDoc.querySelectorAll('.cm-gutters'); + cmGutters.forEach(gutter => { + gutter.style.display = 'none'; + }); + + //hide all pluto-shoulders + let shoulders = iframeDoc.querySelectorAll('pluto-shoulder'); + shoulders.forEach(shoulder => { + shoulder.style.display = 'none'; + }); + + //get pluto-editor parent + let editor = iframeDoc.querySelector('pluto-editor'); + let editorParent = editor.parentElement; + editorParent.style.minHeight = '0'; + + //set body min height to 0 + body.style.minHeight = '0'; + + //hide loading-bar + let loadingBar = iframeDoc.querySelector('loading-bar'); + loadingBar.style.display = 'none'; + + + //pluto-notebook border radius + let notebook = iframeDoc.querySelector('pluto-notebook'); + notebook.style.borderRadius = '4vw'; + notebook.style.width = '100vw'; + notebook.style.height = '100vh'; + + //hide pluto-trafficlight + let trafficLight = iframeDoc.querySelector('pluto-trafficlight'); + trafficLight.style.display = 'none'; + + } + }, 100); + } + }, 100); + } + } + }, 100); +} + + +export function placeAlliFrames() { + //get all divs with class cell-div + let cellDivs = document.querySelectorAll('.cell-div'); + cellDivs.forEach(cellDiv => { + let cellID = cellDiv.getAttribute('cellid'); + if (cellID === null) { + return; + } + placeIframe(cellID, cellDiv); + }); +} + +export function setIFrames() { + const mainExportListener = setInterval(function () { + if ((document.querySelector('#app') !== undefined) || (document.querySelector("#main-export") !== undefined)) { + clearInterval(mainExportListener); + placeAlliFrames(); + } + }, 100); +} \ No newline at end of file diff --git a/src/static/javascript/interface.js b/src/static/javascript/interface.js new file mode 100644 index 0000000..298661a --- /dev/null +++ b/src/static/javascript/interface.js @@ -0,0 +1,71 @@ +// DO NOT LOAD IN IFRAME! + +function setupWebsocket() { + let socket; + while (socket === undefined) { + console.log('Waiting for WebSocket to be defined...'); + new Promise(resolve => setTimeout(resolve, 100)); + socket = new WebSocket('ws://localhost:8080'); + } + + socket.addEventListener('open', function (event) { + console.log('WebSocket is open now.'); + }); + + socket.addEventListener('close', function (event) { + console.log('WebSocket is closed now.'); + }); + + socket.addEventListener('error', function (event) { + console.error('WebSocket error observed:', event); + }); + return socket; +} + +function waitForOpenConnection(socket) { + return new Promise((resolve, reject) => { + if (socket.readyState === WebSocket.OPEN) { + resolve(); + } else { + socket.addEventListener('open', () => { + resolve(); + }); + + socket.addEventListener('error', (err) => { + reject(new Error('WebSocket connection failed: ' + err.message)); + }); + } + }); +} + +export async function callJuliaFunction(func_name, { args = [], kwargs = {}, response_callback = () => { }, internal = false } = {}) { + const socket = setupWebsocket(); + await waitForOpenConnection(socket); + + console.log('Calling Julia function:', func_name, 'with args:', args, 'and kwargs:', kwargs, 'and response callback:', response_callback); + + const cmd = { + "type": "julia_function_call", + "function": func_name, + "args": args, + "kwargs": kwargs, + "internal": internal + } + socket.send(JSON.stringify(cmd)); + + return new Promise((resolve, reject) => { + socket.addEventListener('message', (event) => { + + + if (JSON.parse(event.data).type === 'return') { + socket.close(); + resolve(JSON.parse(event.data).return); + } + else if (JSON.parse(event.data).type === 'response') { + response_callback(JSON.parse(event.data).response); + } + }); + }); +} + + diff --git a/src/static/javascript/internal.js b/src/static/javascript/internal.js new file mode 100644 index 0000000..d1b5504 --- /dev/null +++ b/src/static/javascript/internal.js @@ -0,0 +1,19 @@ +export async function updateAllCells() { + const cells = document.querySelectorAll("pluto-cell"); + await cells[0]._internal_pluto_actions.set_and_run_multiple(Array.from(cells).map(cell => cell.id)); + window.location.reload(); +} + +async function updateCell(cellID) { + const cell = document.getElementById(cellID); + await cell._internal_pluto_actions.set_and_run_multiple([cellID]); +} + +export function resizePlutoNav() { + let element = document.getElementById("pluto-nav").parentElement.parentElement; + element.style.minHeight = "0"; +} + + + + diff --git a/src/static/javascript/loadHTML.js b/src/static/javascript/loadHTML.js new file mode 100644 index 0000000..6e86704 --- /dev/null +++ b/src/static/javascript/loadHTML.js @@ -0,0 +1,16 @@ +import { callJuliaFunction } from './interface.js'; + +export async function insertHTMLToBody() { + const body = document.querySelector('body'); + + const htmlString = await callJuliaFunction("get_html", { + internal: true, + }); + + if (!document.getElementById('main-export')) { + body.insertAdjacentHTML('afterbegin', htmlString); + } else { + document.getElementById('main-export').remove(); + body.insertAdjacentHTML('afterbegin', htmlString); + } +} \ No newline at end of file diff --git a/src/static/javascript/main.js b/src/static/javascript/main.js new file mode 100644 index 0000000..bc183f6 --- /dev/null +++ b/src/static/javascript/main.js @@ -0,0 +1,28 @@ +import { insertHTMLToBody } from './loadHTML.js'; +import { addModalButtonListener, updateCellIDsTable } from './settings.js'; +import { resizePlutoNav } from './internal.js'; +import { addCell, removeCell, findCell } from './settings.js'; +import { updateAllCells } from './internal.js'; +import { setIFrames } from './iFrame.js'; + +// --------------------------------------------------- LOAD HTML --------------------------------------------------- +await insertHTMLToBody(); + +resizePlutoNav(); +setIFrames(); + +// --------------------------------------------------- SETTINGS --------------------------------------------------- +addModalButtonListener(); +updateCellIDsTable(); + +window.addCell = addCell; +window.removeCell = removeCell; +window.findCell = findCell; + +// --------------------------------------------------- RESIZING --------------------------------------------------- + + +// --------------------------------------------------- GENERAL --------------------------------------------------- + +//add updateAllCells function to the window object +window.updateAllCells = updateAllCells; \ No newline at end of file diff --git a/src/static/javascript/settings.js b/src/static/javascript/settings.js new file mode 100644 index 0000000..bd03b19 --- /dev/null +++ b/src/static/javascript/settings.js @@ -0,0 +1,126 @@ +import { placeAlliFrames } from "./iFrame.js"; +import { callJuliaFunction } from "./interface.js"; +import { sendToast } from "./toasts.js"; + +// --------------------------------------------------- SETTINGS MODAL --------------------------------------------------- + +export function addModalButtonListener() { + const openBtn = document.getElementById('open-settings-modal'); + const closeBtn = document.getElementById('close-settings-modal'); + + openBtn.addEventListener('click', () => { + openSettings(); + }); + + closeBtn.addEventListener('click', () => { + closeSettings(); + }); +} + +export function openSettings() { + const modal = document.getElementById('default-modal'); + const pageContent = document.getElementById('main-export'); + + modal.classList.remove('hidden'); + modal.classList.add('flex'); + pageContent.classList.add('blur-sm'); +} + +export function closeSettings() { + const modal = document.getElementById('default-modal'); + const pageContent = document.getElementById('main-export'); + + modal.classList.remove('flex'); + modal.classList.add('hidden'); + pageContent.classList.remove('blur-sm'); +} + +export function findCell(cell_id) { + //find cell with property cellid=cell_id + const cell = document.querySelector(`div[cellid="${cell_id}"]`); + if (cell) { + sendToast(`Cell ${cell_id} found.`, 'success'); + closeSettings(); + cell.classList.add('border-4', 'border-red-500'); + setTimeout(() => { + cell.classList.remove('border-4', 'border-red-500'); + }, 3000); + } else { + sendToast(`Cell ${cell_id} not found.`, 'error'); + } +} + + +// --------------------------------------------------- CELL TABLE --------------------------------------------------- + +export function updateCellIDsTable() { + callJuliaFunction("get_cells", { internal: true }).then( + r => { + const table = document.getElementById("cellids-tbody"); + const rows = []; + r.forEach(cell_id => { + const index = r.indexOf(cell_id) + 1; + console.log(cell_id); + rows.push(` + + + ${index} + + + ${cell_id} + + + + + + + + + + + + + + + + + + + `); + }); + table.innerHTML = ""; + table.innerHTML = rows.join(""); + } + ) +} + +export function removeCell(cell_id) { + callJuliaFunction("remove_cell", { + args: [cell_id], + internal: true + }).then( + _ => { + sendToast(`Cell removed.`, 'success'); + updateCellIDsTable(); + } + ); +} + +export function addCell() { + callJuliaFunction("add_cell", { internal: true }).then( + r => { + placeAlliFrames(); + updateCellIDsTable(); + sendToast(`Cell added.`, 'success'); + } + ); +} \ No newline at end of file diff --git a/src/static/javascript/toasts.js b/src/static/javascript/toasts.js new file mode 100644 index 0000000..a410b3d --- /dev/null +++ b/src/static/javascript/toasts.js @@ -0,0 +1,48 @@ +export function sendToast(message, type) { + const toastContainer = document.getElementById('toast-container'); + let img = ''; + if (type === 'success') { + img = `
+ + Check icon +
`; + } else if (type === 'error') { + img = `
+ + Error icon +
`; + } + else if (type === 'warning') { + img = `
+ + Warning icon +
`; + } + + //generate random id for toast + const id = Math.random().toString(36).substring(7); + const toast = ``; + + //append toast to toast container + toastContainer.insertAdjacentHTML('beforeend', toast); + + //remove toast after 5 seconds + setTimeout(() => { + document.getElementById(`toast-${id}`).remove(); + }, 5000); +} \ No newline at end of file From 597179a183058bdad6037d8aab3a0fbdd6b3bfe8 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 24 Nov 2024 02:54:57 +0100 Subject: [PATCH 2/3] Load all javascript as ES6 modules and css as links instead of forcefully adding it as