Commit 16cee198 by Alexander Kryklia

Merge pull request #386 from edx/alex/editor-tabs-for-videoalpha

adds TabsEditingDescriptor, pluggen in VideoAlphaDescriptor
parents ad73feb7 6225c2a5
<div class="base_wrapper">
<section class="editor-with-tabs">
<div class="wrapper-comp-editor" id="editor-tab-id" data-html_id='test_id'>
<div class="edit-header">
<ul class="editor-tabs">
<li class="inner_tab_wrap"><a href="#tab-0" class="tab">Tab 0 Editor</a></li>
<li class="inner_tab_wrap"><a href="#tab-1" class="tab">Tab 1 Transcripts</a></li>
<li class="inner_tab_wrap" id="settings"><a href="#tab-2" class="tab">Tab 2 Settings</a></li>
</ul>
</div>
<div class="tabs-wrapper">
<div class="component-tab" id="tab-0">
<textarea name="" class="edit-box">XML Editor Text</textarea>
</div>
<div class="component-tab" id="tab-1">
Transcripts
</div>
<div class="component-tab" id="tab-2">
Subtitles
</div>
</div>
<div class="wrapper-comp-settings">
<ul>
<li id="editor-mode"><a>Editor</a></li>
<li id="settings-mode"><a>Settings</a></li>
</ul>
</div>
</div>
</section>
<div class="component-edit-header" style="display: block"/>
</div>
describe "TabsEditingDescriptor", ->
beforeEach ->
@isInactiveClass = "is-inactive"
@isCurrent = "current"
loadFixtures 'tabs-edit.html'
@descriptor = new TabsEditingDescriptor($('.base_wrapper'))
@html_id = 'test_id'
@tab_0_switch = jasmine.createSpy('tab_0_switch');
@tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate');
@tab_1_switch = jasmine.createSpy('tab_1_switch');
@tab_1_modelUpdate = jasmine.createSpy('tab_1_modelUpdate');
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 0 Editor', @tab_0_modelUpdate)
TabsEditingDescriptor.Model.addOnSwitch(@html_id, 'Tab 0 Editor', @tab_0_switch)
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 1 Transcripts', @tab_1_modelUpdate)
TabsEditingDescriptor.Model.addOnSwitch(@html_id, 'Tab 1 Transcripts', @tab_1_switch)
spyOn($.fn, 'hide').andCallThrough()
spyOn($.fn, 'show').andCallThrough()
spyOn(TabsEditingDescriptor.Model, 'initialize')
spyOn(TabsEditingDescriptor.Model, 'updateValue')
afterEach ->
TabsEditingDescriptor.Model.modules= {}
describe "constructor", ->
it "first tab should be visible", ->
expect(@descriptor.$tabs.first()).toHaveClass(@isCurrent)
expect(@descriptor.$content.first()).not.toHaveClass(@isInactiveClass)
describe "onSwitchEditor", ->
it "switching tabs changes styles", ->
@descriptor.$tabs.eq(1).trigger("click")
expect(@descriptor.$tabs.eq(0)).not.toHaveClass(@isCurrent)
expect(@descriptor.$content.eq(0)).toHaveClass(@isInactiveClass)
expect(@descriptor.$tabs.eq(1)).toHaveClass(@isCurrent)
expect(@descriptor.$content.eq(1)).not.toHaveClass(@isInactiveClass)
expect(@tab_1_switch).toHaveBeenCalled()
it "if click on current tab, nothing should happen", ->
spyOn($.fn, 'trigger').andCallThrough()
currentTab = @descriptor.$tabs.filter('.' + @isCurrent)
@descriptor.$tabs.eq(0).trigger("click")
expect(@descriptor.$tabs.filter('.' + @isCurrent)).toEqual(currentTab)
expect($.fn.trigger.calls.length).toEqual(1)
it "onSwitch function call", ->
@descriptor.$tabs.eq(1).trigger("click")
expect(TabsEditingDescriptor.Model.updateValue).toHaveBeenCalled()
expect(@tab_1_switch).toHaveBeenCalled()
describe "save", ->
it "function for current tab should be called", ->
@descriptor.$tabs.eq(1).trigger("click")
data = @descriptor.save().data
expect(@tab_1_modelUpdate).toHaveBeenCalled()
it "detach click event", ->
spyOn($.fn, "off")
@descriptor.save()
expect($.fn.off).toHaveBeenCalledWith(
'click',
'.editor-tabs .tab',
@descriptor.onSwitchEditor
)
describe "editor/settings header", ->
it "is hidden", ->
expect(@descriptor.element.find(".component-edit-header").css('display')).toEqual('none')
describe "TabsEditingDescriptor special save cases", ->
beforeEach ->
@isInactiveClass = "is-inactive"
@isCurrent = "current"
loadFixtures 'tabs-edit.html'
@descriptor = new window.TabsEditingDescriptor($('.base_wrapper'))
@html_id = 'test_id'
describe "save", ->
it "case: no init", ->
data = @descriptor.save().data
expect(data).toEqual(null)
it "case: no function in model update", ->
TabsEditingDescriptor.Model.initialize(@html_id)
data = @descriptor.save().data
expect(data).toEqual(null)
it "case: no function in model update, but value presented", ->
@tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate').andReturn(1)
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 0 Editor', @tab_0_modelUpdate)
@descriptor.$tabs.eq(1).trigger("click")
expect(@tab_0_modelUpdate).toHaveBeenCalled()
data = @descriptor.save().data
expect(data).toEqual(1)
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-comp-editor" id="editor-tab-${html_id}" data-html_id="${html_id}">
<section class="editor-with-tabs">
<div class="edit-header">
<span class="component-name"></span>
<ul class="${'editor-tabs' if (len(tabs) != 1) else 'editor-single-tab-name' }">
% for tab in tabs:
<li class="inner_tab_wrap"><a href="#tab-${html_id}-${loop.index}" class="tab ${'current' if tab.get('current', False) else ''}">${_(tab['name'])}</a></li>
% endfor
</ul>
</div>
<div class="${'tabs-wrapper' if (len(tabs) != 1) else 'editor-single-tab' }">
% for tab in tabs:
<div class="component-tab ${'is-inactive' if not tab.get('current', False) else ''}" id="tab-${html_id}-${loop.index}" >
<%include file="${tab['template']}" args="tabName=tab['name']"/>
</div>
% endfor
</div>
</section>
</div>
<%namespace name='static' file='../../static_content.html'/>
<%
import json
%>
## js templates
<script id="metadata-editor-tpl" type="text/template">
<%static:include path="js/metadata-editor.underscore" />
</script>
<script id="metadata-number-entry" type="text/template">
<%static:include path="js/metadata-number-entry.underscore" />
</script>
<script id="metadata-string-entry" type="text/template">
<%static:include path="js/metadata-string-entry.underscore" />
</script>
<script id="metadata-option-entry" type="text/template">
<%static:include path="js/metadata-option-entry.underscore" />
</script>
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(editable_metadata_fields) | h}'/>
<%! from django.utils.translation import ugettext as _ %>
<%page args="tabName"/>
<div>
<textarea id="xml-${html_id}" class="edit-box">${data | h}</textarea>
</div>
<script type='text/javascript'>
$(document).ready(function(){
## Init CodeMirror editor
var el = $("#xml-${html_id}"),
xml_editor = CodeMirror.fromTextArea(el.get(0), {
mode: "application/xml",
lineNumbers: true,
lineWrapping: true
});
TabsEditingDescriptor.Model.addModelUpdate(
'${html_id}',
'${tabName}',
function() { return xml_editor.getValue(); })
TabsEditingDescriptor.Model.addOnSwitch(
'${html_id}',
'${tabName}',
function(){
## CodeMirror should get focus when tab is active
xml_editor.refresh();
xml_editor.focus();
}
)
});
</script>
.editor{
@include clearfix();
.CodeMirror {
@include box-sizing(border-box);
width: 100%;
position: relative;
height: 379px;
border: 1px solid #3c3c3c;
border-top: 1px solid #8891a1;
background: $white;
color: #3c3c3c;
}
.CodeMirror-scroll {
height: 100%;
}
}
// styles duped from _unit.scss - Edit Header (Component Name, Mode-Editor, Mode-Settings)
.tabs-wrapper{
padding-top: 0;
position: relative;
.wrapper-comp-settings {
// set visibility to metadata editor
display: block;
}
}
.editor-single-tab-name {
display: none;
}
.editor-with-tabs {
@include clearfix();
position: relative;
.edit-header {
@include box-sizing(border-box);
padding: 18px 0 18px $baseline;
top: 0 !important; // ugly override for second level tab override
right: 0;
background-color: $blue;
border-bottom: 1px solid $blue-d2;
color: $white;
//Component Name
.component-name {
@extend .t-copy-sub1;
position: relative;
top: 0;
left: 0;
width: 50%;
color: $white;
font-weight: 600;
em {
display: inline-block;
margin-right: ($baseline/4);
font-weight: 400;
color: $white;
}
}
//Nav-Edit Modes
.editor-tabs {
list-style: none;
right: 0;
top: ($baseline/4);
position: absolute;
padding: 12px ($baseline*0.75);
.inner_tab_wrap {
display: inline-block;
margin-left: 8px;
a.tab {
@include font-size(14);
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
border: 1px solid $blue-d1;
border-radius: 3px;
padding: ($baseline/4) ($baseline);
background-color: $blue;
font-weight: bold;
color: $white;
&.current {
@include linear-gradient($blue, $blue);
color: $blue-d1;
box-shadow: inset 0 1px 2px 1px $shadow-l1;
background-color: $blue-d4;
cursor: default;
}
&:hover {
box-shadow: inset 0 1px 2px 1px $shadow;
background-image: linear-gradient(#009FE6, #009FE6) !important;
}
}
}
}
}
.is-inactive {
display: none;
}
.comp-subtitles-entry {
text-align: center;
.file-upload {
display: none;
}
.comp-subtitles-import-list {
> li {
display: block;
margin: $baseline/2 0px $baseline/2 0;
}
.blue-button {
font-size: 1em;
display: block;
width: 70%;
margin: 0 auto;
text-align: center;
}
}
}
}
.component-tab {
background: $white;
position: relative;
border-top: 1px solid #8891a1;
&#advanced {
padding: 0;
border: none;
}
.blue-button {
@include blue-button;
}
}
"""Descriptors for XBlocks/Xmodules, that provide editing of atrributes"""
from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor
from xblock.core import Scope, String
......@@ -7,6 +9,7 @@ log = logging.getLogger(__name__)
class EditingFields(object):
"""Contains specific template information (the raw data body)"""
data = String(scope=Scope.content, default='')
......@@ -29,6 +32,46 @@ class EditingDescriptor(EditingFields, MakoModuleDescriptor):
return _context
class TabsEditingDescriptor(EditingFields, MakoModuleDescriptor):
"""
Module that provides a raw editing view of its data and children. It does not
perform any validation on its definition---just passes it along to the browser.
This class is intended to be used as a mixin.
Engine (module_edit.js) wants for metadata editor
template to be always loaded, so don't forget to include
settings tab in your module descriptor.
"""
mako_template = "widgets/tabs-aggregator.html"
css = {'scss': [resource_string(__name__, 'css/tabs/tabs.scss')]}
js = {'coffee': [resource_string(
__name__, 'js/src/tabs/tabs-aggregator.coffee')]}
js_module_name = "TabsEditingDescriptor"
tabs = []
def get_context(self):
_context = super(TabsEditingDescriptor, self).get_context()
_context.update({
'tabs': self.tabs,
'html_id': self.location.html_id(), # element_id
'data': self.data,
})
return _context
@classmethod
def get_css(cls):
# load every tab's css
for tab in cls.tabs:
tab_styles = tab.get('css', {})
for css_type, css_content in tab_styles.items():
if css_type in cls.css:
cls.css[css_type].extend(css_content)
else:
cls.css[css_type] = css_content
return cls.css
class XMLEditingDescriptor(EditingDescriptor):
"""
Module that provides a raw editing view of its data as XML. It does not perform
......
class @TabsEditingDescriptor
@isInactiveClass : "is-inactive"
constructor: (element) ->
@element = element;
###
Not tested on syncing of multiple editors of same type in tabs
(Like many CodeMirrors).
###
# hide editor/settings bar
$('.component-edit-header').hide()
@$tabs = $(".tab", @element)
@$content = $(".component-tab", @element)
@element.find('.editor-tabs .tab').each (index, value) =>
$(value).on('click', @onSwitchEditor)
# If default visible tab is not setted or if were marked as current
# more than 1 tab just first tab will be shown
currentTab = @$tabs.filter('.current')
currentTab = @$tabs.first() if currentTab.length isnt 1
@html_id = @$tabs.closest('.wrapper-comp-editor').data('html_id')
currentTab.trigger("click", [true, @html_id])
onSwitchEditor: (e, firstTime, html_id) =>
e.preventDefault();
isInactiveClass = TabsEditingDescriptor.isInactiveClass
$currentTarget = $(e.currentTarget)
if not $currentTarget.hasClass('current') or firstTime is true
previousTab = null
@$tabs.each( (index, value) ->
if $(value).hasClass('current')
previousTab = $(value).html()
)
# init and save data from previous tab
TabsEditingDescriptor.Model.updateValue(@html_id, previousTab)
# Save data from editor in previous tab to editor in current tab here.
# (to be implemented when there is a use case for this functionality)
# call onswitch
onSwitchFunction = TabsEditingDescriptor.Model.modules[@html_id].tabSwitch[$currentTarget.text()]
onSwitchFunction() if $.isFunction(onSwitchFunction)
@$tabs.removeClass('current')
$currentTarget.addClass('current')
# Tabs are implemeted like anchors. Therefore we can use hash to find
# corresponding content
content_id = $currentTarget.attr('href')
@$content
.addClass(isInactiveClass)
.filter(content_id)
.removeClass(isInactiveClass)
save: ->
@element.off('click', '.editor-tabs .tab', @onSwitchEditor)
current_tab = @$tabs.filter('.current').html()
data: TabsEditingDescriptor.Model.getValue(@html_id, current_tab)
@Model :
addModelUpdate : (id, tabName, modelUpdateFunction) ->
###
Function that registers 'modelUpdate' functions of every tab.
These functions are used to update value, which will be returned
by calling save on component.
###
@initialize(id)
@modules[id].modelUpdate[tabName] = modelUpdateFunction
addOnSwitch : (id, tabName, onSwitchFunction) ->
###
Function that registers functions invoked when switching
to particular tab.
###
@initialize(id)
@modules[id].tabSwitch[tabName] = onSwitchFunction
updateValue : (id, tabName) ->
###
Function that invokes when switching tabs.
It ensures that data from previous tab is stored.
If new tab need this data, it should retrieve it from
stored value.
###
@initialize(id)
modelUpdateFunction = @modules[id]['modelUpdate'][tabName]
@modules[id]['value'] = modelUpdateFunction() if $.isFunction(modelUpdateFunction)
getValue : (id, tabName) ->
###
Retrieves stored data on component save.
1. When we switching tabs - previous tab data is always saved to @[id].value
2. If current tab have registered 'modelUpdate' method, it should be invoked 1st.
(If we have edited in 1st tab, then switched to 2nd, 2nd tab should
care about getting data from @[id].value in onSwitch.)
###
if not @modules[id]
return null
if $.isFunction(@modules[id]['modelUpdate'][tabName])
return @modules[id]['modelUpdate'][tabName]()
else
if typeof @modules[id]['value'] is 'undefined'
return null
else
return @modules[id]['value']
# html_id's of descriptors will be stored in modules variable as
# containers for callbacks.
modules: {}
initialize : (id) ->
###
Initialize objects per id. Id is html_id of descriptor.
###
@modules[id] = @modules[id] or {}
@modules[id].tabSwitch = @modules[id]['tabSwitch'] or {}
@modules[id].modelUpdate = @modules[id]['modelUpdate'] or {}
""" Tests for editing descriptors"""
import unittest
import os
import logging
from mock import Mock
from pkg_resources import resource_string
from xmodule.editing_module import TabsEditingDescriptor
from .import get_test_system
log = logging.getLogger(__name__)
class TabsEditingDescriptorTestCase(unittest.TestCase):
""" Testing TabsEditingDescriptor"""
def setUp(self):
super(TabsEditingDescriptorTestCase, self).setUp()
system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
self.tabs = [
{
'name': "Test_css",
'template': "tabs/codemirror-edit.html",
'current': True,
'css': {
'scss': [resource_string(__name__,
'../../test_files/test_tabseditingdescriptor.scss')],
'css': [resource_string(__name__,
'../../test_files/test_tabseditingdescriptor.css')]
}
},
{
'name': "Subtitles",
'template': "videoalpha/subtitles.html",
},
{
'name': "Settings",
'template': "tabs/video-metadata-edit-tab.html"
}
]
TabsEditingDescriptor.tabs = self.tabs
self.descriptor = TabsEditingDescriptor(
runtime=system,
model_data={})
def test_get_css(self):
"""test get_css"""
css = self.descriptor.get_css()
test_files_dir = os.path.dirname(__file__).replace('xmodule/tests', 'test_files')
test_css_file = os.path.join(test_files_dir, 'test_tabseditingdescriptor.scss')
with open(test_css_file) as new_css:
added_css = new_css.read()
self.assertEqual(css['scss'].pop(), added_css)
self.assertEqual(css['css'].pop(), added_css)
def test_get_context(self):
""""test get_context"""
rendered_context = self.descriptor.get_context()
self.assertListEqual(rendered_context['tabs'], self.tabs)
# -*- coding: utf-8 -*-
"""Test for Video Alpha Xmodule functional logic.
These tests data readed from xml, not from mongo.
These tests data readed from xml or from mongo.
we have a ModuleStoreTestCase class defined in
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py. You can
......@@ -12,10 +12,12 @@ in common/lib/xmodule/xmodule/modulestore/tests/factories.py to create
the course, section, subsection, unit, etc.
"""
import unittest
from xmodule.videoalpha_module import VideoAlphaDescriptor
from . import LogicTest
from lxml import etree
from pkg_resources import resource_string
from .import get_test_system
class VideoAlphaModuleTest(LogicTest):
"""Logic tests for VideoAlpha Xmodule."""
......@@ -49,3 +51,31 @@ class VideoAlphaModuleTest(LogicTest):
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, 47079))
class VideoAlphaDescriptorTest(unittest.TestCase):
"""Test for VideoAlphaDescriptor"""
def setUp(self):
system = get_test_system()
self.descriptor = VideoAlphaDescriptor(
runtime=system,
model_data={})
def test_get_context(self):
""""test get_context"""
correct_tabs = [
{
'name': "XML",
'template': "videoalpha/codemirror-edit.html",
'css': {'scss': [resource_string(__name__,
'../css/tabs/codemirror.scss')]},
'current': True,
},
{
'name': "Settings",
'template': "tabs/metadata-edit-tab.html"
}
]
rendered_context = self.descriptor.get_context()
self.assertListEqual(rendered_context['tabs'], correct_tabs)
......@@ -20,7 +20,7 @@ from django.http import Http404
from django.conf import settings
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import TabsEditingDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
......@@ -187,6 +187,23 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
})
class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor):
class VideoAlphaDescriptor(VideoAlphaFields, TabsEditingDescriptor):
"""Descriptor for `VideoAlphaModule`."""
module_class = VideoAlphaModule
tabs = [
{
'name': "XML",
'template': "videoalpha/codemirror-edit.html",
'css': {'scss': [resource_string(__name__, 'css/tabs/codemirror.scss')]},
'current': True,
},
# {
# 'name': "Subtitles",
# 'template': "videoalpha/subtitles.html",
# },
{
'name': "Settings",
'template': "tabs/metadata-edit-tab.html"
}
]
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