Difference between revisions of "Widget:CodeExplorer"
From Coder Merlin
(41 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
<noinclude> | <noinclude> | ||
Parameters: | 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) | ;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) | ;height: integer|string: percentage (as string, e.g. "100%" or integer size in pixels), null for no change (~10 lines) | ||
Line 7: | Line 11: | ||
;theme: string: name of theme (which must be loaded via css) | ;theme: string: name of theme (which must be loaded via css) | ||
;readOnly: boolean: true if editing should be disabled | ;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 | ;initialCode: string: initial code to place in editor | ||
Line 13: | Line 17: | ||
<pre> | <pre> | ||
{{#widget:CodeExplorer | {{#widget:CodeExplorer | ||
| | |userName=john-williams | ||
|sessionID=qh0ubrrme911kcg7db0i0ec6lct94h7f | |||
|experienceID=W1020.23 | |||
|codeExplorerGroupID=WTRS-8527 | |||
|exerciseID=10 | |||
|width=null | |width=null | ||
|height=null | |height=null | ||
Line 19: | Line 27: | ||
|theme=vibrant-ink | |theme=vibrant-ink | ||
|readOnly=false | |readOnly=false | ||
| | |language=swift | ||
|initialCode=func sayHello() { | |initialCode=func sayHello() { | ||
print("Hello, World!") | print("Hello, World!") | ||
Line 27: | Line 35: | ||
</noinclude> | </noinclude> | ||
<includeonly> | <includeonly><form action="" id="codeEditorForm<!--{$exerciseID|validate:int}-->"> | ||
<form action="" id="codeEditorForm<!--{$ | |||
<div class="merlin-code-explorer-container"> | <div class="merlin-code-explorer-container"> | ||
<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" /> | ||
<img class="merlin-code-explorer-banner-language-icon" | <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"> | |||
<button class="btn text-light dropdown-toggle py-0" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | |||
<img class="merlin-code-explorer-banner-language-icon" id="codeExplorerIcon<!--{$exerciseID|validate:int}-->" /> | |||
</button> | |||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> | |||
<div class="dropdown-item" onclick="setCurrentLanguage<!--{$exerciseID|validate:int}-->('assembly');">Assembly</div> | |||
<div class="dropdown-item" onclick="setCurrentLanguage<!--{$exerciseID|validate:int}-->('c');">C</div> | |||
<div class="dropdown-item" onclick="setCurrentLanguage<!--{$exerciseID|validate:int}-->('cpp');">C++</div> | |||
<div class="dropdown-item" onclick="setCurrentLanguage<!--{$exerciseID|validate:int}-->('java');">Java</div> | |||
<div class="dropdown-item" onclick="setCurrentLanguage<!--{$exerciseID|validate:int}-->('python');">Python</div> | |||
<div class="dropdown-item" onclick="setCurrentLanguage<!--{$exerciseID|validate:int}-->('swift');">Swift</div> | |||
</div> | |||
</div> | |||
</div> | </div> | ||
<div class="merlin-code-explorer-code-panel"> | <div class="merlin-code-explorer-code-panel"> | ||
<textarea id="codeEditorTextArea<!--{$ | <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}-->++; | |||
} | |||
// Language and mode | |||
let currentLanguageData<!--{$exerciseID|validate:int}--> = ""; | |||
function setCurrentLanguage<!--{$exerciseID|validate:int}-->(language) { | |||
let languageData = dataFromLanguage(language); | |||
if (typeof languageData != "object") { | |||
alert("Unexpected language: " + language); | |||
} | |||
let mode = languageData[0]; | |||
let sourceLanguage = languageData[1]; | |||
let sourceFileSuffix = languageData[2]; | |||
let iconURL = languageData[3]; | |||
currentLanguageData<!--{$exerciseID|validate:int}--> = languageData; | |||
codeEditor<!--{$exerciseID|validate:int}-->.setOption("mode", mode); | |||
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; | |||
} | |||
} | |||
// Live theater sync | |||
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 = languageServerURL(); | |||
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'); | |||
}, | |||
success: function(data) { | |||
let sourceLanguage = Object.keys(data.sourceLanguage)[0]; | |||
let contents = data.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'); | |||
}, | |||
timeout: 2500 // Should be less than repeat interval | |||
}); | |||
} | |||
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'); | |||
} | |||
}); | |||
} | |||
} | |||
function markupWarningsAndErrorsHTML(string) { | |||
let html = ""; | |||
if (typeof string == "string") { | |||
let lines = string.split("<br/>"); | |||
for (const line of lines) { | |||
let decoration = "merlin-code-explorer-combined-output-default"; | |||
if (line.match(/warning:/)) { | |||
decoration = "merlin-code-explorer-combined-output-warning"; | |||
} | |||
if (line.match(/error:/)) { | |||
decoration = "merlin-code-explorer-combined-output-error"; | |||
} | |||
html += "<span class='" + decoration + "'>" + line + "</span><br/>"; | |||
} | |||
} | |||
return html; | |||
} | |||
function markupRuntimeStandardOutput(string) { | |||
let html = ""; | |||
let decoration = "merlin-code-explorer-combined-runtime-standard-output"; | |||
if (typeof string == "string") { | |||
let lines = string.split("<br/>"); | |||
for (const line of lines) { | |||
html += "<span class='" + decoration + "'>" + line + "</span><br/>"; | |||
} | |||
} | |||
return html; | |||
} | |||
window.addEventListener('load', (event) => { | window.addEventListener('load', (event) => { | ||
codeEditor<!--{$ | codeEditor<!--{$exerciseID|validate:int}--> = CodeMirror.fromTextArea(document.getElementById('codeEditorTextArea<!--{$exerciseID|validate:int}-->'), | ||
{ | { | ||
keyMap: "emacs", | keyMap: "emacs", | ||
lineNumbers: "<!--{$lineNumbers|validate:boolean}-->", | lineNumbers: "<!--{$lineNumbers|validate:boolean}-->", | ||
theme: "<!--{$theme}-->", | theme: "<!--{$theme}-->", | ||
readOnly: "<!--{$readOnly | readOnly: "<!--{$readOnly}-->" | ||
} | } | ||
); | ); | ||
// Set size | |||
codeEditor<!--{$exerciseID|validate:int}-->.setSize("<!--{$width}-->", "<!--{$height}-->"); | |||
// Set language | |||
setCurrentLanguage<!--{$exerciseID|validate:int}-->("<!--{$language}-->"); | |||
// Determine if CEG-ID is set | |||
const isCEGIDSet = '<!--{$codeExplorerGroupID|strip}-->'.length > 0; | |||
// Disable/enable buttons as appropriate | |||
if (isCEGIDSet) { | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
enableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} 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 | ||
$("#codeEditorForm<!--{$ | $("#codeEditorForm<!--{$exerciseID|validate:int}-->").submit(function(event) { | ||
// Suppress standard submission | // Suppress standard submission | ||
event.preventDefault(); | event.preventDefault(); | ||
// Disable buttons | |||
disableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
disableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
disableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
disableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
// Enable animation | |||
let controlPanel = document.querySelector("#codeEditorControlPanel<!--{$exerciseID|validate:int}-->"); | |||
controlPanel.className = "merlin-code-explorer-control-panel-active shimmer"; | |||
// Clear output | // Clear output | ||
$("# | $("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").empty(); | ||
$("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->").empty(); | |||
// Submit form via POST | // Submit form via POST | ||
let username = "<!--{$userName}-->".toLowerCase(); | |||
"https://language-server-stg.codermerlin.com/ | let sessionID = "<!--{$sessionID}-->" | ||
"https://language-server.codermerlin.com/execute"; | let url = (subdomain() == "stg") ? | ||
"https://language-server-stg.codermerlin.com/" : | |||
"https://language-server.codermerlin.com/"; | |||
switch (event.target.submitter) { | |||
case "execute": | |||
url += "experiences/" + "<!--{$experienceID}-->/" + "exercises/" + "<!--{$exerciseID}-->" + "/executions"; | |||
break; | |||
case "submit": | |||
url += "codeExplorerGroups/" + "<!--{$codeExplorerGroupID}-->/" + "experiences/" + "<!--{$experienceID}-->/" + "exercises/" + "<!--{$exerciseID}-->"; | |||
break; | |||
} | |||
let sourceLanguage = currentLanguageData<!--{$exerciseID|validate:int}-->[1]; | |||
let sourceFileSuffix = currentLanguageData<!--{$exerciseID|validate:int}-->[2]; | |||
let requestObject = { | |||
"sourceLanguage": {}, | |||
"sourceFiles": [], | |||
"executionMode": {"compileAndExecute": {}} | |||
}; | |||
requestObject["sourceLanguage"][sourceLanguage] = {}; | |||
requestObject["sourceFiles"] = [{"path": "main" + "." + sourceFileSuffix, | |||
"contents": codeEditor<!--{$exerciseID|validate:int}-->.getValue()}]; | |||
let requestString = JSON.stringify(requestObject); | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('yellow'); | |||
let response = $.ajax({ | |||
" | type: "POST", | ||
"exerciseID" | url, | ||
headers: { | |||
"username": username, | |||
"sessionID": sessionID | |||
}, | |||
data: requestString, | |||
dataType: "json", | |||
contentType : "application/json", | |||
timeout: 30000, | |||
error: function(jqXHR, textStatus, errorThrown) { | |||
// Display error | |||
if (response.status == 503) { | |||
$("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").append("<span class='merlin-code-explorer-combined-output-warning'>" + | |||
"Sorry, server too busy. Please try again soon." + | |||
"</span><br/>"); | |||
} else { | |||
$("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").append("<span class='merlin-code-explorer-combined-output-error'>" + | |||
"Internal error: " + textStatus + "<br/>" + errorThrown + "<br/>" + | |||
"</span><br/>"); | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('red'); | |||
} | |||
}, | |||
success: function(data) { | |||
switch (event.target.submitter) { | |||
case "execute": | |||
let compilationStatus = data.compilationStatus; | |||
let compilationOutput = compilationStatus.standardOutput; | |||
let compilationError = compilationStatus.standardError; | |||
if (compilationStatus.timedOut) { | |||
compilationError += "error: timed out\n"; | |||
} | |||
$("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->"). | |||
append(markupWarningsAndErrorsHTML(consoleToHTML(compilationError))); | |||
$("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->"). | |||
append(markupWarningsAndErrorsHTML(consoleToHTML(compilationOutput))); | |||
let executionStatus = data.executionStatus; | |||
let executionOutput = (typeof executionStatus == "object") ? executionStatus.standardOutput : ""; | |||
let executionError = (typeof executionStatus == "object") ? executionStatus.standardError : ""; | |||
if (typeof executionStatus == "object" && executionStatus.timedOut) { | |||
executionError += "error: timed out\n"; | |||
} | |||
$("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->").append(consoleToHTML(executionError)); | |||
$("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->"). | |||
append(markupRuntimeStandardOutput(consoleToHTML(executionOutput))); | |||
// Set compilation display | |||
const $button = $("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-show-compilation-button"); | |||
const $output = $button.parent().siblings(".merlin-code-explorer-combined-output:first"); | |||
if (compilationStatus.terminationStatus == 0) { | |||
$output.slideUp(); | |||
$button.text("Show Compilation Output"); | |||
} else { | |||
$output.slideDown(); | |||
$button.text("Hide Compilation Output"); | |||
} | |||
break; | |||
case "submit": | |||
let standardOutput = (typeof data == "object") ? data.standardOutput : ""; | |||
let standardError = (typeof data == "object" && typeof data.standardError == "string") ? "error: " + data.standardError : ""; | |||
if (typeof data == "object" && data.timedOut) { | |||
standardError += "error: timed out\n"; | |||
} | |||
$("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->").append(consoleToHTML(standardOutput)); | |||
$("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->"). | |||
append(markupWarningsAndErrorsHTML(consoleToHTML(standardError))); | |||
break; | |||
} // switch | |||
setStatusIndicator<!--{$exerciseID|validate:int}-->('green'); | |||
}, // success | |||
complete: function(jqXHR, textStatus, errorThrown) { | |||
// Re-enable buttons | |||
enableExecuteButton<!--{$exerciseID|validate:int}-->(); | |||
if (isCEGIDSet) { | |||
enableSubmitButton<!--{$exerciseID|validate:int}-->(); | |||
enableSyncButton<!--{$exerciseID|validate:int}-->(); | |||
enableBroadcastButton<!--{$exerciseID|validate:int}-->(); | |||
} | } | ||
if ( | |||
// Disable animation | |||
controlPanel.className = "merlin-code-explorer-control-panel"; | |||
} | |||
}); // ajax | |||
}); // submit function | |||
// Show Compilation button | |||
$("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-show-compilation-button").click(function () { | |||
const $output = $(this).parent().siblings(".merlin-code-explorer-combined-output:first"); | |||
if ($output.is(":visible")) { | |||
$output.slideUp(); | |||
$(this).text("Show Compilation Output"); | |||
} else { | |||
$output.slideDown(); | |||
$(this).text("Hide Compilation Output"); | |||
} | |||
}); | |||
// 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> | |||
</div> | </div> | ||
<div class="merlin-code-explorer-control-panel"> | <div id="codeEditorControlPanel<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-control-panel"> | ||
<input class="merlin-code-explorer- | <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-button merlin-code-explorer-submit-button" onclick="this.form.submitter = 'submit';" type="submit" value="Submit"/> | |||
<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=" | <div id="codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-combined-output"></div> | ||
<div id="codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-combined-output"></div> | |||
</div> | </div> | ||
</form> | </form> | ||
</includeonly> | </includeonly> |
Latest revision as of 08:06, 9 March 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!") } }}