Files
codeocean/app/assets/javascripts/exercises.js.erb
2019-10-20 11:02:57 +02:00

404 lines
14 KiB
Plaintext

$(document).on('turbolinks:load', function() {
// ruby part adds the relative_url_root, if it is set.
var ACE_FILES_PATH = '<%= (defined? Rails.application.config.relative_url_root) && Rails.application.config.relative_url_root != nil && Rails.application.config.relative_url_root != "" ? Rails.application.config.relative_url_root : "" %>' + '/assets/ace/';
var THEME = 'ace/theme/textmate';
var TAB_KEY_CODE = 9;
var execution_environments;
var file_types;
var configureEditors = function() {
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
};
var initializeEditor = function(index, element) {
var editor = ace.edit(element);
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 + ']');
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).data('read-only') !== undefined);
editor.setShowPrintMargin(false);
editor.setTheme(THEME);
var textarea = $('textarea[id="exercise_files_attributes_'+index+'_content"]');
var content = textarea.val();
if (content != undefined)
{
editor.getSession().setValue(content);
editor.getSession().on('change', function(){
textarea.val(editor.getSession().getValue());
});
}
editor.commands.bindKey("ctrl+alt+0", null);
var session = editor.getSession();
session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
}
var initializeEditors = function() {
// initialize ace editors for all code textareas in the dom except the last one. The last one is the dummy area for new files, which is cloned when needed.
// this one must NOT! be initialized.
$('.editor:not(:last)').each(initializeEditor)
};
var addFileForm = function(event) {
event.preventDefault();
var element = $('#dummies').children().first().clone();
// the timestamp is used here, since it is most probably unique. This is strange, but was originally designed that way.
var latestTextAreaIndex = new Date().getTime();
var html = $('<div>').append(element).html().replace(/index/g, latestTextAreaIndex);
$('#files').append(html);
$('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id);
$('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS);
$('#files li:last>div:last').removeClass('in').addClass('show')
$('body, html').scrollTo('#add-file');
// initialize the ace editor for the new textarea.
// pass the correct index and the last ace editor under the node files. this is the last one, since we just added it.
initializeEditor(latestTextAreaIndex, $('#files .editor').last()[0]);
};
var removeFileForm = function(fileUrl) {
// validate fileUrl
var matches = fileUrl.match(/files\/(\d+)/);
if (matches) {
// select the file form based on the delete button
var fileForm = $('*[data-file-url="' + fileUrl + '"]').parent().parent().parent();
fileForm.remove();
// now remove the hidden input representing the file
var fileId = matches[1];
var input = $('input[type="hidden"][value="' + fileId + '"]')
input.remove()
}
}
var deleteFile = function(event) {
event.preventDefault();
var fileUrl = $(event.target).data('file-url');
if (confirm('<%= I18n.t('shared.confirm_destroy') %>')) {
var jqxhr = $.ajax({
// normal file path (without json) would destroy the context object (the exercise) as well, due to redirection
// to the context after the :destroy action.
contentType: 'Application/json',
url: fileUrl + '.json',
method: 'DELETE'
});
jqxhr.done(function () {
removeFileForm(fileUrl)
});
jqxhr.fail(ajaxError);
}
}
var ajaxError = function() {
$.flash.danger({
text: $('#flash').data('message-failure')
});
};
var buildCheckboxes = function() {
$('tbody tr').each(function(index, element) {
var td = $('td.public', element);
var checkbox = $('<input>', {
checked: td.data('value'),
type: 'checkbox'
});
td.on('click', function(event) {
event.preventDefault();
checkbox.prop('checked', !checkbox.prop('checked'));
});
td.html(checkbox);
});
};
var discardFile = function(event) {
event.preventDefault();
$(this).parents('li').remove();
};
var enableBatchUpdate = function() {
$('thead .batch a').on('click', function(event) {
event.preventDefault();
if (!$(event.target).data('toggled')) {
$(event.target).data('toggled', true);
$(event.target).text($(event.target).data('text'));
buildCheckboxes();
} else {
performBatchUpdate();
}
});
};
var enableInlineFileCreation = function() {
$('#add-file').on('click', addFileForm);
$('#files').on('click', 'li .discard-file', discardFile);
$('form.edit_exercise, form.new_exercise').on('submit', function() {
$('#dummies').html('');
});
$('.delete-file').on('click', deleteFile);
};
var findFileTypeByFileExtension = function(file_extension) {
return _.find(file_types, function(file_type) {
return file_type.file_extension === file_extension;
}) || {};
};
var getSelectedExecutionEnvironment = function() {
return _.find(execution_environments, function(execution_environment) {
return execution_environment.id === parseInt($('#exercise_execution_environment_id').val());
}) || {};
};
var highlightCode = function() {
$('pre code').each(function(index, element) {
hljs.highlightBlock(element);
});
};
var inferFileAttributes = function() {
$(document).on('change', 'input[type="file"]', function() {
var filename = $(this).val().split(/\\|\//g).pop();
var file_extension = '.' + filename.split('.')[1];
var file_type = findFileTypeByFileExtension(file_extension);
var name = filename.split('.')[0];
var parent = $(this).parents('li');
parent.find('input[name*="name"]').val(name);
parent.find('select[name*="file_type_id"]').val(file_type.id).trigger('chosen:updated');
});
};
var insertTabAtCursor = function(textarea) {
var selection_start = textarea.get(0).selectionStart;
var selection_end = textarea.get(0).selectionEnd;
textarea.val(textarea.val().substring(0, selection_start) + "\t" + textarea.val().substring(selection_end));
textarea.get(0).selectionStart = selection_start + 1;
textarea.get(0).selectionEnd = selection_start + 1;
};
var observeFileRoleChanges = function() {
$(document).on('change', 'select[name$="[role]"]', function() {
var is_test_file = $(this).val() === 'teacher_defined_test';
var parent = $(this).parents('.card');
var fields = parent.find('.test-related-fields');
if (is_test_file) {
fields.slideDown();
} else {
fields.slideUp();
parent.find('[name$="[feedback_message]"]').val('');
parent.find('[name$="[weight]"]').val(1);
}
});
};
var old_execution_environment = $('#exercise_execution_environment_id').val();
var observeExecutionEnvironment = function() {
$('#exercise_execution_environment_id').on('change', function(){
new_execution_environment = $('#exercise_execution_environment_id').val();
if(new_execution_environment == '' && !$('#exercise_unpublished').prop('checked')){
if(confirm('<%= I18n.t('exercises.form.unpublish_warning') %>')){
$('#exercise_unpublished').prop('checked', true);
} else {
return $('#exercise_execution_environment_id').val(old_execution_environment).trigger("chosen:updated");
}
}
old_execution_environment = new_execution_environment;
});
};
var observeUnpublishedState = function() {
$('#exercise_unpublished').on('change', function(){
if($('#exercise_unpublished').prop('checked')){
if(!confirm('<%= I18n.t('exercises.form.unpublish_warning') %>')){
$('#exercise_unpublished').prop('checked', false);
}
} else if($('#exercise_execution_environment_id').val() == '') {
alert('<%= I18n.t('exercises.form.no_execution_environment_selected') %>');
$('#exercise_unpublished').prop('checked', true);
}
})
};
var observeExportButtons = function(){
$('.export-start').on('click', function(e){
e.preventDefault();
$('#export-modal').modal({
height: 250
});
$('#export-modal').modal('show');
exportExerciseStart($(this).data().exerciseId);
});
$('body').on('click', '.export-retry-button', function(){
exportExerciseStart($(this).data().exerciseId);
});
$('body').on('click', '.export-action', function(){
exportExerciseConfirm($(this).data().exerciseId, $(this).data().exportType);
});
}
var exportExerciseStart = function(exerciseID) {
var $exerciseDiv = $('#export-exercise');
var $messageDiv = $exerciseDiv.children('.export-message');
var $actionsDiv = $exerciseDiv.children('.export-exercise-actions');
$messageDiv.html('requesting status');
$actionsDiv.html('spinning');
return $.ajax({
type: 'POST',
url: '/exercises/' + exerciseID + '/export_external_check',
dataType: 'json',
success: function(response) {
if (response.error) {
$messageDiv.html(response.error);
$actionsDiv.html('Retry?');
}
$messageDiv.html(response.message);
return $actionsDiv.html(response.actions);
},
error: function(a, b, c) {
return alert('error:' + c);
}
});
};
var exportExerciseConfirm = function(exerciseID, pushType) {
var $exerciseDiv = $('#export-exercise');
var $messageDiv = $exerciseDiv.children('.export-message');
var $actionsDiv = $exerciseDiv.children('.export-exercise-actions');
return $.ajax({
type: 'POST',
url: '/exercises/' + exerciseID + '/export_external_confirm',
data: {
push_type: pushType
},
dataType: 'json',
success: function(response) {
$messageDiv.html(response.message)
$actionsDiv.html(response.actions);
if(response.status == 'success') {
$messageDiv.addClass('export-success');
setTimeout((function() {
$('#export-modal').modal('hide');
$messageDiv.html('').removeClass('export-success');
}), 3000);
} else {
$messageDiv.addClass('export-failure');
}
},
error: function(a, b, c) {
return alert('error:' + c);
}
});
};
var overrideTextareaTabBehavior = function() {
$('.form-group textarea[name$="[content]"]').on('keydown', function(event) {
if (event.which === TAB_KEY_CODE) {
event.preventDefault();
insertTabAtCursor($(this));
}
});
};
var performBatchUpdate = function() {
var jqxhr = $.ajax({
data: {
exercises: _.map($('tbody tr'), function(element) {
return {
id: $(element).data('id'),
public: $('.public input', element).prop('checked')
};
})
},
dataType: 'json',
method: 'PUT'
});
jqxhr.done(window.CodeOcean.refresh);
jqxhr.fail(ajaxError);
};
var toggleCodeHeight = function() {
$('code').on('click', function() {
$(this).css({
'max-height': 'initial'
});
});
};
var updateFileTemplates = function(fileType) {
var rel_url_root = '<%= (defined? Rails.application.config.relative_url_root) && Rails.application.config.relative_url_root != nil && Rails.application.config.relative_url_root != "" ? Rails.application.config.relative_url_root : "" %>';
var jqxhr = $.ajax({
url: rel_url_root + '/file_templates/by_file_type/' + fileType + '.json',
dataType: 'json'
});
jqxhr.done(function(response) {
var noTemplateLabel = $('#noTemplateLabel').data('text');
var options = "<option value>" + noTemplateLabel + "</option>";
for (var i = 0; i < response.length; i++) {
options += "<option value='" + response[i].id + "'>" + response[i].name + "</option>"
}
$("#code_ocean_file_file_template_id").find('option').remove().end().append($(options));
});
jqxhr.fail(ajaxError);
}
if ($.isController('exercises') || $.isController('submissions')) {
// ignore tags table since it is in the dom before other tables
if ($('table:not(#tags-table)').isPresent()) {
enableBatchUpdate();
observeExportButtons();
} else if ($('.edit_exercise, .new_exercise').isPresent()) {
execution_environments = $('form').data('execution-environments');
file_types = $('form').data('file-types');
enableInlineFileCreation();
inferFileAttributes();
observeFileRoleChanges();
observeExecutionEnvironment();
observeUnpublishedState();
overrideTextareaTabBehavior();
} else if ($('#files.jstree').isPresent()) {
var fileTypeSelect = $('#code_ocean_file_file_type_id');
fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())});
updateFileTemplates(fileTypeSelect.val());
}
toggleCodeHeight();
if (window.hljs) {
highlightCode();
}
}
if ($('#editor-edit').isPresent()) {
configureEditors();
initializeEditors();
$('.frame').show();
}
});