Commit ae886f1b by cahrens

Add support for raw HTML editor.

STUD-1562
parent 56bf7d86
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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. 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. Studio: Add edit button to leaf xblocks on the container page. STUD-1306.
......
...@@ -5,7 +5,7 @@ Feature: CMS.HTML Editor ...@@ -5,7 +5,7 @@ Feature: CMS.HTML Editor
Scenario: User can view metadata Scenario: User can view metadata
Given I have created a Blank HTML Page Given I have created a Blank HTML Page
And I edit and select Settings 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 # Safari doesn't save the name properly
@skip_safari @skip_safari
......
...@@ -18,9 +18,14 @@ def i_created_blank_html_page(step): ...@@ -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): 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$') @step('I have created an E-text Written in LaTeX$')
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-comp-editor" id="editor-tab" data-base-asset-url="${base_asset_url}"> <div class="wrapper-comp-editor" id="editor-tab" data-base-asset-url="${base_asset_url}" data-editor="${editor}">
<section class="html-editor editor"> <section class="html-editor editor">
<div class="row"> <div class="row">
<textarea class="tiny-mce">${data | h}</textarea> % if editor == 'visual':
<textarea class="tiny-mce">${data | h}</textarea>
% endif
<textarea name="" class="edit-box">${data | h}</textarea>
</div> </div>
</section> </section>
</div> </div>
......
...@@ -36,6 +36,16 @@ class HtmlFields(object): ...@@ -36,6 +36,16 @@ class HtmlFields(object):
default=False, default=False,
scope=Scope.settings 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): class HtmlModule(HtmlFields, XModule):
...@@ -113,6 +123,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -113,6 +123,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
_context.update({ _context.update({
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(self.location) + '/', 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(self.location) + '/',
'enable_latex_compiler': self.use_latex_compiler, 'enable_latex_compiler': self.use_latex_compiler,
'editor': self.editor
}) })
return _context return _context
......
<div class="test-component">
<div class="wrapper-comp-editor" id="editor-tab" data-base-asset-url="/c4x/foo/bar/asset/" data-editor="visual">
<section class="html-editor editor">
<div class="row">
<textarea class="tiny-mce"><p>original visual text</p></textarea>
<textarea name="" class="edit-box">raw text</textarea>
</div>
</section>
</div>
</div>
<section class="html-edit">
<textarea class="tiny-mce">dummy text</textarea>
</section>
<div class="test-component">
<div class="wrapper-comp-editor" id="editor-tab" data-editor="raw">
<section class="html-editor editor">
<div class="row">
<textarea name="" class="edit-box">raw text</textarea>
</div>
</section>
</div>
</div>
...@@ -3,39 +3,32 @@ describe 'HTMLEditingDescriptor', -> ...@@ -3,39 +3,32 @@ describe 'HTMLEditingDescriptor', ->
window.baseUrl = "/static/deadbeef" window.baseUrl = "/static/deadbeef"
afterEach -> afterEach ->
delete window.baseUrl delete window.baseUrl
describe 'HTML Editor', -> describe 'Visual HTML Editor', ->
beforeEach -> beforeEach ->
loadFixtures 'html-edit.html' loadFixtures 'html-edit-visual.html'
@descriptor = new HTMLEditingDescriptor($('.html-edit')) @descriptor = new HTMLEditingDescriptor($('.test-component'))
it 'Returns data from Visual Editor if Visual Editor is dirty', -> it 'Returns data from Visual Editor if text has changed', ->
visualEditorStub = visualEditorStub =
isDirty: () -> true
getContent: () -> 'from visual editor' getContent: () -> 'from visual editor'
spyOn(@descriptor, 'getVisualEditor').andCallFake () -> spyOn(@descriptor, 'getVisualEditor').andCallFake () ->
visualEditorStub visualEditorStub
data = @descriptor.save().data data = @descriptor.save().data
expect(data).toEqual('from visual editor') 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 = visualEditorStub =
isDirty: () -> false getContent: () -> '<p>original visual text</p>'
getContent: () -> 'from visual editor'
spyOn(@descriptor, 'getVisualEditor').andCallFake () -> spyOn(@descriptor, 'getVisualEditor').andCallFake () ->
visualEditorStub visualEditorStub
data = @descriptor.save().data data = @descriptor.save().data
expect(data).toEqual('from visual editor') expect(data).toEqual('raw text')
it 'Performs link rewriting for static assets when saving', -> it 'Performs link rewriting for static assets when saving', ->
visualEditorStub = visualEditorStub =
isDirty: () -> true
getContent: () -> 'from visual editor with /c4x/foo/bar/asset/image.jpg' getContent: () -> 'from visual editor with /c4x/foo/bar/asset/image.jpg'
spyOn(@descriptor, 'getVisualEditor').andCallFake () -> spyOn(@descriptor, 'getVisualEditor').andCallFake () ->
visualEditorStub visualEditorStub
@descriptor.base_asset_url = '/c4x/foo/bar/asset/'
data = @descriptor.save().data data = @descriptor.save().data
expect(data).toEqual('from visual editor with /static/image.jpg') expect(data).toEqual('from visual editor with /static/image.jpg')
it 'When showing visual editor links are rewritten to c4x format', -> 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 = visualEditorStub =
content: 'text /static/image.jpg' content: 'text /static/image.jpg'
startContent: 'text /static/image.jpg' startContent: 'text /static/image.jpg'
...@@ -45,3 +38,10 @@ describe 'HTMLEditingDescriptor', -> ...@@ -45,3 +38,10 @@ describe 'HTMLEditingDescriptor', ->
@descriptor.initInstanceCallback(visualEditorStub) @descriptor.initInstanceCallback(visualEditorStub)
expect(visualEditorStub.getContent()).toEqual('text /c4x/foo/bar/asset/image.jpg') 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')
class @HTMLEditingDescriptor class @HTMLEditingDescriptor
constructor: (element) -> constructor: (element) ->
@element = element; @element = element
@base_asset_url = @element.find("#editor-tab").data('base-asset-url') @base_asset_url = @element.find("#editor-tab").data('base-asset-url')
@editor_choice = @element.find("#editor-tab").data('editor')
if @base_asset_url == undefined if @base_asset_url == undefined
@base_asset_url = null @base_asset_url = null
# Create an array of all content CSS links to use in and pass to Tiny MCE. # We always create the "raw editor" so we can get the text out of it if necessary on save.
# We create this dynamically in order to support hashed files from our Django pipeline. @advanced_editor = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
# CSS files that are to be used by Tiny MCE should contain the string "tinymce" so mode: "text/html"
# they can be found by the search below. lineNumbers: true
# We filter for only those files that are "content" files (as opposed to "skin" files). lineWrapping: true
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
}) })
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) => setupTinyMCE: (ed) =>
ed.addButton('wrapAsCode', { ed.addButton('wrapAsCode', {
title : 'Code block', title : 'Code block',
...@@ -116,6 +127,9 @@ class @HTMLEditingDescriptor ...@@ -116,6 +127,9 @@ class @HTMLEditingDescriptor
initInstanceCallback: (visualEditor) => initInstanceCallback: (visualEditor) =>
visualEditor.setContent(rewriteStaticLinks(visualEditor.getContent({no_events: 1}), '/static/', @base_asset_url)) 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() visualEditor.focus()
getVisualEditor: () -> getVisualEditor: () ->
...@@ -127,6 +141,14 @@ class @HTMLEditingDescriptor ...@@ -127,6 +141,14 @@ class @HTMLEditingDescriptor
return @visualEditor return @visualEditor
save: -> save: ->
visualEditor = @getVisualEditor() text = undefined
text = rewriteStaticLinks(visualEditor.getContent({no_events: 1}), @base_asset_url, '/static/') 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 data: text
---
metadata:
display_name: Raw HTML
editor: raw
data: |
<p>For use with complex HTML, to allow complete control over the final product.</p>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<section class="html-editor editor"> <div class="wrapper-comp-editor" id="editor-tab" data-editor="${editor}">
<div class="row"> <section class="html-editor editor">
<textarea class="tiny-mce">${data | h}</textarea> <div class="row">
</div> % if editor == 'visual':
</section> <textarea class="tiny-mce">${data | h}</textarea>
% endif
<textarea name="" class="edit-box">${data | h}</textarea>
</div>
</section>
</div>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment