+
-
+
@@ -132,7 +132,7 @@
type="text" id="buttonInput" placeholder="Input number">
Calculate
+ 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}
+
+
+ Find
+
+
+ Move up
+
+
+ Move down
+
+
+ Remove
+
+
+
+
+
+
+ `);
+ });
+ 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 = `
`;
+ } else if (type === 'error') {
+ img = `
`;
+ }
+ else if (type === 'warning') {
+ img = `
`;
+ }
+
+ //generate random id for toast
+ const id = Math.random().toString(36).substring(7);
+ const toast = `
+ ${img}
+
${message}
+
+ Close
+
+
+
+
+
`;
+
+ //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