diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 01894935bd..dd3b57da60 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Studio: Add "raw HTML" editor so that authors can write HTML that will not be +changed in any way. STUD-1562 + Blades: Show the HD button only if there is an HD version available. BLD-937. Studio: Add edit button to leaf xblocks on the container page. STUD-1306. diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature index 6da09b75d2..31fcd9fd3b 100644 --- a/cms/djangoapps/contentstore/features/html-editor.feature +++ b/cms/djangoapps/contentstore/features/html-editor.feature @@ -5,7 +5,7 @@ Feature: CMS.HTML Editor Scenario: User can view metadata Given I have created a Blank HTML Page And I edit and select Settings - Then I see only the HTML display name setting + Then I see the HTML component settings # Safari doesn't save the name properly @skip_safari diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py index 8b1717e617..ad1be7f13f 100644 --- a/cms/djangoapps/contentstore/features/html-editor.py +++ b/cms/djangoapps/contentstore/features/html-editor.py @@ -18,9 +18,14 @@ def i_created_blank_html_page(step): ) -@step('I see only the HTML display name setting$') +@step('I see the HTML component settings$') def i_see_only_the_html_display_name(step): - world.verify_all_setting_entries([['Display Name', "Text", False]]) + world.verify_all_setting_entries( + [ + ['Display Name', "Text", False], + ['Editor', "Visual", False] + ] + ) @step('I have created an E-text Written in LaTeX$') diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index a27225e36b..15cf178965 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -1,9 +1,12 @@ <%! from django.utils.translation import ugettext as _ %> -
+
- + % if editor == 'visual': + + % endif +
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index d8888018d6..526ead81f9 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -36,6 +36,16 @@ class HtmlFields(object): default=False, scope=Scope.settings ) + editor = String( + help="Supports switching between the Visual Editor and the Raw HTML Editor. The change does not take effect until Save is pressed.", + display_name="Editor", + default="visual", + values=[ + {"display_name": "Visual", "value": "visual"}, + {"display_name": "Raw", "value": "raw"} + ], + scope=Scope.settings + ) class HtmlModule(HtmlFields, XModule): @@ -113,6 +123,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): _context.update({ 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(self.location) + '/', 'enable_latex_compiler': self.use_latex_compiler, + 'editor': self.editor }) return _context diff --git a/common/lib/xmodule/xmodule/js/fixtures/html-edit-visual.html b/common/lib/xmodule/xmodule/js/fixtures/html-edit-visual.html new file mode 100644 index 0000000000..6f1ff82e50 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/html-edit-visual.html @@ -0,0 +1,10 @@ +
+
+
+
+ + +
+
+
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/html-edit.html b/common/lib/xmodule/xmodule/js/fixtures/html-edit.html deleted file mode 100644 index 11f7868ff7..0000000000 --- a/common/lib/xmodule/xmodule/js/fixtures/html-edit.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/common/lib/xmodule/xmodule/js/fixtures/html-editor-raw.html b/common/lib/xmodule/xmodule/js/fixtures/html-editor-raw.html new file mode 100644 index 0000000000..54cbf8ba69 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/html-editor-raw.html @@ -0,0 +1,9 @@ +
+
+
+
+ +
+
+
+
diff --git a/common/lib/xmodule/xmodule/js/spec/html/edit_spec.coffee b/common/lib/xmodule/xmodule/js/spec/html/edit_spec.coffee index 9b5cb0c582..104e045d29 100644 --- a/common/lib/xmodule/xmodule/js/spec/html/edit_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/html/edit_spec.coffee @@ -3,39 +3,32 @@ describe 'HTMLEditingDescriptor', -> window.baseUrl = "/static/deadbeef" afterEach -> delete window.baseUrl - describe 'HTML Editor', -> + describe 'Visual HTML Editor', -> beforeEach -> - loadFixtures 'html-edit.html' - @descriptor = new HTMLEditingDescriptor($('.html-edit')) - it 'Returns data from Visual Editor if Visual Editor is dirty', -> + loadFixtures 'html-edit-visual.html' + @descriptor = new HTMLEditingDescriptor($('.test-component')) + it 'Returns data from Visual Editor if text has changed', -> visualEditorStub = - isDirty: () -> true getContent: () -> 'from visual editor' spyOn(@descriptor, 'getVisualEditor').andCallFake () -> visualEditorStub data = @descriptor.save().data expect(data).toEqual('from visual editor') - it 'Returns data from Visual Editor even if Visual Editor is not dirty', -> + it 'Returns data from Raw Editor if text has not changed', -> visualEditorStub = - isDirty: () -> false - getContent: () -> 'from visual editor' + getContent: () -> '

original visual text

