Difference between revisions of "Widget:CodeExplorer"
From Coder Merlin
Line 40: | Line 40: | ||
<div class="merlin-code-explorer-banner"> | <div class="merlin-code-explorer-banner"> | ||
<img class="merlin-code-explorer-banner-merlin-icon" src="/wiki/resources/assets/MerlinRoundIcon.png" /> | <img class="merlin-code-explorer-banner-merlin-icon" src="/wiki/resources/assets/MerlinRoundIcon.png" /> | ||
<span class="merlin-code-explorer-banner-text">CoderMerlin™ Code Explorer: <!--{$experienceID}--> (<!--{$exerciseID}-->)</span> | <span class="merlin-code-explorer-banner-text">CoderMerlin™ Code Explorer: <!--{$experienceID}--> (<!--{$exerciseID}-->)</span> | ||
<span class="merlin-code-explorer-banner-text"><!--{$codeExplorerGroupID|strip}--></span> | |||
<span id="codeEditorStatusIndicator<!--{$exerciseID|validate:int}-->">🟢</span> | |||
<div class="dropdown"> | <div class="dropdown"> | ||
<button class="btn text-light dropdown-toggle py-0" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | <button class="btn text-light dropdown-toggle py-0" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||
Line 58: | Line 60: | ||
<textarea id="codeEditorTextArea<!--{$exerciseID|validate:int}-->"><!--{$initialCode}--></textarea> | <textarea id="codeEditorTextArea<!--{$exerciseID|validate:int}-->"><!--{$initialCode}--></textarea> | ||
<script> | <script> | ||
let codeEditor<!--{$exerciseID|validate:int}-->; | |||
// Preserved background colors | |||
let executeButtonBackgroundColor<!--{$exerciseID|validate:int}-->; | |||
let submitButtonBackgroundColor<!--{$exerciseID|validate:int}-->; | |||
let syncButtonBackgroundColor<!--{$exerciseID|validate:int}-->; | |||
let broadcastButtonBackgroundColor<!--{$exerciseID|validate:int}-->; | |||
// Broadcast | |||
let wasChangedSinceBroadcast<!--{$exerciseID|validate:int}--> = 1; // We start at one to ensure that even an empty file is broadcast | |||
function onChange<!--{$exerciseID|validate:int}-->(codeMirrorInstance, changeObject) { | |||
wasChangedSinceBroadcast<!--{$exerciseID|validate:int}-->++; | |||
} | |||
let currentLanguageData<!--{$exerciseID|validate:int}--> = ""; | let currentLanguageData<!--{$exerciseID|validate:int}--> = ""; | ||
function setCurrentLanguage<!--{$exerciseID|validate:int}-->(language) { | function setCurrentLanguage<!--{$exerciseID|validate:int}-->(language) { | ||
Line 72: | Line 88: | ||
codeEditor<!--{$exerciseID|validate:int}-->.setOption("mode", mode); | codeEditor<!--{$exerciseID|validate:int}-->.setOption("mode", mode); | ||
document.getElementById("codeExplorerIcon<!--{$exerciseID|validate:int}-->").src = iconURL; | document.getElementById("codeExplorerIcon<!--{$exerciseID|validate:int}-->").src = iconURL; | ||
} | |||
// Status indicator | |||
function setStatusIndicator<!--{$exerciseID|validate:int}-->(status) { | |||
let indicator = $("#codeEditorStatusIndicator<!--{$exerciseID|validate:int}-->").get(0); | |||
switch (status) { | |||
case "red": | |||
indicator.innerText = "🔴" | |||
break; | |||
case "yellow": | |||
indicator.innerText = "🟡" | |||
break; | |||
case "green": | |||
indicator.innerText = "🟢" | |||
break; | |||
} | |||
} | |||
function syncToLiveTheater<!--{$exerciseID|validate:int}-->() { | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('yellow'); | |||
// Clear output | |||
$("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").empty(); | |||
$("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->").empty(); | |||
// Get results | |||
let username = "<!--{$userName}-->".toLowerCase(); | |||
let sessionID = "<!--{$sessionID}-->" | |||
let url = (subdomain() == "stg") ? | |||
"https://language-server-stg.codermerlin.com/" : | |||
"https://language-server.codermerlin.com/"; | |||
url += "codeExplorerGroups/" + "<!--{$codeExplorerGroupID|strip}-->/"; | |||
url += "experiences/" + "<!--{$experienceID}-->/" + "exercises/" + "<!--{$exerciseID}-->" + "/broadcast"; | |||
let response = $.ajax({ | |||
type: "GET", | |||
url, | |||
headers: { | |||
"username": username, | |||
"sessionID": sessionID | |||
}, | |||
dataType: "json", | |||
error: function(xmlhttprequest, textstatus, message) { | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('red'); | |||
}, | |||
timeout: 2500 // Should be less than repeat interval | |||
}); | |||
response.done(function(responseObject) { | |||
let sourceLanguage = Object.keys(responseObject.sourceLanguage)[0]; | |||
let contents = responseObject.sourceFiles[0].contents; | |||
// Set language | |||
setCurrentLanguage<!--{$exerciseID|validate:int}-->(sourceLanguage); | |||
// Set content (currently only first file) | |||
codeEditor<!--{$exerciseID|validate:int}-->.setValue(contents); | |||
// Set status | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('green'); | |||
}); | |||
} | |||
function broadcastToLiveTheater<!--{$exerciseID|validate:int}-->() { | |||
// Only broadcast if changes have occurred since the previous broadcast | |||
if (wasChangedSinceBroadcast<!--{$exerciseID|validate:int}--> > 0) { | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('yellow'); | |||
let username = "<!--{$userName}-->".toLowerCase(); | |||
let sessionID = "<!--{$sessionID}-->" | |||
let url = (subdomain() == "stg") ? | |||
"https://language-server-stg.codermerlin.com/" : | |||
"https://language-server.codermerlin.com/"; | |||
url += "codeExplorerGroups/" + "<!--{$codeExplorerGroupID|strip}-->/"; | |||
url += "experiences/" + "<!--{$experienceID}-->/" + "exercises/" + "<!--{$exerciseID}-->" + "/broadcast"; | |||
let sourceLanguage = currentLanguageData<!--{$exerciseID|validate:int}-->[1]; | |||
let sourceFileSuffix = currentLanguageData<!--{$exerciseID|validate:int}-->[2]; | |||
let requestObject = { | |||
"sourceLanguage": {}, | |||
"sourceFiles": [] | |||
}; | |||
requestObject["sourceLanguage"][sourceLanguage] = {}; | |||
requestObject["sourceFiles"] = [{"path": "main" + "." + sourceFileSuffix, | |||
"contents": codeEditor<!--{$exerciseID|validate:int}-->.getValue()}]; | |||
let requestString = JSON.stringify(requestObject); | |||
let response = $.ajax({ | |||
type: "POST", | |||
url, | |||
headers: { | |||
"username": username, | |||
"sessionID": sessionID | |||
}, | |||
data: requestString, | |||
dataType: "json", | |||
contentType : "application/json", | |||
timeout: 2500, // Should be shorter than refresh interval | |||
error: function(xmlhttprequest, textstatus, message) { | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('red'); | |||
}, | |||
success: function(data) { | |||
// Clear changes (only if successful) | |||
wasChangedSinceBroadcast<!--{$exerciseID|validate:int}--> = 0; | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('green'); | |||
} | |||
}); | |||
} | |||
} | } | ||
Line 104: | Line 228: | ||
} | } | ||
window.addEventListener('load', (event) => { | window.addEventListener('load', (event) => { | ||
codeEditor<!--{$exerciseID|validate:int}--> = CodeMirror.fromTextArea(document.getElementById('codeEditorTextArea<!--{$exerciseID|validate:int}-->'), | codeEditor<!--{$exerciseID|validate:int}--> = CodeMirror.fromTextArea(document.getElementById('codeEditorTextArea<!--{$exerciseID|validate:int}-->'), | ||
Line 114: | Line 237: | ||
} | } | ||
); | ); | ||
// Set size | // Set size | ||
codeEditor<!--{$exerciseID|validate:int}-->.setSize("<!--{$width}-->", "<!--{$height}-->"); | codeEditor<!--{$exerciseID|validate:int}-->.setSize("<!--{$width}-->", "<!--{$height}-->"); | ||
// Set language | // Set language | ||
setCurrentLanguage<!--{$exerciseID|validate:int}-->("<!--{$language}-->"); | setCurrentLanguage<!--{$exerciseID|validate:int}-->("<!--{$language}-->"); | ||
// Determine if | // Determine if CEG-ID is set | ||
const isCEGIDSet = '<!--{$codeExplorerGroupID|strip}-->'.length > 0; | |||
if ( | |||
// Disable/enable buttons as appropriate | |||
if (isCEGIDSet) { | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
enableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} else { | } else { | ||
disableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
disableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
disableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} | } | ||
// Monitor events | |||
codeEditor<!--{$exerciseID|validate:int}-->.on("change", onChange<!--{$exerciseID|validate:int}-->); | |||
// Attach handler to form | // Attach handler to form | ||
Line 137: | Line 268: | ||
// Disable buttons | // Disable buttons | ||
disableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
disableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
disableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
disableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
// Enable animation | // Enable animation | ||
Line 205: | Line 332: | ||
// Re-enable buttons | // Re-enable buttons | ||
enableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
if (isCEGIDSet) { | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
enableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} | } | ||
Line 268: | Line 395: | ||
// Re-enable buttons | // Re-enable buttons | ||
enableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
if (isCEGIDSet) { | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
enableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} | } | ||
Line 279: | Line 406: | ||
}); | }); | ||
}); | }); | ||
// Show Compilation | |||
$("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-show-compilation-button").click(function () { | $("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-show-compilation-button").click(function () { | ||
const $output = $(this).parent().siblings(".merlin-code-explorer-combined-output:first"); | const $output = $(this).parent().siblings(".merlin-code-explorer-combined-output:first"); | ||
Line 289: | Line 418: | ||
} | } | ||
}); | }); | ||
// Broadcast button | |||
function enableBroadcastButton<!--{$exerciseID|validate:int}-->() { | |||
let broadcastButton = $("#codeEditorBroadcastButton<!--{$exerciseID|validate:int}-->"); | |||
broadcastButton.attr("disabled", false); | |||
if (broadcastButtonBackgroundColor<!--{$exerciseID|validate:int}--> != undefined) { | |||
broadcastButton.css("background-color", broadcastButtonBackgroundColor<!--{$exerciseID|validate:int}-->); | |||
} | |||
} | |||
function disableBroadcastButton<!--{$exerciseID|validate:int}-->() { | |||
let broadcastButton = $("#codeEditorBroadcastButton<!--{$exerciseID|validate:int}-->"); | |||
// Save original background color | |||
if (broadcastButtonBackgroundColor<!--{$exerciseID|validate:int}--> == undefined) { | |||
broadcastButtonBackgroundColor<!--{$exerciseID|validate:int}--> = broadcastButton.css("background-color"); | |||
} | |||
broadcastButton.attr("disabled", true); | |||
broadcastButton.css("background-color", "gray"); | |||
} | |||
// Sync button | |||
function enableSyncButton<!--{$exerciseID|validate:int}-->() { | |||
let syncButton = $("#codeEditorSyncButton<!--{$exerciseID|validate:int}-->"); | |||
syncButton.attr("disabled", false); | |||
if (syncButtonBackgroundColor<!--{$exerciseID|validate:int}--> != undefined) { | |||
syncButton.css("background-color", syncButtonBackgroundColor<!--{$exerciseID|validate:int}-->); | |||
} | |||
} | |||
function disableSyncButton<!--{$exerciseID|validate:int}-->() { | |||
let syncButton = $("#codeEditorSyncButton<!--{$exerciseID|validate:int}-->"); | |||
// Save original background color | |||
if (syncButtonBackgroundColor<!--{$exerciseID|validate:int}--> == undefined) { | |||
syncButtonBackgroundColor<!--{$exerciseID|validate:int}--> = syncButton.css("background-color"); | |||
} | |||
syncButton.attr("disabled", true); | |||
syncButton.css("background-color", "gray"); | |||
} | |||
// Execute button | |||
function enableExecuteButton<!--{$exerciseID|validate:int}-->() { | |||
let executeButton = $("#codeEditorExecuteButton<!--{$exerciseID|validate:int}-->"); | |||
executeButton.attr("disabled", false); | |||
if (executeButtonBackgroundColor<!--{$exerciseID|validate:int}--> != undefined) { | |||
executeButton.css("background-color", executeButtonBackgroundColor<!--{$exerciseID|validate:int}-->); | |||
} | |||
} | |||
function disableExecuteButton<!--{$exerciseID|validate:int}-->() { | |||
let executeButton = $("#codeEditorExecuteButton<!--{$exerciseID|validate:int}-->"); | |||
// Save original background color | |||
if (executeButtonBackgroundColor<!--{$exerciseID|validate:int}--> == undefined) { | |||
executeButtonBackgroundColor<!--{$exerciseID|validate:int}--> = executeButton.css("background-color"); | |||
} | |||
executeButton.attr("disabled", true); | |||
executeButton.css("background-color", "gray"); | |||
} | |||
// Submit button | |||
function disableSubmitButton<!--{$exerciseID|validate:int}-->() { | |||
let submitButton = $("#codeEditorSubmitButton<!--{$exerciseID|validate:int}-->"); | |||
// Save original background color | |||
if (submitButtonBackgroundColor<!--{$exerciseID|validate:int}--> == undefined) { | |||
submitButtonBackgroundColor<!--{$exerciseID|validate:int}--> = submitButton.css("background-color"); | |||
} | |||
submitButton.attr("disabled", true); | |||
submitButton.css("background-color", "gray"); | |||
} | |||
function enableSubmitButton<!--{$exerciseID|validate:int}-->() { | |||
let submitButton = $("#codeEditorSubmitButton<!--{$exerciseID|validate:int}-->"); | |||
submitButton.attr("disabled", false); | |||
if (submitButtonBackgroundColor<!--{$exerciseID|validate:int}--> != undefined) { | |||
submitButton.css("background-color", submitButtonBackgroundColor<!--{$exerciseID|validate:int}-->); | |||
} | |||
submitButton.prop("value", "Submit to <!--{$codeExplorerGroupID|strip}-->"); | |||
} | |||
// Live Theater button | |||
let isLiveTheaterSyncActive = false; | |||
let liveTheaterSyncIntervalId = 0; | |||
function turnOnLiveTheater<!--{$exerciseID|validate:int}-->() { | |||
if (isLiveTheaterSyncActive) { | |||
console.error("turnOnLiveTheater() invoked when already on."); | |||
return; | |||
} | |||
let syncIcon = $("#codeEditorSyncIcon<!--{$exerciseID|validate:int}-->").get(0); | |||
syncIcon.classList.add("spin"); | |||
syncToLiveTheater<!--{$exerciseID|validate:int}-->(); | |||
liveTheaterSyncIntervalId = setInterval(syncToLiveTheater<!--{$exerciseID|validate:int}-->, 3000); | |||
isLiveTheaterSyncActive = true; | |||
disableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
disableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
disableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} | |||
function turnOffLiveTheater<!--{$exerciseID|validate:int}-->() { | |||
if (!isLiveTheaterSyncActive) { | |||
console.error("turnOffLiveTheater() invoked when already off."); | |||
return; | |||
} | |||
let syncIcon = $("#codeEditorSyncIcon<!--{$exerciseID|validate:int}-->").get(0); | |||
syncIcon.classList.remove("spin"); | |||
clearInterval(liveTheaterSyncIntervalId); | |||
isLiveTheaterSyncActive = false; | |||
enableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('green'); | |||
} | |||
$("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-live-theater-sync-button").click(function () { | |||
if (isLiveTheaterSyncActive) { | |||
turnOffLiveTheater<!--{$exerciseID|validate:int}-->(); | |||
} else { | |||
turnOnLiveTheater<!--{$exerciseID|validate:int}-->(); | |||
} | |||
}); | |||
// Live Broadcast button | |||
let isLiveTheaterBroadcastActive = false; | |||
let liveTheaterBroadcastIntervalId = 0; | |||
function turnOnLiveBroadcast<!--{$exerciseID|validate:int}-->() { | |||
if (isLiveTheaterBroadcastActive) { | |||
console.error("turnOnLiveBroadcast() invoked when already on."); | |||
return; | |||
} | |||
let broadcastIcon = $("#codeEditorBroadcastIcon<!--{$exerciseID|validate:int}-->").get(0); | |||
broadcastIcon.classList.add("pulse"); | |||
broadcastToLiveTheater<!--{$exerciseID|validate:int}-->(); | |||
liveTheaterBroadcastIntervalId = setInterval(broadcastToLiveTheater<!--{$exerciseID|validate:int}-->, 3000); | |||
isLiveTheaterBroadcastActive = true; | |||
disableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
disableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
disableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
} | |||
function turnOffLiveBroadcast<!--{$exerciseID|validate:int}-->() { | |||
if (!isLiveTheaterBroadcastActive) { | |||
console.error("turnOffLiveBroadcast() invoked when already off."); | |||
return; | |||
} | |||
let broadcastIcon = $("#codeEditorBroadcastIcon<!--{$exerciseID|validate:int}-->").get(0); | |||
broadcastIcon.classList.remove("pulse"); | |||
clearInterval(liveTheaterBroadcastIntervalId); | |||
isLiveTheaterBroadcastActive = false; | |||
enableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('green'); | |||
} | |||
$("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-live-theater-broadcast-button").click(function () { | |||
if (isLiveTheaterBroadcastActive) { | |||
turnOffLiveBroadcast<!--{$exerciseID|validate:int}-->(); | |||
} else { | |||
turnOnLiveBroadcast<!--{$exerciseID|validate:int}-->(); | |||
} | |||
}); | |||
}); | }); | ||
</script> | </script> | ||
</div> | </div> | ||
<div id="codeEditorControlPanel<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-control-panel"> | <div id="codeEditorControlPanel<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-control-panel"> | ||
<input id="codeEditorExecuteButton<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-execute-button" onclick="this.form.submitter = 'execute';" type="submit" value="Run" /> | <input id="codeEditorExecuteButton<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-button merlin-code-explorer-execute-button" onclick="this.form.submitter = 'execute';" type="submit" value="Run" /> | ||
<input id="codeEditorSubmitButton<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-submit-button" onclick="this.form.submitter = 'submit';" type="submit" value="Submit"/> | <input id="codeEditorSubmitButton<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-button merlin-code-explorer-submit-button" onclick="this.form.submitter = 'submit';" type="submit" value="Submit"/> | ||
<button class="merlin-code-explorer-show-compilation-button" type="button">Hide Compilation Output</button> | <button class="merlin-code-explorer-button merlin-code-explorer-show-compilation-button" type="button">Hide Compilation Output</button> | ||
<button id="codeEditorSyncButton<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-button merlin-code-explorer-live-theater-sync-button" type="button">Sync <div id="codeEditorSyncIcon<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-live-theater-sync-button-icon"><i class="fa fa-sync"></i></div></button> | |||
<button id="codeEditorBroadcastButton<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-button merlin-code-explorer-live-theater-broadcast-button" type="button">Broadcast <div id="codeEditorBroadcastIcon<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-live-theater-broadcast-button-icon"><i class="fa fa-wifi"></i></div></button> | |||
</div> | </div> | ||
<div id="codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-combined-output"></div> | <div id="codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-combined-output"></div> |
Revision as of 18:11, 2 February 2023
Parameters:
- userName
- string: The current user's username
- sessionID
- string: The ID of the current user's session
- experienceID
- string: The experienceID of the page from which the widget is invoked
- codeExplorerGroupID
- string: The code explorer group. If empty, the submit button will be disabled.
- exerciseID
- integer: exercise id for editor, must be unique per page
- width
- integer|string: percentage (as string, e.g. "100%" or integer size in pixels), null for no change (full width)
- height
- integer|string: percentage (as string, e.g. "100%" or integer size in pixels), null for no change (~10 lines)
- lineNumbers
- boolean: true to display line numbers
- theme
- string: name of theme (which must be loaded via css)
- readOnly
- boolean: true if editing should be disabled
- language
- string: language for compiling and highlighting (which must be loaded via js)
- initialCode
- string: initial code to place in editor
Example:
{{#widget:CodeExplorer |userName=john-williams |sessionID=qh0ubrrme911kcg7db0i0ec6lct94h7f |experienceID=W1020.23 |codeExplorerGroupID=WTRS-8527 |exerciseID=10 |width=null |height=null |lineNumbers=true |theme=vibrant-ink |readOnly=false |language=swift |initialCode=func sayHello() { print("Hello, World!") } }}