Skip to content

Commit

Permalink
Updated webr
Browse files Browse the repository at this point in the history
  • Loading branch information
royfrancis committed Mar 17, 2024
1 parent 28810e3 commit 2e98d42
Show file tree
Hide file tree
Showing 16 changed files with 1,446 additions and 927 deletions.
2 changes: 1 addition & 1 deletion _extensions/coatless/webr/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: webr
title: Embedded webr code cells
author: James Joseph Balamuta
version: 0.4.0-dev.3
version: 0.4.1-dev.1
quarto-required: ">=1.2.198"
contributes:
filters:
Expand Down
245 changes: 245 additions & 0 deletions _extensions/coatless/webr/qwebr-cell-elements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Supported Evaluation Types for Context
globalThis.EvalTypes = Object.freeze({
Interactive: 'interactive',
Setup: 'setup',
Output: 'output',
});

// Function that dispatches the creation request
globalThis.qwebrCreateHTMLElement = function (
cellData
) {

// Extract key components
const evalType = cellData.options.context;
const qwebrCounter = cellData.id;

// We make an assumption that insertion points are defined by the Lua filter as:
// qwebr-insertion-location-{qwebrCounter}
const elementLocator = document.getElementById(`qwebr-insertion-location-${qwebrCounter}`);

// Figure out the routine to use to insert the element.
let qwebrElement;
switch ( evalType ) {
case EvalTypes.Interactive:
qwebrElement = qwebrCreateInteractiveElement(qwebrCounter, cellData.options);
break;
case EvalTypes.Output:
qwebrElement = qwebrCreateNonInteractiveOutputElement(qwebrCounter, cellData.options);
break;
case EvalTypes.Setup:
qwebrElement = qwebrCreateNonInteractiveSetupElement(qwebrCounter, cellData.options);
break;
default:
qwebrElement = document.createElement('div');
qwebrElement.textContent = 'Error creating `quarto-webr` element';
}

// Insert the dynamically generated object at the document location.
elementLocator.appendChild(qwebrElement);
};

// Function that setups the interactive element creation
globalThis.qwebrCreateInteractiveElement = function (qwebrCounter, qwebrOptions) {

// Create main div element
var mainDiv = document.createElement('div');
mainDiv.id = 'qwebr-interactive-area-' + qwebrCounter;
mainDiv.className = `qwebr-interactive-area`;
if (qwebrOptions.classes) {
mainDiv.className += " " + qwebrOptions.classes
}

// Add a unique cell identifier that users can customize
if (qwebrOptions.label) {
mainDiv.setAttribute('data-id', qwebrOptions.label);
}

// Create toolbar div
var toolbarDiv = document.createElement('div');
toolbarDiv.className = 'qwebr-editor-toolbar';
toolbarDiv.id = 'qwebr-editor-toolbar-' + qwebrCounter;

// Create a div to hold the left buttons
var leftButtonsDiv = document.createElement('div');
leftButtonsDiv.className = 'qwebr-editor-toolbar-left-buttons';

// Create a div to hold the right buttons
var rightButtonsDiv = document.createElement('div');
rightButtonsDiv.className = 'qwebr-editor-toolbar-right-buttons';

// Create Run Code button
var runCodeButton = document.createElement('button');
runCodeButton.className = 'btn btn-default qwebr-button qwebr-button-run';
runCodeButton.disabled = true;
runCodeButton.type = 'button';
runCodeButton.id = 'qwebr-button-run-' + qwebrCounter;
runCodeButton.textContent = '🟡 Loading webR...';
runCodeButton.title = `Run code (Shift + Enter)`;

// Append buttons to the leftButtonsDiv
leftButtonsDiv.appendChild(runCodeButton);

// Create Reset button
var resetButton = document.createElement('button');
resetButton.className = 'btn btn-light btn-xs qwebr-button qwebr-button-reset';
resetButton.type = 'button';
resetButton.id = 'qwebr-button-reset-' + qwebrCounter;
resetButton.title = 'Start over';
resetButton.innerHTML = '<i class="fa-solid fa-arrows-rotate"></i>';

// Create Copy button
var copyButton = document.createElement('button');
copyButton.className = 'btn btn-light btn-xs qwebr-button qwebr-button-copy';
copyButton.type = 'button';
copyButton.id = 'qwebr-button-copy-' + qwebrCounter;
copyButton.title = 'Copy code';
copyButton.innerHTML = '<i class="fa-regular fa-copy"></i>';

// Append buttons to the rightButtonsDiv
rightButtonsDiv.appendChild(resetButton);
rightButtonsDiv.appendChild(copyButton);

// Create console area div
var consoleAreaDiv = document.createElement('div');
consoleAreaDiv.id = 'qwebr-console-area-' + qwebrCounter;
consoleAreaDiv.className = 'qwebr-console-area';

// Create editor div
var editorDiv = document.createElement('div');
editorDiv.id = 'qwebr-editor-' + qwebrCounter;
editorDiv.className = 'qwebr-editor';

// Create output code area div
var outputCodeAreaDiv = document.createElement('div');
outputCodeAreaDiv.id = 'qwebr-output-code-area-' + qwebrCounter;
outputCodeAreaDiv.className = 'qwebr-output-code-area';
outputCodeAreaDiv.setAttribute('aria-live', 'assertive');

// Create pre element inside output code area
var preElement = document.createElement('pre');
preElement.style.visibility = 'hidden';
outputCodeAreaDiv.appendChild(preElement);

// Create output graph area div
var outputGraphAreaDiv = document.createElement('div');
outputGraphAreaDiv.id = 'qwebr-output-graph-area-' + qwebrCounter;
outputGraphAreaDiv.className = 'qwebr-output-graph-area';

// Append buttons to the toolbar
toolbarDiv.appendChild(leftButtonsDiv);
toolbarDiv.appendChild(rightButtonsDiv);

// Append all elements to the main div
mainDiv.appendChild(toolbarDiv);
consoleAreaDiv.appendChild(editorDiv);
consoleAreaDiv.appendChild(outputCodeAreaDiv);
mainDiv.appendChild(consoleAreaDiv);
mainDiv.appendChild(outputGraphAreaDiv);

return mainDiv;
}