' spyOn(@descriptor, 'getVisualEditor').andCallFake () -> visualEditorStub data = @descriptor.save().data - expect(data).toEqual('from visual editor') + expect(data).toEqual('raw text') it 'Performs link rewriting for static assets when saving', -> visualEditorStub = - isDirty: () -> true getContent: () -> 'from visual editor with /c4x/foo/bar/asset/image.jpg' spyOn(@descriptor, 'getVisualEditor').andCallFake () -> visualEditorStub - @descriptor.base_asset_url = '/c4x/foo/bar/asset/' data = @descriptor.save().data expect(data).toEqual('from visual editor with /static/image.jpg') it 'When showing visual editor links are rewritten to c4x format', -> - @descriptor = new HTMLEditingDescriptor($('.html-edit')) - @descriptor.base_asset_url = '/c4x/foo/bar/asset/' - visualEditorStub = content: 'text /static/image.jpg' startContent: 'text /static/image.jpg' @@ -45,3 +38,10 @@ describe 'HTMLEditingDescriptor', -> @descriptor.initInstanceCallback(visualEditorStub) expect(visualEditorStub.getContent()).toEqual('text /c4x/foo/bar/asset/image.jpg') + describe 'Raw HTML Editor', -> + beforeEach -> + loadFixtures 'html-editor-raw.html' + @descriptor = new HTMLEditingDescriptor($('.test-component')) + it 'Returns data from raw editor', -> + data = @descriptor.save().data + expect(data).toEqual('raw text') diff --git a/common/lib/xmodule/xmodule/js/src/html/edit.coffee b/common/lib/xmodule/xmodule/js/src/html/edit.coffee index af0b047999..59fbb6f981 100644 --- a/common/lib/xmodule/xmodule/js/src/html/edit.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/edit.coffee @@ -1,68 +1,79 @@ class @HTMLEditingDescriptor constructor: (element) -> - @element = element; + @element = element @base_asset_url = @element.find("#editor-tab").data('base-asset-url') + @editor_choice = @element.find("#editor-tab").data('editor') if @base_asset_url == undefined @base_asset_url = null - # Create an array of all content CSS links to use in and pass to Tiny MCE. - # We create this dynamically in order to support hashed files from our Django pipeline. - # CSS files that are to be used by Tiny MCE should contain the string "tinymce" so - # they can be found by the search below. - # We filter for only those files that are "content" files (as opposed to "skin" files). - tiny_mce_css_links = [] - $("link[rel=stylesheet][href*='tinymce']").filter("[href*='content']").each -> - tiny_mce_css_links.push $(this).attr("href") - return - -# This is a workaround for the fact that tinyMCE's baseURL property is not getting correctly set on AWS -# instances (like sandbox). It is not necessary to explicitly set baseURL when running locally. - tinyMCE.baseURL = "#{baseUrl}/js/vendor/tinymce/js/tinymce" -# This is necessary for the LMS bulk e-mail acceptance test. In that particular scenario, -# tinyMCE incorrectly decides that the suffix should be "", which means it fails to load files. - tinyMCE.suffix = ".min" - @tiny_mce_textarea = $(".tiny-mce", @element).tinymce({ - script_url : "#{baseUrl}/js/vendor/tinymce/js/tinymce/tinymce.full.min.js", - theme : "modern", - skin: 'studio-tmce4', - schema: "html5", - # Necessary to preserve relative URLs to our images. - convert_urls : false, - content_css : tiny_mce_css_links.join(", "), - formats : { - # tinyMCE does block level for code by default - code: {inline: 'code'} - }, - # Disable visual aid on borderless table. - visual: false, - plugins: "textcolor, link, image, codemirror", - codemirror: { - path: "#{baseUrl}/js/vendor" - }, - image_advtab: true, - # We may want to add "styleselect" when we collect all styles used throughout the LMS - toolbar: "formatselect | fontselect | bold italic underline forecolor wrapAsCode | bullist numlist outdent indent blockquote | link unlink image | code", - block_formats: "Paragraph=p;Preformatted=pre;Heading 1=h1;Heading 2=h2;Heading 3=h3", - width: '100%', - height: '400px', - menubar: false, - statusbar: false, - - # Necessary to avoid stripping of style tags. - valid_children : "+body[style]", - - # Allow any elements to be used, e.g. link, script, math - valid_elements: "*[*]", - extended_valid_elements: "*[*]", - invalid_elements: "", - - setup: @setupTinyMCE, - # Cannot get access to tinyMCE Editor instance (for focusing) until after it is rendered. - # The tinyMCE callback passes in the editor as a parameter. - init_instance_callback: @initInstanceCallback + # We always create the "raw editor" so we can get the text out of it if necessary on save. + @advanced_editor = CodeMirror.fromTextArea($(".edit-box", @element)[0], { + mode: "text/html" + lineNumbers: true + lineWrapping: true }) + if @editor_choice == 'visual' + @$advancedEditorWrapper = $(@advanced_editor.getWrapperElement()) + @$advancedEditorWrapper.addClass('is-inactive') + # Create an array of all content CSS links to use in and pass to Tiny MCE. + # We create this dynamically in order to support hashed files from our Django pipeline. + # CSS files that are to be used by Tiny MCE should contain the string "tinymce" so + # they can be found by the search below. + # We filter for only those files that are "content" files (as opposed to "skin" files). + tiny_mce_css_links = [] + $("link[rel=stylesheet][href*='tinymce']").filter("[href*='content']").each -> + tiny_mce_css_links.push $(this).attr("href") + return + + # This is a workaround for the fact that tinyMCE's baseURL property is not getting correctly set on AWS + # instances (like sandbox). It is not necessary to explicitly set baseURL when running locally. + tinyMCE.baseURL = "#{baseUrl}/js/vendor/tinymce/js/tinymce" + # This is necessary for the LMS bulk e-mail acceptance test. In that particular scenario, + # tinyMCE incorrectly decides that the suffix should be "", which means it fails to load files. + tinyMCE.suffix = ".min" + @tiny_mce_textarea = $(".tiny-mce", @element).tinymce({ + script_url : "#{baseUrl}/js/vendor/tinymce/js/tinymce/tinymce.full.min.js", + theme : "modern", + skin: 'studio-tmce4', + schema: "html5", + # Necessary to preserve relative URLs to our images. + convert_urls : false, + content_css : tiny_mce_css_links.join(", "), + formats : { + # tinyMCE does block level for code by default + code: {inline: 'code'} + }, + # Disable visual aid on borderless table. + visual: false, + plugins: "textcolor, link, image, codemirror", + codemirror: { + path: "#{baseUrl}/js/vendor" + }, + image_advtab: true, + # We may want to add "styleselect" when we collect all styles used throughout the LMS + toolbar: "formatselect | fontselect | bold italic underline forecolor wrapAsCode | bullist numlist outdent indent blockquote | link unlink image | code", + block_formats: "Paragraph=p;Preformatted=pre;Heading 1=h1;Heading 2=h2;Heading 3=h3", + width: '100%', + height: '400px', + menubar: false, + statusbar: false, + + # Necessary to avoid stripping of style tags. + valid_children : "+body[style]", + + # Allow any elements to be used, e.g. link, script, math + valid_elements: "*[*]", + extended_valid_elements: "*[*]", + invalid_elements: "", + + setup: @setupTinyMCE, + # Cannot get access to tinyMCE Editor instance (for focusing) until after it is rendered. + # The tinyMCE callback passes in the editor as a parameter. + init_instance_callback: @initInstanceCallback + }) + setupTinyMCE: (ed) => ed.addButton('wrapAsCode', { title : 'Code block', @@ -116,6 +127,9 @@ class @HTMLEditingDescriptor initInstanceCallback: (visualEditor) => visualEditor.setContent(rewriteStaticLinks(visualEditor.getContent({no_events: 1}), '/static/', @base_asset_url)) + # Unfortunately, just setting visualEditor.isNortDirty = true is not enough to convince TinyMCE we + # haven't dirtied the Editor. Store the raw content so we can compare it later. + @starting_content = visualEditor.getContent({format:"raw", no_events: 1}) visualEditor.focus() getVisualEditor: () -> @@ -127,6 +141,14 @@ class @HTMLEditingDescriptor return @visualEditor save: -> - visualEditor = @getVisualEditor() - text = rewriteStaticLinks(visualEditor.getContent({no_events: 1}), @base_asset_url, '/static/') + text = undefined + if @editor_choice == 'visual' + visualEditor = @getVisualEditor() + content = visualEditor.getContent({format:"raw", no_events: 1}) + if @starting_content != content + text = rewriteStaticLinks(content, @base_asset_url, '/static/') + + if text == undefined + text = @advanced_editor.getValue() + data: text diff --git a/common/lib/xmodule/xmodule/templates/html/raw.yaml b/common/lib/xmodule/xmodule/templates/html/raw.yaml new file mode 100644 index 0000000000..3285147900 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/html/raw.yaml @@ -0,0 +1,6 @@ +--- +metadata: + display_name: Raw HTML + editor: raw +data: | +

For use with complex HTML, to allow complete control over the final product.

diff --git a/lms/templates/widgets/html-edit.html b/lms/templates/widgets/html-edit.html index a3ad0ef7d3..8dcf6b67b8 100644 --- a/lms/templates/widgets/html-edit.html +++ b/lms/templates/widgets/html-edit.html @@ -1,7 +1,12 @@ <%! from django.utils.translation import ugettext as _ %> -
-
- -
-
+
+
+
+ % if editor == 'visual': + + % endif + +
+
+
\ No newline at end of file