diff --git a/config/config.toml b/config/config.toml index f1f3f95..720fb46 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,8 +1,10 @@ -[cdn] -bootstrap_css = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" -bootstrap_js = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" +[websocket] +url = "127.0.0.1" +port = 8080 -[paths] -javascript_dir = "/internal/javascript" +[fileserver] +url = "127.0.0.1" +port = 8085 -[setup] +[fileserver_api] +url = "http://localhost:8085/" diff --git a/docs/src/index.md b/docs/src/index.md index 876b196..38fdae0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -19,11 +19,11 @@ YOUR_PACKAGE_NAME$> julia --project -e 'using Pluto; Pluto.run(notebook="PlutoBo ## Write your own code There is some hierarchy: -- `main` in `src/Main.jl` gets called in the beginning, so use this as julia entry point -- functions that should be callable from js need to go into `src/Functions.jl` -- all javascript files in `static/javascript/` are getting executed, though `static/javascript/javascript.js` is the last one, so use this as js entry point +- `main` in `src/Main.jl` gets called in the beginning, use this as Julia entry point +- `static/javascript/main.js` is getting executed in the beginning too, use this as JS entrypoint +- functions that should be callable from js should to go into `src/Functions.jl`. They can be anywhere else too, but make sure they are included in `src/YOUR_PACKAGE_NAME.jl` -There is a simple example in `src/Functions.jl`, `src/static/index.html` and `src/static/javascript/javascript.js` about calling a julia function from javascript with callbacks. +There is a simple example in `src/Functions.jl`, `static/index.html` and `static/javascript/main.js` about calling a Julia function from JS with callbacks. ### Simple calling of a Julia function within HTML using a button and an input @@ -48,14 +48,17 @@ Now we create an input with `id=buttonInput` for our number in `static/index.htm ``` as well as a button ```HTML - + ``` and a div to show our output ```HTML
Output:
``` -Since we used `calculateVeryHardStuff` in our button, we need to define it aswell. We do this in `static/javascript/javascript.js` +Now we need to define a function that does something when we click the button. We do this in `static/javascript/main.js` ```JavaScript +//Import callJuliaFunction. Internal JS files are on /internal/static/javascript +import { callJuliaFunction } from "/internal/static/javascript/interface.js"; + function calculateVeryHardStuff() { // get our input const input = document.getElementById("buttonInput"); @@ -87,9 +90,19 @@ function calculateVeryHardStuff() { } ) } + +//add our function as click event to the button +document.getElementById("calculateButton").addEventListener("click", calculateVeryHardStuff); ``` And that's it, we have created a simple application using HTML and JavaScript as input and Julia for calculating! +Importing other JS files is easily done using ES6 Modules: +```javascript +//imports function from static/javascript/folder/file.js +import { function } from "./folder/file.js"; +``` +Don't forget to export functions you want to import elsewhere. + # PlutoBoard.jl Documentation ## PlutoBoard interface @@ -107,22 +120,15 @@ callJuliaFunction() ## Internal functions -### Julia internal functions ```@docs -load_scripts_and_links() -load_html() -load_html_string_to_body() +add_cell(;ws) +remove_cell(uuid::String; ws) +get_cells(;ws) + +get_html() load_js() open_file(path::String) copy_with_delete(from::String, to::String) setup() get_package_name() -``` - -### JavaScript internal functions -```@docs -placeIframe() -updateAllCells() -updateCell() -insertHTMLToBody() ``` \ No newline at end of file diff --git a/internal/javascript/Dummy.jl b/internal/javascript/Dummy.jl deleted file mode 100644 index abba1e4..0000000 --- a/internal/javascript/Dummy.jl +++ /dev/null @@ -1,79 +0,0 @@ -export callJuliaFunction, placeIframe, updateAllCells, updateCell, insertHTMLToBody - -""" -```javascript -async callJuliaFunction( - func_name, - { args = [], kwargs = {}, response_callback = () => { } } = {} -) -> Promise -``` -`kwargs` are kwargs for the Julia function to be called. - -Calls specified Julia function with args and callback within browser context from JavaScript. - -Example usage: -```javascript -callJuliaFunction("get_cube", { - args: [number], - response_callback: ( - r => { - const outP = document.getElementById("buttonOutput"); - outP.innerHTML = `Cube of \${number}... \${Math.round(r * 100)}%`; - } - ) - }).then( - r => { - const outP = document.getElementById("buttonOutput"); - outP.innerHTML = `The cube of \${number} is \${r}`; - } - ) -``` -""" -function callJuliaFunction() -end - - -""" -```javascript -placeIframe(targetCellID, destinationDiv) -``` -Places an iframe of the site itself in `destinationDiv` only showing `targetCellID`. -""" -function placeIframe() -end - -""" -```javascript -placeAlliFrames() -``` -Calls `placeIframe` for every div with `cellid` attribute. -""" -function placeAlliFrames() -end - -""" -```javascript -async updateAllCells() -``` -Forcefully reevaluates all Pluto cells. -""" -function updateAllCells() -end - -""" -```javascript -async updateCell(cellid) -``` -Reevaluates Pluto cell with `cellid`. -""" -function updateCell() -end - -""" -```javascript -insertHTMLToBody(html, css) -``` -Appends html and css string below body. -""" -function insertHTMLToBody() -end diff --git a/internal/javascript/iFrame.js b/internal/javascript/iFrame.js deleted file mode 100644 index 874adc2..0000000 --- a/internal/javascript/iFrame.js +++ /dev/null @@ -1,168 +0,0 @@ -//DO NOT LOAD IN IFRAME - -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; - 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); -} - - -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); - }); -} - -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/internal/javascript/internal.js b/internal/javascript/internal.js deleted file mode 100644 index 6ba0f24..0000000 --- a/internal/javascript/internal.js +++ /dev/null @@ -1,218 +0,0 @@ -// DO LOAD IN IFRAME! - -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]); -} - -function insertHTMLToBody(html, css) { - let body = document.querySelector('body'); - if (!body.querySelector('style')) { - body.insertAdjacentHTML('beforeend', css); - } else { - body.querySelector('style').remove(); - body.insertAdjacentHTML('afterbegin', css); - } - - if (!document.getElementById('main-export')) { - body.insertAdjacentHTML('afterbegin', html); - } else { - document.getElementById('main-export').remove(); - body.insertAdjacentHTML('afterbegin', html); - } -} - -function openNav() { - document.getElementById("mySidebar").style.width = "250px"; -} - -function closeNav() { - document.getElementById("mySidebar").style.width = "0"; -} - -function resizeInitial() { - let element = document.getElementById("pluto-nav").parentElement.parentElement; - element.style.minHeight = "0"; -} - - - -// --------------------------------------------------- SETTINGS MODAL --------------------------------------------------- - -const modal = document.getElementById('default-modal'); -const openBtn = document.getElementById('open-settings-modal'); -const closeBtn = document.getElementById('close-settings-modal'); -const pageContent = document.getElementById('main-export'); - -function openSettings() { - modal.classList.remove('hidden'); - modal.classList.add('flex'); - pageContent.classList.add('blur-sm'); -} - -function closeSettings() { - modal.classList.remove('flex'); - modal.classList.add('hidden'); - pageContent.classList.remove('blur-sm'); -} - -openBtn.addEventListener('click', () => { - openSettings(); -}); - -closeBtn.addEventListener('click', () => { - closeSettings(); -}); - -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'); - } -} - - -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); -} - - -// --------------------------------------------------- CELL TABLE --------------------------------------------------- - -function updateCellIDsTable() { - callJuliaFunction("get_cells", { internal: true }).then( - r => { - const table = document.getElementById("cellids-tbody"); - const rows = []; - r.forEach(cell_id => { - index = r.indexOf(cell_id) + 1; - console.log(cell_id); - rows.push(` - - - ${index} - - - ${cell_id} - - - - - - - - - - - - - - - - - - - `); - }); - table.innerHTML = ""; - table.innerHTML = rows.join(""); - } - ) -} - -function removeCell(cell_id) { - callJuliaFunction("remove_cell", { - args: [cell_id], - internal: true - }).then( - _ => { - sendToast(`Cell removed.`, 'success'); - updateCellIDsTable(); - } - ); -} - -function addCell() { - callJuliaFunction("add_cell", { internal: true }).then( - r => { - sendToast(`Cell added.`, 'success'); - updateCellIDsTable(); - } - ); -} -updateCellIDsTable(); - -// --------------------------------------------------- Run functions --------------------------------------------------- - -resizeInitial(); - - - diff --git a/setup/PlutoBoardNotebook.jl b/setup/PlutoBoardNotebook.jl index 38924d0..a8579c2 100644 --- a/setup/PlutoBoardNotebook.jl +++ b/setup/PlutoBoardNotebook.jl @@ -32,9 +32,6 @@ end # ╔═╡ c8f44fcc-b2b4-49e6-8c5c-93ee51e42d1a 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,13 +75,18 @@ function handle_julia_function_call(ws, parsed) end +# ╔═╡ 231a2a26-b3ec-4ad0-8335-db43a53f1a86 +PlutoBoard.run_fileserver() + # ╔═╡ 147ed5fe-0133-4eef-96f2-afafe9385f27 begin - if user_package.global_websocket !== nothing - schedule(user_package.global_websocket, InterruptException(), error = true) + try + schedule(PlutoBoard.websocket, InterruptException(), error = true) + catch e + @info e end - - user_package.global_websocket = @async WebSockets.listen("0.0.0.0", 8080) do ws + + PlutoBoard.websocket = @async WebSockets.listen(PlutoBoard.config["websocket"]["url"], PlutoBoard.config["websocket"]["port"]) do ws for msg in ws parsed = PlutoBoard.parse_to_symbol(PlutoBoard.JSON.parse(msg)) type = parsed[:type] @@ -96,10 +98,10 @@ 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: @@ -110,9 +112,9 @@ x # ╠═96ff4362-fda0-4cae-9786-2dc29626479c # ╠═64f17c2b-5f54-4df5-8d7d-d57f3b314b5b # ╠═c8f44fcc-b2b4-49e6-8c5c-93ee51e42d1a -# ╠═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.css b/setup/static/index.css index 25c3111..e69de29 100644 --- a/setup/static/index.css +++ b/setup/static/index.css @@ -1,7 +0,0 @@ -#messages { - color: white; -} - -ads { - background-image: url("static/img.svg"); -} \ No newline at end of file diff --git a/setup/static/index.html b/setup/static/index.html index 53c2fc9..b6360c8 100644 --- a/setup/static/index.html +++ b/setup/static/index.html @@ -1,90 +1,3 @@ - -
- - -
- -
- -
- - - - - -
You can place Pluto cells wherever you want on the screen! Here they are just in a simple grid.

