Difference between revisions of "Widget:CodeExplorer"

From Coder Merlin
 
(24 intermediate revisions by 2 users not shown)
Line 5: Line 5:
;experienceID: string: The experienceID of the page from which the widget is invoked
;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.
;codeExplorerGroupID: string: The code explorer group.  If empty, the submit button will be disabled.
;uniqueID: integer: id for editor, must be unique per page
;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 11: 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
;mode: string: language for highlighting (which must be loaded via js)
;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 21: Line 21:
|experienceID=W1020.23
|experienceID=W1020.23
|codeExplorerGroupID=WTRS-8527
|codeExplorerGroupID=WTRS-8527
|uniqueID=10
|exerciseID=10
|width=null
|width=null
|height=null
|height=null
Line 27: Line 27:
|theme=vibrant-ink
|theme=vibrant-ink
|readOnly=false
|readOnly=false
|mode=swift
|language=swift
|initialCode=func sayHello() {
|initialCode=func sayHello() {
     print("Hello, World!")
     print("Hello, World!")
Line 35: Line 35:
</noinclude>
</noinclude>


<includeonly>
<includeonly><form action="" id="codeEditorForm<!--{$exerciseID|validate:int}-->">
<form action="" id="codeEditorForm<!--{$uniqueID|validate:int}-->">
     <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" />
         <span class="merlin-code-explorer-banner-text">CoderMerlin™ Code Explorer</span>
         <span class="merlin-code-explorer-banner-text">CoderMerlin™ Code Explorer: <!--{$experienceID}--> (<!--{$exerciseID}-->)</span>
         <img class="merlin-code-explorer-banner-language-icon" src="/wiki/images/thumb/3/3b/Swift-og.png/600px-Swift-og.png" />
         <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<!--{$uniqueID|validate:int}-->"><!--{$initialCode}--></textarea>
         <textarea id="codeEditorTextArea<!--{$exerciseID|validate:int}-->"><!--{$initialCode}--></textarea>
         <script>
         <script>
             var codeEditor<!--{$uniqueID|validate:int}-->;
             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<!--{$uniqueID|validate:int}--> = CodeMirror.fromTextArea(document.getElementById('codeEditorTextArea<!--{$uniqueID|validate:int}-->'),
                 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}-->"
                        mode: "<!--{$mode}-->"
                     }
                     }
                 );
                 );
                 // Set size
                 // Set size
                 codeEditor<!--{$uniqueID|validate:int}-->.setSize("<!--{$width}-->", "<!--{$height}-->");
                 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;


                 // Determine if submit button is enabled, if not, disable
                 // Disable/enable buttons as appropriate
                var isSubmitEnabled = '<!--{$codeExplorerGroupID|strip}-->'.length > 0;
                if (isCEGIDSet) {
                if (isSubmitEnabled) {
                    enableSubmitButton<!--{$exerciseID|validate:int}-->();
                     var submitButton = $("#codeEditorSubmitButton<!--{$uniqueID|validate:int}-->");
                     enableSyncButton<!--{$exerciseID|validate:int}-->();
                     submitButton.prop("value", "Submit to <!--{$codeExplorerGroupID|strip}-->");              
                     enableBroadcastButton<!--{$exerciseID|validate:int}-->();
                 } else {
                 } else {
                     var submitButton = $("#codeEditorSubmitButton<!--{$uniqueID|validate:int}-->");
                     disableSubmitButton<!--{$exerciseID|validate:int}-->();
                     submitButton.attr("disabled", true);
                     disableSyncButton<!--{$exerciseID|validate:int}-->();
                     submitButton.css("background-color", "gray");
                     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<!--{$uniqueID|validate:int}-->").submit(function(event) {
                 $("#codeEditorForm<!--{$exerciseID|validate:int}-->").submit(function(event) {


                     // Suppress standard submission                                                                                                                                                                                                                                                         
                     // Suppress standard submission                                                                                                                                                                                                                                                         
Line 78: Line 265:


                     // Disable buttons
                     // Disable buttons
                     var executeButton = $("#codeEditorExecuteButton<!--{$uniqueID|validate:int}-->");
                     disableExecuteButton<!--{$exerciseID|validate:int}-->();
                     var submitButton = $("#codeEditorSubmitButton<!--{$uniqueID|validate:int}-->");
                     disableSubmitButton<!--{$exerciseID|validate:int}-->();
                     var executeButtonBackgroundColor = executeButton.css("background-color");
                     disableSyncButton<!--{$exerciseID|validate:int}-->();
                     var submitButtonBackgroundColor = submitButton.css("background-color");
                     disableBroadcastButton<!--{$exerciseID|validate:int}-->();
                     executeButton.attr("disabled", true);
 
                     executeButton.css("background-color", "gray");
                     // Enable animation
                     submitButton.attr("disabled", true);
                     let controlPanel = document.querySelector("#codeEditorControlPanel<!--{$exerciseID|validate:int}-->");
                    submitButton.css("background-color", "gray");
                     controlPanel.className = "merlin-code-explorer-control-panel-active shimmer";


                     // Clear output
                     // Clear output
                     $("#codeEditorCombinedOutput<!--{$uniqueID|validate:int}-->").empty();
                     $("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").empty();
                    $("#codeEditorExecutionOutput<!--{$exerciseID|validate:int}-->").empty();


                     // Submit form via POST   
                     // Submit form via POST   
                     var url = (subdomain() == "stg") ?  
                     let username = "<!--{$userName}-->".toLowerCase();
                    let sessionID =  "<!--{$sessionID}-->"
                    let url = (subdomain() == "stg") ?  
                         "https://language-server-stg.codermerlin.com/" :
                         "https://language-server-stg.codermerlin.com/" :
                         "https://language-server.codermerlin.com/";  
                         "https://language-server.codermerlin.com/";  
                     switch (event.target.submitter) {
                     switch (event.target.submitter) {
                         case "execute":
                         case "execute":
                             url += "execution/" + "experience/" + "<!--{$experienceID}-->/" + "unique/" + "<!--{$uniqueID}-->";
                             url += "experiences/" + "<!--{$experienceID}-->/" + "exercises/" + "<!--{$exerciseID}-->" + "/executions";
                             break;
                             break;
                         case "submit":
                         case "submit":
                             url += "codeExplorerGroup/" + "id/" + "<!--{$codeExplorerGroupID}-->/" + "experience/" + "<!--{$experienceID}-->/" + "unique/" + "<!--{$uniqueID}-->";
                             url += "codeExplorerGroups/" + "<!--{$codeExplorerGroupID}-->/" + "experiences/" + "<!--{$experienceID}-->/" + "exercises/" + "<!--{$exerciseID}-->";
                             break;
                             break;
                     }
                     }
                    console.log(url);
   
   
                     var response = $.post(url, {
                     let sourceLanguage = currentLanguageData<!--{$exerciseID|validate:int}-->[1];
                         "username": "<!--{$userName}-->",
                    let sourceFileSuffix = currentLanguageData<!--{$exerciseID|validate:int}-->[2];
                        "sessionID": "<!--{$sessionID}-->",
                    let requestObject = {
                        "sourceCodeLanguage": "swift",
                        "sourceLanguage": {},
                        "sourceCode": codeEditor<!--{$uniqueID|validate:int}-->.getValue()
                        "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",
                        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";
                                    }


                    // Collect response data                                                                                                                                                                                                                                                             
                                    $("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").
                    response.done(function(data) {
                                        append(markupWarningsAndErrorsHTML(consoleToHTML(compilationError)));
                        var executionCodeResponse = $(data)[0];
                                    $("#codeEditorCompilerOutput<!--{$exerciseID|validate:int}-->").
                        var standardError = consoleToHTML(executionCodeResponse.standardError);
                                        append(markupWarningsAndErrorsHTML(consoleToHTML(compilationOutput)));
                        var standardOutput = consoleToHTML(executionCodeResponse.standardOutput);
                        var timedOut = executionCodeResponse.timedOut;


                        // If timed out, append line to standardError
                                    let executionStatus = data.executionStatus;
                        if (timedOut) {
                                    let executionOutput = (typeof executionStatus == "object") ? executionStatus.standardOutput : "";
                            standardError += "error: timed out<br/>";
                                    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)));


                        var standardErrorLines = standardError.split("<br/>");
                                    // Set compilation display
                        var errorClass = "merlin-code-explorer-combined-output-error";
                                    const $button = $("#codeEditorForm<!--{$exerciseID|validate:int}--> .merlin-code-explorer-show-compilation-button");
                        for (const errorLine of standardErrorLines) {
                                    const $output = $button.parent().siblings(".merlin-code-explorer-combined-output:first");
                            if (errorLine.match(/warning:/)) {
                                    if (compilationStatus.terminationStatus == 0) {
                                errorClass = "merlin-code-explorer-combined-output-warning";
                                        $output.slideUp();
                            };
                                        $button.text("Show Compilation Output");
                            if (errorLine.match(/error:/)) {
                                    } else {
                                errorClass = "merlin-code-explorer-combined-output-error";
                                        $output.slideDown();
                            };
                                        $button.text("Hide Compilation Output");
                            $("#codeEditorCombinedOutput<!--{$uniqueID|validate:int}-->").append("<span class='" + errorClass + "'>" + errorLine + "</span><br/>");
                                    }
                         };
                                    break;
                        $("#codeEditorCombinedOutput<!--{$uniqueID|validate:int}-->").append(standardOutput);
                                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}-->();
                            }


                        // Re-enable buttons
                            // Disable animation
                        executeButton.attr("disabled", false);
                            controlPanel.className = "merlin-code-explorer-control-panel";
                        executeButton.css("background-color", executeButtonBackgroundColor);
                        if (isSubmitEnabled) {
                            submitButton.attr("disabled", false);
                            submitButton.css("background-color", submitButtonBackgroundColor);
                         }
                         }
                      });
                    }); // 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>
    </script>
     </div>
     </div>
     <div class="merlin-code-explorer-control-panel">
     <div id="codeEditorControlPanel<!--{$exerciseID|validate:int}-->" class="merlin-code-explorer-control-panel">
         <input id="codeEditorExecuteButton<!--{$uniqueID|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<!--{$uniqueID|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-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&nbsp;&nbsp;<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&nbsp;&nbsp;<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="codeEditorCombinedOutput<!--{$uniqueID|validate:int}-->" class="merlin-code-explorer-combined-output"></div>
     <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!")
}
}}