Skip to content

Commit 1634d98

Browse files
authored
Add Rhistory tracking (#213)
* Add Rhistory tracking * Redo check on sidebar after attempting to create ... * Rewrite safeFileName to use a date string of YYYY-MM-DD-HH-MM-SS and regex cleared string to avoid special characters.
1 parent 088bbd2 commit 1634d98

7 files changed

+390
-5
lines changed

_extensions/webr/qwebr-cell-initialization.js

+4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ qwebrInstance.then(
7878
break;
7979
case 'setup':
8080
const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`);
81+
82+
// Store code in history
83+
qwebrLogCodeToHistory(cellCode, entry.options);
84+
8185
// Run the code in a non-interactive state with all output thrown away
8286
await mainWebR.evalRVoid(`${cellCode}`);
8387
break;

_extensions/webr/qwebr-compute-engine.js

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ globalThis.qwebrPrefixComment = function(x, comment) {
2424
return `${comment}${x}`;
2525
};
2626

27+
// Function to store the code in the history
28+
globalThis.qwebrLogCodeToHistory = function(codeToRun, options) {
29+
qwebrRCommandHistory.push(
30+
`# Ran code in ${options.label} at ${new Date().toLocaleString()} ----\n${codeToRun}`
31+
);
32+
}
33+
2734
// Function to parse the pager results
2835
globalThis.qwebrParseTypePager = async function (msg) {
2936

@@ -113,6 +120,9 @@ globalThis.qwebrComputeEngine = async function(
113120
captureOutputOptions.captureGraphics = false;
114121
}
115122

123+
// Store the code to run in history
124+
qwebrLogCodeToHistory(codeToRun, options);
125+
116126
// Setup a webR canvas by making a namespace call into the {webr} package
117127
// Evaluate the R code
118128
// Remove the active canvas silently
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Define a global storage and retrieval solution ----
2+
3+
// Store commands executed in R
4+
globalThis.qwebrRCommandHistory = [];
5+
6+
// Function to retrieve the command history
7+
globalThis.qwebrFormatRHistory = function() {
8+
return qwebrRCommandHistory.join("\n\n");
9+
}
10+
11+
// Retrieve HTML Elements ----
12+
13+
// Get the command modal
14+
const command_history_modal = document.getElementById("qwebr-history-modal");
15+
16+
// Get the button that opens the command modal
17+
const command_history_btn = document.getElementById("qwebrRHistoryButton");
18+
19+
// Get the <span> element that closes the command modal
20+
const command_history_close_span = document.getElementById("qwebr-command-history-close-btn");
21+
22+
// Get the download button for r history information
23+
const command_history_download_btn = document.getElementById("qwebr-download-history-btn");
24+
25+
// Plug in command history into modal/download button ----
26+
27+
// Function to populate the modal with command history
28+
function populateCommandHistoryModal() {
29+
document.getElementById("qwebr-command-history-contents").innerHTML = qwebrFormatRHistory() || "No commands have been executed yet.";
30+
}
31+
32+
// Function to format the current date and time to
33+
// a string with the format YYYY-MM-DD-HH-MM-SS
34+
function formatDateTime() {
35+
const now = new Date();
36+
37+
const year = now.getFullYear();
38+
const day = String(now.getDate()).padStart(2, '0');
39+
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based
40+
const hours = String(now.getHours()).padStart(2, '0');
41+
const minutes = String(now.getMinutes()).padStart(2, '0');
42+
const seconds = String(now.getSeconds()).padStart(2, '0');
43+
44+
return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
45+
}
46+
47+
48+
// Function to convert document title with datetime to a safe filename
49+
function safeFileName() {
50+
// Get the current page title
51+
let pageTitle = document.title;
52+
53+
// Combine the current page title with the current date and time
54+
let pageNameWithDateTime = `Rhistory-${pageTitle}-${formatDateTime()}`;
55+
56+
// Replace unsafe characters with safe alternatives
57+
let safeFilename = pageNameWithDateTime.replace(/[\\/:\*\?! "<>\|]/g, '-');
58+
59+
return safeFilename;
60+
}
61+
62+
63+
// Function to download list contents as text file
64+
function downloadRHistory() {
65+
// Get the current page title + datetime and use it as the filename
66+
const filename = `${safeFileName()}.R`;
67+
68+
// Get the text contents of the R History list
69+
const text = qwebrFormatRHistory();
70+
71+
// Create a new Blob object with the text contents
72+
const blob = new Blob([text], { type: 'text/plain' });
73+
74+
// Create a new anchor element for the download
75+
const a = document.createElement('a');
76+
a.style.display = 'none';
77+
a.href = URL.createObjectURL(blob);
78+
a.download = filename;
79+
80+
// Append the anchor to the body, click it, and remove it
81+
document.body.appendChild(a);
82+
a.click();
83+
document.body.removeChild(a);
84+
}
85+
86+
// Register event handlers ----
87+
88+
// When the user clicks the View R History button, open the command modal
89+
command_history_btn.onclick = function() {
90+
populateCommandHistoryModal();
91+
command_history_modal.style.display = "block";
92+
}
93+
94+
// When the user clicks on <span> (x), close the command modal
95+
command_history_close_span.onclick = function() {
96+
command_history_modal.style.display = "none";
97+
}
98+
99+
// When the user clicks anywhere outside of the command modal, close it
100+
window.onclick = function(event) {
101+
if (event.target == command_history_modal) {
102+
command_history_modal.style.display = "none";
103+
}
104+
}
105+
106+
// Add an onclick event listener to the download button so that
107+
// the user can download the R history as a text file
108+
command_history_download_btn.onclick = function() {
109+
downloadRHistory();
110+
};

_extensions/webr/qwebr-document-status.js

+207
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,41 @@ globalThis.qwebrUpdateStatusHeader = function(message) {
2828
<span>${message}</span>`;
2929
}
3030

31+
// Function to return true if element is found, false if not
32+
globalThis.qwebrCheckHTMLElementExists = function(selector) {
33+
const element = document.querySelector(selector);
34+
return !!element;
35+
}
36+
37+
// Function that detects whether reveal.js slides are present
38+
globalThis.qwebrIsRevealJS = function() {
39+
// If the '.reveal .slides' selector exists, RevealJS is likely present
40+
return qwebrCheckHTMLElementExists('.reveal .slides');
41+
}
42+
43+
// Initialize the Quarto sidebar element
44+
function qwebrSetupQuartoSidebar() {
45+
var newSideBarDiv = document.createElement('div');
46+
newSideBarDiv.id = 'quarto-margin-sidebar';
47+
newSideBarDiv.className = 'sidebar margin-sidebar';
48+
newSideBarDiv.style.top = '0px';
49+
newSideBarDiv.style.maxHeight = 'calc(0px + 100vh)';
50+
51+
return newSideBarDiv;
52+
}
53+
54+
// Position the sidebar in the document
55+
function qwebrPlaceQuartoSidebar() {
56+
// Get the reference to the element with id 'quarto-document-content'
57+
var referenceNode = document.getElementById('quarto-document-content');
58+
59+
// Create the new div element
60+
var newSideBarDiv = qwebrSetupQuartoSidebar();
61+
62+
// Insert the new div before the 'quarto-document-content' element
63+
referenceNode.parentNode.insertBefore(newSideBarDiv, referenceNode);
64+
}
65+
3166
function qwebrPlaceMessageContents(content, html_location = "title-block-header", revealjs_location = "title-slide") {
3267

3368
// Get references to header elements
@@ -49,6 +84,7 @@ function qwebrPlaceMessageContents(content, html_location = "title-block-header"
4984
}
5085

5186

87+
5288
function qwebrOffScreenCanvasSupportWarningMessage() {
5389

5490
// Verify canvas is supported.
@@ -154,5 +190,176 @@ function displayStartupMessage(showStartupMessage, showHeaderMessage) {
154190
qwebrPlaceMessageContents(quartoTitleMeta);
155191
}
156192

193+
function qwebrAddCommandHistoryModal() {
194+
// Create the modal div
195+
var modalDiv = document.createElement('div');
196+
modalDiv.id = 'qwebr-history-modal';
197+
modalDiv.className = 'qwebr-modal';
198+
199+
// Create the modal content div
200+
var modalContentDiv = document.createElement('div');
201+
modalContentDiv.className = 'qwebr-modal-content';
202+
203+
// Create the span for closing the modal
204+
var closeSpan = document.createElement('span');
205+
closeSpan.id = 'qwebr-command-history-close-btn';
206+
closeSpan.className = 'qwebr-modal-close';
207+
closeSpan.innerHTML = '&times;';
208+
209+
// Create the h1 element for the modal
210+
var modalH1 = document.createElement('h1');
211+
modalH1.textContent = 'R History Command Contents';
212+
213+
// Create an anchor element for downloading the Rhistory file
214+
var downloadLink = document.createElement('a');
215+
downloadLink.href = '#';
216+
downloadLink.id = 'qwebr-download-history-btn';
217+
downloadLink.className = 'qwebr-download-btn';
218+
219+
// Create an 'i' element for the icon
220+
var icon = document.createElement('i');
221+
icon.className = 'bi bi-file-code';
222+
223+
// Append the icon to the anchor element
224+
downloadLink.appendChild(icon);
225+
226+
// Add the text 'Download R History' to the anchor element
227+
downloadLink.appendChild(document.createTextNode(' Download R History File'));
228+
229+
// Create the pre for command history contents
230+
var commandContentsPre = document.createElement('pre');
231+
commandContentsPre.id = 'qwebr-command-history-contents';
232+
commandContentsPre.className = 'qwebr-modal-content-code';
233+
234+
// Append the close span, h1, and history contents pre to the modal content div
235+
modalContentDiv.appendChild(closeSpan);
236+
modalContentDiv.appendChild(modalH1);
237+
modalContentDiv.appendChild(downloadLink);
238+
modalContentDiv.appendChild(commandContentsPre);
239+
240+
// Append the modal content div to the modal div
241+
modalDiv.appendChild(modalContentDiv);
242+
243+
// Append the modal div to the body
244+
document.body.appendChild(modalDiv);
245+
}
246+
247+
function qwebrRegisterRevealJSCommandHistoryModal() {
248+
// Select the <ul> element inside the <div> with data-panel="Custom0"
249+
let ulElement = document.querySelector('div[data-panel="Custom0"] > ul.slide-menu-items');
250+
251+
// Find the last <li> element with class slide-tool-item
252+
let lastItem = ulElement.querySelector('li.slide-tool-item:last-child');
253+
254+
// Calculate the next data-item value
255+
let nextItemValue = 0;
256+
if (lastItem) {
257+
nextItemValue = parseInt(lastItem.dataset.item) + 1;
258+
}
259+
260+
// Create a new <li> element
261+
let newListItem = document.createElement('li');
262+
newListItem.className = 'slide-tool-item';
263+
newListItem.dataset.item = nextItemValue.toString(); // Set the next available data-item value
264+
265+
// Create the <a> element inside the <li>
266+
let newLink = document.createElement('a');
267+
newLink.href = '#';
268+
newLink.id = 'qwebrRHistoryButton'; // Set the ID for the new link
269+
270+
// Create the <kbd> element inside the <a>
271+
let newKbd = document.createElement('kbd');
272+
newKbd.textContent = ' '; // Set to empty as we are not registering a keyboard shortcut
273+
274+
// Create text node for the link text
275+
let newText = document.createTextNode(' View R History');
276+
277+
// Append <kbd> and text node to <a>
278+
newLink.appendChild(newKbd);
279+
newLink.appendChild(newText);
280+
281+
// Append <a> to <li>
282+
newListItem.appendChild(newLink);
283+
284+
// Append <li> to <ul>
285+
ulElement.appendChild(newListItem);
286+
}
287+
288+
// Handle setting up the R history modal
289+
function qwebrCodeLinks() {
290+
291+
if (qwebrIsRevealJS()) {
292+
qwebrRegisterRevealJSCommandHistoryModal();
293+
return;
294+
}
295+
296+
// Create the container div
297+
var containerDiv = document.createElement('div');
298+
containerDiv.className = 'quarto-code-links';
299+
300+
// Create the h2 element
301+
var h2 = document.createElement('h2');
302+
h2.textContent = 'webR Code Links';
303+
304+
// Create the ul element
305+
var ul = document.createElement('ul');
306+
307+
// Create the li element
308+
var li = document.createElement('li');
309+
310+
// Create the a_history_btn element
311+
var a_history_btn = document.createElement('a');
312+
a_history_btn.href = 'javascript:void(0)';
313+
a_history_btn.setAttribute('id', 'qwebrRHistoryButton');
314+
315+
// Create the i_history_btn element
316+
var i_history_btn = document.createElement('i');
317+
i_history_btn.className = 'bi bi-file-code';
318+
319+
// Create the text node for the link text
320+
var text_history_btn = document.createTextNode('View R History');
321+
322+
// Append the icon element and link text to the a element
323+
a_history_btn.appendChild(i_history_btn);
324+
a_history_btn.appendChild(text_history_btn);
325+
326+
// Append the a element to the li element
327+
li.appendChild(a_history_btn);
328+
329+
// Append the li element to the ul element
330+
ul.appendChild(li);
331+
332+
// Append the h2 and ul elements to the container div
333+
containerDiv.appendChild(h2);
334+
containerDiv.appendChild(ul);
335+
336+
// Append the container div to the element with the ID 'quarto-margin-sidebar'
337+
var sidebar = document.getElementById('quarto-margin-sidebar');
338+
339+
// If the sidebar element is not found, create it
340+
if(!sidebar) {
341+
qwebrPlaceQuartoSidebar();
342+
}
343+
344+
// Re-select the sidebar element (if it was just created)
345+
sidebar = document.getElementById('quarto-margin-sidebar');
346+
347+
348+
// If the sidebar element exists, append the container div to it
349+
if(sidebar) {
350+
// Append the container div to the sidebar
351+
sidebar.appendChild(containerDiv);
352+
} else {
353+
// Get a debugger ...
354+
console.warn('Element with ID "quarto-margin-sidebar" not found.');
355+
}
356+
}
357+
358+
// Call the function to append the code links for qwebR into the right sidebar
359+
qwebrCodeLinks();
360+
361+
// Add the command history modal
362+
qwebrAddCommandHistoryModal();
363+
157364
displayStartupMessage(qwebrShowStartupMessage, qwebrShowHeaderMessage);
158365
qwebrOffScreenCanvasSupportWarningMessage();

0 commit comments

Comments
 (0)