// Function that adds output structure for non-interactive output
globalThis.qwebrCreateNonInteractiveOutputElement = function(qwebrCounter, qwebrOptions) {
// Create main div element
var mainDiv = document.createElement('div');
mainDiv.id = 'qwebr-noninteractive-area-' + qwebrCounter;
mainDiv.className = `qwebr-noninteractive-area`;
if (qwebrOptions.classes) {
mainDiv.className += " " + qwebrOptions.classes
}

// Add a unique cell identifier that users can customize
if (qwebrOptions.label) {
mainDiv.setAttribute('data-id', qwebrOptions.label);
}

// Create a status container div
var statusContainer = createLoadingContainer(qwebrCounter);

// Create output code area div
var outputCodeAreaDiv = document.createElement('div');
outputCodeAreaDiv.id = 'qwebr-output-code-area-' + qwebrCounter;
outputCodeAreaDiv.className = 'qwebr-output-code-area';
outputCodeAreaDiv.setAttribute('aria-live', 'assertive');

// Create pre element inside output code area
var preElement = document.createElement('pre');
preElement.style.visibility = 'hidden';
outputCodeAreaDiv.appendChild(preElement);

// Create output graph area div
var outputGraphAreaDiv = document.createElement('div');
outputGraphAreaDiv.id = 'qwebr-output-graph-area-' + qwebrCounter;
outputGraphAreaDiv.className = 'qwebr-output-graph-area';

// Append all elements to the main div
mainDiv.appendChild(statusContainer);
mainDiv.appendChild(outputCodeAreaDiv);
mainDiv.appendChild(outputGraphAreaDiv);

return mainDiv;
};

// Function that adds a stub in the page to indicate a setup cell was used.
globalThis.qwebrCreateNonInteractiveSetupElement = function(qwebrCounter, qwebrOptions) {
// Create main div element
var mainDiv = document.createElement('div');
mainDiv.id = `qwebr-noninteractive-setup-area-${qwebrCounter}`;
mainDiv.className = `qwebr-noninteractive-setup-area`;
if (qwebrOptions.classes) {
mainDiv.className += " " + qwebrOptions.classes
}


// Add a unique cell identifier that users can customize
if (qwebrOptions.label) {
mainDiv.setAttribute('data-id', qwebrOptions.label);
}

// Create a status container div
var statusContainer = createLoadingContainer(qwebrCounter);

// Append status onto the main div
mainDiv.appendChild(statusContainer);

return mainDiv;
}


