interface.js

/**
 * Updates cell in a Pluto notebook by its ID
 * @param {string} cellID -
 * @returns {Promise<void>}
 */
export async function updateCell(cellID) {
    //updates a cell by its ID
    const cell = document.getElementById(cellID);
    await cell._internal_pluto_actions.set_and_run_multiple([cellID]);
}

/**
 * Updates multiple cells in a Pluto notebook by their IDs
 * @param {string[]} cellIDs - 
 * @returns {Promise<void>}
 */
export async function updateCellsByID(cellIDs) {
    //updates multiple cells by their IDs
    const cells = cellIDs.map(cellID => document.getElementById(cellID));
    await cells[0]._internal_pluto_actions.set_and_run_multiple(cellIDs);
}

/**
 * Updates all cells containing a specific reactive variable in their `rv` attribute
 * @param {string} rv - 
 * @returns {Promise<void>}
 */
export async function updateCellByReactiveVariableAttribute(rv) {
    //updates all cells containing `rv` in their `rv` attribute
    const cells = [...document.querySelectorAll(`div[rv]`)].filter(el => new RegExp(`\\b${rv}\\b`).test(el.getAttribute('rv')));
    for (let i = 0; i < cells.length; i++) {
        await updateCell(cells[i].getAttribute('cellid'));
    }
}

/**
 * Updates all cells containing a specific reactive variable in their innerHTML
 * @param {string} rv - 
 * @returns {Promise<void>}
 */
export async function updateCellsByReactiveVariable(rv) {
    //updates all cells containing `rv` in their innerHTML
    const cellIDS = [...document.querySelectorAll(`div[class="awesome-wrapping-plugin-the-line cm-line"]`)].filter(el => new RegExp(`${rv}`).test(el.innerHTML)).map(el => el.closest(`pluto-cell`)?.id);

    await updateCellsByID(cellIDS);
    console.log(`Updated cells with reactive variable ${rv}:`, cellIDS);
}

/**
 * Updates all cells that contain a specific variable. Must be the the actual variable. `variable` will not work if the cell contains `user_package.variable`.
 * @param {string} varName - 
 * @returns {Promise<void>}
 */
export async function updateCellByVariable(varName) {
    //fetch cellids needed to get updated
    const cellIDS = await callJuliaFunction("find_cells_with_variable", {args: [varName], internal: true});
    console.log(`Updating cells with variable ${varName}:`, cellIDS);
    //update cells
    await updateCellsByID(cellIDS);
}

/**
 * Sets up a WebSocket connection to PlutoBoard.jl
 * @returns {WebSocket}
 */
function setupWebsocket() {
    let socket;
    while (socket === undefined) {
        info('Waiting for WebSocket to be defined');
        new Promise(resolve => setTimeout(resolve, 100));
        socket = new WebSocket('ws://localhost:8080');
    }

    socket.addEventListener('open', function (event) {
        info('WebSocket is open now.');
    });

    socket.addEventListener('close', function (event) {
        info('WebSocket is closed now.');
    });

    socket.addEventListener('error', function (event) {
        error('WebSocket error observed:', event);
    });
    return socket;
}

/**
 * Waits for the WebSocket connection to be open
 * @param {WebSocket} socket - 
 * @returns {Promise<void>}
 */
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));
            });
        }
    });
}

/**
 * Calls a Julia function via WebSocket and returns a Promise that resolves with the return value of the function. Callbacks can be provided to handle responses.
 * @param {string} func_name - 
 * @param {Object} options - 
 * @param {Array} [options.args=[]] - 
 * @param {Object} [options.kwargs={}] - 
 * @param {Function} [options.response_callback=() => {}] - 
 * @param {boolean} [options.internal=false] - 
 * @returns {Promise<any>}
 */
export async function callJuliaFunction(func_name, {
    args = [], kwargs = {}, response_callback = () => {
    }, internal = false
} = {}) {
    const socket = setupWebsocket();
    await waitForOpenConnection(socket);

    info(`Calling Julia function ${func_name} with args: ${args}, kwargs: ${JSON.stringify(kwargs)}, internal: ${internal}`);

    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);
            }
        });
    });
}

/**
 * Logs an informational message to the console with a specific prefix.
 * @param {string} message - 
 * @returns {void}
 */
export function info(message) {
    console.log(`[PlutoBoard.jl] ${message}`);
}

/**
 * Logs a warning message to the console with a specific prefix.
 * @param {string} message - 
 * @returns {void}
 */
export function warn(message) {
    console.warn(`[PlutoBoard.jl] ${message}`);
}

/**
 * Logs an error message to the console with a specific prefix.
 * @param {string} message - 
 * @returns {void}
 */
export function error(message) {
    console.error(`[PlutoBoard.jl] ${message}`);
}

/**
 * Places an iframe in a specific destination div and hides every cell except the one with the given targetCellID.
 * @param {string} targetCellID - 
 * @param {HTMLElement} destinationDiv - 
 * @returns {void}
 */
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);
}

/**
 * Places all iFrames needed given the user's index.html file.
 * @returns {void}
 */
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);
    });
}