Files
codeocean/app/assets/javascripts/exercises.js.erb
Sebastian Serth b94289f36f Fix adding files to exercises (as admin)
Prevent hidden selection field from being removed after cloning the dummy. Also ensure the caret looks nice and add the file extension if possible
2018-11-29 22:32:12 +01:00

295 lines
10 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 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();
} else if ($('.edit_exercise, .new_exercise').isPresent()) {
execution_environments = $('form').data('execution-environments');
file_types = $('form').data('file-types');
enableInlineFileCreation();
inferFileAttributes();
observeFileRoleChanges();
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();
}
});