// Function to create loading container with specified ID
globalThis.createLoadingContainer = function(qwebrCounter) {

// Create a status container
const container = document.createElement('div');
container.id = `qwebr-non-interactive-loading-container-${qwebrCounter}`;
container.className = 'qwebr-non-interactive-loading-container qwebr-cell-needs-evaluation';

// Create an R project logo to indicate its a code space
const rProjectIcon = document.createElement('i');
rProjectIcon.className = 'fa-brands fa-r-project fa-3x qwebr-r-project-logo';

// Setup a loading icon from font awesome
const spinnerIcon = document.createElement('i');
spinnerIcon.className = 'fa-solid fa-spinner fa-spin fa-1x qwebr-icon-status-spinner';

// Add a section for status text
const statusText = document.createElement('p');
statusText.id = `qwebr-status-text-${qwebrCounter}`;
statusText.className = `qwebr-status-text qwebr-cell-needs-evaluation`;
statusText.innerText = 'Loading webR...';

// Incorporate an inner container
const innerContainer = document.createElement('div');

// Append elements to the inner container
innerContainer.appendChild(spinnerIcon);
innerContainer.appendChild(statusText);

// Append elements to the main container
container.appendChild(rProjectIcon);
container.appendChild(innerContainer);

return container;
}
110 changes: 110 additions & 0 deletions _extensions/coatless/webr/qwebr-cell-initialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Handle cell initialization initialization
qwebrCellDetails.map(
(entry) => {
// Handle the creation of the element
qwebrCreateHTMLElement(entry);
// In the event of interactive, initialize the monaco editor
if (entry.options.context == EvalTypes.Interactive) {
qwebrCreateMonacoEditorInstance(entry);
}
}
);

// Identify non-interactive cells (in order)
const filteredEntries = qwebrCellDetails.filter(entry => {
const contextOption = entry.options && entry.options.context;
return ['output', 'setup'].includes(contextOption) || (contextOption == "interactive" && entry.options && entry.options.autorun === 'true');
});

// Condition non-interactive cells to only be run after webR finishes its initialization.
qwebrInstance.then(
async () => {
const nHiddenCells = filteredEntries.length;
var currentHiddenCell = 0;


// Modify button state
qwebrSetInteractiveButtonState(`🟡 Running hidden code cells ...`, false);

// Begin processing non-interactive sections
// Due to the iteration policy, we must use a for() loop.
// Otherwise, we would need to switch to using reduce with an empty
// starting promise
for (const entry of filteredEntries) {

// Determine cell being examined
currentHiddenCell = currentHiddenCell + 1;
const formattedMessage = `Evaluating hidden cell ${currentHiddenCell} out of ${nHiddenCells}`;

// Update the document status header
if (qwebrShowStartupMessage) {
qwebrUpdateStatusHeader(formattedMessage);
}

// Display the update in non-active areas
qwebrUpdateStatusMessage(formattedMessage);

// Extract details on the active cell
const evalType = entry.options.context;
const cellCode = entry.code;
const qwebrCounter = entry.id;

if (['output', 'setup'].includes(evalType)) {
// Disable further global status updates
const activeContainer = document.getElementById(`qwebr-non-interactive-loading-container-${qwebrCounter}`);
activeContainer.classList.remove('qwebr-cell-needs-evaluation');
activeContainer.classList.add('qwebr-cell-evaluated');

// Update status on the code cell
const activeStatus = document.getElementById(`qwebr-status-text-${qwebrCounter}`);
activeStatus.innerText = " Evaluating hidden code cell...";
activeStatus.classList.remove('qwebr-cell-needs-evaluation');
activeStatus.classList.add('qwebr-cell-evaluated');
}

switch (evalType) {
case 'interactive':
// TODO: Make this more standardized.
// At the moment, we're overriding the interactive status update by pretending its
// output-like.
const tempOptions = entry.options;
tempOptions["context"] = "output"
// Run the code in a non-interactive state that is geared to displaying output
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, tempOptions);
break;
case 'output':
// Run the code in a non-interactive state that is geared to displaying output
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, entry.options);
break;
case 'setup':
const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`);
// Run the code in a non-interactive state with all output thrown away
await mainWebR.evalRVoid(`${cellCode}`);
break;
default:
break;
}

if (['output', 'setup'].includes(evalType)) {
// Disable further global status updates
const activeContainer = document.getElementById(`qwebr-non-interactive-loading-container-${qwebrCounter}`);
// Disable visibility
activeContainer.style.visibility = 'hidden';
activeContainer.style.display = 'none';
}
}
}
).then(
() => {
// Release document status as ready

if (qwebrShowStartupMessage) {
qwebrStartupMessage.innerText = "🟢 Ready!"
}

qwebrSetInteractiveButtonState(
`<i class="fa-solid fa-play qwebr-icon-run-code"></i> <span>Run Code</span>`,
true
);
}
);
Loading

0 comments on commit 2e98d42

Please sign in to comment.