diff --git a/app/assets/javascripts/base.js b/app/assets/javascripts/base.js index 235a6e39..b7263d48 100644 --- a/app/assets/javascripts/base.js +++ b/app/assets/javascripts/base.js @@ -8,20 +8,18 @@ window.CodeOcean = { } }; -$(function() { - var ANIMATION_DURATION = 500; +var ANIMATION_DURATION = 500; - $.isController = function(name) { - return $('.container[data-controller="' + name + '"]').isPresent(); - }; +$.isController = function(name) { + return $('.container[data-controller="' + name + '"]').isPresent(); +}; - $.fn.isPresent = function() { - return this.length > 0; - }; +$.fn.isPresent = function() { + return this.length > 0; +}; - $.fn.scrollTo = function(selector) { - $(this).animate({ - scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop() - }, ANIMATION_DURATION); - }; -}); +$.fn.scrollTo = function(selector) { + $(this).animate({ + scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop() + }, ANIMATION_DURATION); +}; diff --git a/app/assets/javascripts/bootstrap-dropdown-submenu.js b/app/assets/javascripts/bootstrap-dropdown-submenu.js index aed16bbb..10809c02 100644 --- a/app/assets/javascripts/bootstrap-dropdown-submenu.js +++ b/app/assets/javascripts/bootstrap-dropdown-submenu.js @@ -14,7 +14,7 @@ $(document).on('turbolinks:load', function() { var menu = $(this).parent().find("ul"); var menupos = menu.offset(); - var newPos; + var newPos = 0.0; if ((menupos.left + menu.width()) + 30 > $(window).width()) { newPos = -menu.width(); } else { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 6bb6dc27..42f082f1 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -33,6 +33,7 @@ var CodeOceanEditor = { <% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %> sendEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>, eventURL: "<%= @config['codeocean_events'] ? events_path : '' %>", + fileTypeURL: "<%= file_types_path %>", configureEditors: function () { @@ -43,7 +44,7 @@ configureEditors: function () { confirmDestroy: function (event) { event.preventDefault(); - if (confirm($(this).data('message-confirm'))) { + if (confirm($(event.target).data('message-confirm'))) { this.destroyFile(); } }, @@ -63,7 +64,7 @@ configureEditors: function () { if ($('#output-' + index).isPresent()) { return $('#output-' + index); } else { - var element = $('
').attr('id', 'output-' + index); + var element = $('').attr('id', 'output-' + index); $('#output').append(element); return element; } @@ -79,13 +80,13 @@ configureEditors: function () { } }, - getPanelClass: function (result) { + getCardClass: function (result) { if (result.stderr && !result.score) { - return 'panel-danger'; + return 'card bg-danger text-white'; } else if (result.score < 1) { - return 'panel-warning'; + return 'card bg-warning'; } else { - return 'panel-success'; + return 'card bg-success text-white'; } }, @@ -107,11 +108,22 @@ configureEditors: function () { 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.hasClass('jstree-loading')) { + filetree.jstree("deselect_all"); + filetree.jstree().select_node(file_id); + } else { + setTimeout(this.selectFileInJsTree.bind(null, filetree, file_id), 250); + } + }, + showFirstFile: function() { var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first(); var file_id = frame.find('.editor').data('file-id'); this.setActiveFile(frame.data('filename'), file_id); - $('#files').jstree().select_node(file_id); + var filetree = $('#files'); + this.selectFileInJsTree(filetree, file_id); this.showFrame(frame); this.toggleButtonStates(); }, @@ -124,11 +136,11 @@ configureEditors: function () { getProgressBarClass: function (percentage) { if (percentage < this.ADEQUATE_PERCENTAGE) { - return 'progress-bar progress-bar-striped progress-bar-danger'; + return 'progress-bar progress-bar-striped bg-danger'; } else if (percentage < this.SUCCESSFULL_PERCENTAGE) { - return 'progress-bar progress-bar-striped progress-bar-warning'; + return 'progress-bar progress-bar-striped bg-warning'; } else { - return 'progress-bar progress-bar-striped progress-bar-success'; + return 'progress-bar progress-bar-striped bg-success'; } }, @@ -158,7 +170,7 @@ configureEditors: function () { category: 'editor_paste', data: pasteObject.text, exercise_id: $('#editor').data('exercise-id'), - file_id: $(this).data('file-id') + file_id: $(event.target).data('file-id') }); } }, @@ -266,6 +278,21 @@ configureEditors: function () { this.initializeRequestForComments() }, + 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); + }); + }, + initializeFileTree: function () { $('#files').jstree($('#files').data('entries')); $('#files').on('click', 'li.jstree-leaf', function (event) { @@ -296,8 +323,8 @@ configureEditors: function () { handleSideBarToggle: function() { $('#sidebar').toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed'); - $('#sidebar-collapsed').toggleClass('hidden'); - $('#sidebar-uncollapsed').toggleClass('hidden'); + $('#sidebar-collapsed').toggleClass('d-none'); + $('#sidebar-uncollapsed').toggleClass('d-none'); }, initializeRegexes: function () { @@ -369,17 +396,17 @@ configureEditors: function () { return Modernizr.websockets; }, - populatePanel: function (panel, result, index) { - panel.removeClass('panel-default').addClass(this.getPanelClass(result)); - panel.find('.panel-title .filename').text(result.filename); - panel.find('.panel-title .number').text(index + 1); - panel.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed); - panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count); - panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2))); - panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight); - panel.find('.row .col-sm-9').eq(2).html(result.message); - if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(' ')); - //panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index); + 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-sm-9').eq(0).find('.number').eq(0).text(result.passed); + card.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count); + card.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2))); + card.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight); + card.find('.row .col-sm-9').eq(2).html(result.message); + if (result.error_messages) card.find('.row .col-sm-9').eq(3).text(result.error_messages.join(' ')); + //card.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index); }, publishCodeOceanEvent: function (payload) { @@ -556,14 +583,14 @@ configureEditors: function () { }, showOutputBar: function() { - $('#output_sidebar_collapsed').addClass('hidden'); - $('#output_sidebar_uncollapsed').removeClass('hidden'); + $('#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('hidden'); - $('#output_sidebar_uncollapsed').addClass('hidden'); + $('#output_sidebar_collapsed').removeClass('d-none'); + $('#output_sidebar_uncollapsed').addClass('d-none'); $('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed'); }, @@ -572,12 +599,12 @@ configureEditors: function () { }, initializeDescriptionToggle: function() { - $('#exercise-headline').on('click', this.toggleDescriptionPanel.bind(this)); - $('a#toggle').on('click', this.toggleDescriptionPanel.bind(this)); + $('#exercise-headline').on('click', this.toggleDescriptionCard.bind(this)); + $('a#toggle').on('click', this.toggleDescriptionCard.bind(this)); }, - toggleDescriptionPanel: function() { - $('#description-panel').toggleClass('description-panel-collapsed').toggleClass('description-panel'); + toggleDescriptionCard: function() { + $('#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')); diff --git a/app/assets/javascripts/editor/evaluation.js b/app/assets/javascripts/editor/evaluation.js index feced0eb..23e18bf0 100644 --- a/app/assets/javascripts/editor/evaluation.js +++ b/app/assets/javascripts/editor/evaluation.js @@ -9,7 +9,7 @@ CodeOceanEditorEvaluation = { this.clearScoringOutput(); this.createSubmission('#assess', null, function (response) { this.showSpinner($('#assess')); - $('#score_div').removeClass('hidden'); + $('#score_div').removeClass('d-none'); var url = response.score_url; this.initializeSocketForScoring(url); }.bind(this)); @@ -26,9 +26,9 @@ CodeOceanEditorEvaluation = { printScoringResult: function (result, index) { $('#results').show(); - var panel = $('#dummies').children().first().clone(); - this.populatePanel(panel, result, index); - $('#results ul').first().append(panel); + var card = $('#dummies').children().first().clone(); + this.populateCard(card, result, index); + $('#results ul').first().append(card); }, printScoringResults: function (response) { @@ -60,7 +60,7 @@ CodeOceanEditorEvaluation = { renderHint: function (object) { var hint = object.data || object.hint; if (hint) { - $('#hint .panel-body').text(hint); + $('#hint .card-body').text(hint); $('#hint').fadeIn(); } }, diff --git a/app/assets/javascripts/editor/participantsupport.js b/app/assets/javascripts/editor/participantsupport.js index a826cbf0..f0329256 100644 --- a/app/assets/javascripts/editor/participantsupport.js +++ b/app/assets/javascripts/editor/participantsupport.js @@ -1,12 +1,12 @@ CodeOceanEditorFlowr = { isFlowrEnabled: true, - flowrResultHtml: '', + flowrResultHtml: '', handleStderrOutputForFlowr: function () { if (!this.isFlowrEnabled) return; var flowrUrl = $('#flowrHint').data('url'); - var flowrHintBody = $('#flowrHint .panel-body'); + var flowrHintBody = $('#flowrHint .card-body'); var queryParameters = { query: this.flowrOutputBuffer }; @@ -19,8 +19,8 @@ CodeOceanEditorFlowr = { var resultTile = $(collapsibleTileHtml); resultTile.find('h4 > a').text(question.title + ' | Found via ' + question.source); - resultTile.find('.panel-body').html(question.body); - resultTile.find('.panel-body').append('Open this question'); + resultTile.find('.card-body').html(question.body); + resultTile.find('.card-body').append('Open this question'); flowrHintBody.append(resultTile); }); diff --git a/app/assets/javascripts/editor/prompt.js b/app/assets/javascripts/editor/prompt.js index 1cbdfc8f..e767ba43 100644 --- a/app/assets/javascripts/editor/prompt.js +++ b/app/assets/javascripts/editor/prompt.js @@ -2,19 +2,19 @@ CodeOceanEditorPrompt = { prompt: '#prompt', showPrompt: function(msg) { - var label = $('#prompt .input-group-addon'); + var label = $('#prompt .input-group-text'); var prompt = $(this.prompt); label.text(msg.data || label.data('prompt')); - if (prompt.isPresent() && prompt.hasClass('hidden')) { - prompt.removeClass('hidden'); + if (prompt.isPresent() && prompt.hasClass('d-none')) { + prompt.removeClass('d-none'); } $('#prompt input').focus(); }, hidePrompt: function() { var prompt = $(this.prompt); - if (prompt.isPresent() && !prompt.hasClass('hidden')) { - prompt.addClass('hidden'); + if (prompt.isPresent() && !prompt.hasClass('d-none')) { + prompt.addClass('d-none'); } }, diff --git a/app/assets/javascripts/editor/submissions.js b/app/assets/javascripts/editor/submissions.js index 30f63a75..6c4fd96a 100644 --- a/app/assets/javascripts/editor/submissions.js +++ b/app/assets/javascripts/editor/submissions.js @@ -155,7 +155,7 @@ CodeOceanEditorSubmissions = { $('#stop').data('url', submission.stop_url); this.running = true; this.showSpinner($('#run')); - $('#score_div').addClass('hidden'); + $('#score_div').addClass('d-none'); this.toggleButtonStates(); var url = submission.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor this.initializeSocketForRunning(url); @@ -175,7 +175,7 @@ CodeOceanEditorSubmissions = { if ($('#test').is(':visible')) { this.createSubmission('#test', null, function(response) { this.showSpinner($('#test')); - $('#score_div').addClass('hidden'); + $('#score_div').addClass('d-none'); var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor this.initializeSocketForTesting(url); }.bind(this)); diff --git a/app/assets/javascripts/editor/turtle.js b/app/assets/javascripts/editor/turtle.js index bc5552eb..1bf80b32 100644 --- a/app/assets/javascripts/editor/turtle.js +++ b/app/assets/javascripts/editor/turtle.js @@ -38,10 +38,10 @@ CodeOceanEditorTurtle = { showCanvas: function () { if ($('#turtlediv').isPresent() - && this.turtlecanvas.hasClass('hidden')) { + && this.turtlecanvas.hasClass('d-none')) { // initialize two-column layout $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); - this.turtlecanvas.removeClass('hidden'); + this.turtlecanvas.removeClass('d-none'); } } diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index e964eebc..71167533 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -66,10 +66,9 @@ $(document).on('turbolinks:load', function() { $('#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 select').remove(); + $('#files li:last>div:last').removeClass('in').addClass('show') $('body, html').scrollTo('#add-file'); - // if we collapse the file forms by default, we need to click on the new element in order to open it. - // however, this crashes for more files (if we add several ones by clicking the add button more often), since the elements are probably not correctly added to the files list. - //$('#files li:last>div:first>a>div').click(); // 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. @@ -93,7 +92,7 @@ $(document).on('turbolinks:load', function() { var deleteFile = function(event) { event.preventDefault(); - var fileUrl = $(this).data('file-url'); + var fileUrl = $(event.target).data('file-url'); if (confirm('<%= I18n.t('shared.confirm_destroy') %>')) { var jqxhr = $.ajax({ @@ -139,9 +138,9 @@ $(document).on('turbolinks:load', function() { var enableBatchUpdate = function() { $('thead .batch a').on('click', function(event) { event.preventDefault(); - if (!$(this).data('toggled')) { - $(this).data('toggled', true); - $(this).text($(this).data('text')); + if (!$(event.target).data('toggled')) { + $(event.target).data('toggled', true); + $(event.target).text($(event.target).data('text')); buildCheckboxes(); } else { performBatchUpdate(); @@ -199,7 +198,7 @@ $(document).on('turbolinks:load', function() { var observeFileRoleChanges = function() { $(document).on('change', 'select[name$="[role]"]', function() { var is_test_file = $(this).val() === 'teacher_defined_test'; - var parent = $(this).parents('.panel'); + var parent = $(this).parents('.card'); var fields = parent.find('.test-related-fields'); if (is_test_file) { fields.slideDown(); @@ -262,17 +261,13 @@ $(document).on('turbolinks:load', function() { jqxhr.fail(ajaxError); } - if ($.isController('exercises')) { + 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'); - // new MarkdownEditor('#exercise_instructions'); - // new MarkdownEditor('#exercise_description') - // todo: add an ace editor for each file - new PagedownEditor('#exercise_description'); enableInlineFileCreation(); inferFileAttributes(); diff --git a/app/assets/javascripts/forms.js b/app/assets/javascripts/forms.js index 6bee8d0d..b74840bb 100644 --- a/app/assets/javascripts/forms.js +++ b/app/assets/javascripts/forms.js @@ -14,11 +14,11 @@ $(document).on('turbolinks:load', function() { var alternative_input = parent.find('.alternative-input'); if (alternative_input.attr('disabled')) { - $(this).text($(this).data('text-toggled')); + $(this).text($(event.target).data('text-toggled')); original_input.attr('disabled', true).hide(); alternative_input.attr('disabled', false).show(); } else { - $(this).text($(this).data('text-initial')); + $(this).text($(event.target).data('text-initial')); alternative_input.attr('disabled', true).hide(); original_input.attr('disabled', false).show(); } @@ -26,5 +26,27 @@ $(document).on('turbolinks:load', function() { }); window.CodeOcean.CHOSEN_OPTIONS = CHOSEN_OPTIONS; - $('select:visible').chosen(CHOSEN_OPTIONS); + chosen_inputs = $('select').filter(function(){ + return !$(this).parents('ul').is('#dummies'); + }); + + // enable chosen hook when editing an exercise to update ace code highlighting + if ($.isController('exercises') && $('.edit_exercise, .new_exercise').isPresent()) { + chosen_inputs.filter(function(){ + return $(this).attr('id').includes('file_type_id'); + }).on('change chosen:ready', function(event, parameter) { + // Set ACE editor mode (for code highlighting) on change of file type and after initialization + editorInstance = $(event.target).closest('.card-body').find('.editor')[0]; + selectedFileType = event.target.value; + CodeOceanEditor.updateEditorModeToFileTypeID(editorInstance, selectedFileType); + }) + } + + chosen_inputs.chosen(CHOSEN_OPTIONS); +}); + +// Remove some elements before going back to an older site. Otherwise, they might not work. +$(document).on('turbolinks:before-cache', function() { + $('.chosen-container').remove(); + $('#wmd-button-row-description').remove(); }); diff --git a/app/assets/javascripts/markdown_ace_editor.js b/app/assets/javascripts/markdown_ace_editor.js index 42e566fe..bd9845f3 100644 --- a/app/assets/javascripts/markdown_ace_editor.js +++ b/app/assets/javascripts/markdown_ace_editor.js @@ -9,7 +9,7 @@ }); editor.setShowPrintMargin(false); var session = editor.getSession(); - session.setMode('markdown'); + session.setMode('ace/mode/markdown'); session.setUseWrapMode(true); session.setValue($(selector).val()); }; diff --git a/app/assets/javascripts/pagedown.js b/app/assets/javascripts/pagedown.js deleted file mode 100644 index b48c2ae6..00000000 --- a/app/assets/javascripts/pagedown.js +++ /dev/null @@ -1,10 +0,0 @@ -(function() { - var ACE_FILES_PATH = '/assets/ace/'; - - window.PagedownEditor = function(selector) { - var converter = Markdown.getSanitizingConverter(); - var editor = new Markdown.Editor( converter ); - - editor.run(); - }; -})(); \ No newline at end of file diff --git a/app/assets/javascripts/pagedown/markdown.editor.js.erb b/app/assets/javascripts/pagedown/markdown.editor.js.erb new file mode 100644 index 00000000..4f5ecb6d --- /dev/null +++ b/app/assets/javascripts/pagedown/markdown.editor.js.erb @@ -0,0 +1,2131 @@ +// needs Markdown.Converter.js at the moment + +(function () { + + var util = {}, + position = {}, + ui = {}, + doc = window.document, + re = window.RegExp, + nav = window.navigator, + SETTINGS = { lineLength: 72 }, + + // Used to work around some browser bugs where we can't use feature testing. + uaSniffed = { + isIE: /msie/.test(nav.userAgent.toLowerCase()), + isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), + isOpera: /opera/.test(nav.userAgent.toLowerCase()) + }; + + + // ------------------------------------------------------------------- + // YOUR CHANGES GO HERE + // + // I've tried to localize the things you are likely to change to + // this area. + // ------------------------------------------------------------------- + + // The text that appears on the upper part of the dialog box when + // entering links. + var linkDialogTitle = "<%= I18n.t('components.markdown_editor.insert_link.dialog_title', default: 'Insert link') %>"; + var linkInputLabel = "<%= I18n.t('components.markdown_editor.insert_link.input_label', default: 'Link URL') %>"; + var linkInputPlaceholder = "http://example.com/ \"optional title\""; + var linkInputHelp = "<%= I18n.t('components.markdown_editor.insert_link.input_help', default: 'Enter URL to point link to and optional title to display when mouse is placed over the link') %>"; + + var imageDialogTitle = "<%= I18n.t('components.markdown_editor.insert_image.dialog_title', default: 'Insert image') %>"; + var imageInputLabel = "<%= I18n.t('components.markdown_editor.insert_image.input_label', default: 'Image URL') %>"; + var imageInputPlaceholder = "http://example.com/images/diagram.jpg \"optional title\""; + var imageInputHelp = "<%= I18n.t('components.markdown_editor.insert_link.input_help', default: 'Enter URL where image is located and optional title to display when mouse is placed over the image') %>"; + + var defaultHelpHoverTitle = "Markdown Editing Help"; + + // ------------------------------------------------------------------- + // END OF YOUR CHANGES + // ------------------------------------------------------------------- + + // help, if given, should have a property "handler", the click handler for the help button, + // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help"). + // If help isn't given, not help button is created. + // + // The constructed editor object has the methods: + // - getConverter() returns the markdown converter object that was passed to the constructor + // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. + // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. + Markdown.Editor = function (markdownConverter, idPostfix, help) { + + idPostfix = idPostfix || ""; + + var hooks = this.hooks = new Markdown.HookCollection(); + hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed + hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text + hooks.addFalse("insertImageDialog"); + /* called with one parameter: a callback to be called with the URL of the image. If the application creates + * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen + * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. + */ + + this.getConverter = function () { + return markdownConverter; + } + + var that = this, + cards; + + this.run = function () { + if (cards) + return; // already initialized + + cards = new CardCollection(idPostfix); + var commandManager = new CommandManager(hooks); + var previewManager = new PreviewManager(markdownConverter, cards, function () { + hooks.onPreviewRefresh(); + }); + var undoManager, uiManager; + + if (!/\?noundo/.test(doc.location.href)) { + undoManager = new UndoManager(function () { + previewManager.refresh(); + if (uiManager) // not available on the first call + uiManager.setUndoRedoButtonStates(); + }, cards); + this.textOperation = function (f) { + undoManager.setCommandMode(); + f(); + that.refreshPreview(); + } + } + + uiManager = new UIManager(idPostfix, cards, undoManager, previewManager, commandManager, help); + uiManager.setUndoRedoButtonStates(); + + var forceRefresh = that.refreshPreview = function () { + previewManager.refresh(true); + }; + + forceRefresh(); + }; + + }; + + // before: contains all the text in the input box BEFORE the selection. + // after: contains all the text in the input box AFTER the selection. + function Chunks() { } + + // startRegex: a regular expression to find the start tag + // endRegex: a regular expresssion to find the end tag + Chunks.prototype.findTags = function (startRegex, endRegex) { + + var chunkObj = this; + var regex; + + if (startRegex) { + + regex = util.extendRegExp(startRegex, "", "$"); + + this.before = this.before.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + + regex = util.extendRegExp(startRegex, "^", ""); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + } + + if (endRegex) { + + regex = util.extendRegExp(endRegex, "", "$"); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + + regex = util.extendRegExp(endRegex, "^", ""); + + this.after = this.after.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + } + }; + + // If remove is false, the whitespace is transferred + // to the before/after regions. + // + // If remove is true, the whitespace disappears. + Chunks.prototype.trimWhitespace = function (remove) { + var beforeReplacer, afterReplacer, that = this; + if (remove) { + beforeReplacer = afterReplacer = ""; + } else { + beforeReplacer = function (s) { + that.before += s; + return ""; + }; + afterReplacer = function (s) { that.after = s + that.after; return ""; } + } + + this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); + }; + + + Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { + + if (nLinesBefore === undefined) { + nLinesBefore = 1; + } + + if (nLinesAfter === undefined) { + nLinesAfter = 1; + } + + nLinesBefore++; + nLinesAfter++; + + var regexText; + var replacementText; + + // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 + if (navigator.userAgent.match(/Chrome/)) { + "X".match(/()./); + } + + this.selection = this.selection.replace(/(^\n*)/, ""); + + this.startTag = this.startTag + re.$1; + + this.selection = this.selection.replace(/(\n*$)/, ""); + this.endTag = this.endTag + re.$1; + this.startTag = this.startTag.replace(/(^\n*)/, ""); + this.before = this.before + re.$1; + this.endTag = this.endTag.replace(/(\n*$)/, ""); + this.after = this.after + re.$1; + + if (this.before) { + + regexText = replacementText = ""; + + while (nLinesBefore--) { + regexText += "\\n?"; + replacementText += "\n"; + } + + if (findExtraNewlines) { + regexText = "\\n*"; + } + this.before = this.before.replace(new re(regexText + "$", ""), replacementText); + } + + if (this.after) { + + regexText = replacementText = ""; + + while (nLinesAfter--) { + regexText += "\\n?"; + replacementText += "\n"; + } + if (findExtraNewlines) { + regexText = "\\n*"; + } + + this.after = this.after.replace(new re(regexText, ""), replacementText); + } + }; + + // end of Chunks + + // A collection of the important regions on the page. + // Cached so we don't have to keep traversing the DOM. + // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around + // this issue: + // Internet explorer has problems with CSS sprite buttons that use HTML + // lists. When you click on the background image "button", IE will + // select the non-existent link text and discard the selection in the + // textarea. The solution to this is to cache the textarea selection + // on the button's mousedown event and set a flag. In the part of the + // code where we need to grab the selection, we check for the flag + // and, if it's set, use the cached area instead of querying the + // textarea. + // + // This ONLY affects Internet Explorer (tested on versions 6, 7 + // and 8) and ONLY on button clicks. Keyboard shortcuts work + // normally since the focus never leaves the textarea. + function CardCollection(postfix) { + this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); + this.preview = doc.getElementById("wmd-preview" + postfix); + this.input = doc.getElementById("wmd-input" + postfix); + } + // Returns true if the DOM element is visible, false if it's hidden. + // Checks if display is anything other than none. + util.isVisible = function (elem) { + + if (window.getComputedStyle) { + // Most browsers + return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; + } + else if (elem.currentStyle) { + // IE + return elem.currentStyle["display"] !== "none"; + } + }; + + + // Adds a listener callback to a DOM element which is fired on a specified + // event. + util.addEvent = function (elem, event, listener) { + if (elem.attachEvent) { + // IE only. The "on" is mandatory. + elem.attachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.addEventListener(event, listener, false); + } + }; + + + // Removes a listener callback from a DOM element which is fired on a specified + // event. + util.removeEvent = function (elem, event, listener) { + if (elem.detachEvent) { + // IE only. The "on" is mandatory. + elem.detachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.removeEventListener(event, listener, false); + } + }; + + // Converts \r\n and \r to \n. + util.fixEolChars = function (text) { + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\r/g, "\n"); + return text; + }; + + // Extends a regular expression. Returns a new RegExp + // using pre + regex + post as the expression. + // Used in a few functions where we have a base + // expression and we want to pre- or append some + // conditions to it (e.g. adding "$" to the end). + // The flags are unchanged. + // + // regex is a RegExp, pre and post are strings. + util.extendRegExp = function (regex, pre, post) { + + if (pre === null || pre === undefined) { + pre = ""; + } + if (post === null || post === undefined) { + post = ""; + } + + var pattern = regex.toString(); + var flags; + + // Replace the flags with empty space and store them. + pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { + flags = flagsPart; + return ""; + }); + + // Remove the slash delimiters on the regular expression. + pattern = pattern.replace(/(^\/|\/$)/g, ""); + pattern = pre + pattern + post; + + return new re(pattern, flags); + }; + + // UNFINISHED + // The assignment in the while loop makes jslint cranky. + // I'll change it to a better loop later. + position.getTop = function (elem, isInner) { + var result = elem.offsetTop; + if (!isInner) { + while (elem = elem.offsetParent) { + result += elem.offsetTop; + } + } + return result; + }; + + position.getHeight = function (elem) { + return elem.offsetHeight || elem.scrollHeight; + }; + + position.getWidth = function (elem) { + return elem.offsetWidth || elem.scrollWidth; + }; + + position.getPageSize = function () { + + var scrollWidth, scrollHeight; + var innerWidth, innerHeight; + + // It's not very clear which blocks work with which browsers. + if (self.innerHeight && self.scrollMaxY) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = self.innerHeight + self.scrollMaxY; + } + else if (doc.body.scrollHeight > doc.body.offsetHeight) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = doc.body.scrollHeight; + } + else { + scrollWidth = doc.body.offsetWidth; + scrollHeight = doc.body.offsetHeight; + } + + if (self.innerHeight) { + // Non-IE browser + innerWidth = self.innerWidth; + innerHeight = self.innerHeight; + } + else if (doc.documentElement && doc.documentElement.clientHeight) { + // Some versions of IE (IE 6 w/ a DOCTYPE declaration) + innerWidth = doc.documentElement.clientWidth; + innerHeight = doc.documentElement.clientHeight; + } + else if (doc.body) { + // Other versions of IE + innerWidth = doc.body.clientWidth; + innerHeight = doc.body.clientHeight; + } + + var maxWidth = Math.max(scrollWidth, innerWidth); + var maxHeight = Math.max(scrollHeight, innerHeight); + return [maxWidth, maxHeight, innerWidth, innerHeight]; + }; + + // Handles pushing and popping TextareaStates for undo/redo commands. + // I should rename the stack variables to list. + function UndoManager(callback, cards) { + + var undoObj = this; + var undoStack = []; // A stack of undo states + var stackPtr = 0; // The index of the current state + var mode = "none"; + var lastState; // The last state + var timer; // The setTimeout handle for cancelling the timer + var inputStateObj; + + // Set the mode for later logic steps. + var setMode = function (newMode, noSave) { + if (mode != newMode) { + mode = newMode; + if (!noSave) { + saveState(); + } + } + + if (!uaSniffed.isIE || mode != "moving") { + timer = setTimeout(refreshState, 1); + } + else { + inputStateObj = null; + } + }; + + var refreshState = function (isInitialState) { + inputStateObj = new TextareaState(cards, isInitialState); + timer = undefined; + }; + + this.setCommandMode = function () { + mode = "command"; + saveState(); + timer = setTimeout(refreshState, 0); + }; + + this.canUndo = function () { + return stackPtr > 1; + }; + + this.canRedo = function () { + if (undoStack[stackPtr + 1]) { + return true; + } + return false; + }; + + // Removes the last state and restores it. + this.undo = function () { + + if (undoObj.canUndo()) { + if (lastState) { + // What about setting state -1 to null or checking for undefined? + lastState.restore(); + lastState = null; + } + else { + undoStack[stackPtr] = new TextareaState(cards); + undoStack[--stackPtr].restore(); + + if (callback) { + callback(); + } + } + } + + mode = "none"; + cards.input.focus(); + refreshState(); + }; + + // Redo an action. + this.redo = function () { + + if (undoObj.canRedo()) { + + undoStack[++stackPtr].restore(); + + if (callback) { + callback(); + } + } + + mode = "none"; + cards.input.focus(); + refreshState(); + }; + + // Push the input area state to the stack. + var saveState = function () { + var currState = inputStateObj || new TextareaState(cards); + + if (!currState) { + return false; + } + if (mode == "moving") { + if (!lastState) { + lastState = currState; + } + return; + } + if (lastState) { + if (undoStack[stackPtr - 1].text != lastState.text) { + undoStack[stackPtr++] = lastState; + } + lastState = null; + } + undoStack[stackPtr++] = currState; + undoStack[stackPtr + 1] = null; + if (callback) { + callback(); + } + }; + + var handleCtrlYZ = function (event) { + + var handled = false; + + if (event.ctrlKey || event.metaKey) { + + // IE and Opera do not support charCode. + var keyCode = event.charCode || event.keyCode; + var keyCodeChar = String.fromCharCode(keyCode); + + switch (keyCodeChar) { + + case "y": + undoObj.redo(); + handled = true; + break; + + case "z": + if (!event.shiftKey) { + undoObj.undo(); + } + else { + undoObj.redo(); + } + handled = true; + break; + } + } + + if (handled) { + if (event.preventDefault) { + event.preventDefault(); + } + if (window.event) { + window.event.returnValue = false; + } + } + }; + + // Set the mode depending on what is going on in the input area. + var handleModeChange = function (event) { + + if (!event.ctrlKey && !event.metaKey) { + + var keyCode = event.keyCode; + + if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { + // 33 - 40: page up/dn and arrow keys + // 63232 - 63235: page up/dn and arrow keys on safari + setMode("moving"); + } + else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { + // 8: backspace + // 46: delete + // 127: delete + setMode("deleting"); + } + else if (keyCode == 13) { + // 13: Enter + setMode("newlines"); + } + else if (keyCode == 27) { + // 27: escape + setMode("escape"); + } + else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { + // 16-20 are shift, etc. + // 91: left window key + // I think this might be a little messed up since there are + // a lot of nonprinting keys above 20. + setMode("typing"); + } + } + }; + + var setEventHandlers = function () { + util.addEvent(cards.input, "keypress", function (event) { + // keyCode 89: y + // keyCode 90: z + if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) { + event.preventDefault(); + } + }); + + var handlePaste = function () { + if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != cards.input.value)) { + if (timer == undefined) { + mode = "paste"; + saveState(); + refreshState(); + } + } + }; + + util.addEvent(cards.input, "keydown", handleCtrlYZ); + util.addEvent(cards.input, "keydown", handleModeChange); + util.addEvent(cards.input, "mousedown", function () { + setMode("moving"); + }); + + cards.input.onpaste = handlePaste; + cards.input.ondrop = handlePaste; + }; + + var init = function () { + setEventHandlers(); + refreshState(true); + saveState(); + }; + + init(); + } + + // end of UndoManager + + // The input textarea state/contents. + // This is used to implement undo/redo by the undo manager. + function TextareaState(cards, isInitialState) { + + // Aliases + var stateObj = this; + var inputArea = cards.input; + this.init = function () { + if (!util.isVisible(inputArea)) { + return; + } + if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box + return; + } + + this.setInputAreaSelectionStartEnd(); + this.scrollTop = inputArea.scrollTop; + if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { + this.text = inputArea.value; + } + + }; + + // Sets the selected text in the input box after we've performed an + // operation. + this.setInputAreaSelection = function () { + + if (!util.isVisible(inputArea)) { + return; + } + + if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { + + inputArea.focus(); + inputArea.selectionStart = stateObj.start; + inputArea.selectionEnd = stateObj.end; + inputArea.scrollTop = stateObj.scrollTop; + } + else if (doc.selection) { + + if (doc.activeElement && doc.activeElement !== inputArea) { + return; + } + + inputArea.focus(); + var range = inputArea.createTextRange(); + range.moveStart("character", -inputArea.value.length); + range.moveEnd("character", -inputArea.value.length); + range.moveEnd("character", stateObj.end); + range.moveStart("character", stateObj.start); + range.select(); + } + }; + + this.setInputAreaSelectionStartEnd = function () { + + if (!cards.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { + + stateObj.start = inputArea.selectionStart; + stateObj.end = inputArea.selectionEnd; + } + else if (doc.selection) { + + stateObj.text = util.fixEolChars(inputArea.value); + + // IE loses the selection in the textarea when buttons are + // clicked. On IE we cache the selection. Here, if something is cached, + // we take it. + var range = cards.ieCachedRange || doc.selection.createRange(); + + var fixedRange = util.fixEolChars(range.text); + var marker = "\x07"; + var markedRange = marker + fixedRange + marker; + range.text = markedRange; + var inputText = util.fixEolChars(inputArea.value); + + range.moveStart("character", -markedRange.length); + range.text = fixedRange; + + stateObj.start = inputText.indexOf(marker); + stateObj.end = inputText.lastIndexOf(marker) - marker.length; + + var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; + + if (len) { + range.moveStart("character", -fixedRange.length); + while (len--) { + fixedRange += "\n"; + stateObj.end += 1; + } + range.text = fixedRange; + } + + if (cards.ieCachedRange) + stateObj.scrollTop = cards.ieCachedScrollTop; // this is set alongside with ieCachedRange + + cards.ieCachedRange = null; + + this.setInputAreaSelection(); + } + }; + + // Restore this state into the input area. + this.restore = function () { + + if (stateObj.text != undefined && stateObj.text != inputArea.value) { + inputArea.value = stateObj.text; + } + this.setInputAreaSelection(); + inputArea.scrollTop = stateObj.scrollTop; + }; + + // Gets a collection of HTML chunks from the inptut textarea. + this.getChunks = function () { + + var chunk = new Chunks(); + chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); + chunk.startTag = ""; + chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); + chunk.endTag = ""; + chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); + chunk.scrollTop = stateObj.scrollTop; + + return chunk; + }; + + // Sets the TextareaState properties given a chunk of markdown. + this.setChunks = function (chunk) { + + chunk.before = chunk.before + chunk.startTag; + chunk.after = chunk.endTag + chunk.after; + + this.start = chunk.before.length; + this.end = chunk.before.length + chunk.selection.length; + this.text = chunk.before + chunk.selection + chunk.after; + this.scrollTop = chunk.scrollTop; + }; + this.init(); + } + function PreviewManager(converter, cards, previewRefreshCallback) { + + var managerObj = this; + var timeout; + var elapsedTime; + var oldInputText; + var maxDelay = 3000; + var startType = "delayed"; // The other legal value is "manual" + + // Adds event listeners to elements + var setupEvents = function (inputElem, listener) { + + util.addEvent(inputElem, "input", listener); + inputElem.onpaste = listener; + inputElem.ondrop = listener; + + util.addEvent(inputElem, "keypress", listener); + util.addEvent(inputElem, "keydown", listener); + }; + + var getDocScrollTop = function () { + + var result = 0; + + if (window.innerHeight) { + result = window.pageYOffset; + } + else + if (doc.documentElement && doc.documentElement.scrollTop) { + result = doc.documentElement.scrollTop; + } + else + if (doc.body) { + result = doc.body.scrollTop; + } + + return result; + }; + + var makePreviewHtml = function () { + + // If there is no registered preview card + // there is nothing to do. + if (!cards.preview) + return; + + + var text = cards.input.value; + if (text && text == oldInputText) { + return; // Input text hasn't changed. + } + else { + oldInputText = text; + } + + var prevTime = new Date().getTime(); + + text = converter.makeHtml(text); + + // Calculate the processing time of the HTML creation. + // It's used as the delay time in the event listener. + var currTime = new Date().getTime(); + elapsedTime = currTime - prevTime; + + pushPreviewHtml(text); + }; + + // setTimeout is already used. Used as an event listener. + var applyTimeout = function () { + + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + if (startType !== "manual") { + + var delay = 0; + + if (startType === "delayed") { + delay = elapsedTime; + } + + if (delay > maxDelay) { + delay = maxDelay; + } + timeout = setTimeout(makePreviewHtml, delay); + } + }; + + var getScaleFactor = function (card) { + if (card.scrollHeight <= card.clientHeight) { + return 1; + } + return card.scrollTop / (card.scrollHeight - card.clientHeight); + }; + + var setCardScrollTops = function () { + if (cards.preview) { + cards.preview.scrollTop = (cards.preview.scrollHeight - cards.preview.clientHeight) * getScaleFactor(cards.preview); + } + }; + + this.refresh = function (requiresRefresh) { + + if (requiresRefresh) { + oldInputText = ""; + makePreviewHtml(); + } + else { + applyTimeout(); + } + }; + + this.processingTime = function () { + return elapsedTime; + }; + + var isFirstTimeFilled = true; + + // IE doesn't let you use innerHTML if the element is contained somewhere in a table + // (which is the case for inline editing) -- in that case, detach the element, set the + // value, and reattach. Yes, that *is* ridiculous. + var ieSafePreviewSet = function (text) { + var preview = cards.preview; + var parent = preview.parentNode; + var sibling = preview.nextSibling; + parent.removeChild(preview); + preview.innerHTML = text; + if (!sibling) + parent.appendChild(preview); + else + parent.insertBefore(preview, sibling); + }; + + var nonSuckyBrowserPreviewSet = function (text) { + cards.preview.innerHTML = text; + }; + + var previewSetter; + + var previewSet = function (text) { + if (previewSetter) + return previewSetter(text); + + try { + nonSuckyBrowserPreviewSet(text); + previewSetter = nonSuckyBrowserPreviewSet; + } catch (e) { + previewSetter = ieSafePreviewSet; + previewSetter(text); + } + }; + + var pushPreviewHtml = function (text) { + + var emptyTop = position.getTop(cards.input) - getDocScrollTop(); + + if (cards.preview) { + previewSet(text); + previewRefreshCallback(); + } + + setCardScrollTops(); + + if (isFirstTimeFilled) { + isFirstTimeFilled = false; + return; + } + + var fullTop = position.getTop(cards.input) - getDocScrollTop(); + + if (uaSniffed.isIE) { + setTimeout(function () { + window.scrollBy(0, fullTop - emptyTop); + }, 0); + } + else { + window.scrollBy(0, fullTop - emptyTop); + } + }; + + var init = function () { + + setupEvents(cards.input, applyTimeout); + makePreviewHtml(); + + if (cards.preview) { + cards.preview.scrollTop = 0; + } + }; + + init(); + } + // This simulates a modal dialog box and asks for the URL when you + // click the hyperlink or image buttons. + // + // text: The html for the input box. + // defaultInputText: The default value that appears in the input box. + // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. + // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel + // was chosen). + ui.prompt = function (title, inputLabel, inputPlaceholder, inputHelp, callback) { + + // These variables need to be declared at this level since they are used + // in multiple functions. + var dialog; // The dialog box. + var input; // The text box where you enter the hyperlink. + + + if (inputPlaceholder === undefined) { + inputPlaceholder = ""; + } + + // Used as a keydown event handler. Esc dismisses the prompt. + // Key code 27 is ESC. + var checkEscape = function (key) { + var code = (key.charCode || key.keyCode); + if (code === 27) { + close(true); + } + }; + + // Dismisses the hyperlink input box. + // isCancel is true if we don't care about the input text. + // isCancel is false if we are going to keep the text. + var close = function (isCancel) { + util.removeEvent(doc.body, "keydown", checkEscape); + var text = input.value; + + if (isCancel) { + text = null; + } + else { + // Fixes common pasting errors. + text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); + if (!/^(?:https?|ftp):\/\//.test(text)) + text = 'http://' + text; + } + + $(dialog).modal('hide'); + + callback(text); + return false; + }; + + + + // Create the text input box form/window. + var createDialog = function () { + //+ //+ + // The main dialog box. + dialog = doc.createElement("div"); + dialog.className = "modal fade"; + dialog.style.display = "none"; + + var dialogContainer = doc.createElement("div"); + dialogContainer.className = "modal-dialog"; + dialog.appendChild(dialogContainer); + + var dialogContent = doc.createElement("div"); + dialogContent.className = "modal-content"; + dialogContainer.appendChild(dialogContent); + + // The header. + var header = doc.createElement("div"); + header.className = "modal-header"; + header.innerHTML = '+ //+ // + //'+title+'
'; + dialogContent.appendChild(header); + + // The body. + var body = doc.createElement("div"); + body.className = "modal-body"; + dialogContent.appendChild(body); + + // The footer. + var footer = doc.createElement("div"); + footer.className = "modal-footer"; + dialogContent.appendChild(footer); + + // The web form container for the text box and buttons. + var form = doc.createElement("form"); + form.onsubmit = function () { return close(false); }; + body.appendChild(form); + + // The input text box + var formGroup = doc.createElement("div"); + formGroup.className = "form-group"; + form.appendChild(formGroup); + + var label = doc.createElement("label"); + label.htmlFor = "url-" + new Date().getTime(); + label.innerHTML = inputLabel; + formGroup.appendChild(label); + + input = doc.createElement("input"); + input.id = label.htmlFor; + input.type = "text"; + input.className = "form-control"; + input.placeholder = inputPlaceholder; + formGroup.appendChild(input); + + var helpBlock = doc.createElement("span"); + helpBlock.className = "help-block form-text"; + helpBlock.innerHTML = inputHelp || ''; + formGroup.appendChild(helpBlock); + + // The ok button + var okButton = doc.createElement("button"); + okButton.className = "btn btn-primary"; + okButton.type = "button"; + okButton.onclick = function () { return close(false); }; + okButton.innerHTML = "OK"; + + // The cancel button + var cancelButton = doc.createElement("button"); + cancelButton.className = "btn btn-secondary"; + cancelButton.type = "button"; + cancelButton.onclick = function () { return close(true); }; + cancelButton.innerHTML = "Cancel"; + + footer.appendChild(okButton); + footer.appendChild(cancelButton); + + util.addEvent(doc.body, "keydown", checkEscape); + + doc.body.appendChild(dialog); + + }; + + // Why is this in a zero-length timeout? + // Is it working around a browser bug? + setTimeout(function () { + + createDialog(); + + var defTextLen = 0; + if (input.selectionStart !== undefined) { + input.selectionStart = 0; + input.selectionEnd = defTextLen; + } + else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(false); + range.moveStart("character", -defTextLen); + range.moveEnd("character", defTextLen); + range.select(); + } + + $(dialog).on('shown', function () { + input.focus(); + }); + + $(dialog).on('hidden', function () { + dialog.parentNode.removeChild(dialog); + }); + + $(dialog).modal() + + }, 0); + }; + + function UIManager(postfix, cards, undoManager, previewManager, commandManager, helpOptions) { + + var inputBox = cards.input, + buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. + + makeSpritedButtonRow(); + + var keyEvent = "keydown"; + if (uaSniffed.isOpera) { + keyEvent = "keypress"; + } + + util.addEvent(inputBox, keyEvent, function (key) { + + // Check to see if we have a button key and, if so execute the callback. + if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { + + var keyCode = key.charCode || key.keyCode; + var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); + + switch (keyCodeStr) { + case "b": + doClick(buttons.bold); + break; + case "i": + doClick(buttons.italic); + break; + case "l": + doClick(buttons.link); + break; + case "q": + doClick(buttons.quote); + break; + case "k": + doClick(buttons.code); + break; + case "g": + doClick(buttons.image); + break; + case "o": + doClick(buttons.olist); + break; + case "u": + doClick(buttons.ulist); + break; + case "h": + doClick(buttons.heading); + break; + case "r": + doClick(buttons.hr); + break; + case "y": + doClick(buttons.redo); + break; + case "z": + if (key.shiftKey) { + doClick(buttons.redo); + } + else { + doClick(buttons.undo); + } + break; + default: + return; + } + + + if (key.preventDefault) { + key.preventDefault(); + } + + if (window.event) { + window.event.returnValue = false; + } + } + }); + + // Auto-indent on shift-enter + util.addEvent(inputBox, "keyup", function (key) { + if (key.shiftKey && !key.ctrlKey && !key.metaKey) { + var keyCode = key.charCode || key.keyCode; + // Character 13 is Enter + if (keyCode === 13) { + var fakeButton = {}; + fakeButton.textOp = bindCommand("doAutoindent"); + doClick(fakeButton); + } + } + }); + + // special handler because IE clears the context of the textbox on ESC + if (uaSniffed.isIE) { + util.addEvent(inputBox, "keydown", function (key) { + var code = key.keyCode; + if (code === 27) { + return false; + } + }); + } + + + // Perform the button's action. + function doClick(button) { + + inputBox.focus(); + + if (button.textOp) { + + if (undoManager) { + undoManager.setCommandMode(); + } + + var state = new TextareaState(cards); + + if (!state) { + return; + } + + var chunks = state.getChunks(); + + // Some commands launch a "modal" prompt dialog. Javascript + // can't really make a modal dialog box and the WMD code + // will continue to execute while the dialog is displayed. + // This prevents the dialog pattern I'm used to and means + // I can't do something like this: + // + // var link = CreateLinkDialog(); + // makeMarkdownLink(link); + // + // Instead of this straightforward method of handling a + // dialog I have to pass any code which would execute + // after the dialog is dismissed (e.g. link creation) + // in a function parameter. + // + // Yes this is awkward and I think it sucks, but there's + // no real workaround. Only the image and link code + // create dialogs and require the function pointers. + var fixupInputArea = function () { + + inputBox.focus(); + + if (chunks) { + state.setChunks(chunks); + } + + state.restore(); + previewManager.refresh(); + }; + + var noCleanup = button.textOp(chunks, fixupInputArea); + + if (!noCleanup) { + fixupInputArea(); + } + + } + + if (button.execute) { + button.execute(undoManager); + } + } + function setupButton(button, isEnabled) { + + if (isEnabled) { + button.disabled = false; + + if (!button.isHelp) { + button.onclick = function () { + if (this.onmouseout) { + this.onmouseout(); + } + doClick(this); + return false; + } + } + } + else { + button.disabled = true; + } + } + + function bindCommand(method) { + if (typeof method === "string") + method = commandManager[method]; + return function () { method.apply(commandManager, arguments); } + } + + function makeSpritedButtonRow() { + + var buttonBar = cards.buttonBar; + var buttonRow = document.createElement("div"); + buttonRow.id = "wmd-button-row" + postfix; + buttonRow.className = 'btn-toolbar'; + buttonRow = buttonBar.appendChild(buttonRow); + + var makeButton = function (id, title, iconClass, textOp, group) { + var button = document.createElement("button"); + button.className = "btn btn-secondary btn-sm"; + var buttonImage = document.createElement("i"); + buttonImage.className = iconClass; + button.appendChild(buttonImage); + button.id = id + postfix; + button.title = title; + button.setAttribute("data-toggle", "tooltip"); + button.setAttribute("data-placement", "top"); + if (textOp) + button.textOp = textOp; + setupButton(button, true); + if (group) { + group.appendChild(button); + } else { + buttonRow.appendChild(button); + } + return button; + }; + var makeGroup = function (num) { + var group = document.createElement("div"); + group.className = "m-1 btn-group wmd-button-group" + num; + group.id = "wmd-button-group" + num + postfix; + buttonRow.appendChild(group); + return group + }; + + var group1 = makeGroup(1); + buttons.bold = makeButton("wmd-bold-button", "<%= I18n.t('components.markdown_editor.bold.button_title', default: 'Bold (Ctrl+B)') %>", "m-1 fa fa-bold", bindCommand("doBold"), group1); + buttons.italic = makeButton("wmd-italic-button", "<%= I18n.t('components.markdown_editor.italic.button_title', default: 'Italic (Ctrl+I)') %>", "m-1 fa fa-italic", bindCommand("doItalic"), group1); + + var group2 = makeGroup(2); + buttons.link = makeButton("wmd-link-button", "<%= I18n.t('components.markdown_editor.insert_link.button_title', default: 'Link (Ctrl+L)') %>", "m-1 fa fa-link", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, false); + }), group2); + buttons.image = makeButton("wmd-image-button", "<%= I18n.t('components.markdown_editor.insert_image.button_title', default: 'Image (Ctrl+G)') %>", "m-1 fa fa-picture-o", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, true); + }), group2); + buttons.quote = makeButton("wmd-quote-button", "<%= I18n.t('components.markdown_editor.blockquoute.button_title', default: 'Blockquote (Ctrl+Q)') %>", "m-1 fa fa-quote-left", bindCommand("doBlockquote"), group2); + buttons.code = makeButton("wmd-code-button", "<%= I18n.t('components.markdown_editor.code_sample.button_title', default: 'Code Sample (Ctrl+K)') %>", "m-1 fa fa-code", bindCommand("doCode"), group2); + + var group3 = makeGroup(3); + buttons.ulist = makeButton("wmd-ulist-button", "<%= I18n.t('components.markdown_editor.bulleted_list.button_title', default: 'Bulleted List (Ctrl+U)') %>", "m-1 fa fa-list-ul", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, false); + }), group3); + buttons.olist = makeButton("wmd-olist-button", "<%= I18n.t('components.markdown_editor.numbered_list.button_title', default: 'Numbered List (Ctrl+O)') %>", "m-1 fa fa-list-ol", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, true); + }), group3); + buttons.heading = makeButton("wmd-heading-button", "<%= I18n.t('components.markdown_editor.heading.button_title', default: 'Heading (Ctrl+H)') %>", "m-1 fa fa-font", bindCommand("doHeading"), group3); + + var group4 = makeGroup(4); + buttons.undo = makeButton("wmd-undo-button", "<%= I18n.t('components.markdown_editor.undo.button_title', default: 'Undo (Ctrl+Z)') %>", "m-1 fa fa-undo", null, group4); + buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; + + var redoTitle = /win/.test(nav.platform.toLowerCase()) ? + "<%= I18n.t('components.markdown_editor.redo.button_title.win', default: 'Redo (Ctrl+Y)') %>" : + "<%= I18n.t('components.markdown_editor.redo.button_title.other', default: 'Redo (Ctrl+Shift+Z)') %>"; // mac and other non-Windows platforms + + buttons.redo = makeButton("wmd-redo-button", redoTitle, "m-1 fa fa-repeat", null, group4); + buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; + + if (helpOptions) { + var group5 = makeGroup(5); + group5.className = group5.className + " ml-auto"; + var helpButton = document.createElement("button"); + var helpButtonImage = document.createElement("i"); + helpButtonImage.className = "m-1 fa fa-info"; + helpButton.appendChild(helpButtonImage); + helpButton.className = "btn btn-info btn-sm"; + helpButton.id = "wmd-help-button" + postfix; + helpButton.isHelp = true; + helpButton.setAttribute("data-toggle", "tooltip"); + helpButton.setAttribute("data-placement", "top"); + helpButton.title = helpOptions.title || defaultHelpHoverTitle; + helpButton.onclick = helpOptions.handler; + + setupButton(helpButton, true); + group5.appendChild(helpButton); + buttons.help = helpButton; + } + + setUndoRedoButtonStates(); + } + + function setUndoRedoButtonStates() { + if (undoManager) { + setupButton(buttons.undo, undoManager.canUndo()); + setupButton(buttons.redo, undoManager.canRedo()); + } + } + this.setUndoRedoButtonStates = setUndoRedoButtonStates; + + } + + function CommandManager(pluginHooks) { + this.hooks = pluginHooks; + } + + var commandProto = CommandManager.prototype; + + // The markdown symbols - 4 spaces = code, > = blockquote, etc. + commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; + + // Remove markdown symbols from the chunk selection. + commandProto.unwrap = function (chunk) { + var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); + chunk.selection = chunk.selection.replace(txt, "$1 $2"); + }; + + commandProto.wrap = function (chunk, len) { + this.unwrap(chunk); + var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), + that = this; + + chunk.selection = chunk.selection.replace(regex, function (line, marked) { + if (new re("^" + that.prefixes, "").test(line)) { + return line; + } + return marked + "\n"; + }); + + chunk.selection = chunk.selection.replace(/\s+$/, ""); + }; + + commandProto.doBold = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 2, "strong text"); + }; + + commandProto.doItalic = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 1, "emphasized text"); + }; + + // chunk: The selected region that will be enclosed with */** + // nStars: 1 for italics, 2 for bold + // insertText: If you just click the button without highlighting text, this gets inserted + commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) { + + // Get rid of whitespace and fixup newlines. + chunk.trimWhitespace(); + chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); + + // Look for stars before and after. Is the chunk already marked up? + // note that these regex matches cannot fail + var starsBefore = /(\**$)/.exec(chunk.before)[0]; + var starsAfter = /(^\**)/.exec(chunk.after)[0]; + + var prevStars = Math.min(starsBefore.length, starsAfter.length); + + // Remove stars if we have to since the button acts as a toggle. + if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { + chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); + chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); + } + else if (!chunk.selection && starsAfter) { + // It's not really clear why this code is necessary. It just moves + // some arbitrary stuff around. + chunk.after = chunk.after.replace(/^([*_]*)/, ""); + chunk.before = chunk.before.replace(/(\s?)$/, ""); + var whitespace = re.$1; + chunk.before = chunk.before + starsAfter + whitespace; + } + else { + + // In most cases, if you don't have any selected text and click the button + // you'll get a selected, marked up region with the default text inserted. + if (!chunk.selection && !starsAfter) { + chunk.selection = insertText; + } + + // Add the true markup. + var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? + chunk.before = chunk.before + markup; + chunk.after = markup + chunk.after; + } + + + }; + + commandProto.stripLinkDefs = function (text, defsToAdd) { + + text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, + function (totalMatch, id, link, newlines, title) { + defsToAdd[id] = totalMatch.replace(/\s*$/, ""); + if (newlines) { + // Strip the title and return that separately. + defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); + return newlines + title; + } + return ""; + }); + + return text; + }; + + commandProto.addLinkDef = function (chunk, linkDef) { + + var refNumber = 0; // The current reference number + var defsToAdd = {}; // + // Start with a clean slate by removing all previous link definitions. + chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); + chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); + chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); + + var defs = ""; + var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; + + var addDefNumber = function (def) { + refNumber++; + def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); + defs += "\n" + def; + }; + + // note that + // a) the recursive call to getLink cannot go infinite, because by definition + // of regex, inner is always a proper substring of wholeMatch, and + // b) more than one level of nesting is neither supported by the regex + // nor making a lot of sense (the only use case for nesting is a linked image) + var getLink = function (wholeMatch, before, inner, afterInner, id, end) { + inner = inner.replace(regex, getLink); + if (defsToAdd[id]) { + addDefNumber(defsToAdd[id]); + return before + inner + afterInner + refNumber + end; + } + return wholeMatch; + }; + + chunk.before = chunk.before.replace(regex, getLink); + + if (linkDef) { + addDefNumber(linkDef); + } + else { + chunk.selection = chunk.selection.replace(regex, getLink); + } + + var refOut = refNumber; + + chunk.after = chunk.after.replace(regex, getLink); + + if (chunk.after) { + chunk.after = chunk.after.replace(/\n*$/, ""); + } + if (!chunk.after) { + chunk.selection = chunk.selection.replace(/\n*$/, ""); + } + + chunk.after += "\n\n" + defs; + + return refOut; + }; + + // takes the line as entered into the add link/as image dialog and makes + // sure the URL and the optinal title are "nice". + function properlyEncoded(linkdef) { + return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) { + link = link.replace(/\?.*$/, function (querypart) { + return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical + }); + link = decodeURIComponent(link); // unencode first, to prevent double encoding + link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29'); + link = link.replace(/\?.*$/, function (querypart) { + return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded + }); + if (title) { + title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); + title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); + } + return title ? link + ' "' + title + '"' : link; + }); + } + + commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) { + + chunk.trimWhitespace(); + chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); + var background; + + if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { + + chunk.startTag = chunk.startTag.replace(/!?\[/, ""); + chunk.endTag = ""; + this.addLinkDef(chunk, null); + + } + else { + + // We're moving start and end tag back into the selection, since (as we're in the else block) we're not + // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the + // link text. linkEnteredCallback takes care of escaping any brackets. + chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; + chunk.startTag = chunk.endTag = ""; + + if (/\n\n/.test(chunk.selection)) { + this.addLinkDef(chunk, null); + return; + } + var that = this; + // The function to be executed when you enter a link and press OK or Cancel. + // Marks up the link and adds the ref. + var linkEnteredCallback = function (link) { + + if (link !== null) { + // ( $1 + // [^\\] anything that's not a backslash + // (?:\\\\)* an even number (this includes zero) of backslashes + // ) + // (?= followed by + // [[\]] an opening or closing bracket + // ) + // + // In other words, a non-escaped bracket. These have to be escaped now to make sure they + // don't count as the end of the link or similar. + // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), + // the bracket in one match may be the "not a backslash" character in the next match, so it + // should not be consumed by the first match. + // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the + // start of the string, so this also works if the selection begins with a bracket. We cannot solve + // this by anchoring with ^, because in the case that the selection starts with two brackets, this + // would mean a zero-width match at the start. Since zero-width matches advance the string position, + // the first bracket could then not act as the "not a backslash" for the second. + chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); + + var linkDef = " [999]: " + properlyEncoded(link); + + var num = that.addLinkDef(chunk, linkDef); + chunk.startTag = isImage ? "![" : "["; + chunk.endTag = "][" + num + "]"; + + if (!chunk.selection) { + if (isImage) { + chunk.selection = "enter image description here"; + } + else { + chunk.selection = "enter link description here"; + } + } + } + postProcessing(); + }; + + + if (isImage) { + if (!this.hooks.insertImageDialog(linkEnteredCallback)) + ui.prompt(imageDialogTitle, imageInputLabel, imageInputPlaceholder, imageInputHelp, linkEnteredCallback); + } + else { + ui.prompt(linkDialogTitle, linkInputLabel, linkInputPlaceholder, linkInputHelp, linkEnteredCallback); + } + return true; + } + }; + + // When making a list, hitting shift-enter will put your cursor on the next line + // at the current indent level. + commandProto.doAutoindent = function (chunk, postProcessing) { + + var commandMgr = this, + fakeSelection = false; + + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); + + // There's no selection, end the cursor wasn't at the end of the line: + // The user wants to split the current list item / code line / blockquote line + // (for the latter it doesn't really matter) in two. Temporarily select the + // (rest of the) line to achieve this. + if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { + chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { + chunk.selection = wholeMatch; + return ""; + }); + fakeSelection = true; + } + + if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doList) { + commandMgr.doList(chunk); + } + } + if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doBlockquote) { + commandMgr.doBlockquote(chunk); + } + } + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + if (commandMgr.doCode) { + commandMgr.doCode(chunk); + } + } + + if (fakeSelection) { + chunk.after = chunk.selection + chunk.after; + chunk.selection = ""; + } + }; + + commandProto.doBlockquote = function (chunk, postProcessing) { + + chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, + function (totalMatch, newlinesBefore, text, newlinesAfter) { + chunk.before += newlinesBefore; + chunk.after = newlinesAfter + chunk.after; + return text; + }); + + chunk.before = chunk.before.replace(/(>[ \t]*)$/, + function (totalMatch, blankLine) { + chunk.selection = blankLine + chunk.selection; + return ""; + }); + + chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); + chunk.selection = chunk.selection || "Blockquote"; + + // The original code uses a regular expression to find out how much of the + // text *directly before* the selection already was a blockquote: + + /* + if (chunk.before) { + chunk.before = chunk.before.replace(/\n?$/, "\n"); + } + chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, + function (totalMatch) { + chunk.startTag = totalMatch; + return ""; + }); + */ + + // This comes down to: + // Go backwards as many lines a possible, such that each line + // a) starts with ">", or + // b) is almost empty, except for whitespace, or + // c) is preceeded by an unbroken chain of non-empty lines + // leading up to a line that starts with ">" and at least one more character + // and in addition + // d) at least one line fulfills a) + // + // Since this is essentially a backwards-moving regex, it's susceptible to + // catastrophic backtracking and can cause the browser to hang; + // see e.g. http://meta.stackoverflow.com/questions/9807. + // + // Hence we replaced this by a simple state machine that just goes through the + // lines and checks for a), b), and c). + + var match = "", + leftOver = "", + line; + if (chunk.before) { + var lines = chunk.before.replace(/\n$/, "").split("\n"); + var inChain = false; + for (var i = 0; i < lines.length; i++) { + var good = false; + line = lines[i]; + inChain = inChain && line.length > 0; // c) any non-empty line continues the chain + if (/^>/.test(line)) { // a) + good = true; + if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain + inChain = true; + } else if (/^[ \t]*$/.test(line)) { // b) + good = true; + } else { + good = inChain; // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain + } + if (good) { + match += line + "\n"; + } else { + leftOver += match + line; + match = "\n"; + } + } + if (!/(^|\n)>/.test(match)) { // d) + leftOver += match; + match = ""; + } + } + + chunk.startTag = match; + chunk.before = leftOver; + + // end of change + + if (chunk.after) { + chunk.after = chunk.after.replace(/^\n?/, "\n"); + } + + chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, + function (totalMatch) { + chunk.endTag = totalMatch; + return ""; + } + ); + + var replaceBlanksInTags = function (useBracket) { + + var replacement = useBracket ? "> " : ""; + + if (chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + if (chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + }; + + if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) { + this.wrap(chunk, SETTINGS.lineLength - 2); + chunk.selection = chunk.selection.replace(/^/gm, "> "); + replaceBlanksInTags(true); + chunk.skipLines(); + } else { + chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); + this.unwrap(chunk); + replaceBlanksInTags(false); + + if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); + } + + if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); + } + } + + chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); + + if (!/\n/.test(chunk.selection)) { + chunk.selection = chunk.selection.replace(/^(> *)/, + function (wholeMatch, blanks) { + chunk.startTag += blanks; + return ""; + }); + } + }; + + commandProto.doCode = function (chunk, postProcessing) { + + var hasTextBefore = /\S[ ]*$/.test(chunk.before); + var hasTextAfter = /^[ ]*\S/.test(chunk.after); + + // Use 'four space' markdown if the selection is on its own + // line or is multiline. + if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { + + chunk.before = chunk.before.replace(/[ ]{4}$/, + function (totalMatch) { + chunk.selection = totalMatch + chunk.selection; + return ""; + }); + + var nLinesBack = 1; + var nLinesForward = 1; + + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + nLinesBack = 0; + } + if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { + nLinesForward = 0; + } + + chunk.skipLines(nLinesBack, nLinesForward); + + if (!chunk.selection) { + chunk.startTag = " "; + chunk.selection = "enter code here"; + } + else { + if (/^[ ]{0,3}\S/m.test(chunk.selection)) { + if (/\n/.test(chunk.selection)) + chunk.selection = chunk.selection.replace(/^/gm, " "); + else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior + chunk.before += " "; + } + else { + chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, ""); + } + } + } + else { + // Use backticks (`) to delimit the code block. + + chunk.trimWhitespace(); + chunk.findTags(/`/, /`/); + + if (!chunk.startTag && !chunk.endTag) { + chunk.startTag = chunk.endTag = "`"; + if (!chunk.selection) { + chunk.selection = "enter code here"; + } + } + else if (chunk.endTag && !chunk.startTag) { + chunk.before += chunk.endTag; + chunk.endTag = ""; + } + else { + chunk.startTag = chunk.endTag = ""; + } + } + }; + + commandProto.doList = function (chunk, postProcessing, isNumberedList) { + + // These are identical except at the very beginning and end. + // Should probably use the regex extension function to make this clearer. + var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; + var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; + + // The default bullet is a dash but others are possible. + // This has nothing to do with the particular HTML bullet, + // it's just a markdown bullet. + var bullet = "-"; + + // The number in a numbered list. + var num = 1; + + // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. + var getItemPrefix = function () { + var prefix; + if (isNumberedList) { + prefix = " " + num + ". "; + num++; + } + else { + prefix = " " + bullet + " "; + } + return prefix; + }; + + // Fixes the prefixes of the other list items. + var getPrefixedItem = function (itemText) { + + // The numbering flag is unset when called by autoindent. + if (isNumberedList === undefined) { + isNumberedList = /^\s*\d/.test(itemText); + } + + // Renumber/bullet the list element. + itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, + function (_) { + return getItemPrefix(); + }); + + return itemText; + }; + + chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); + + if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { + chunk.before += chunk.startTag; + chunk.startTag = ""; + } + + if (chunk.startTag) { + + var hasDigits = /\d+[.]/.test(chunk.startTag); + chunk.startTag = ""; + chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); + this.unwrap(chunk); + chunk.skipLines(); + + if (hasDigits) { + // Have to renumber the bullet points if this is a numbered list. + chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); + } + if (isNumberedList == hasDigits) { + return; + } + } + + var nLinesUp = 1; + + chunk.before = chunk.before.replace(previousItemsRegex, + function (itemText) { + if (/^\s*([*+-])/.test(itemText)) { + bullet = re.$1; + } + nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + if (!chunk.selection) { + chunk.selection = "List item"; + } + + var prefix = getItemPrefix(); + + var nLinesDown = 1; + + chunk.after = chunk.after.replace(nextItemsRegex, + function (itemText) { + nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + chunk.trimWhitespace(true); + chunk.skipLines(nLinesUp, nLinesDown, true); + chunk.startTag = prefix; + var spaces = prefix.replace(/./g, " "); + this.wrap(chunk, SETTINGS.lineLength - spaces.length); + chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); + + }; + + commandProto.doHeading = function (chunk, postProcessing) { + + // Remove leading/trailing whitespace and reduce internal spaces to single spaces. + chunk.selection = chunk.selection.replace(/\s+/g, " "); + chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); + + // If we clicked the button with no selected text, we just + // make a level 2 hash header around some default text. + if (!chunk.selection) { + chunk.startTag = "## "; + chunk.selection = "Heading"; + chunk.endTag = ""; + return; + } + + var headerLevel = 0; // The existing header level of the selected text. + + // Remove any existing hash heading markdown and save the header level. + chunk.findTags(/#+[ ]*/, /[ ]*#+/); + if (/#+/.test(chunk.startTag)) { + headerLevel = re.lastMatch.length; + } + chunk.startTag = chunk.endTag = ""; + + // Try to get the current header level by looking for - and = in the line + // below the selection. + chunk.findTags(null, /\s?(-+|=+)/); + if (/=+/.test(chunk.endTag)) { + headerLevel = 1; + } + if (/-+/.test(chunk.endTag)) { + headerLevel = 2; + } + + // Skip to the next line so we can create the header markdown. + chunk.startTag = chunk.endTag = ""; + chunk.skipLines(1, 1); + + // We make a level 2 header if there is no current header. + // If there is a header level, we substract one from the header level. + // If it's already a level 1 header, it's removed. + var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; + + if (headerLevelToCreate > 0) { + + // The button only creates level 1 and 2 underline headers. + // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? + var headerChar = headerLevelToCreate >= 2 ? "-" : "="; + var len = chunk.selection.length; + if (len > SETTINGS.lineLength) { + len = SETTINGS.lineLength; + } + chunk.endTag = "\n"; + while (len--) { + chunk.endTag += headerChar; + } + } + }; + + commandProto.doHorizontalRule = function (chunk, postProcessing) { + chunk.startTag = "----------\n"; + chunk.selection = ""; + chunk.skipLines(2, 1, true); + } + + +})(); diff --git a/app/assets/javascripts/pagedown/pagedown.js.erb b/app/assets/javascripts/pagedown/pagedown.js.erb new file mode 100644 index 00000000..cada3497 --- /dev/null +++ b/app/assets/javascripts/pagedown/pagedown.js.erb @@ -0,0 +1,43 @@ +//= require markdown.converter +// markdown.editor is slightly adjusted to work with Bootstrap 4. +// Taken from https://github.com/hughevans/pagedown-bootstrap-rails, V2.1.4 +//= require markdown.editor +//= require markdown.sanitizer +//= require markdown.extra + +renderPagedown = function() { + $(".wmd-output").each(function (i) { + const converter = new Markdown.Converter(); + const content = $(this).html(); + return $(this).html(converter.makeHtml(content)); + }) +}; + +createPagedownEditor = function( selector, context ) { + if (context == null) { context = 'body'; } + return $(selector, context).each(function(i, input) { + if ($(input).data('is_rendered')) { + return; + } + const attr = $(input).attr('id').split('wmd-input')[1]; + const converter = new Markdown.Converter(); + Markdown.Extra.init(converter); + const help = { + handler() { + window.open('http://daringfireball.net/projects/markdown/syntax'); + return false; + }, + title: "<%= I18n.t('components.markdown_editor.help', default: 'Markdown Editing Help') %>" + }; + + const editor = new Markdown.Editor(converter, attr, help); + editor.run(); + $('[data-toggle="tooltip"]').tooltip(); + return $(input).data('is_rendered', true); + }); +}; + +$(document).on('turbolinks:load', function() { + renderPagedown(); + return createPagedownEditor('.wmd-input'); +}); diff --git a/app/assets/javascripts/sortable.js b/app/assets/javascripts/sortable.js index c9a767c8..fd9a5070 100644 --- a/app/assets/javascripts/sortable.js +++ b/app/assets/javascripts/sortable.js @@ -1,4 +1,4 @@ -$(document).ready(function(){ +$(document).on('turbolinks:load', function(){ (function vendorTableSorter(){ /* SortTable diff --git a/app/assets/javascripts/statistics_activity_history.js b/app/assets/javascripts/statistics_activity_history.js index dbad1841..c4f03e5b 100644 --- a/app/assets/javascripts/statistics_activity_history.js +++ b/app/assets/javascripts/statistics_activity_history.js @@ -49,7 +49,7 @@ $(document).on('turbolinks:load', function() { var refreshData = function (callback) { var params = new URLSearchParams(window.location.search.slice(1)); - var jqxhr = $.ajax(prefix + '-activity-history.json', { + var jqxhr = $.ajax('/statistics/graphs/' + prefix + '-activity-history.json', { dataType: 'json', data: {from: params.get('from'), to: params.get('to'), interval: params.get('interval')}, method: 'GET' diff --git a/app/assets/javascripts/statistics_graphs.js b/app/assets/javascripts/statistics_graphs.js index 5ad7597f..8caabe1a 100644 --- a/app/assets/javascripts/statistics_graphs.js +++ b/app/assets/javascripts/statistics_graphs.js @@ -101,7 +101,7 @@ $(document).on('turbolinks:load', function() { }); } - manageGraph('user-activity', 'graphs/user-activity', 10); - manageGraph('rfc-activity', 'graphs/rfc-activity', 30); + manageGraph('user-activity', '/statistics/graphs/user-activity', 10); + manageGraph('rfc-activity', '/statistics/graphs/rfc-activity', 30); } }); diff --git a/app/assets/javascripts/submission_statistics.js b/app/assets/javascripts/submission_statistics.js index 1adbf404..7cef0e02 100644 --- a/app/assets/javascripts/submission_statistics.js +++ b/app/assets/javascripts/submission_statistics.js @@ -5,30 +5,40 @@ $(document).on('turbolinks:load', function() { var currentSubmission = 0; var active_file = undefined; - var fileTrees = [] + var fileTrees = []; var editor = undefined; - var fileTypeById = {} + var fileTypeById = {}; var showActiveFile = function() { var session = editor.getSession(); - var fileType = fileTypeById[active_file.file_type_id] + var fileType = fileTypeById[active_file.file_type_id]; session.setMode(fileType.editor_mode); session.setTabSize(fileType.indent_size); session.setValue(active_file.content); session.setUseSoftTabs(true); session.setUseWrapMode(true); + // The event ready.jstree is fired too early and thus doesn't work. + var selectFileInJsTree = function() { + if (!filetree.hasClass('jstree-loading')) { + filetree.jstree("deselect_all"); + filetree.jstree().select_node(active_file.file_id); + } else { + setTimeout(selectFileInJsTree, 250); + } + }; + + filetree = $(fileTrees[currentSubmission]); + selectFileInJsTree(); + // Finally change jstree element to prevent flickering showFileTree(currentSubmission); - filetree = $(fileTrees[currentSubmission]) - filetree.jstree("deselect_all"); - filetree.jstree().select_node(active_file.file_id); }; var initializeFileTree = function() { $('.files').each(function(index, element) { fileTree = $(element).jstree($(element).data('entries')); fileTree.on('click', 'li.jstree-leaf', function() { - var id = parseInt($(this).attr('id')) + var id = parseInt($(this).attr('id')); _.each(files[currentSubmission], function(file) { if (file.file_id === id) { active_file = file; @@ -42,8 +52,8 @@ $(document).on('turbolinks:load', function() { var showFileTree = function(index) { $('.files').hide(); - $(fileTrees[index].context).show(); - } + $(fileTrees[index]).show(); + }; if ($.isController('exercises') && $('#timeline').isPresent()) { @@ -85,7 +95,7 @@ $(document).on('turbolinks:load', function() { if (file.name === active_file.name) { fileIndex = index; } - }) + }); active_file = currentFiles[fileIndex]; showActiveFile(); }); @@ -94,10 +104,10 @@ $(document).on('turbolinks:load', function() { clearInterval(playInterval); playInterval = undefined; playButton.find('span.fa').removeClass('fa-pause').addClass('fa-play') - } + }; playButton.on('click', function(event) { - if (playInterval == undefined) { + if (playInterval === undefined) { playInterval = setInterval(function() { if ($.isController('exercises') && $('#timeline').isPresent() && slider.val() < submissions.length - 1) { slider.val(parseInt(slider.val()) + 1); @@ -112,7 +122,7 @@ $(document).on('turbolinks:load', function() { } }); - active_file = files[0][0] + active_file = files[0][0]; initializeFileTree(); showActiveFile(); } diff --git a/app/assets/stylesheets/base.css.scss b/app/assets/stylesheets/base.css.scss index 127b36ca..96087248 100644 --- a/app/assets/stylesheets/base.css.scss +++ b/app/assets/stylesheets/base.css.scss @@ -28,6 +28,7 @@ span.caret { .progress-bar { line-height: initial; min-width: 2em; + color: white; } } @@ -35,7 +36,7 @@ span.caret { margin-top: 0.5em; } -.badge { +.badge-pill { font-size: 100%; } diff --git a/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss b/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss index d0855298..ceb88020 100644 --- a/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss +++ b/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss @@ -3,10 +3,14 @@ } .dropdown-submenu > .dropdown-menu { - top: 0; + top: -0.2em; left: 100%; } +.dropdown-submenu.open > ul.dropdown-menu { + display: block; +} + .dropdown-submenu > a:after { display: block; content: " "; @@ -25,11 +29,11 @@ border-left-color: #ffffff; } -.dropdown-submenu.pull-left { +.dropdown-submenu.float-left { float: none; } -.dropdown-submenu.pull-left > .dropdown-menu { +.dropdown-submenu.float-left > .dropdown-menu { left: -100%; margin-left: 10px; -webkit-border-radius: 6px 0 6px 6px; diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index dad17193..1b591fc8 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -144,7 +144,8 @@ button i.fa-spin { min-height: 1px; padding-left: 15px; padding-right: 15px; - box-sizing: border-box + box-sizing: border-box; + margin-left: auto; } .output-col-collapsed { @@ -155,7 +156,8 @@ button i.fa-spin { min-height: 1px; padding-left: 15px; padding-right: 15px; - box-sizing: border-box + box-sizing: border-box; + margin-left: auto; } .enforce-top-margin { @@ -166,14 +168,14 @@ button i.fa-spin { margin-right: 10px !important; } -.description-panel-collapsed { +.description-card-collapsed { -webkit-transition: width 2s; transition: width 2s; height: 0px; visibility: hidden; } -.description-panel { +.description-card { height: auto; -webkit-transition: height 2s; transition: height 2s; diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss index 79141988..1b3c6c76 100644 --- a/app/assets/stylesheets/exercise_collections.scss +++ b/app/assets/stylesheets/exercise_collections.scss @@ -57,10 +57,6 @@ rect.value-bar { } } -.table-responsive#exercise-list { - max-height: 512px; -} - .exercise-id-tooltip { position: absolute; display: none; diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index 817fdeac..6790776a 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -35,11 +35,19 @@ input[type='file'] { margin: 10px 0 10px 0; } - .lead.description-panel-collapsed { + .lead.description-card-collapsed { margin: 0; } } +[data-toggle="collapse"] .fa:before { + content: "\f139"; +} + +[data-toggle="collapse"].collapsed .fa:before { + content: "\f13a"; +} + // Graph Settings .axis path { diff --git a/app/assets/stylesheets/request-for-comments.css.scss b/app/assets/stylesheets/request-for-comments.css.scss index 77b0810b..09209c2d 100644 --- a/app/assets/stylesheets/request-for-comments.css.scss +++ b/app/assets/stylesheets/request-for-comments.css.scss @@ -127,7 +127,7 @@ background-color:#f9f9f9 } -.ace_tooltip { +:not(.allow_ace_tooltip) > .ace_tooltip { display: none !important; } diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index 4dee2b34..e636e9b0 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -76,8 +76,8 @@ tr.highlight { grid-gap: 10px; > a { - color: #fff; - text-decoration: none; + color: #fff !important; + text-decoration: none !important; > div { border: 2px solid #0055ba; diff --git a/app/controllers/file_types_controller.rb b/app/controllers/file_types_controller.rb index 8f29b72b..d450df3d 100644 --- a/app/controllers/file_types_controller.rb +++ b/app/controllers/file_types_controller.rb @@ -38,7 +38,7 @@ class FileTypesController < ApplicationController end def set_editor_modes - @editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').map do |filename| + @editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').sort.map do |filename| name = filename.gsub(/\w+\/|mode-|.js$/, '') [name, "ace/mode/#{name}"] end diff --git a/app/helpers/pagedown_form_builder.rb b/app/helpers/pagedown_form_builder.rb new file mode 100644 index 00000000..d38407f2 --- /dev/null +++ b/app/helpers/pagedown_form_builder.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class PagedownFormBuilder < ActionView::Helpers::FormBuilder + def pagedown(method, args) + # Adopt simple form builder to work with form_for + @attribute_name = method + @input_html_options = args[:input_html] + + @template.capture do + @template.concat wmd_button_bar + @template.concat wmd_textarea + @template.concat wmd_preview if show_wmd_preview? + end + end + + private + + def wmd_button_bar + @template.content_tag :div, nil, id: "wmd-button-bar-#{base_id}" + end + + def wmd_textarea + @template.text_area @object_name, @attribute_name, + **@input_html_options, + class: 'form-control wmd-input', + id: "wmd-input-#{base_id}" + end + + def wmd_preview + @template.content_tag :div, nil, + class: 'wmd-preview', + id: "wmd-preview-#{base_id}" + end + + def show_wmd_preview? + @input_html_options[:preview].present? + end + + def base_id + options[:pagedown_id_suffix] || @attribute_name + end +end diff --git a/app/models/error_template.rb b/app/models/error_template.rb index 4265a736..83c2f360 100644 --- a/app/models/error_template.rb +++ b/app/models/error_template.rb @@ -3,6 +3,6 @@ class ErrorTemplate < ApplicationRecord has_and_belongs_to_many :error_template_attributes def to_s - "#{id} [#{name}]" + name end end diff --git a/app/models/error_template_attribute.rb b/app/models/error_template_attribute.rb index 0a228958..42661132 100644 --- a/app/models/error_template_attribute.rb +++ b/app/models/error_template_attribute.rb @@ -2,6 +2,6 @@ class ErrorTemplateAttribute < ApplicationRecord has_and_belongs_to_many :error_template def to_s - "#{id} [#{key}]" + key end end diff --git a/app/views/application/_breadcrumbs.html.slim b/app/views/application/_breadcrumbs.html.slim index 151e33e8..7899ae0e 100644 --- a/app/views/application/_breadcrumbs.html.slim +++ b/app/views/application/_breadcrumbs.html.slim @@ -3,17 +3,17 @@ - if model = Kernel.const_get(controller_path.classify) rescue nil - object = model.find_by(id: params[:id]) - if model.try(:nested_resource?) - li = model.model_name.human(count: 2) + li.breadcrumb-item = model.model_name.human(count: 2) - if object - li = object + li.breadcrumb-item = object - else - li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) + li.breadcrumb-item = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) - if object - li = link_to(object, send(:"#{model.model_name.singular}_path", object)) - li.active + li.breadcrumb-item = link_to(object, send(:"#{model.model_name.singular}_path", object)) + li.breadcrumb-item.active - if I18n.translation_present?("shared.#{params[:action]}") = t("shared.#{params[:action]}") - else = t("#{controller_name}.index.#{params[:action]}") - else - li.active = t("breadcrumbs.#{controller_name}.#{params[:action]}") + li.breadcrumb-item.active = t("breadcrumbs.#{controller_name}.#{params[:action]}") diff --git a/app/views/application/_flash.html.slim b/app/views/application/_flash.html.slim index a1f7191c..4a78c032 100644 --- a/app/views/application/_flash.html.slim +++ b/app/views/application/_flash.html.slim @@ -1,6 +1,7 @@ #flash-container #flash.container.fixed_error_messages data-message-failure=t('shared.message_failure') - %w[alert danger info notice success warning].each do |severity| - div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" - p id="flash-#{severity}" = flash[severity] - span.fa.fa-times + div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)} alert-dismissible fade show" + p.mb-0 id="flash-#{severity}" = flash[severity] + button type="button" class="close" data-dismiss="alert" aria-label="Close" + span.text-white aria-hidden="true" × diff --git a/app/views/application/_locale_selector.html.slim b/app/views/application/_locale_selector.html.slim index b6dbbfbf..b278ab5a 100644 --- a/app/views/application/_locale_selector.html.slim +++ b/app/views/application/_locale_selector.html.slim @@ -1,7 +1,7 @@ -li.dropdown - a.dropdown-toggle data-toggle='dropdown' href='#' +li.nav-item.dropdown + a.nav-link.dropdown-toggle.mx-3 data-toggle='dropdown' href='#' = t("locales.#{I18n.locale}") span.caret - ul.dropdown-menu role='menu' + ul.dropdown-menu.p-0.mt-1 role='menu' - I18n.available_locales.sort_by { |locale| t("locales.#{locale}") }.each do |locale| - li = link_to(t("locales.#{locale}"), url_for(params.permit!.merge(locale: locale))) + li = link_to(t("locales.#{locale}"), url_for(params.permit!.merge(locale: locale)), class: 'dropdown-item') diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index 168a747f..006de33b 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -1,14 +1,14 @@ - if current_user.try(:internal_user?) ul.nav.navbar-nav - li.dropdown - a.dropdown-toggle data-toggle='dropdown' href='#' + li.nav-item.dropdown + a.nav-link.dropdown-toggle.mx-3 data-toggle='dropdown' href='#' = t('shared.administration') span.caret - ul.dropdown-menu role='menu' + ul.dropdown-menu.p-0.mt-1 role='menu' - if current_user.admin? - li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path, 'data-turbolinks' => "false") - li = link_to(t('breadcrumbs.statistics.show'), statistics_path) - li.divider + li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path, class: 'dropdown-item', 'data-turbolinks' => "false") + li = link_to(t('breadcrumbs.statistics.show'), statistics_path, class: 'dropdown-item') + li.dropdown-divider role='separator' = render('navigation_submenu', title: t('activerecord.models.exercise.other'), models: [Exercise, ExerciseCollection, ProxyExercise, Tag, Submission], link: exercises_path, cached: true) = render('navigation_submenu', title: t('navigation.sections.users'), models: [InternalUser, ExternalUser], diff --git a/app/views/application/_navigation_collection_link.html.slim b/app/views/application/_navigation_collection_link.html.slim index 412ea0bd..fc45dd4a 100644 --- a/app/views/application/_navigation_collection_link.html.slim +++ b/app/views/application/_navigation_collection_link.html.slim @@ -1,2 +1,2 @@ - if policy(model).index? - li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) + li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"), class: 'dropdown-item') diff --git a/app/views/application/_navigation_submenu.html.slim b/app/views/application/_navigation_submenu.html.slim index 23daf814..047955d7 100644 --- a/app/views/application/_navigation_submenu.html.slim +++ b/app/views/application/_navigation_submenu.html.slim @@ -1,6 +1,6 @@ -li.dropdown.dropdown-submenu +li.dropdown-submenu - link = link.nil? ? "#" : link - a href=link class="dropdown-toggle" data-toggle="dropdown" = title - ul class="dropdown-menu" + a.dropdown-item.dropdown-toggle href=link data-toggle="dropdown" = title + ul.dropdown-menu.p-0 - models.each do |model| = render('navigation_collection_link', model: model, cached: true) diff --git a/app/views/application/_session.html.slim b/app/views/application/_session.html.slim index 38e58588..db401908 100644 --- a/app/views/application/_session.html.slim +++ b/app/views/application/_session.html.slim @@ -1,19 +1,19 @@ - if current_user - li.dropdown - a.dropdown-toggle data-toggle='dropdown' href='#' + li.nav-item.dropdown + a.nav-link.dropdown-toggle data-toggle='dropdown' href='#' i.fa.fa-user = current_user span.caret - ul.dropdown-menu role='menu' + ul.dropdown-menu.p-0.mt-1 role='menu' - if current_user.internal_user? - li = link_to(t('consumers.show.link'), current_user.consumer) if current_user.consumer - li = link_to(t('internal_users.show.link'), current_user) - li = link_to(t('request_for_comments.index.all'), request_for_comments_path) - li = link_to(t('request_for_comments.index.get_my_rfc_activity'), my_rfc_activity_path) - li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path) + li = link_to(t('consumers.show.link'), current_user.consumer, class: 'dropdown-item') if current_user.consumer + li = link_to(t('internal_users.show.link'), current_user, class: 'dropdown-item') + li = link_to(t('request_for_comments.index.all'), request_for_comments_path, class: 'dropdown-item') + li = link_to(t('request_for_comments.index.get_my_rfc_activity'), my_rfc_activity_path, class: 'dropdown-item') + li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path, class: 'dropdown-item') - if current_user.internal_user? - li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete) + li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete, class: 'dropdown-item') - else - li = link_to(sign_in_path) do + li.nav-item = link_to(sign_in_path, class: 'nav-link') do i.fa.fa-sign-in = t('sessions.new.link') diff --git a/app/views/code_harbor_links/index.html.slim b/app/views/code_harbor_links/index.html.slim index 953985c4..ca4a81d4 100644 --- a/app/views/code_harbor_links/index.html.slim +++ b/app/views/code_harbor_links/index.html.slim @@ -9,7 +9,7 @@ h1 = CodeHarborLink.model_name.human(count: 2) tbody - @code_harbor_links.each do |code_harbor_link| tr - td = code_harbor_link.oauth2token + td = link_to(code_harbor_link.oauth2token, code_harbor_link) td = link_to(t('shared.show'), code_harbor_link) td = link_to(t('shared.edit'), edit_code_harbor_link_path(code_harbor_link)) td = link_to(t('shared.destroy'), code_harbor_link, data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/app/views/code_ocean/files/_form.html.slim b/app/views/code_ocean/files/_form.html.slim index 46c5b2c2..a00912c0 100644 --- a/app/views/code_ocean/files/_form.html.slim +++ b/app/views/code_ocean/files/_form.html.slim @@ -12,5 +12,5 @@ = f.label(:file_template_id, t('activerecord.attributes.file.file_template_id')) = f.collection_select(:file_template_id, FileTemplate.all.order(:name), :id, :name, {:include_blank => true}, class: 'form-control') = f.hidden_field(:context_id) - .hidden#noTemplateLabel data-text=t('file_template.no_template_label') + .d-none#noTemplateLabel data-text=t('file_template.no_template_label') .actions = render('shared/submit_button', f: f, object: CodeOcean::File.new) diff --git a/app/views/consumers/index.html.slim b/app/views/consumers/index.html.slim index c05581ed..b707c1f0 100644 --- a/app/views/consumers/index.html.slim +++ b/app/views/consumers/index.html.slim @@ -9,7 +9,7 @@ h1 = Consumer.model_name.human(count: 2) tbody - @consumers.each do |consumer| tr - td = consumer.name + td = link_to(consumer.name, consumer) td = link_to(t('shared.show'), consumer) td = link_to(t('shared.edit'), edit_consumer_path(consumer)) td = link_to(t('shared.destroy'), consumer, data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/app/views/error_template_attributes/_form.html.slim b/app/views/error_template_attributes/_form.html.slim index 4fc28b02..72160cfd 100644 --- a/app/views/error_template_attributes/_form.html.slim +++ b/app/views/error_template_attributes/_form.html.slim @@ -9,8 +9,9 @@ .form-group = f.label(:regex) = f.text_field(:regex, class: 'form-control', required: true) - .help-block == t('error_templates.hints.signature') - .form-group - = f.check_box(:important) + .help-block.form-text == t('error_templates.hints.signature') + .form-check.form-group + label.form-check-label + = f.check_box(:important, class: 'form-check-input') = t('activerecord.attributes.error_template_attribute.important') .actions = render('shared/submit_button', f: f, object: @error_template_attribute) diff --git a/app/views/error_template_attributes/index.html.slim b/app/views/error_template_attributes/index.html.slim index 81d5cac9..268b1547 100644 --- a/app/views/error_template_attributes/index.html.slim +++ b/app/views/error_template_attributes/index.html.slim @@ -17,9 +17,10 @@ h1 = ErrorTemplateAttribute.model_name.human(count: 2) span class="fa fa-star" aria-hidden="true" - else span class="fa fa-star-o" aria-hidden="true" - td = error_template_attribute.key + td = link_to(error_template_attribute.key, error_template_attribute) td = error_template_attribute.description - td = error_template_attribute.regex + td + code = error_template_attribute.regex td = link_to(t('shared.show'), error_template_attribute) td = link_to(t('shared.edit'), edit_error_template_attribute_path(error_template_attribute)) td = link_to(t('shared.destroy'), error_template_attribute, data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/app/views/error_template_attributes/show.html.slim b/app/views/error_template_attributes/show.html.slim index 2bdd01ca..5d6ef58b 100644 --- a/app/views/error_template_attributes/show.html.slim +++ b/app/views/error_template_attributes/show.html.slim @@ -2,7 +2,10 @@ h1 = @error_template_attribute = render('shared/edit_button', object: @error_template_attribute) -- [:key, :description, :regex, :important].each do |attribute| +- [:key, :description].each do |attribute| = row(label: "error_template_attribute.#{attribute}", value: @error_template_attribute.send(attribute)) += row(label: "error_template_attribute.key") do + code = @error_template_attribute.key += row(label: "error_template_attribute.important", value: @error_template_attribute.important) // todo: used by diff --git a/app/views/error_templates/_form.html.slim b/app/views/error_templates/_form.html.slim index d9716ce3..f912ec7b 100644 --- a/app/views/error_templates/_form.html.slim +++ b/app/views/error_templates/_form.html.slim @@ -9,12 +9,12 @@ .form-group = f.label(:signature) = f.text_field(:signature, class: 'form-control') - .help-block == t('error_templates.hints.signature') + .help-block.form-text == t('error_templates.hints.signature') .form-group = f.label(:description) = f.text_field(:description, class: 'form-control') .form-group = f.label(:hint) = f.text_field(:hint, class: 'form-control') - .help-block == t('error_templates.hints.hint_templates') + .help-block.form-text == t('error_templates.hints.hint_templates') .actions = render('shared/submit_button', f: f, object: @error_template) diff --git a/app/views/error_templates/index.html.slim b/app/views/error_templates/index.html.slim index f44b3f67..1f532a86 100644 --- a/app/views/error_templates/index.html.slim +++ b/app/views/error_templates/index.html.slim @@ -11,7 +11,7 @@ h1 = ErrorTemplate.model_name.human(count: 2) tbody - @error_templates.each do |error_template| tr - td = error_template.name + td = link_to(error_template.name, error_template) td = error_template.description td = link_to(error_template.execution_environment) td = link_to(t('shared.show'), error_template) diff --git a/app/views/error_templates/show.html.slim b/app/views/error_templates/show.html.slim index 9936ef7f..ae6ea2a4 100644 --- a/app/views/error_templates/show.html.slim +++ b/app/views/error_templates/show.html.slim @@ -4,10 +4,12 @@ h1 = row(label: 'error_template.name', value: @error_template.name) = row(label: 'exercise.execution_environment', value: link_to(@error_template.execution_environment)) -- [:signature, :description, :hint].each do |attribute| += row(label: "error_template.signature") do + code = @error_template.signature +- [:description, :hint].each do |attribute| = row(label: "error_template.#{attribute}", value: @error_template.send(attribute)) -h3 +h2.mt-4 = t 'error_templates.attributes' .table-responsive @@ -27,9 +29,10 @@ h3 span class="fa fa-star" aria-hidden="true" - else span class="fa fa-star-o" aria-hidden="true" - td = attribute.key + td = link_to(attribute.key, attribute) td = attribute.description - td = attribute.regex + td + code = attribute.regex td = link_to(t('shared.show'), attribute) td = link_to(t('shared.destroy'), attribute_error_template_url(:error_template_attribute_id => attribute.id), :method => :delete) @@ -37,4 +40,4 @@ h3 = collection_select({}, :error_template_attribute_id, ErrorTemplateAttribute.where.not(id: @error_template.error_template_attributes.select(:id).to_a).order('important DESC', :key), :id, :key, {include_blank: false}, class: '') - button.btn.btn-default = t('error_templates.add_attribute') + button.btn.btn-outline-primary = t('error_templates.add_attribute') diff --git a/app/views/execution_environments/_form.html.slim b/app/views/execution_environments/_form.html.slim index e1b02c0a..13ee252e 100644 --- a/app/views/execution_environments/_form.html.slim +++ b/app/views/execution_environments/_form.html.slim @@ -12,17 +12,17 @@ a.toggle-input data={text_initial: t('shared.new'), text_toggled: t('shared.back')} href='#' = t('shared.new') .original-input = f.select(:docker_image, @docker_images, {}, class: 'form-control') = f.text_field(:docker_image, class: 'alternative-input form-control', disabled: true) - .help-block == t('.hints.docker_image') + .help-block.form-text == t('.hints.docker_image') .form-group = f.label(:exposed_ports) = f.text_field(:exposed_ports, class: 'form-control', placeholder: '3000, 4000') - .help-block == t('.hints.exposed_ports') + .help-block.form-text == t('.hints.exposed_ports') .form-group = f.label(:memory_limit) = f.number_field(:memory_limit, class: 'form-control', min: DockerClient::MINIMUM_MEMORY_LIMIT, value: f.object.memory_limit || DockerClient::DEFAULT_MEMORY_LIMIT) - .checkbox - label - = f.check_box(:network_enabled) + .form-check.mb-3 + label.form-check-label + = f.check_box(:network_enabled, class: 'form-check-input') = t('activerecord.attributes.execution_environment.network_enabled') .form-group = f.label(:permitted_execution_time) @@ -33,11 +33,11 @@ .form-group = f.label(:run_command) = f.text_field(:run_command, class: 'form-control', placeholder: 'command %{filename}', required: true) - .help-block == t('.hints.command') + .help-block.form-text == t('.hints.command') .form-group = f.label(:test_command) = f.text_field(:test_command, class: 'form-control', placeholder: 'command %{filename}') - .help-block == t('.hints.command') + .help-block.form-text == t('.hints.command') .form-group = f.label(:testing_framework) = f.select(:testing_framework, @testing_framework_adapters, {include_blank: true}, class: 'form-control') diff --git a/app/views/execution_environments/index.html.slim b/app/views/execution_environments/index.html.slim index dc30898f..20e99620 100644 --- a/app/views/execution_environments/index.html.slim +++ b/app/views/execution_environments/index.html.slim @@ -15,7 +15,7 @@ h1 = ExecutionEnvironment.model_name.human(count: 2) tbody - @execution_environments.each do |execution_environment| tr - td = execution_environment.name + td = link_to(execution_environment.name, execution_environment) td = link_to(execution_environment.author, execution_environment.author) td = execution_environment.pool_size td = execution_environment.memory_limit diff --git a/app/views/execution_environments/show.html.slim b/app/views/execution_environments/show.html.slim index da9fd9c8..6d917a61 100644 --- a/app/views/execution_environments/show.html.slim +++ b/app/views/execution_environments/show.html.slim @@ -5,7 +5,10 @@ h1 = row(label: 'execution_environment.name', value: @execution_environment.name) = row(label: 'execution_environment.user', value: link_to(@execution_environment.author, @execution_environment.author)) = row(label: 'execution_environment.file_type', value: @execution_environment.file_type.present? ? link_to(@execution_environment.file_type, @execution_environment.file_type) : nil) -- [:docker_image, :exposed_ports, :memory_limit, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command].each do |attribute| +- [:docker_image, :exposed_ports, :memory_limit, :network_enabled, :permitted_execution_time, :pool_size].each do |attribute| = row(label: "execution_environment.#{attribute}", value: @execution_environment.send(attribute)) +- [:run_command, :test_command].each do |attribute| + = row(label: "execution_environment.#{attribute}") do + code = @execution_environment.send(attribute) = row(label: 'execution_environment.testing_framework', value: @testing_framework_adapter.try(:framework_name)) = row(label: 'execution_environment.help', value: render_markdown(@execution_environment.help)) diff --git a/app/views/exercise_collections/_add_exercise_modal.slim b/app/views/exercise_collections/_add_exercise_modal.slim index 62080f4f..e0880b58 100644 --- a/app/views/exercise_collections/_add_exercise_modal.slim +++ b/app/views/exercise_collections/_add_exercise_modal.slim @@ -2,7 +2,8 @@ form#exercise-selection .form-group - span.label = t('activerecord.attributes.exercise_collections.exercises') + span.badge = t('activerecord.attributes.exercise_collections.exercises') + .mb-2 = collection_select({}, :exercise_ids, exercises, :id, :title, {}, {id: 'add-exercise-list', class: 'form-control', multiple: true}) button.btn.btn-primary#add-exercises = t('exercise_collections.form.add_exercises') diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 6b09206e..07fc874f 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -3,9 +3,10 @@ .form-group = f.label(t('activerecord.attributes.exercise_collections.name')) = f.text_field(:name, class: 'form-control', required: true) - .form-group - = f.label(t('activerecord.attributes.exercise_collections.use_anomaly_detection')) - = f.check_box(:use_anomaly_detection, {class: 'form-control'}) + .form-check.form-group + label.form-check-label + = f.check_box(:use_anomaly_detection, class: 'form-check-input') + = t('activerecord.attributes.exercise_collections.use_anomaly_detection') .form-group = f.label(t('activerecord.attributes.exercise_collections.user')) = f.collection_select(:user_id, InternalUser.order(:name), :id, :name, {}, {class: 'form-control'}) @@ -26,10 +27,10 @@ td = link_to(t('shared.show'), item.exercise, 'data-turbolinks' => "false") td a.remove-exercise href='#' = t('shared.destroy') - .hidden + .d-none = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true}) .exercise-actions - button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') + button.btn.btn-outline-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') button.btn.btn-secondary#sort-button type='button' = t('exercise_collections.form.sort_by_title') .actions = render('shared/submit_button', f: f, object: @exercise_collection) diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index 83789df3..4fa6a8ac 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -7,7 +7,7 @@ h1 = row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection) = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) -h4 = t('activerecord.attributes.exercise_collections.exercises') +h4.mt-4 = t('activerecord.attributes.exercise_collections.exercises') .table-responsive#exercise-list table.table thead @@ -24,5 +24,5 @@ h4 = t('activerecord.attributes.exercise_collections.exercises') td = exercise_collection_item.position td = link_to(exercise.title, exercise) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) - td = exercise.user.name + td = link_to_if(exercise.user && policy(exercise.user).show?, exercise.user.name, exercise.user) td = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 60c49a01..74c9f49d 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -6,7 +6,7 @@ h1 = @exercise_collection = row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's') #graph - #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time) + #data.d-none(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time) #legend - {time: t('exercises.statistics.average_worktime'), min: 'min. anomaly threshold', diff --git a/app/views/exercises/_code_field.html.slim b/app/views/exercises/_code_field.html.slim index bb2a806f..8fd19eec 100644 --- a/app/views/exercises/_code_field.html.slim +++ b/app/views/exercises/_code_field.html.slim @@ -3,5 +3,5 @@ | a.toggle-input data={text_initial: t('shared.upload_file'), text_toggled: t('shared.back')} href='#' = t('shared.upload_file') = form.text_area(attribute, class: 'code-field form-control', rows: 16, style: "display:none;") - = form.file_field(attribute, class: 'alternative-input form-control', disabled: true) + = form.file_field(attribute, class: 'alternative-input form-control-file', disabled: true) = render partial: 'editor_edit', locals: { exercise: @exercise } diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index f741cb56..44fc0aec 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -6,8 +6,7 @@ - hide_rfc_button = @hide_rfc_button || false #editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise) div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) - div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) - div id='frames' class='editor-col' + div.editor-col.col.p-0 id='frames' #editor-buttons.btn-group.enforce-bottom-margin = render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy')) = render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render')) @@ -24,6 +23,7 @@ = t('exercises.editor.lastsaved') span button style="display:none" id="autosave" + div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') diff --git a/app/views/exercises/_editor_button.html.slim b/app/views/exercises/_editor_button.html.slim index ae69529e..7dd492d1 100644 --- a/app/views/exercises/_editor_button.html.slim +++ b/app/views/exercises/_editor_button.html.slim @@ -1,4 +1,4 @@ -button.btn class=local_assigns.fetch(:classes, 'btn-primary btn-sm') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button' +button.btn class=local_assigns.fetch(:classes, 'btn-primary btn-lg') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button' i.fa.fa-circle-o-notch.fa-spin - i class=icon + i class=(label.present? ? icon : "#{icon} m-0") = label diff --git a/app/views/exercises/_editor_edit.html.slim b/app/views/exercises/_editor_edit.html.slim index 83f27d68..18ea0109 100644 --- a/app/views/exercises/_editor_edit.html.slim +++ b/app/views/exercises/_editor_edit.html.slim @@ -1,5 +1,5 @@ -#editor-edit.panel-group.row.original-input data-exercise-id=@exercise.id +#editor-edit.original-input data-exercise-id=@exercise.id #frames .edit-frame - .editor-content.hidden - .editor \ No newline at end of file + .editor-content.d-none + .editor.allow_ace_tooltip \ No newline at end of file diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index c51dd855..32612828 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,18 +1,18 @@ -div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) +div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'd-none') + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) - if @exercise.allow_file_creation and not @exercise.hide_file_tree? - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-cause' => 'file', :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus', id: 'create-file-collapsed', label:'', title: t('exercises.editor.create_file')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-cause' => 'file', :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus', id: 'create-file-collapsed', label:'', title: t('exercises.editor.create_file')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) - - if !@course_token.blank? - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) + //- if !@course_token.blank? + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) -div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) +div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'd-none' : '') + = render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) - div class=(@exercise.hide_file_tree ? 'hidden' : '') + div class=(@exercise.hide_file_tree ? 'd-none' : '') hr #files data-entries=FileTree.new(files).to_js_tree @@ -20,11 +20,11 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') hr - if @exercise.allow_file_creation and not @exercise.hide_file_tree? - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file')) - = render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file')) + = render('editor_button', classes: 'btn-block btn-warning btn', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) //- if !@course_token.blank? .input-group.enforce-top-margin diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index eff1541c..8d79ed43 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -11,5 +11,5 @@ - else = link_to(file.native_file.file.name_with_extension, file.native_file.url) - else - .editor-content.hidden data-file-id=file.ancestor_id = file.content + .editor-content.d-none data-file-id=file.ancestor_id = file.content .editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id \ No newline at end of file diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index 2c760ec5..661b3eff 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,58 +1,61 @@ div id='output_sidebar_collapsed' - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') -div id='output_sidebar_uncollapsed' class='hidden col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output') + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') +div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output') .row - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar')) + = render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar')) - div.enforce-big-top-margin.hidden id='score_div' - #results - h2 = t('exercises.implement.results') - p.test-count == t('exercises.implement.test_count', count: 0) - ul.list-unstyled - ul#dummies.hidden.list-unstyled - li.panel.panel-default - .panel-heading - h3.panel-title == t('exercises.implement.file', filename: '', number: 0) - .panel-body - = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) - = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) - = row(label: 'exercises.implement.feedback') - = row(label: 'exercises.implement.error_messages') - /= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#')) - #score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score) - h4 - span == "#{t('activerecord.attributes.submission.score')}: " - span.score - .progress - .progress-bar role='progressbar' + div.position-absolute.d-flex.mb-1.w-100 style="overflow: scroll; left: 0; bottom: 0; height: calc(100% - 3rem);" + div.w-100 + div.enforce-big-top-margin.d-none id='score_div' + #results + h2 = t('exercises.implement.results') + p.test-count == t('exercises.implement.test_count', count: 0) + ul.list-unstyled + ul#dummies.d-none.list-unstyled + li.card.mt-2 + .card-header + h3.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0) + .card-body.bg-white.text-dark + = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) + = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) + = row(label: 'exercises.implement.feedback') + = row(label: 'exercises.implement.error_messages') + /= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#')) + #score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score) + h4 + span == "#{t('activerecord.attributes.submission.score')}: " + span.score + .progress + .progress-bar role='progressbar' - br - - if lti_outcome_service?(@exercise.id, external_user_id, consumer_id) - p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) - - else - p.text-center = render('editor_button', classes: 'btn-lg btn-warning-outline', data: {:'data-placement' => 'bottom', :'data-tooltip' => true}, icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed')) - hr + br + - if lti_outcome_service?(@exercise.id, external_user_id, consumer_id) + p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) + - else + p.text-center = render('editor_button', classes: 'btn-lg btn-outline-warning disabled', data: {:'data-placement' => 'bottom', :'data-tooltip' => true}, icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed')) + hr - div.enforce-big-top-margin - #turtlediv - canvas#turtlecanvas.hidden width=400 height=400 - div.enforce-big-top-margin - #hint - .panel.panel-warning - .panel-heading = t('exercises.implement.hint') - .panel-body - div.enforce-big-top-margin - #prompt.input-group.hidden - span.input-group-addon data-prompt=t('exercises.editor.input') = t('exercises.editor.input') - input#prompt-input.form-control type='text' - span.input-group-btn - button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send') - #error-hints - .heading = t('exercises.implement.error_hints.heading') - ul.body - #output - pre = t('exercises.implement.no_output_yet') - - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] - #flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' - .panel-heading = 'Gain more insights here' - .panel-body + div.enforce-big-top-margin + #turtlediv + canvas#turtlecanvas.d-none width=400 height=400 + div.enforce-big-top-margin + #hint + .card.bg-warning + .card-header = t('exercises.implement.hint') + .card-body + div.enforce-big-top-margin + #prompt.input-group.d-none + div.input-group-prepend + span.input-group-text data-prompt=t('exercises.editor.input') = t('exercises.editor.input') + input#prompt-input.form-control type='text' + span.input-group-btn + button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send') + #error-hints + .heading = t('exercises.implement.error_hints.heading') + ul.body + #output.mt-2 + pre = t('exercises.implement.no_output_yet') + - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] + #flowrHint.card.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' + .card-header = 'Gain more insights here' + .card-body diff --git a/app/views/exercises/_file_form.html.slim b/app/views/exercises/_file_form.html.slim index 57f43d43..d85751fc 100644 --- a/app/views/exercises/_file_form.html.slim +++ b/app/views/exercises/_file_form.html.slim @@ -1,41 +1,42 @@ - id = f.object.id -li.panel.panel-default - .panel-heading role="tab" id="heading" - a.file-heading data-toggle="collapse" href="#collapse#{id}" +li.card.mt-2 + .card-header role="tab" id="heading" + a.file-heading.collapsed data-toggle="collapse" href="#collapse#{id}" div.clearfix role="button" + i class="fa" aria-hidden="true" span = f.object.name - .panel-collapse.collapse class=('in' if f.object.name.nil?) id="collapse#{id}" role="tabpanel" - .panel-body + .card-collapse.collapse class=('in' if f.object.name.nil?) id="collapse#{id}" role="tabpanel" + .card-body - if policy(f.object).destroy? .clearfix - .btn.btn-warning.btn-sm.pull-right.delete-file data-file-url=code_ocean_file_path(id) = t('shared.destroy') + .btn.btn-warning.btn-sm.float-right.delete-file data-file-url=code_ocean_file_path(id) = t('shared.destroy') .form-group = f.label(:name, t('activerecord.attributes.file.name')) = f.text_field(:name, class: 'form-control') .form-group = f.label(:path, t('activerecord.attributes.file.path')) = f.text_field(:path, class: 'form-control') - .help-block = t('.hints.path') + .help-block.form-text = t('.hints.path') .form-group = f.label(:file_type_id, t('activerecord.attributes.file.file_type_id')) = f.collection_select(:file_type_id, @file_types, :id, :name, {}, class: 'form-control') .form-group = f.label(:role, t('activerecord.attributes.file.role')) = f.select(:role, CodeOcean::File::TEACHER_DEFINED_ROLES.map { |role| [t("files.roles.#{role}"), role] }, {include_blank: true}, class: 'form-control') - .checkbox - label - = f.check_box(:hidden) + .form-check + label.form-check-label + = f.check_box(:hidden, class: 'form-check-input') = t('activerecord.attributes.file.hidden') - .checkbox - label - = f.check_box(:read_only) + .form-check.mb-3 + label.form-check-label + = f.check_box(:read_only, class: 'form-check-input') = t('activerecord.attributes.file.read_only') .test-related-fields style="display: #{f.object.teacher_defined_test? ? 'initial' : 'none'};" .form-group = f.label(:name, t('activerecord.attributes.file.feedback_message')) = f.text_area(:feedback_message, class: 'form-control', maxlength: 255) - .help-block = t('.hints.feedback_message') + .help-block.form-text = t('.hints.feedback_message') .form-group = f.label(:role, t('activerecord.attributes.file.weight')) = f.number_field(:weight, class: 'form-control', min: 1, step: 'any') diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index 646c359a..b6d0980b 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -1,14 +1,14 @@ - execution_environments = ExecutionEnvironment.where('file_type_id IS NOT NULL').select(:file_type_id, :id) - file_types = FileType.where('file_extension IS NOT NULL').select(:file_extension, :id) -= form_for(@exercise, data: {execution_environments: execution_environments, file_types: file_types}, multipart: true) do |f| += form_for(@exercise, data: {execution_environments: execution_environments, file_types: file_types}, multipart: true, builder: PagedownFormBuilder) do |f| = render('shared/form_errors', object: @exercise) .form-group = f.label(:title) = f.text_field(:title, class: 'form-control', required: true) .form-group = f.label(:description) - = f.pagedown_editor :description + = f.pagedown :description, input_html: { preview: true, rows: 10 } .form-group = f.label(:execution_environment_id) = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {}, class: 'form-control') @@ -16,34 +16,34 @@ = f.label(:instructions) = f.hidden_field(:instructions) .form-control.markdown - .checkbox - label - = f.check_box(:public) + .form-check + label.form-check-label + = f.check_box(:public, class: 'form-check-input') = t('activerecord.attributes.exercise.public') - .checkbox - label - = f.check_box(:hide_file_tree) + .form-check + label.form-check-label + = f.check_box(:hide_file_tree, class: 'form-check-input') = t('activerecord.attributes.exercise.hide_file_tree') - .checkbox - label - = f.check_box(:allow_file_creation) + .form-check + label.form-check-label + = f.check_box(:allow_file_creation, class: 'form-check-input') = t('activerecord.attributes.exercise.allow_file_creation') - .checkbox - label - = f.check_box(:allow_auto_completion) + .form-check.mb-3 + label.form-check-label + = f.check_box(:allow_auto_completion, class: 'form-check-input') = t('activerecord.attributes.exercise.allow_auto_completion') .form-group = f.label(t('activerecord.attributes.exercise.difficulty')) - = f.number_field :expected_difficulty, in: 1..10, step: 1 + = f.number_field :expected_difficulty, in: 1..10, step: 1, class: 'form-control' h2 = t('exercises.form.tags') - ul.list-unstyled.panel-group - li.panel.panel-default - .panel-heading role="tab" id="heading" + ul.list-unstyled.card-group + li.card + .card-header role="tab" id="heading" a.file-heading data-toggle="collapse" href="#tag-collapse" div.clearfix role="button" span = t('exercises.form.click_to_collapse') - .panel-collapse.collapse id="tag-collapse" role="tabpanel" + .card-collapse.collapse id="tag-collapse" role="tabpanel" .table-responsive table.table#tags-table thead @@ -55,15 +55,15 @@ tr td = b.check_box td = b.object.tag.name - td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1 + td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1, class: 'form-control-sm' h2 = t('activerecord.attributes.exercise.files') - ul#files.list-unstyled.panel-group + ul#files.list-unstyled = f.fields_for :files do |files_form| = render('file_form', f: files_form) - a#add-file.btn.btn-default.btn-sm.pull-right href='#' = t('.add_file') - ul#dummies.hidden = f.fields_for(:files, CodeOcean::File.new, child_index: 'index') do |files_form| + a#add-file.btn.btn-secondary.btn-sm.float-right href='#' = t('.add_file') + ul#dummies.d-none = f.fields_for(:files, CodeOcean::File.new, child_index: 'index') do |files_form| = render('file_form', f: files_form) .actions = render('shared/submit_button', f: f, object: @exercise) \ No newline at end of file diff --git a/app/views/exercises/_request_comment_dialogcontent.html.slim b/app/views/exercises/_request_comment_dialogcontent.html.slim index 8fb71781..70405ea0 100644 --- a/app/views/exercises/_request_comment_dialogcontent.html.slim +++ b/app/views/exercises/_request_comment_dialogcontent.html.slim @@ -2,7 +2,7 @@ h5#rfc_intervention_text style='display: none;' = t('exercises.implement.rfc_int h5 = t('exercises.implement.comment.question') -textarea.form-control#question(style='resize:none;') +textarea.form-control.flex-grow-1#question(style='resize:none;') p = '' / data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. / But if we use this button, it will work since the correct cause is supplied diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim index 5d91716a..e89627ed 100644 --- a/app/views/exercises/external_users/statistics.html.slim +++ b/app/views/exercises/external_users/statistics.html.slim @@ -10,21 +10,21 @@ h1 = "#{@exercise} (external user #{@external_user})" - file_types.add(ActiveSupport::JSON.encode(file.file_type)) - all_files.push(submission.files) - .hidden#data data-submissions=ActiveSupport::JSON.encode(@submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types) + .d-none#data data-submissions=ActiveSupport::JSON.encode(@submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types) #stats-editor.row - index = 0 - all_files.each do |files| - .files class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') data-index=index data-entries=FileTree.new(files).to_js_tree + .files class=(@exercise.hide_file_tree ? 'd-none col-sm-3' : 'col-sm-3') data-index=index data-entries=FileTree.new(files).to_js_tree - index += 1 div class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9') #current-file.editor .flex-container - button.btn.btn-default id='play-button' + button.btn.btn-secondary id='play-button' span.fa.fa-play #submissions-slider.flex-item - input type='range' orient='horizontal' list='datapoints' min=0 max=@submissions.length-1 value=0 + input type='range' orient='horizontal' list='datapoints' min=0 max=@submissions.length-1 value=0 style="width: 100%" datalist#datapoints - index=0 - @submissions.each do |submission| @@ -59,7 +59,7 @@ h1 = "#{@exercise} (external user #{@external_user})" td = td = @working_times_until[index] if index > 0 p = t('.addendum') - .hidden#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until); + .d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until); div#progress_chart.col-lg-12 .graph-functions-2 diff --git a/app/views/exercises/feedback.html.slim b/app/views/exercises/feedback.html.slim index 508b1478..5090d0e2 100644 --- a/app/views/exercises/feedback.html.slim +++ b/app/views/exercises/feedback.html.slim @@ -8,17 +8,17 @@ h1 = link_to(@exercise, exercise_path(@exercise)) - if @feedbacks.nil? or @feedbacks.size == 0 .no-feedback = t('user_exercise_feedback.no_feedback') - ul.list-unstyled.panel-group + ul.list-unstyled - @feedbacks.each do |feedback| - li.panel.panel-default - .panel-heading role="tab" id="heading" + li.card.mt-2 + .card-header role="tab" id="heading" div.clearfix.feedback-header span.username = link_to(feedback.user.name, statistics_external_user_exercise_path(id: @exercise.id, external_user_id: feedback.user.id)) - if feedback.anomaly_notification i class="fa fa-envelope-o" data-placement="top" data-toggle="tooltip" data-container="body" title=feedback.anomaly_notification.reason span.date = feedback.created_at - .panel-collapse role="tabpanel" - .panel-body.feedback + .card-collapse role="tabpanel" + .card-body.feedback .text = feedback.feedback_text .difficulty = "#{t('user_exercise_feedback.difficulty')} #{feedback.difficulty}" if feedback.difficulty .worktime = "#{t('user_exercise_feedback.working_time')} #{feedback.user_estimated_worktime}" if feedback.user_estimated_worktime diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index a243bd91..f90a838d 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -2,13 +2,13 @@ #editor-column.col-md-12 .exercise.clearfix div - span.badge.pull-right.score + span.badge.badge-pill.float-right.score h1 id="exercise-headline" i class="fa fa-chevron-down" id="description-symbol" = @exercise.title - #description-panel.lead.description-panel + #description-card.lead.description-card = render_markdown(@exercise.description) a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide') = t('shared.hide') diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 25cd13e0..35da19cb 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -9,45 +9,43 @@ h1 = Exercise.model_name.human(count: 2) = f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.exercise.title')) .table-responsive - table.table + table.table.mt-4 thead tr - th = sort_link(@search, :title, t('activerecord.attributes.exercise.title')) - th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) - th = t('.test_files') - th = t('activerecord.attributes.exercise.maximum_score') - th = t('activerecord.attributes.exercise.tags') - th = t('activerecord.attributes.exercise.difficulty') - th + th.p-1 = sort_link(@search, :title, t('activerecord.attributes.exercise.title')) + th.p-1 = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) + th.p-1 = t('.test_files') + th.p-1 = t('activerecord.attributes.exercise.maximum_score') + th.p-1 = t('activerecord.attributes.exercise.tags') + th.p-1 = t('activerecord.attributes.exercise.difficulty') + th.p-1 = t('activerecord.attributes.exercise.public') - if policy(Exercise).batch_update? br span.batch = link_to(t('shared.batch_update'), '#', 'data-text' => t('shared.update', model: t('activerecord.models.exercise.other'))) - th colspan=6 = t('shared.actions') + th.p-1 colspan=6 = t('shared.actions') tbody - @exercises.each do |exercise| tr data-id=exercise.id - td = exercise.title - td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) - td = exercise.files.teacher_defined_tests.count - td = exercise.maximum_score - td = exercise.exercise_tags.count - td = exercise.expected_difficulty - td.public data-value=exercise.public? = symbol_for(exercise.public?) - td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? - td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? - td = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") if policy(exercise).statistics? + td.p-1.pt-2 = link_to(exercise.title, exercise, 'data-turbolinks' => "false") if policy(exercise).show? + td.p-1.pt-2 = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) + td.p-1.pt-2 = exercise.files.teacher_defined_tests.count + td.p-1.pt-2 = exercise.maximum_score + td.p-1.pt-2 = exercise.exercise_tags.count + td.p-1.pt-2 = exercise.expected_difficulty + td.p-1.pt-2.public data-value=exercise.public? = symbol_for(exercise.public?) + td.p-1.pt-2 = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? + td.p-1.pt-2 = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? + td.p-1.pt-2 = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") if policy(exercise).statistics? - td + td.p-1 .btn-group - button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') - span.caret - span.sr-only Toggle Dropdown - ul.dropdown-menu.pull-right role="menu" - li = link_to(t('shared.show'), exercise, 'data-turbolinks' => "false") if policy(exercise).show? - li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise)) if policy(exercise).feedback? - li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(exercise).destroy? - li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(exercise).clone? + button.btn.btn-outline-primary.btn-sm.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') + ul.dropdown-menu.float-right role="menu" + li = link_to(t('shared.show'), exercise, 'data-turbolinks' => "false", class: 'dropdown-item') if policy(exercise).show? + li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).feedback? + li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(exercise).destroy? + li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(exercise).clone? = render('shared/pagination', collection: @exercises) p = render('shared/new_button', model: Exercise) diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index 29994548..20307202 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -12,7 +12,7 @@ h1 = row(label: 'exercise.title', value: @exercise.title) = row(label: 'exercise.user', value: link_to_if(policy(@exercise.author).show?, @exercise.author, @exercise.author)) -= row(label: 'exercise.description', value: render_markdown(@exercise.description)) += row(label: 'exercise.description', value: render_markdown(@exercise.description), class: 'm-0') = row(label: 'exercise.execution_environment', value: link_to_if(policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment)) /= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions)) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score) @@ -20,24 +20,23 @@ h1 = row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) = row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?) = row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?) -= row(label: 'exercise.embedding_parameters') do - = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) = row(label: 'exercise.difficulty', value: @exercise.expected_difficulty) = row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) += row(label: 'exercise.embedding_parameters', class: 'mb-4') do + = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: embedding_parameters(@exercise)) -h2 = t('activerecord.attributes.exercise.files') +h2.mt-4 = t('activerecord.attributes.exercise.files') -ul.list-unstyled.panel-group#files +ul.list-unstyled#files - @exercise.files.each do |file| - li.panel.panel-default - .panel-heading role="tab" id="heading" - a.file-heading data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}" + li.card.mt-2 + .card-header role="tab" id="heading" + a.file-heading.collapsed data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}" div.clearfix role="button" + i class="fa" aria-hidden="true" span = file.name_with_extension - // probably set an icon here that shows that the rows can be collapsed - //span.pull-right.collapse.in class="collapse#{file.id}" ☼ - .panel-collapse.collapse class="collapse#{file.id}" role="tabpanel" - .panel-body + .card-collapse.collapse class="collapse#{file.id}" role="tabpanel" + .card-body - if policy(file).destroy? - .clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm pull-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete) + .clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm float-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete) = render('shared/file', file: file) diff --git a/app/views/exercises/statistics.html.slim b/app/views/exercises/statistics.html.slim index 09611491..b19b04dc 100644 --- a/app/views/exercises/statistics.html.slim +++ b/app/views/exercises/statistics.html.slim @@ -32,7 +32,7 @@ h1 = @exercise -working_time = @exercise.average_working_time_for(user.id) or 0 -working_time_array.push working_time hr - .hidden#data data-working-time=ActiveSupport::JSON.encode(working_time_array) + .d-none#data data-working-time=ActiveSupport::JSON.encode(working_time_array) .graph-functions div#chart_1 hr diff --git a/app/views/external_users/show.html.slim b/app/views/external_users/show.html.slim index f0028745..33e2dd6b 100644 --- a/app/views/external_users/show.html.slim +++ b/app/views/external_users/show.html.slim @@ -4,9 +4,9 @@ h1 = @user.name //= row(label: 'external_user.email', value: @user.email) = row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer)) -h4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) +h4.mt-4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) -h4 = t('.tag_statistics') +h4.mt-4 = t('.tag_statistics') #loading .spinner = t('.loading_tag_statistics') diff --git a/app/views/file_templates/index.html.slim b/app/views/file_templates/index.html.slim index 3022ea53..ba7c3eb7 100644 --- a/app/views/file_templates/index.html.slim +++ b/app/views/file_templates/index.html.slim @@ -10,7 +10,7 @@ h1 = FileTemplate.model_name.human(count: 2) tbody - @file_templates.each do |file_template| tr - td = file_template.name + td = link_to(file_template.name, file_template) td = link_to(file_template.file_type, file_type_path(file_template.file_type)) td = link_to(t('shared.show'), file_template) td = link_to(t('shared.edit'), edit_file_template_path(file_template)) diff --git a/app/views/file_types/_form.html.slim b/app/views/file_types/_form.html.slim index d36f54cf..e234a457 100644 --- a/app/views/file_types/_form.html.slim +++ b/app/views/file_types/_form.html.slim @@ -12,16 +12,16 @@ .form-group = f.label(:indent_size) = f.number_field(:indent_size, class: 'form-control', placeholder: 2, required: true) - .checkbox - label - = f.check_box(:binary) + .form-check + label.form-check-label + = f.check_box(:binary, class: 'form-check-input') = t('activerecord.attributes.file_type.binary') - .checkbox - label - = f.check_box(:executable) + .form-check + label.form-check-label + = f.check_box(:executable, class: 'form-check-input') = t('activerecord.attributes.file_type.executable') - .checkbox - label - = f.check_box(:renderable) + .form-check.mb-3 + label.form-check-label + = f.check_box(:renderable, class: 'form-check-input') = t('activerecord.attributes.file_type.renderable') .actions = render('shared/submit_button', f: f, object: @file_type) diff --git a/app/views/file_types/index.html.slim b/app/views/file_types/index.html.slim index a8a4d294..95f1394f 100644 --- a/app/views/file_types/index.html.slim +++ b/app/views/file_types/index.html.slim @@ -11,7 +11,7 @@ h1 = FileType.model_name.human(count: 2) tbody - @file_types.each do |file_type| tr - td = file_type.name + td = link_to(file_type.name, file_type) td = link_to(file_type.author, file_type.author) td = file_type.file_extension td = link_to(t('shared.show'), file_type) diff --git a/app/views/file_types/show.json.jbuilder b/app/views/file_types/show.json.jbuilder new file mode 100644 index 00000000..0842614e --- /dev/null +++ b/app/views/file_types/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @file_type, :id, :name, :editor_mode, :file_extension, :executable, :renderable, :binary diff --git a/app/views/hints/_form.html.slim b/app/views/hints/_form.html.slim index 09814d96..21ab6bc1 100644 --- a/app/views/hints/_form.html.slim +++ b/app/views/hints/_form.html.slim @@ -3,15 +3,15 @@ .form-group = f.label(:name) = f.text_field(:name, class: 'form-control', required: true) - .form + .form-group = f.label(:locale) = f.select(:locale, I18n.available_locales.map { |locale| [t("locales.#{locale}"), locale] }, {}, class: 'form-control') .form-group = f.label(:message) = f.text_field(:message, class: 'form-control', placeholder: "'$2' has no method '$1'.", required: true) - .help-block = t('.hints.message') + .help-block.form-text = t('.hints.message') .form-group = f.label(:regular_expression) = f.text_field(:regular_expression, class: 'form-control', placeholder: 'undefined method (\w+) for (\w+)', required: true) - .help-block = t('.hints.regular_expression') + .help-block.form-text = t('.hints.regular_expression') .actions = render('shared/submit_button', f: f, object: @hint) diff --git a/app/views/internal_users/_form.html.slim b/app/views/internal_users/_form.html.slim index f7f89299..4a6ff45c 100644 --- a/app/views/internal_users/_form.html.slim +++ b/app/views/internal_users/_form.html.slim @@ -5,7 +5,7 @@ = f.collection_select(:consumer_id, Consumer.all.sort_by(&:name), :id, :name, {}, class: 'form-control') .form-group = f.label(:email) - = f.text_field(:email, class: 'form-control', required: true) + = f.email_field(:email, class: 'form-control', required: true) .form-group = f.label(:name) = f.text_field(:name, class: 'form-control', required: true) diff --git a/app/views/internal_users/activate.html.slim b/app/views/internal_users/activate.html.slim index cd52abca..25795ed6 100644 --- a/app/views/internal_users/activate.html.slim +++ b/app/views/internal_users/activate.html.slim @@ -9,4 +9,4 @@ h1 = t('.headline') = f.label(:password_confirmation) = f.password_field(:password_confirmation, class: 'form-control', required: true) = f.hidden_field(:activation_token) - .actions = submit_tag(t('.submit'), class: 'btn btn-default') + .actions = submit_tag(t('.submit'), class: 'btn btn-primary') diff --git a/app/views/internal_users/forgot_password.html.slim b/app/views/internal_users/forgot_password.html.slim index 8ccb884a..ca10fdac 100644 --- a/app/views/internal_users/forgot_password.html.slim +++ b/app/views/internal_users/forgot_password.html.slim @@ -3,5 +3,5 @@ h1 = t('.headline') = form_tag do .form-group = label_tag(:email, t('activerecord.attributes.internal_user.email')) - = text_field_tag(:email, params[:email], autofocus: true, class: 'form-control', required: true) - .actions = submit_tag(t('.submit'), class: 'btn btn-default') + = email_field_tag(:email, params[:email], autofocus: true, class: 'form-control', required: true) + .actions = submit_tag(t('.submit'), class: 'btn btn-primary') diff --git a/app/views/internal_users/index.html.slim b/app/views/internal_users/index.html.slim index c4b6c1a0..29f07649 100644 --- a/app/views/internal_users/index.html.slim +++ b/app/views/internal_users/index.html.slim @@ -12,7 +12,7 @@ h1 = InternalUser.model_name.human(count: 2) = f.select(:role_eq, User::ROLES.map { |role| [t("users.roles.#{role}"), role] }, {}, class: 'form-control', prompt: t('activerecord.attributes.internal_user.role')) .table-responsive - table.table + table.table.mt-4 thead tr th = t('activerecord.attributes.internal_user.name') diff --git a/app/views/internal_users/reset_password.html.slim b/app/views/internal_users/reset_password.html.slim index a743ef46..857121c2 100644 --- a/app/views/internal_users/reset_password.html.slim +++ b/app/views/internal_users/reset_password.html.slim @@ -9,4 +9,4 @@ h1 = t('.headline') = f.label(:password_confirmation) = f.password_field(:password_confirmation, class: 'form-control', required: true) = f.hidden_field(:reset_password_token) - .actions = submit_tag(t('.submit'), class: 'btn btn-default') + .actions = submit_tag(t('.submit'), class: 'btn btn-primary') diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index becb0200..9a5f8de6 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -13,22 +13,18 @@ html lang='en' = yield(:head) = csrf_meta_tags body - nav.navbar.navbar-default role='navigation' + nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4 role='navigation' .container - .navbar-header - button.navbar-toggle data-target='#navbar-collapse' data-toggle='collapse' type='button' - span.sr-only Toggle navigation - span.icon-bar - span.icon-bar - span.icon-bar - .navbar-brand - i.fa.fa-code - = application_name + button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation' + span.navbar-toggler-icon.sr-only + .navbar-brand + i.fa.fa-code + = application_name #navbar-collapse.collapse.navbar-collapse = render('navigation', cached: true) - ul.nav.navbar-nav.navbar-right + ul.nav.navbar-nav.ml-auto = render('locale_selector', cached: true) - li = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}) + li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link') = render('session') .container data-controller=controller_name = render('flash') diff --git a/app/views/proxy_exercises/_form.html.slim b/app/views/proxy_exercises/_form.html.slim index bd57bf06..601ef950 100644 --- a/app/views/proxy_exercises/_form.html.slim +++ b/app/views/proxy_exercises/_form.html.slim @@ -1,11 +1,11 @@ -= form_for(@proxy_exercise, multipart: true) do |f| += form_for(@proxy_exercise, multipart: true, builder: PagedownFormBuilder) do |f| = render('shared/form_errors', object: @proxy_exercise) .form-group = f.label(:title) = f.text_field(:title, class: 'form-control', required: true) .form-group = f.label(:description) - = f.pagedown_editor :description + = f.pagedown :description, input_html: { preview: true, rows: 10 } h3 Exercises .table-responsive diff --git a/app/views/proxy_exercises/index.html.slim b/app/views/proxy_exercises/index.html.slim index 85ef3890..a2e7e460 100644 --- a/app/views/proxy_exercises/index.html.slim +++ b/app/views/proxy_exercises/index.html.slim @@ -6,30 +6,30 @@ h1 = ProxyExercise.model_name.human(count: 2) = f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.proxy_exercise.title')) .table-responsive - table.table + table.table.mt-4 thead tr - th = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title')) - th = t('activerecord.attributes.exercise.token') - th = t('activerecord.attributes.proxy_exercise.files_count') - th colspan=6 = t('shared.actions') + th.p-1 = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title')) + th.p-1 = t('activerecord.attributes.exercise.token') + th.p-1 = t('activerecord.attributes.proxy_exercise.files_count') + th.p-1 colspan=6 = t('shared.actions') tbody - @proxy_exercises.each do |proxy_exercise| tr data-id=proxy_exercise.id - td = link_to(proxy_exercise.title,proxy_exercise) - td = proxy_exercise.token - td = proxy_exercise.count_files - td = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit? + td.p-1.pt-2 = link_to(proxy_exercise.title,proxy_exercise) + td.p-1.pt-2 = proxy_exercise.token + td.p-1.pt-2 = proxy_exercise.count_files + td.p-1.pt-2 = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit? - td + td.p-1 .btn-group - button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') + button.btn.btn-outline-primary.btn-sm.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') span.caret span.sr-only Toggle Dropdown - ul.dropdown-menu.pull-right role="menu" - li = link_to(t('shared.show'), proxy_exercise, 'data-turbolinks' => "false") if policy(proxy_exercise).show? - li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(proxy_exercise).destroy? - li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(proxy_exercise).clone? + ul.dropdown-menu.float-right role="menu" + li = link_to(t('shared.show'), proxy_exercise, 'data-turbolinks' => "false", class: 'dropdown-item') if policy(proxy_exercise).show? + li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(proxy_exercise).destroy? + li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(proxy_exercise).clone? = render('shared/pagination', collection: @proxy_exercises) p = render('shared/new_button', model: ProxyExercise) diff --git a/app/views/proxy_exercises/show.html.slim b/app/views/proxy_exercises/show.html.slim index dee0a7a0..3b05e830 100644 --- a/app/views/proxy_exercises/show.html.slim +++ b/app/views/proxy_exercises/show.html.slim @@ -14,7 +14,8 @@ h1 = row(label: 'proxy_exercise.files_count', value: @exercises.count) = row(label: 'exercise.description', value: @proxy_exercise.description) = row(label: 'exercise.token', value: @proxy_exercise.token) -h3 Exercises + +h2.mt-4 Exercises .table-responsive table.table thead diff --git a/app/views/request_for_comments/_admin_menu.html.slim b/app/views/request_for_comments/_admin_menu.html.slim index 3f71b2ed..cfdfccf1 100644 --- a/app/views/request_for_comments/_admin_menu.html.slim +++ b/app/views/request_for_comments/_admin_menu.html.slim @@ -1,9 +1,8 @@ -br -h4 Admin Menu -h5 - ul - li = link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id) - li = link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id) - ul - li = link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id) - li = link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id) +hr +h5.mt-4 Admin Menu +ul.text + li = link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id) + li = link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id) +ul.text + li = link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id) + li = link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id) diff --git a/app/views/request_for_comments/_mark_as_solved.html.slim b/app/views/request_for_comments/_mark_as_solved.html.slim index b3df57fd..cac16d06 100644 --- a/app/views/request_for_comments/_mark_as_solved.html.slim +++ b/app/views/request_for_comments/_mark_as_solved.html.slim @@ -4,4 +4,4 @@ button.btn.btn-primary#mark-as-solved-button = t('request_for_comments.mark_as_s p = t('request_for_comments.write_a_thank_you_node') textarea#thank-you-note button.btn.btn-primary#send-thank-you-note = t('request_for_comments.send_thank_you_note') - button.btn.btn-default#cancel-thank-you-note = t('request_for_comments.cancel_thank_you_note') + button.btn.btn-secondary#cancel-thank-you-note = t('request_for_comments.cancel_thank_you_note') diff --git a/app/views/request_for_comments/index.html.slim b/app/views/request_for_comments/index.html.slim index d253f0ab..eb222e31 100644 --- a/app/views/request_for_comments/index.html.slim +++ b/app/views/request_for_comments/index.html.slim @@ -9,7 +9,7 @@ h1 = RequestForComment.model_name.human(count: 2) = f.select(:solved_not_eq, [[t('request_for_comments.show_all'), 2], [t('request_for_comments.show_unsolved'), 1], [t('request_for_comments.show_solved'), 0]]) .table-responsive - table.table.sortable + table.table.sortable.mt-4 thead tr th @@ -39,7 +39,7 @@ h1 = RequestForComment.model_name.human(count: 2) - else td = '-' td = request_for_comment.comments_count - td = request_for_comment.user.displayname + td = link_to_if(request_for_comment.user && policy(request_for_comment.user).show?, request_for_comment.user.displayname, request_for_comment.user) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at)) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.last_comment.nil? ? request_for_comment.updated_at : request_for_comment.last_comment)) diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb index 5f6a53f5..2bc57dc8 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -25,7 +25,7 @@-+
<%= t('activerecord.attributes.request_for_comments.question')%>
@@ -42,7 +42,7 @@<% output_runs = testruns.select { |run| run.cause == 'run' } %> <% if output_runs.size > 0 %> -<%= t('request_for_comments.runtime_output') %>
+<%= t('request_for_comments.runtime_output') %>
<% output_runs.each do |testrun| %> @@ -63,7 +63,7 @@ <% assess_runs = testruns.select { |run| run.cause == 'assess' } %> <% if assess_runs.size > 0 %> -<%= t('request_for_comments.test_results') %>
+<%= t('request_for_comments.test_results') %>
<% assess_runs.each do |testrun| %>@@ -86,7 +86,7 @@
-- ++
<%= t('request_for_comments.howto_title') %>
@@ -96,7 +96,7 @@