Add support for raw HTML editor.
STUD-1562
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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$')
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<%! 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">
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
10
common/lib/xmodule/xmodule/js/fixtures/html-edit-visual.html
Normal file
10
common/lib/xmodule/xmodule/js/fixtures/html-edit-visual.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<section class="html-edit">
|
||||
<textarea class="tiny-mce">dummy text</textarea>
|
||||
</section>
|
||||
@@ -0,0 +1,9 @@
|
||||
<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', ->
|
||||
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: () -> '<p>original visual text</p>'
|
||||
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')
|
||||
|
||||
@@ -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
|
||||
|
||||
6
common/lib/xmodule/xmodule/templates/html/raw.yaml
Normal file
6
common/lib/xmodule/xmodule/templates/html/raw.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Raw HTML
|
||||
editor: raw
|
||||
data: |
|
||||
<p>For use with complex HTML, to allow complete control over the final product.</p>
|
||||
@@ -1,7 +1,12 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<section class="html-editor editor">
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
<div class="wrapper-comp-editor" id="editor-tab" data-editor="${editor}">
|
||||
<section class="html-editor editor">
|
||||
<div class="row">
|
||||
% if editor == 'visual':
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
% endif
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
Reference in New Issue
Block a user