Files
codeocean/app/assets/javascripts/editor/editor.js.erb
Sebastian Serth a97b56f36f Allow removing all event handles in JavaScript
This is useful to remove any handlers, e.g., when submitting an exercise. Hopefully, we will see less Javascript errors due to cumbersome behavior with these changes...

Also, I am cleaning up unused event handlers. The `showOutput` method doesn't seem to be used, we should observe this in the future.

Closes CODEOCEAN-KQ
2023-04-02 18:15:24 +02:00

1052 lines
41 KiB
Plaintext

var CodeOceanEditor = {
//ACE-Editor-Path
// ruby part adds the relative_url_root, if it is set.
ACE_FILES_PATH: '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>',
THEME: 'ace/theme/textmate',
//Color-Encoding for Percentages in Progress Bars (For submissions)
ADEQUATE_PERCENTAGE: 50,
SUCCESSFULL_PERCENTAGE: 90,
//Key-Codes (for Hotkeys)
R_KEY_CODE: 82,
S_KEY_CODE: 83,
T_KEY_CODE: 84,
ENTER_KEY_CODE: 13,
//Request-For-Comments-Configuration
REQUEST_FOR_COMMENTS_DELAY: 0,
REQUEST_TOOLTIP_TIME: 5000,
REQUEST_TOOLTIP_DELAY: 15 * 60 * 1000,
editors: [],
editor_for_file: new Map(),
regex_for_language: new Map(),
tracepositions_regex: undefined,
active_file: undefined,
active_frame: undefined,
running: false,
lastCopyText: null,
<% self.class.include Rails.application.routes.url_helpers %>
<% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
// Important notice: Changing the config values requires any content-wise
// modification for this file in the development environment. Lacking to do so
// will result in the old, server-side cached serving of this file even across
// server restarts!
sendEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>,
eventURL: "<%= @config['codeocean_events'] ? events_path : '' %>",
fileTypeURL: "<%= file_types_path %>",
configureEditors: function () {
_.each(['modePath', 'themePath', 'workerPath'], function (attribute) {
ace.config.set(attribute, this.ACE_FILES_PATH);
}.bind(this));
},
confirmDestroy: function (event) {
event.preventDefault();
if (confirm($(event.target).data('message-confirm'))) {
this.destroyFile();
}
},
confirmReset: function (event) {
event.preventDefault();
const initiator = $(event.target.closest("button"));
if (confirm(initiator.data('message-confirm'))) {
this.resetCode(initiator);
}
},
confirmResetActiveFile: function (event) {
event.preventDefault();
const initiator = $(event.target.closest("button"));
let message = initiator.data('message-confirm');
message = message.replace('%{filename}', CodeOceanEditor.active_file.filename.replace(/#$/, ''))
if (confirm(message)) {
this.resetCode(initiator, true); // delete only active file
}
},
fileActionsAvailable: function () {
return this.isActiveFileRenderable() || this.isActiveFileRunnable() || this.isActiveFileStoppable() || this.isActiveFileTestable();
},
findOrCreateOutputElement: function (index) {
if ($('#output-' + index).isPresent()) {
return $('#output-' + index);
} else {
var element = $('<div class="mb-2 output-element">').attr('id', 'output-' + index);
$('#output').append(element);
return element;
}
},
getCardClass: function (result) {
if (result.file_role === 'teacher_defined_linter') {
return 'card bg-info text-white'
} else if (result.stderr && !result.score) {
return 'card bg-danger text-white';
} else if (result.score < 1) {
return 'card bg-warning text-white';
} else {
return 'card bg-success text-white';
}
},
showOutput: function (event) {
const target = $(event.target).attr('href');
if (target !== "#") {
event.preventDefault();
this.showOutputBar();
$('body').scrollTo($(event.target).attr('href'));
}
},
renderProgressBar: function (score, maximum_score) {
var percentage = score / maximum_score * 100;
var progress_bar = $('#score .progress-bar');
progress_bar.removeClass().addClass(this.getProgressBarClass(percentage));
progress_bar.attr({
'aria-valuemax': maximum_score,
'aria-valuemin': 0,
'aria-valuenow': score
});
progress_bar.css('width', percentage + '%');
},
// The event ready.jstree is fired too early and thus doesn't work.
selectFileInJsTree: function (filetree, file_id) {
if (!filetree.is(':visible'))
// The left sidebar is not shown and thus the filetree is not rendered.
return;
if (!filetree.hasClass('jstree-loading')) {
filetree.jstree("deselect_all");
filetree.jstree().select_node(file_id);
} else {
setTimeout(CodeOceanEditor.selectFileInJsTree.bind(null, filetree, file_id), 250);
}
},
showFirstFile: function (own_solution = false) {
let frame;
let filetree;
let editorSelector;
if (own_solution) {
frame = $('.own-frame[data-role="main_file"]').isPresent() ? $('.own-frame[data-role="main_file"]') : $('.own-frame').first();
filetree = $('#own-files');
editorSelector = '.own-editor';
} else {
frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
filetree = $('#files');
editorSelector = '.editor';
}
var file_id = frame.find(editorSelector).data('file-id');
this.setActiveFile(frame.data('filename'), file_id);
this.selectFileInJsTree(filetree, file_id);
this.showFrame(frame);
this.toggleButtonStates();
},
showFrame: function (frame) {
if (frame.hasClass('own-frame')) {
$('.own-frame').hide();
} else {
$('.frame').hide();
}
this.active_frame = frame;
frame.show();
this.resizeParentOfAceEditor(frame.find('.ace_editor'));
},
getProgressBarClass: function (percentage) {
if (percentage < this.ADEQUATE_PERCENTAGE) {
return 'progress-bar progress-bar-striped bg-danger';
} else if (percentage < this.SUCCESSFULL_PERCENTAGE) {
return 'progress-bar progress-bar-striped bg-warning';
} else {
return 'progress-bar progress-bar-striped bg-success';
}
},
handleKeyPress: function (event) {
if (event.altKey && event.which === this.R_KEY_CODE) {
$('#run').trigger('click');
} else if (event.altKey && event.which === this.S_KEY_CODE) {
$('#assess').trigger('click');
} else if (event.altKey && event.which === this.T_KEY_CODE) {
$('#test').trigger('click');
} else {
return;
}
event.preventDefault();
},
handleCopyEvent: function (text) {
CodeOceanEditor.lastCopyText = text;
},
handlePasteEvent: function (pasteObject, event) {
var same = (CodeOceanEditor.lastCopyText === pasteObject.text);
// if the text is not copied from within the editor (from any file), send an event to the backend
if (!same) {
CodeOceanEditor.publishCodeOceanEvent({
category: 'editor_paste',
data: pasteObject.text,
exercise_id: $('#editor').data('exercise-id'),
file_id: $(event.container).data('file-id')
});
}
},
hideSpinner: function () {
$('button i.fa-solid, button i.fa-regular').show();
$('button i.fa-spin').removeClass('d-inline-block').addClass('d-none');
},
resizeAceEditors: function (own_solution = false) {
let editorSelector;
if (own_solution) {
editorSelector = $('.own-editor')
} else {
editorSelector = $('.editor')
}
editorSelector.each(function (index, element) {
this.resizeParentOfAceEditor(element);
}.bind(this));
window.dispatchEvent(new Event('resize'));
},
resizeSidebars: function () {
$('#content-left-sidebar').height(this.calculateEditorHeight('#content-left-sidebar', false));
$('#content-right-sidebar').height(this.calculateEditorHeight('#content-right-sidebar', false));
},
calculateEditorHeight: function (element, considerStatusbar) {
const jqueryElement = $(element);
if (jqueryElement.length === 0) {
return 0;
}
const bottom = considerStatusbar ? ($('#statusbar').height() || 0) : 0;
// calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
return window.innerHeight - jqueryElement.offset().top - bottom - 5;
},
resizeParentOfAceEditor: function (element) {
const editorHeight = this.calculateEditorHeight(element, true);
$(element).parent().height(editorHeight);
},
initializeEditors: function (own_solution = false) {
// Initialize the editors array if not present already. This is mainly required for community solutions
this.editors = this.editors || [];
let editorSelector;
if (own_solution) {
editorSelector = $('.own-editor')
} else {
editorSelector = $('.editor')
}
editorSelector.each(function (index, element) {
// Resize frame on load
this.resizeParentOfAceEditor(element);
// Resize frame on window size change
$(window).resize(function () {
this.resizeParentOfAceEditor(element);
this.resizeSidebars();
}.bind(this));
var editor = ace.edit(element);
if (this.qa_api) {
editor.getSession().on("change", function (deltaObject) {
this.qa_api.executeCommand('syncEditor', [this.active_file, deltaObject]);
}.bind(this));
}
var document = editor.getSession().getDocument();
// insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added
var file_id = $(element).data('file-id');
var content = $('.editor-content[data-file-id=' + file_id + ']');
this.setActiveFile($(element).parent().data('filename'), file_id);
document.insertLines(0, content.text().split(/\n/));
// remove last (empty) that is there by default line
document.removeLines(document.getLength() - 1, document.getLength() - 1);
editor.setReadOnly($(element).parent().data('read-only') !== undefined);
if (editor.getReadOnly()) {
editor.setHighlightActiveLine(false);
editor.setHighlightGutterLine(false);
editor.renderer.$cursorLayer.element.style.opacity = 0;
}
editor.setShowPrintMargin(false);
editor.setTheme(this.THEME);
// set options for autocompletion
if ($(element).data('allow-auto-completion')) {
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: false,
enableLiveAutocompletion: true
});
}
editor.commands.bindKey("ctrl+alt+0", null);
this.editors.push(editor);
this.editor_for_file.set($(element).parent().data('filename'), editor);
var session = editor.getSession();
var mode = $(element).data('mode')
session.setMode(mode);
if (mode === 'ace/mode/python') {
editor.setTheme('ace/theme/tomorrow')
}
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
// set regex for parsing error traces based on the mode of the main file.
if ($(element).parent().data('role') === "main_file") {
this.tracepositions_regex = this.regex_for_language.get($(element).data('mode'));
}
/*
* Register event handlers
*/
// editor itself
editor.on("paste", this.handlePasteEvent.bind(element));
editor.on("copy", this.handleCopyEvent.bind(element));
// listener for autosave
session.on("change", function (deltaObject, session) {
// TODO: This is a workaround for a bug in Ace. Remove when upgrading Ace.
this.handleUTF16Surrogates(deltaObject, session);
this.resetSaveTimer();
}.bind(this));
}.bind(this));
},
handleUTF16Surrogates: function (AceDeltaObject, AceSession) {
if (AceDeltaObject.data === undefined || AceDeltaObject.data.action !== "removeText") {
return;
}
const codePoint = AceDeltaObject.data.text.codePointAt(0);
if (0xDC00 <= codePoint && codePoint <= 0xDFFF) {
// The text contains a UTF-16 surrogate pair, and the only the lower part is removed.
// We need to remove the high surrogate pair as well.
const currentCharacter = AceDeltaObject.data.range
const previousCharacter = {
start: {
row: currentCharacter.start.row,
column: currentCharacter.start.column - 1
},
end: {
row: currentCharacter.start.row,
column: currentCharacter.start.column
}
}
AceSession.remove(previousCharacter);
}
},
initializeEventHandlers: function () {
$(document).on('click', '#results a', this.showOutput.bind(this));
$(document).on('keydown', this.handleKeyPress.bind(this));
this.initializeFileTreeButtons();
this.initializeWorkspaceButtons();
this.initializeRequestForComments()
},
teardownEventHandlers: function () {
$(document).unbind('click');
$(document).unbind('keydown');
this.teardownWorkspaceButtons();
this.teardownRequestForComments();
bootstrap.Modal.getInstance($('#comment-modal'))?.hide();
this.teardownFileTreeButtons();
},
updateEditorModeToFileTypeID: function (editor, fileTypeID) {
var newMode = 'ace/mode/text'
$.ajax(this.fileTypeURL + '/' + fileTypeID, {
dataType: 'json'
}).done(function (data) {
if (data['editor_mode'] !== null) {
newMode = data['editor_mode'];
}
}).fail(_.noop)
.always(function () {
ace.edit(editor).session.setMode(newMode);
if (newMode === 'ace/mode/python') {
ace.edit(editor).setTheme('ace/theme/tomorrow')
}
});
},
initializeFileTree: function (own_solution = false) {
let filesInstance;
if (own_solution) {
filesInstance = $('#own-files');
} else {
filesInstance = $('#files');
}
filesInstance.jstree(filesInstance.data('entries'));
filesInstance.on('click', 'li.jstree-leaf > a', function (event) {
const file_id = parseInt($(event.target).parent().attr('id'));
const frame = $('[data-file-id="' + file_id + '"]').parent();
this.setActiveFile(frame.data('filename'), file_id);
this.showFrame(frame);
this.toggleButtonStates();
}.bind(this));
},
initializeFileTreeButtons: function () {
$('#create-file').on('click', this.showFileDialog.bind(this));
$('#destroy-file').on('click', this.confirmDestroy.bind(this));
$('#destroy-file-collapsed').on('click', this.confirmDestroy.bind(this));
$('#download').on('click', this.downloadCode.bind(this));
},
teardownFileTreeButtons: function () {
$('#create-file').unbind('click');
$('#destroy-file').unbind('click');
$('#destroy-file-collapsed').unbind('click');
$('#download').unbind('click');
},
initializeSideBarCollapse: function () {
$('#sidebar-collapse-collapsed').on('click', this.handleSideBarToggle.bind(this));
$('#sidebar-collapse').on('click', this.handleSideBarToggle.bind(this));
const tipButton = $('#tips-collapsed');
if (tipButton) {
tipButton.on('click', this.handleSideBarToggle.bind(this));
}
$('#sidebar').on('transitionend', this.resizeAceEditors.bind(this));
$('#sidebar').on('transitionend', this.resizeSidebars.bind(this));
},
handleSideBarToggle: function () {
const sidebar = $('#sidebar');
sidebar.toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed');
if (sidebar.hasClass('w-25') || sidebar.hasClass('restore-to-w-25')) {
sidebar.toggleClass('w-25').toggleClass('restore-to-w-25');
}
$('#sidebar-collapsed').toggleClass('d-none');
$('#sidebar-uncollapsed').toggleClass('d-none');
},
initializeRegexes: function () {
// These RegEx are run on the HTML escaped output!
this.regex_for_language.set("ace/mode/python", /File &quot;(.+?)&quot;, line (\d+)/g);
this.regex_for_language.set("ace/mode/java", /(?:\.\/)?(.*\.java):(\d+):/g);
},
initializeWorkspaceButtons: function () {
$('#submit').one('click', this.submitCode.bind(this));
$('#assess').on('click', this.scoreCode.bind(this));
$('#render').on('click', this.renderCode.bind(this));
$('#run').on('click', this.runCode.bind(this));
$('#stop').on('click', this.stopCode.bind(this));
$('#test').on('click', this.testCode.bind(this));
$('#start-over').on('click', this.confirmReset.bind(this));
$('#start-over-active-file').on('click', this.confirmResetActiveFile.bind(this));
},
teardownWorkspaceButtons: function () {
$('#submit').unbind('click');
$('#assess').unbind('click');
$('#render').unbind('click');
$('#run').unbind('click');
$('#stop').unbind('click');
$('#test').unbind('click');
$('#start-over').unbind('click');
$('#start-over-active-file').unbind('click');
},
initializeRequestForComments: function () {
var button = $('#requestComments');
button.prop('disabled', true);
button.on('click', function () {
button.tooltip('hide');
$('#rfc_intervention_text').hide()
new bootstrap.Modal($('#comment-modal')).show();
});
$('#askForCommentsButton').on('click', this.requestComments.bind(this));
$('#closeAskForCommentsButton').on('click', function () {
bootstrap.Modal.getInstance($('#comment-modal')).hide();
});
setTimeout(function () {
button.prop('disabled', false);
setTimeout(function () {
button.tooltip('show');
setTimeout(function () {
button.tooltip('hide');
}, this.REQUEST_TOOLTIP_TIME);
}, this.REQUEST_TOOLTIP_DELAY)
}.bind(this), this.REQUEST_FOR_COMMENTS_DELAY);
},
teardownRequestForComments: function () {
$('#requestComments').unbind('click');
$('#askForCommentsButton').unbind('click');
$('#closeAskForCommentsButton').unbind('click');
},
isActiveFileRenderable: function () {
if (this.active_frame.data() === undefined) {
return false;
}
return 'renderable' in this.active_frame.data();
},
isActiveFileRunnable: function () {
return this.isActiveFileExecutable() && ['main_file', 'user_defined_file', 'executable_file'].includes(this.active_frame.data('role'));
},
isActiveFileStoppable: function () {
return this.isActiveFileRunnable() && this.running;
},
isActiveFileSubmission: function () {
return ['Submission'].includes(this.active_frame.data('contextType'));
},
isActiveFileTestable: function () {
return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test', 'teacher_defined_linter'].includes(this.active_frame.data('role'));
},
populateCard: function (card, result, index) {
card.addClass(this.getCardClass(result));
card.find('.card-title .filename').text(result.filename);
card.find('.card-title .number').text(index + 1);
card.find('.row .col-md-9').eq(0).find('.number').eq(0).text(result.passed);
card.find('.row .col-md-9').eq(0).find('.number').eq(1).text(result.count);
if (result.weight !== 0) {
card.find('.row .col-md-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
card.find('.row .col-md-9').eq(1).find('.number').eq(1).text(result.weight);
} else {
// Hide score row if no score could be achieved
card.find('.attribute-row.row').eq(1).addClass('d-none');
}
card.find('.row .col-md-9').eq(2).html(result.message);
// Add error message from code to card
if (result.error_messages) {
const targetNode = card.find('.row .col-md-9').eq(3);
let errorMessagesToShow = [];
result.error_messages.forEach(function (item) {
if (item) {
errorMessagesToShow.push(item)
}
})
// delete all current elements
targetNode.text('');
// create a new list and append each element
const ul = document.createElement("ul");
// Extract detailed linter results
if (result.file_role === 'teacher_defined_linter') {
const detailed_linter_results = result.detailed_linter_results;
const severity_groups = detailed_linter_results.reduce(function(map, obj) {
map[obj.severity] = map[obj.severity] || []
map[obj.severity].push(obj);
return map;
}, {});
for (let severity in severity_groups) {
if (!severity_groups.hasOwnProperty(severity)) {
continue;
}
const linter_results = severity_groups[severity]
const li = document.createElement("li");
const text = $.parseHTML(`<u>${severity}:</u>`);
$(li).append(text);
ul.append(li);
const sub_ul = document.createElement("ul");
sub_ul.setAttribute('class', 'inline_list');
for (let check_run of linter_results) {
const sub_li = document.createElement("li");
let scope = '';
if (check_run.scope) {
scope = `, ${check_run.scope}()`;
}
const context = `${check_run.file_name}: ${check_run.line}${scope}`;
const line_link = `<a href='#' data-file='${check_run.file_name}' data-line='${check_run.line}'>${context}</a>`;
const message = `${check_run.name}: ${check_run.result} (${line_link})`;
const sub_text = $.parseHTML(message);
$(sub_li).append(sub_text).on("click", "a", this.jumpToSourceLine.bind(this));
sub_ul.append(sub_li);
}
li.append(sub_ul);
}
// Just show standard results for normal test results
} else {
errorMessagesToShow.forEach(function (item) {
var li = document.createElement("li");
var text = document.createTextNode(item);
$(li).append(text);
ul.append(li);
})
}
// one or more errors?
if (errorMessagesToShow.length > 1) {
ul.setAttribute('class', 'inline_list');
} else {
ul.setAttribute('class', 'single_entry_inline_list');
}
targetNode.append(ul);
}
//card.find('.row .col-md-9').eq(4).find('a').attr('href', '#output-' + index);
},
createEventHandler: function (eventType, data) {
return function (event) {
CodeOceanEditor.publishCodeOceanEvent({
category: eventType,
data: data,
exercise_id: $('#editor').data('exercise-id'),
file_id: CodeOceanEditor.active_file.id,
});
event.stopPropagation();
};
},
publishCodeOceanEvent: function (payload) {
if (this.sendEvents) {
$.ajax(this.eventURL, {
type: 'POST',
cache: false,
dataType: 'JSON',
data: {
event: payload
},
success: _.noop,
error: _.noop
});
}
},
sendError: function (message, submission_id) {
this.showSpinner($('#render'));
var jqxhr = this.ajax({
data: {
error: {
message: message,
submission_id: submission_id
}
},
url: $('#editor').data('errors-url')
});
jqxhr.always(this.hideSpinner);
},
toggleButtonStates: function () {
$('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file');
$('#start-over-active-file').prop('disabled', this.active_frame.data('role') === 'user_defined_file' || this.active_frame.data('read-only') !== undefined);
$('#dummy').toggle(!this.fileActionsAvailable());
$('#render').toggle(this.isActiveFileRenderable());
$('#run-stop-button-group').tooltip('hide');
$('#run').toggle(this.isActiveFileRunnable() && !this.running);
$('#stop').toggle(this.isActiveFileStoppable());
$('#test').toggle(this.isActiveFileTestable());
},
jumpToSourceLine: function (event) {
const file = $(event.target).data('file');
const line = $(event.target).data('line');
const frame = $('div.frame[data-filename="' + file + '"]');
this.showFrame(frame);
this.toggleButtonStates();
const file_id = frame.find('.editor').data('file-id');
this.setActiveFile(frame.data('filename'), file_id);
this.selectFileInJsTree($('#files'), file_id);
const editor = this.editor_for_file.get(file);
editor.gotoLine(line, 0);
event.preventDefault();
},
augmentStacktraceInOutput: function () {
if (this.tracepositions_regex) {
$('#output > .output-element').each($.proxy(function(index, element) {
element = $(element)
const text = _.escape(element.text());
element.on("click", "a", this.jumpToSourceLine.bind(this));
let matches;
let augmented_text = element.html();
while (matches = this.tracepositions_regex.exec(text)) {
const frame = $('div.frame[data-filename="' + matches[1] + '"]')
if (frame.length > 0) {
augmented_text = augmented_text.replace(new RegExp(_.unescape(matches[0]), 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
}
}
element.html(augmented_text);
}, this));
}
},
resetOutputTab: function () {
this.clearOutput();
$('#flowrHint').fadeOut();
this.clearHints();
this.showOutputBar();
this.clearFileDownloads();
},
isActiveFileBinary: function () {
if (this.active_frame.data() === undefined) {
return false;
}
return 'binary' in this.active_frame.data();
},
isActiveFileExecutable: function () {
if (this.active_frame.data() === undefined) {
return false;
}
return 'executable' in this.active_frame.data();
},
setActiveFile: function (filename, fileId) {
this.active_file = {
filename: filename,
id: fileId
};
},
showSpinner: function (initiator) {
$(initiator).closest('[data-bs-toggle="tooltip"]').tooltip('hide');
$(initiator).find('i.fa-solid, i.fa-regular').hide();
$(initiator).find('i.fa-spin').addClass('d-inline-block').removeClass('d-none');
},
showStatus: function (output) {
if (output.status === 'timeout' || output.status === 'buffer_overflow') {
this.showTimeoutMessage();
} else if (output.status === 'container_depleted') {
this.showContainerDepletedMessage();
} else if (output.status === 'out_of_memory') {
this.showOutOfMemoryMessage();
}
},
clearHints: function () {
var container = $('#error-hints');
container.find('ul.body > li.hint').remove();
container.fadeOut();
},
showHint: function (message) {
var template = function (description, hint) {
return '\
<li class="hint">\
<div class="description">\
' + description + '\
</div>\
<div class="hint">\
' + hint + '\
</div>\
</li>\
'
};
var container = $('#error-hints');
container.find('ul.body').append(template(message.description, message.hint));
container.fadeIn();
},
prepareFileDownloads: function(message) {
const fileTree = $('#download-file-tree');
fileTree.jstree(message.data);
fileTree.on('select_node.jstree', function (node, selected, _event) {
selected.instance.deselect_all();
const downloadPath = selected.node.original.download_path;
if (downloadPath) {
window.location = downloadPath;
}
}.bind(this));
$('#download-files').removeClass('d-none');
},
clearFileDownloads: function() {
$('#download-files').addClass('d-none');
$('#download-file-tree').replaceWith($('<div id="download-file-tree">'));
},
showContainerDepletedMessage: function () {
$.flash.danger({
icon: ['fa-regular', 'fa-clock'],
text: $('#editor').data('message-depleted')
});
},
showOutOfMemoryMessage: function () {
$.flash.info({
icon: ['fa-regular', 'fa-clock'],
text: $('#editor').data('message-out-of-memory')
});
},
showTimeoutMessage: function () {
$.flash.info({
icon: ['fa-regular', 'fa-clock'],
text: $('#editor').data('message-timeout')
});
},
showWebsocketError: function (error) {
if (window.navigator.userAgent.indexOf('Edge') > -1 || window.navigator.userAgent.indexOf('Trident') > -1) {
// Mute errors in Microsoft Edge and Internet Explorer
return;
}
$.flash.danger({
text: $('#flash').data('websocket-failure'),
showPermanent: true
});
Sentry.captureException(JSON.stringify(error, ["message", "arguments", "type", "name", "data"]));
},
showFileDialog: function (event) {
event.preventDefault();
this.createSubmission('#create-file', null, function (response) {
$('#code_ocean_file_context_id').val(response.id);
new bootstrap.Modal($('#modal-file')).show();
}.bind(this));
},
initializeOutputBarToggle: function () {
$('#toggle-sidebar-output').on('click', this.hideOutputBar.bind(this));
$('#toggle-sidebar-output-collapsed').on('click', this.showOutputBar.bind(this));
$('#output_sidebar').on('transitionend', this.resizeAceEditors.bind(this));
$('#output_sidebar').on('transitionend', this.resizeSidebars.bind(this));
},
showOutputBar: function () {
$('#output_sidebar_collapsed').addClass('d-none');
$('#output_sidebar_uncollapsed').removeClass('d-none');
$('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col');
},
hideOutputBar: function () {
$('#output_sidebar_collapsed').removeClass('d-none');
$('#output_sidebar_uncollapsed').addClass('d-none');
$('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed');
},
initializeSideBarTooltips: function () {
$('[data-bs-toggle="tooltip"]').tooltip()
},
initializeDescriptionToggle: function () {
$('#exercise-headline').on('click', this.toggleDescriptionCard.bind(this));
$('a#toggle').on('click', this.toggleDescriptionCard.bind(this));
},
toggleDescriptionCard: function (event) {
$('#description-card').toggleClass('description-card-collapsed').toggleClass('description-card');
$('#description-symbol').toggleClass('fa-chevron-down').toggleClass('fa-chevron-right');
var toggle = $('a#toggle');
toggle.text(toggle.text() == toggle.data('hide') ? toggle.data('show') : toggle.data('hide'));
this.resizeAceEditors();
this.resizeSidebars();
event.preventDefault();
},
/**
* interventions
* */
initializeInterventionTimer: function () {
const editor = $('#editor');
if (editor.data('rfc-interventions') || editor.data('break-interventions') || editor.data('tips-interventions')) { // split in break or rfc intervention
window.onblur = function () {
window.blurred = true;
};
window.onfocus = function () {
window.blurred = false;
};
const delta = 100; // time in ms to wait for window event before time gets stopped
let tid;
$.ajax({
data: {
exercise_id: editor.data('exercise-id'),
user_id: editor.data('user-id')
},
dataType: 'json',
method: 'GET',
// get working times for this exercise
url: editor.data('working-times-url'),
success: function (data) {
const percentile75 = data['working_time_75_percentile'];
const accumulatedWorkTimeUser = data['working_time_accumulated'];
let minTimeIntervention = 20 * 60 * 1000;
let timeUntilIntervention;
if ((accumulatedWorkTimeUser - percentile75) > 0) {
// working time is already over 75 percentile
timeUntilIntervention = minTimeIntervention;
} else {
// working time is less than 75 percentile
// ensure we give user at least minTimeIntervention before we bother the user
timeUntilIntervention = Math.max(percentile75 - accumulatedWorkTimeUser, minTimeIntervention);
}
tid = setInterval(function () {
if (window.blurred) {
return;
}
timeUntilIntervention -= delta;
if (timeUntilIntervention <= 0) {
const interventionSaveUrl = editor.data('intervention-save-url');
clearInterval(tid);
// timeUntilIntervention passed
if (editor.data('tips-interventions')) {
const modal = $('#tips-intervention-modal');
if (!modal.isPresent()) {
// The modal is not present (e.g., because the site was navigated), so we don't continue here.
return;
}
modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
new bootstrap.Modal(modal).show();
$.ajax({
data: {
intervention_type: 'TipsIntervention'
},
dataType: 'json',
type: 'POST',
url: interventionSaveUrl
});
} else if (editor.data('break-interventions')) {
const modal = $('#break-intervention-modal');
if (!modal.isPresent()) {
// The modal is not present (e.g., because the site was navigated), so we don't continue here.
return;
}
modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
new bootstrap.Modal(modal).show();
$.ajax({
data: {
intervention_type: 'BreakIntervention'
},
dataType: 'json',
type: 'POST',
url: interventionSaveUrl
});
} else if (editor.data('rfc-interventions')) {
const button = $('#requestComments');
// only show intervention if user did not requested for a comment already
if (!button.prop('disabled')) {
$('#rfc_intervention_text').show();
const modal = $('#comment-modal');
if (!modal.isPresent()) {
// The modal is not present (e.g., because the site was navigated), so we don't continue here.
return;
}
modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
modal.on('hidden.bs.modal', function () {
modal.find('.modal-footer').text('');
});
new bootstrap.Modal(modal).show();
$.ajax({
data: {
intervention_type: 'QuestionIntervention'
},
dataType: 'json',
type: 'POST',
url: interventionSaveUrl
});
}
}
}
}, delta);
}
});
}
},
initializeSearchButton: function () {
$('#btn-search-col').button().click(function () {
var search = $('#search-input-text').val();
var course_token = $('#editor').data('course_token')
var save_search_url = $('#editor').data('search-save-url')
window.open("https://open.hpi.de/courses/" + course_token + "/pinboard?query=" + search, '_blank');
// save search
$.ajax({
data: {
search_text: search
},
dataType: 'json',
type: 'POST',
url: save_search_url
});
})
$('#sidebar-search-collapsed').on('click', this.handleSideBarToggle.bind(this));
},
initializeEverything: function () {
CodeOceanEditor.editors = [];
this.initializeRegexes();
this.initializeCodePilot();
this.configureEditors();
this.initializeEditors();
this.initializeEventHandlers();
this.initializeFileTree();
this.initializeSideBarCollapse();
this.initializeOutputBarToggle();
this.initializeDescriptionToggle();
this.initializeSideBarTooltips();
this.initializeInterventionTimer();
this.initializeSearchButton();
this.initPrompt();
this.renderScore();
this.showFirstFile();
this.resizeAceEditors();
this.resizeSidebars();
this.initializeDeadlines();
CodeOceanEditorTips.initializeEventHandlers();
window.addEventListener("turbolinks:before-render", this.autosaveIfChanged.bind(this));
window.addEventListener("beforeunload", this.autosaveIfChanged.bind(this));
// create autosave when the editor is opened the first time
this.autosave();
}
};