-
+
-
+
@@ -132,7 +45,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 69% rename from setup/static/javascript/javascript.js rename to setup/static/javascript/main.js index dc4dcfc..597099f 100644 --- a/setup/static/javascript/javascript.js +++ b/setup/static/javascript/main.js @@ -1,5 +1,7 @@ -console.log("Hello from PlutoBoard.jl") +import { info } from "/internal/static/javascript/logger.js"; +import { callJuliaFunction } from "/internal/static/javascript/interface.js"; +info("Hello from main.js"); function calculateVeryHardStuff() { const input = document.getElementById("buttonInput"); @@ -20,4 +22,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/EditCells.jl b/src/EditCells.jl index 0dd9d91..bde55f1 100644 --- a/src/EditCells.jl +++ b/src/EditCells.jl @@ -1,3 +1,5 @@ +export add_cell, remove_cell, get_cells + const CELL_HEAD = "# ╔═╡ "; const ORDER_TITLE = "# ╔═╡ Cell order:"; const ORDER_HEAD = "# " * ".."; @@ -5,6 +7,15 @@ const SHOWN_HEAD = "# ╠═"; const HIDDEN_HEAD = "# ╟─"; const LAST_CELL = SHOWN_HEAD * "147ed5fe-0133-4eef-96f2-afafe9385f27"; + +""" + add_cell( + ; + ws::WebSocket + ) -> nothing + +Adds a cell at the bottom of the notebook +""" function add_cell(; ws) uuid = UUIDs.uuid4() @@ -28,6 +39,15 @@ function add_cell(; ws) end end +""" + remove_cell( + uuid::String + ; + ws::WebSocket + ) -> nothing + +Removes cell with uuid +""" function remove_cell(uuid::String; ws) file_content = "" open("PlutoBoardNotebook.jl", "r") do file @@ -51,6 +71,14 @@ function remove_cell(uuid::String; ws) end end +""" + get_cells( + ; + ws::WebSocket + ) -> Array{String} + +Returns an array of all cells uuids +""" function get_cells(; ws) file_content = "" open("PlutoBoardNotebook.jl", "r") do file diff --git a/src/LoadHTML.jl b/src/LoadHTML.jl index c087dc7..65ba00b 100644 --- a/src/LoadHTML.jl +++ b/src/LoadHTML.jl @@ -1,4 +1,4 @@ -export set_fullscreen, load_scripts_and_links, load_html, load_html_string_to_body, load_js +export set_fullscreen, load_scripts_and_links, load_html, get_html, load_js function set_fullscreen() if PlutoBoard.fullscreen @@ -46,142 +46,72 @@ end """ - load_html_string_to_body() -> HypertextLiteral.Result + get_html() -> String -Returns a HypertextLiteral.Result object to call a javascript function to insert the html and css to the body. +Returns contents of `index.html` as `String` """ -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 """ load_js() -> HypertextLiteral.Result -Returns a HypertextLiteral.Result object to load all javascript files. +Returns a HypertextLiteral.Result object to load entry js files as modules and css scripts """ 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..2e53407 100644 --- a/src/PlutoBoard.jl +++ b/src/PlutoBoard.jl @@ -11,8 +11,9 @@ include("InterfaceFunctions.jl") include("LoadHTML.jl") include("Utilities.jl") include("JSCommands/HandleCommands.jl") -include("../internal/javascript/Dummy.jl") +include("static/javascript/JSDocs.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,11 @@ hide_notebook::Bool = true scripts_urls::Array{String} = [] stylesheet_urls::Array{String} = [] +fileserver = nothing +const SERVE_DIR = joinpath(pwd(), "static") + +websocket = nothing + end export PlutoBoard diff --git a/src/fileserver/FileServer.jl b/src/fileserver/FileServer.jl new file mode 100644 index 0000000..34b0fe8 --- /dev/null +++ b/src/fileserver/FileServer.jl @@ -0,0 +1,94 @@ +function run_fileserver() + global fileserver + try + schedule(fileserver, InterruptException(), error = true) + catch e + @info e + end + + fileserver = @async PlutoBoard.start_server() +end + +function serve_file(req::HTTP.Request) + 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 + +function start_server() + @info "Starting server on port to server $SERVE_DIR" + HTTP.serve(config["fileserver"]["url"], config["fileserver"]["port"]) do req + try + serve_file(req) + catch e + @error "Error serving request: $e" + return HTTP.Response(500, "Internal Server Error") + end + 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/css/alwaysLoad.css b/src/static/css/alwaysLoad.css index 39013fa..b1df627 100644 --- a/src/static/css/alwaysLoad.css +++ b/src/static/css/alwaysLoad.css @@ -10,4 +10,12 @@ iframe { pluto-trafficlight { display: none; -} \ No newline at end of file +} + +/* #messages { + color: white; +} + +ads { + background-image: url("static/img.svg"); +} */ \ No newline at end of file diff --git a/src/static/css/iFrame.css b/src/static/css/iFrame.css new file mode 100644 index 0000000..6f213b2 --- /dev/null +++ b/src/static/css/iFrame.css @@ -0,0 +1,63 @@ +body { + background-color: transparent; + min-height: 0; +} + +pluto-cell { + display: none; +} + +header, +footer { + display: none; +} + +.not-iframe { + display: none; +} + +#helpbox-wrapper { + display: none; +} + +main { + margin: 0; + padding: 0; + display: contents; +} + +preamble { + display: none !important; +} + +.add_cell { + display: none; +} + +.cm-gutters { + display: none; +} + +pluto-shoulder { + display: none; +} + +pluto-editor { + parent { + min-height: 0; + } +} + +loading-bar { + display: none; +} + +pluto-notebook { + border-radius: 4vw; + width: 100vw; + height: 100vh; +} + +pluto-trafficlight { + display: none; +} \ No newline at end of file diff --git a/src/static/css/internal.css b/src/static/css/internal.css index abec48e..aae3b8f 100644 --- a/src/static/css/internal.css +++ b/src/static/css/internal.css @@ -1,30 +1,32 @@ /* PlutoBoard.jl internal stylesheet */ -pluto-editor { - display: none; -} - -body { - overflow: hidden; -} - -pluto-input { - display: none; -} - -.add_cell { - display: none; -} - -pluto-shoulder { - display: none; -} - -pluto-runarea { - display: none; -} - - -.btn-run { - width: 100%; +:root:not(.in-iframe) { + pluto-editor { + display: none; + } + + /* body { + overflow: hidden; + } */ + + pluto-input { + display: none; + } + + .add_cell { + display: none; + } + + pluto-shoulder { + display: none; + } + + pluto-runarea { + display: none; + } + + + .btn-run { + width: 100%; + } } \ No newline at end of file diff --git a/src/static/html/settings.html b/src/static/html/settings.html new file mode 100644 index 0000000..bf4e71c --- /dev/null +++ b/src/static/html/settings.html @@ -0,0 +1,84 @@ + +
+ + +
+ + + + +
+ +
\ No newline at end of file diff --git a/src/static/javascript/JSDocs.jl b/src/static/javascript/JSDocs.jl new file mode 100644 index 0000000..f5bf4c2 --- /dev/null +++ b/src/static/javascript/JSDocs.jl @@ -0,0 +1,87 @@ +export callJuliaFunction + +""" +```javascript +async callJuliaFunction( + func_name, + { args = [], kwargs = {}, response_callback = () => { }, internal = false } = {} +) -> Promise +``` +Calls specified Julia function with args and callback within browser context from JavaScript. + +Args: +- `func_name: String`: Function name of the called Julia function + +Kwargs: +- `args: Array`: Args for the Julia function +- `kwargs: Dict`: Kwargs for the Julia function +- `response_callback: Function`: Function that gets called when a new message is being sent from the Julia function +- `internal: Boolean = false`: Whether the targeted function is in PlutoBoard package. Only used for internal calls, defaults to false + + +Example usage: +```javascript +callJuliaFunction("get_cube", { + args: [number], + response_callback: ( + r => { + const outP = document.getElementById("buttonOutput"); + outP.innerHTML = `Cube of \${number}... \${Math.round(r * 100)}%`; + } + ) + }).then( + r => { + const outP = document.getElementById("buttonOutput"); + outP.innerHTML = `The cube of \${number} is \${r}`; + } + ) +``` +""" +function callJuliaFunction() +end + + +# """ +# ```javascript +# placeIframe(targetCellID, destinationDiv) +# ``` +# Places an iframe of the site itself in `destinationDiv` only showing `targetCellID`. +# """ +# function placeIframe() +# end + +# """ +# ```javascript +# placeAlliFrames() +# ``` +# Calls `placeIframe` for every div with `cellid` attribute. +# """ +# function placeAlliFrames() +# end + +# """ +# ```javascript +# async updateAllCells() +# ``` +# Forcefully reevaluates all Pluto cells. +# """ +# function updateAllCells() +# end + +# """ +# ```javascript +# async updateCell(cellid) +# ``` +# Reevaluates Pluto cell with `cellid`. +# """ +# function updateCell() +# end + +# """ +# ```javascript +# insertHTMLToBody(html, css) +# ``` +# Appends html and css string below body. +# """ +# function insertHTMLToBody() +# end diff --git a/src/static/javascript/iFrame.js b/src/static/javascript/iFrame.js new file mode 100644 index 0000000..9be83b1 --- /dev/null +++ b/src/static/javascript/iFrame.js @@ -0,0 +1,80 @@ +import { info } from "./logger.js"; + +export function placeIframe(targetCellID, destinationDiv) { + const iFrameID = `cell-iframe-${targetCellID}`; + + 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); + + let interval2 = setInterval(function () { + if (document.querySelector(`#${iFrameID}`).contentDocument.getElementById(targetCellID)) { + clearInterval(interval2); + info(`IFrame with cellid ${targetCellID} loaded`); + + let iframeDoc = document.querySelector(`#${iFrameID}`).contentDocument; + + const cell = iframeDoc.getElementById(targetCellID); + cell.style.margin = '0.8vw'; + cell.style.padding = '2vw'; + cell.style.display = 'block'; + + + //get iFrame.css and add it to head in iFrame + let css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'http://localhost:8085/internal/static/css/iFrame.css'; + iframeDoc.head.appendChild(css); + } + }, 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/internal/javascript/interface.js b/src/static/javascript/interface.js similarity index 74% rename from internal/javascript/interface.js rename to src/static/javascript/interface.js index a0ef1b5..3986959 100644 --- a/internal/javascript/interface.js +++ b/src/static/javascript/interface.js @@ -1,23 +1,23 @@ -// DO NOT LOAD IN IFRAME! +import { error, info } from "./logger.js"; function setupWebsocket() { let socket; while (socket === undefined) { - console.log('Waiting for WebSocket to be defined...'); + info('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.'); + info('WebSocket is open now.'); }); socket.addEventListener('close', function (event) { - console.log('WebSocket is closed now.'); + info('WebSocket is closed now.'); }); socket.addEventListener('error', function (event) { - console.error('WebSocket error observed:', event); + error('WebSocket error observed:', event); }); return socket; } @@ -38,13 +38,13 @@ function waitForOpenConnection(socket) { }); } -async function callJuliaFunction(func_name, { args = [], kwargs = {}, response_callback = () => { }, internal = false } = {}) { +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); + info(`Calling Julia function ${func_name} with args: ${args}, kwargs: ${JSON.stringify(kwargs)}, internal: ${internal}`); - cmd = { + const cmd = { "type": "julia_function_call", "function": func_name, "args": args, @@ -55,8 +55,6 @@ async function callJuliaFunction(func_name, { args = [], kwargs = {}, response_c return new Promise((resolve, reject) => { socket.addEventListener('message', (event) => { - - if (JSON.parse(event.data).type === 'return') { socket.close(); resolve(JSON.parse(event.data).return); 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..87cd806 --- /dev/null +++ b/src/static/javascript/loadHTML.js @@ -0,0 +1,22 @@ +import { callJuliaFunction } from './interface.js'; + +export async function insertHTMLToBody() { + const body = document.querySelector('body'); + + // const htmlString = await callJuliaFunction("get_html", { + // internal: true, + // }); + + const mainSite = await ((await fetch("http://127.0.0.1:8085/index.html")).text()); + + const settingsSite = await ((await fetch("http://127.0.0.1:8085/internal/static/html/settings.html")).text()); + + const htmlString = settingsSite + mainSite; + + 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/logger.js b/src/static/javascript/logger.js new file mode 100644 index 0000000..21ecd07 --- /dev/null +++ b/src/static/javascript/logger.js @@ -0,0 +1,11 @@ +export function info(message) { + console.log(`[PlutoBoard.jl] ${message}`); +} + +export function warn(message) { + console.warn(`[PlutoBoard.jl] ${message}`); +} + +export function error(message) { + console.error(`[PlutoBoard.jl] ${message}`); +} \ 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..d0ded98 --- /dev/null +++ b/src/static/javascript/main.js @@ -0,0 +1,33 @@ +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; + + + + + diff --git a/src/static/javascript/settings.js b/src/static/javascript/settings.js new file mode 100644 index 0000000..e59fa85 --- /dev/null +++ b/src/static/javascript/settings.js @@ -0,0 +1,124 @@ +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; + 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 => { + 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