diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f4074283fe..bffd4d4c78 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -221,3 +221,9 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): has_score = True always_recalculate_grades = True template_dir_name = "combinedopenended" + + #Specify whether or not to pass in S3 interface + needs_s3_interface = True + + #Specify whether or not to pass in open ended interface + needs_open_ended_interface = True diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee new file mode 100644 index 0000000000..b723f230e9 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee @@ -0,0 +1,299 @@ +class @MarkdownEditingDescriptor extends XModule.Descriptor + # TODO really, these templates should come from or also feed the cheatsheet + @multipleChoiceTemplate : "( ) incorrect\n( ) incorrect\n(x) correct\n" + @checkboxChoiceTemplate: "[x] correct\n[ ] incorrect\n[x] correct\n" + @stringInputTemplate: "= answer\n" + @numberInputTemplate: "= answer +- x%\n" + @selectTemplate: "[[incorrect, (correct), incorrect]]\n" + @headerTemplate: "Header\n=====\n" + @explanationTemplate: "[explanation]\nShort explanation\n[explanation]\n" + + constructor: (element) -> + @element = element + + if $(".markdown-box", @element).length != 0 + @markdown_editor = CodeMirror.fromTextArea($(".markdown-box", element)[0], { + lineWrapping: true + mode: null + }) + @setCurrentEditor(@markdown_editor) + # Add listeners for toolbar buttons (only present for markdown editor) + @element.on('click', '.xml-tab', @onShowXMLButton) + @element.on('click', '.format-buttons a', @onToolbarButton) + @element.on('click', '.cheatsheet-toggle', @toggleCheatsheet) + # Hide the XML text area + $(@element.find('.xml-box')).hide() + else + @createXMLEditor() + + ### + Creates the XML Editor and sets it as the current editor. If text is passed in, + it will replace the text present in the HTML template. + + text: optional argument to override the text passed in via the HTML template + ### + createXMLEditor: (text) -> + @xml_editor = CodeMirror.fromTextArea($(".xml-box", @element)[0], { + mode: "xml" + lineNumbers: true + lineWrapping: true + }) + if text + @xml_editor.setValue(text) + @setCurrentEditor(@xml_editor) + + ### + User has clicked to show the XML editor. Before XML editor is swapped in, + the user will need to confirm the one-way conversion. + ### + onShowXMLButton: (e) => + e.preventDefault(); + if @confirmConversionToXml() + @createXMLEditor(MarkdownEditingDescriptor.markdownToXml(@markdown_editor.getValue())) + # Need to refresh to get line numbers to display properly (and put cursor position to 0) + @xml_editor.setCursor(0) + @xml_editor.refresh() + # Hide markdown-specific toolbar buttons + $(@element.find('.editor-bar')).hide() + + ### + Have the user confirm the one-way conversion to XML. + Returns true if the user clicked OK, else false. + ### + confirmConversionToXml: -> + # TODO: use something besides a JavaScript confirm dialog? + return confirm("If you use the Advanced Editor, this problem will be converted to XML and you will not be able to return to the Simple Editor Interface.\n\nProceed to the Advanced Editor and convert this problem to XML?") + + ### + Event listener for toolbar buttons (only possible when markdown editor is visible). + ### + onToolbarButton: (e) => + e.preventDefault(); + selection = @markdown_editor.getSelection() + revisedSelection = null + switch $(e.currentTarget).attr('class') + when "multiple-choice-button" then revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice(selection) + when "string-button" then revisedSelection = MarkdownEditingDescriptor.insertStringInput(selection) + when "number-button" then revisedSelection = MarkdownEditingDescriptor.insertNumberInput(selection) + when "checks-button" then revisedSelection = MarkdownEditingDescriptor.insertCheckboxChoice(selection) + when "dropdown-button" then revisedSelection = MarkdownEditingDescriptor.insertSelect(selection) + when "header-button" then revisedSelection = MarkdownEditingDescriptor.insertHeader(selection) + when "explanation-button" then revisedSelection = MarkdownEditingDescriptor.insertExplanation(selection) + else # ignore click + + if revisedSelection != null + @markdown_editor.replaceSelection(revisedSelection) + @markdown_editor.focus() + + ### + Event listener for toggling cheatsheet (only possible when markdown editor is visible). + ### + toggleCheatsheet: (e) => + e.preventDefault(); + if !$(@markdown_editor.getWrapperElement()).find('.simple-editor-cheatsheet')[0] + @cheatsheet = $($('#simple-editor-cheatsheet').html()) + $(@markdown_editor.getWrapperElement()).append(@cheatsheet) + + setTimeout (=> @cheatsheet.toggleClass('shown')), 10 + + ### + Stores the current editor and hides the one that is not displayed. + ### + setCurrentEditor: (editor) -> + if @current_editor + $(@current_editor.getWrapperElement()).hide() + @current_editor = editor + $(@current_editor.getWrapperElement()).show() + $(@current_editor).focus(); + + ### + Called when save is called. Listeners are unregistered because editing the block again will + result in a new instance of the descriptor. Note that this is NOT the case for cancel-- + when cancel is called the instance of the descriptor is reused if edit is selected again. + ### + save: -> + @element.off('click', '.xml-tab', @changeEditor) + @element.off('click', '.format-buttons a', @onToolbarButton) + @element.off('click', '.cheatsheet-toggle', @toggleCheatsheet) + if @current_editor == @markdown_editor + { + data: MarkdownEditingDescriptor.markdownToXml(@markdown_editor.getValue()) + metadata: + markdown: @markdown_editor.getValue() + } + else + { + data: @xml_editor.getValue() + metadata: + markdown: null + } + + @insertMultipleChoice: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericChoice(selectedText, '(', ')', MarkdownEditingDescriptor.multipleChoiceTemplate) + + @insertCheckboxChoice: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericChoice(selectedText, '[', ']', MarkdownEditingDescriptor.checkboxChoiceTemplate) + + @insertGenericChoice: (selectedText, choiceStart, choiceEnd, template) -> + if selectedText.length > 0 + # Replace adjacent newlines with a single newline, strip any trailing newline + cleanSelectedText = selectedText.replace(/\n+/g, '\n').replace(/\n$/,'') + lines = cleanSelectedText.split('\n') + revisedLines = '' + for line in lines + revisedLines += choiceStart + # a stand alone x before other text implies that this option is "correct" + if /^\s*x\s+(\S)/i.test(line) + # Remove the x and any initial whitespace as long as there's more text on the line + line = line.replace(/^\s*x\s+(\S)/i, '$1') + revisedLines += 'x' + else + revisedLines += ' ' + revisedLines += choiceEnd + ' ' + line + '\n' + return revisedLines + else + return template + + @insertStringInput: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericInput(selectedText, '= ', '', MarkdownEditingDescriptor.stringInputTemplate) + + @insertNumberInput: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericInput(selectedText, '= ', '', MarkdownEditingDescriptor.numberInputTemplate) + + @insertSelect: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericInput(selectedText, '[[', ']]', MarkdownEditingDescriptor.selectTemplate) + + @insertHeader: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericInput(selectedText, '', '\n====\n', MarkdownEditingDescriptor.headerTemplate) + + @insertExplanation: (selectedText) -> + return MarkdownEditingDescriptor.insertGenericInput(selectedText, '[explanation]\n', '\n[explanation]', MarkdownEditingDescriptor.explanationTemplate) + + @insertGenericInput: (selectedText, lineStart, lineEnd, template) -> + if selectedText.length > 0 + # TODO: should this insert a newline afterwards? + return lineStart + selectedText + lineEnd + else + return template + +# We may wish to add insertHeader. Here is Tom's code. +# function makeHeader() { +# var selection = simpleEditor.getSelection(); +# var revisedSelection = selection + '\n'; +# for(var i = 0; i < selection.length; i++) { +#revisedSelection += '='; +# } +# simpleEditor.replaceSelection(revisedSelection); +#} +# + @markdownToXml: (markdown)-> + toXml = `function(markdown) { + var xml = markdown; + + // replace headers + xml = xml.replace(/(^.*?$)(?=\n\=\=+$)/gm, '