Merge pull request #2981 from edx/andya/container-editing
Add "Edit" button to each leaf xblock on the container page
This commit is contained in:
@@ -5,6 +5,8 @@ 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 edit button to leaf xblocks on the container page. STUD-1306.
|
||||
|
||||
Blades: Add LTI context_id parameter. BLD-584.
|
||||
|
||||
Blades: Update LTI resource_link_id parameter. BLD-768.
|
||||
|
||||
@@ -350,7 +350,7 @@ def attach_file(filename, sub_path):
|
||||
|
||||
def upload_file(filename, sub_path=''):
|
||||
attach_file(filename, sub_path)
|
||||
button_css = '.upload-dialog .action-upload'
|
||||
button_css = '.wrapper-modal-window-assetupload .action-upload'
|
||||
world.css_click(button_css)
|
||||
|
||||
|
||||
|
||||
@@ -169,11 +169,11 @@ def change_display_name(step, display_name):
|
||||
world.edit_component_and_select_settings()
|
||||
index = world.get_setting_entry_index(DISPLAY_NAME)
|
||||
world.set_field_value(index, display_name)
|
||||
world.save_component(step)
|
||||
world.save_component()
|
||||
|
||||
|
||||
@step(u'I unset the display name')
|
||||
def unset_display_name(step):
|
||||
world.edit_component_and_select_settings()
|
||||
world.revert_setting_entry(DISPLAY_NAME)
|
||||
world.save_component(step)
|
||||
world.save_component()
|
||||
|
||||
@@ -6,6 +6,7 @@ from nose.tools import assert_equal, assert_in # pylint: disable=E0611
|
||||
from terrain.steps import reload_the_page
|
||||
from common import type_in_codemirror
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from cms.envs.common import FEATURES
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -105,9 +106,16 @@ def click_component_from_menu(category, component_type, is_advanced):
|
||||
|
||||
@world.absorb
|
||||
def edit_component_and_select_settings():
|
||||
world.wait_for(lambda _driver: world.css_visible('a.edit-button'))
|
||||
world.css_click('a.edit-button')
|
||||
world.css_click('#settings-mode a')
|
||||
world.edit_component()
|
||||
world.ensure_settings_visible()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def ensure_settings_visible():
|
||||
# Select the 'settings' tab if there is one (it isn't displayed if it is the only option)
|
||||
settings_button = world.browser.find_by_css('.settings-button')
|
||||
if len(settings_button) > 0:
|
||||
world.css_click('.settings-button')
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -116,6 +124,17 @@ def edit_component():
|
||||
world.css_click('a.edit-button')
|
||||
|
||||
|
||||
@world.absorb
|
||||
def select_editor_tab(tab_name):
|
||||
editor_tabs = world.browser.find_by_css('.editor-tabs a')
|
||||
expected_tab_text = tab_name.strip().upper()
|
||||
matching_tabs = [tab for tab in editor_tabs if tab.text.upper() == expected_tab_text]
|
||||
assert len(matching_tabs) == 1
|
||||
tab = matching_tabs[0]
|
||||
tab.click()
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
def enter_xml_in_advanced_problem(step, text):
|
||||
"""
|
||||
Edits an advanced problem (assumes only on page),
|
||||
@@ -123,7 +142,7 @@ def enter_xml_in_advanced_problem(step, text):
|
||||
"""
|
||||
world.edit_component()
|
||||
type_in_codemirror(0, text)
|
||||
world.save_component(step)
|
||||
world.save_component()
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -173,14 +192,14 @@ def verify_all_setting_entries(expected_entries):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def save_component(step):
|
||||
world.css_click("a.save-button")
|
||||
def save_component():
|
||||
world.css_click("a.action-save")
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def save_component_and_reopen(step):
|
||||
save_component(step)
|
||||
save_component()
|
||||
# We have a known issue that modifications are still shown within the edit window after cancel (though)
|
||||
# they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
|
||||
reload_the_page(step)
|
||||
@@ -189,7 +208,7 @@ def save_component_and_reopen(step):
|
||||
|
||||
@world.absorb
|
||||
def cancel_component(step):
|
||||
world.css_click("a.cancel-button")
|
||||
world.css_click("a.action-cancel")
|
||||
# We have a known issue that modifications are still shown within the edit window after cancel (though)
|
||||
# they are not persisted. Refresh the browser to make sure the changes were not persisted.
|
||||
reload_the_page(step)
|
||||
|
||||
@@ -4,13 +4,13 @@ Feature: CMS.Discussion Component Editor
|
||||
|
||||
Scenario: User can view discussion component metadata
|
||||
Given I have created a Discussion Tag
|
||||
And I edit and select Settings
|
||||
And I edit the component
|
||||
Then I see three alphabetized settings and their expected values
|
||||
|
||||
# Safari doesn't save the name properly
|
||||
@skip_safari
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Discussion Tag
|
||||
And I edit and select Settings
|
||||
And I edit the component
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
|
||||
@@ -21,3 +21,8 @@ def i_see_only_the_settings_and_values(step):
|
||||
['Display Name', "Discussion", False],
|
||||
['Subcategory', "Topic-Level Student-Visible Label", False]
|
||||
])
|
||||
|
||||
|
||||
@step('I edit the component$')
|
||||
def i_edit_and_select_settings(_step):
|
||||
world.edit_component()
|
||||
|
||||
@@ -123,7 +123,7 @@ def perform_action_in_plugin(action):
|
||||
|
||||
@step('I save the page$')
|
||||
def i_click_on_save(step):
|
||||
world.save_component(step)
|
||||
world.save_component()
|
||||
|
||||
|
||||
@step('the page text contains:')
|
||||
|
||||
@@ -44,14 +44,13 @@ def click_edit_or_delete(step, edit_or_delete):
|
||||
|
||||
@step(u'I change the name to "([^"]*)"$')
|
||||
def change_name(step, new_name):
|
||||
settings_css = '#settings-mode a'
|
||||
settings_css = '.settings-button'
|
||||
world.css_click(settings_css)
|
||||
input_css = 'input.setting-input'
|
||||
world.css_fill(input_css, new_name)
|
||||
if world.is_firefox():
|
||||
world.trigger_event(input_css)
|
||||
save_button = 'a.save-button'
|
||||
world.css_click(save_button)
|
||||
world.save_component()
|
||||
|
||||
|
||||
@step(u'I drag the first static page to the last$')
|
||||
|
||||
@@ -286,5 +286,5 @@ def set_weight(weight):
|
||||
|
||||
|
||||
def open_high_level_source():
|
||||
world.css_click('a.edit-button')
|
||||
world.edit_component()
|
||||
world.css_click('.launch-latex-compiler > a')
|
||||
|
||||
@@ -209,24 +209,21 @@ def check_text_in_the_captions(_step, text):
|
||||
|
||||
@step('I see value "([^"]*)" in the field "([^"]*)"$')
|
||||
def check_transcripts_field(_step, values, field_name):
|
||||
world.click_link_by_text('Advanced')
|
||||
world.select_editor_tab('Advanced')
|
||||
field_id = '#' + world.browser.find_by_xpath('//label[text()="%s"]' % field_name.strip())[0]['for']
|
||||
values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')]
|
||||
assert any(values_list)
|
||||
world.click_link_by_text('Basic')
|
||||
world.select_editor_tab('Basic')
|
||||
|
||||
|
||||
@step('I save changes$')
|
||||
def save_changes(_step):
|
||||
save_css = 'a.save-button'
|
||||
world.css_click(save_css)
|
||||
world.wait_for_ajax_complete()
|
||||
world.save_component()
|
||||
|
||||
|
||||
@step('I open tab "([^"]*)"$')
|
||||
def open_tab(_step, tab_name):
|
||||
world.click_link_by_text(tab_name.strip())
|
||||
world.wait_for_ajax_complete()
|
||||
world.select_editor_tab(tab_name)
|
||||
|
||||
|
||||
@step('I set value "([^"]*)" to the field "([^"]*)"$')
|
||||
|
||||
@@ -72,7 +72,7 @@ class RequestHandlerWithSessionId(object):
|
||||
def success_upload_file(filename):
|
||||
upload_file(filename, sub_path="uploads/")
|
||||
world.css_has_text('#upload_confirm', 'Success!')
|
||||
world.is_css_not_present('.wrapper-dialog-assetupload', wait_time=30)
|
||||
world.is_css_not_present('.wrapper-modal-window-assetupload', wait_time=30)
|
||||
|
||||
|
||||
def get_translations_container():
|
||||
@@ -112,11 +112,10 @@ def set_show_captions(step, setting):
|
||||
# Prevent cookies from overriding course settings
|
||||
world.browser.cookies.delete('hide_captions')
|
||||
|
||||
world.css_click('a.edit-button')
|
||||
world.wait_for(lambda _driver: world.css_visible('a.save-button'))
|
||||
world.click_link_by_text('Advanced')
|
||||
world.edit_component()
|
||||
world.select_editor_tab('Advanced')
|
||||
world.browser.select('Transcript Display', setting)
|
||||
world.css_click('a.save-button')
|
||||
world.save_component()
|
||||
|
||||
|
||||
@step('when I view the video it (.*) show the captions$')
|
||||
@@ -161,7 +160,7 @@ def correct_video_settings(_step):
|
||||
|
||||
@step('my video display name change is persisted on save$')
|
||||
def video_name_persisted(step):
|
||||
world.css_click('a.save-button')
|
||||
world.save_component()
|
||||
reload_the_page(step)
|
||||
world.wait_for_xmodule()
|
||||
world.edit_component()
|
||||
|
||||
@@ -317,14 +317,17 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
|
||||
while parent and parent.category != 'sequential':
|
||||
ancestor_xblocks.append(parent)
|
||||
parent = get_parent_xblock(parent)
|
||||
|
||||
ancestor_xblocks.reverse()
|
||||
|
||||
unit = ancestor_xblocks[0] if ancestor_xblocks else None
|
||||
unit_publish_state = compute_publish_state(unit) if unit else None
|
||||
|
||||
return render_to_response('container.html', {
|
||||
'context_course': course,
|
||||
'xblock': xblock,
|
||||
'xblock_locator': locator,
|
||||
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
|
||||
'unit': unit,
|
||||
'unit_publish_state': unit_publish_state,
|
||||
'ancestor_xblocks': ancestor_xblocks,
|
||||
})
|
||||
else:
|
||||
|
||||
@@ -7,6 +7,9 @@ from xmodule.modulestore.django import loc_mapper, modulestore
|
||||
|
||||
__all__ = ['edge', 'event', 'landing']
|
||||
|
||||
EDITING_TEMPLATES = [
|
||||
"basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal"
|
||||
]
|
||||
|
||||
# points to the temporary course landing page with log in and sign up
|
||||
def landing(request, org, course, coursename):
|
||||
|
||||
@@ -34,6 +34,7 @@ from ..utils import get_modulestore
|
||||
|
||||
from .access import has_course_access
|
||||
from .helpers import _xmodule_recurse
|
||||
from contentstore.utils import compute_publish_state, PublishState
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
@@ -224,11 +225,12 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v
|
||||
})
|
||||
elif view_name in ('student_view', 'container_preview'):
|
||||
is_container_view = (view_name == 'container_preview')
|
||||
component_publish_state = compute_publish_state(component)
|
||||
is_read_only_view = component_publish_state == PublishState.public
|
||||
|
||||
# Only show the new style HTML for the container view, i.e. for non-verticals
|
||||
# Note: this special case logic can be removed once the unit page is replaced
|
||||
# with the new container view.
|
||||
is_read_only_view = is_container_view
|
||||
context = {
|
||||
'runtime_type': 'studio',
|
||||
'container_view': is_container_view,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
@@ -170,7 +169,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
"""
|
||||
# Only add the Studio wrapper when on the container page. The unit page will remain as is for now.
|
||||
if context.get('container_view', None) and view == 'student_view':
|
||||
locator = loc_mapper().translate_location(xblock.course_id, xblock.location)
|
||||
locator = loc_mapper().translate_location(xblock.course_id, xblock.location, published=False)
|
||||
template_context = {
|
||||
'xblock_context': context,
|
||||
'xblock': xblock,
|
||||
|
||||
@@ -3,7 +3,9 @@ Unit tests for the container view.
|
||||
"""
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import compute_publish_state, PublishState
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
|
||||
@@ -26,13 +28,19 @@ class ContainerViewTestCase(CourseTestCase):
|
||||
category="video", display_name="My Video")
|
||||
|
||||
def test_container_html(self):
|
||||
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
|
||||
self._test_html_content(
|
||||
self.child_vertical,
|
||||
expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical"/>',
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden" '
|
||||
'data-locator="{branch_name}/Child_Vertical">'.format(branch_name=branch_name)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit"\s*'
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'),
|
||||
r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'
|
||||
).format(branch_name=branch_name)
|
||||
)
|
||||
|
||||
def test_container_on_container_html(self):
|
||||
@@ -40,27 +48,54 @@ class ContainerViewTestCase(CourseTestCase):
|
||||
Create the scenario of an xblock with children (non-vertical) on the container page.
|
||||
This should create a container page that is a child of another container page.
|
||||
"""
|
||||
xblock_with_child = ItemFactory.create(parent_location=self.child_vertical.location,
|
||||
category="wrapper", display_name="Wrapper")
|
||||
ItemFactory.create(parent_location=xblock_with_child.location,
|
||||
category="html", display_name="Child HTML")
|
||||
published_xblock_with_child = ItemFactory.create(
|
||||
parent_location=self.child_vertical.location,
|
||||
category="wrapper", display_name="Wrapper"
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent_location=published_xblock_with_child.location,
|
||||
category="html", display_name="Child HTML"
|
||||
)
|
||||
draft_xblock_with_child = modulestore('draft').convert_to_draft(published_xblock_with_child.location)
|
||||
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
|
||||
self._test_html_content(
|
||||
xblock_with_child,
|
||||
expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/draft/block/Wrapper"/>',
|
||||
published_xblock_with_child,
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden" '
|
||||
'data-locator="{branch_name}/Wrapper">'.format(branch_name=branch_name)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit"\s*'
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical"\s*'
|
||||
r'<a href="/container/{branch_name}/Child_Vertical"\s*'
|
||||
r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'),
|
||||
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
|
||||
).format(branch_name=branch_name)
|
||||
)
|
||||
self._test_html_content(
|
||||
draft_xblock_with_child,
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden" '
|
||||
'data-locator="{branch_name}/Wrapper">'.format(branch_name=branch_name)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="/container/{branch_name}/Child_Vertical"\s*'
|
||||
r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
|
||||
).format(branch_name=branch_name)
|
||||
)
|
||||
|
||||
def _test_html_content(self, xblock, expected_section_tag, expected_breadcrumbs):
|
||||
def _test_html_content(self, xblock, branch_name, expected_section_tag, expected_breadcrumbs):
|
||||
"""
|
||||
Get the HTML for a container page and verify the section tag is correct
|
||||
and the breadcrumbs trail is correct.
|
||||
"""
|
||||
url = xblock_studio_url(xblock, self.course)
|
||||
publish_state = compute_publish_state(xblock)
|
||||
resp = self.client.get_html(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
html = resp.content
|
||||
@@ -68,5 +103,12 @@ class ContainerViewTestCase(CourseTestCase):
|
||||
# Verify the navigation link at the top of the page is correct.
|
||||
self.assertRegexpMatches(html, expected_breadcrumbs)
|
||||
# Verify the link that allows users to change publish status.
|
||||
expected_unit_link = 'This content is published with unit <a href="/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit">Unit</a>.'
|
||||
expected_message = None
|
||||
if publish_state == PublishState.public:
|
||||
expected_message = 'you need to edit unit <a href="/unit/{branch_name}/Unit">Unit</a> as a draft.'
|
||||
else:
|
||||
expected_message = 'your changes will be published with unit <a href="/unit/{branch_name}/Unit">Unit</a>.'
|
||||
expected_unit_link = expected_message.format(
|
||||
branch_name=branch_name
|
||||
)
|
||||
self.assertIn(expected_unit_link, html)
|
||||
|
||||
@@ -218,8 +218,14 @@ define([
|
||||
|
||||
"js/spec/views/unit_spec",
|
||||
"js/spec/views/xblock_spec",
|
||||
"js/spec/views/xblock_editor_spec",
|
||||
|
||||
# these tests are run separate in the cms-squire suite, due to process
|
||||
"js/spec/views/pages/container_spec",
|
||||
|
||||
"js/spec/views/modals/base_modal_spec",
|
||||
"js/spec/views/modals/edit_xblock_spec",
|
||||
|
||||
# these tests are run separately in the cms-squire suite, due to process
|
||||
# isolation issues with Squire.js
|
||||
# "coffee/spec/views/assets_spec"
|
||||
])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["jquery", "backbone", "coffee/src/main", "js/spec/create_sinon", "jasmine-stealth", "jquery.cookie"],
|
||||
require ["jquery", "backbone", "coffee/src/main", "js/spec_helpers/create_sinon", "jasmine-stealth", "jquery.cookie"],
|
||||
($, Backbone, main, create_sinon) ->
|
||||
describe "CMS", ->
|
||||
it "should initialize URL", ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["js/models/section", "js/spec/create_sinon", "js/utils/module"], (Section, create_sinon, ModuleUtils) ->
|
||||
define ["js/models/section", "js/spec_helpers/create_sinon", "js/utils/module"], (Section, create_sinon, ModuleUtils) ->
|
||||
describe "Section", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
define ["jasmine", "js/spec_helpers/create_sinon", "squire"],
|
||||
(jasmine, create_sinon, Squire) ->
|
||||
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "js/spec/create_sinon"],
|
||||
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "js/spec_helpers/create_sinon"],
|
||||
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, create_sinon) ->
|
||||
|
||||
describe "Course Updates and Handouts", ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmodule"], ($, ModuleEdit, ModuleModel) ->
|
||||
define ["jquery", "js/spec_helpers/edit_helpers", "coffee/src/views/module_edit", "js/models/module_info", "xmodule"], ($, edit_helpers, ModuleEdit, ModuleModel) ->
|
||||
|
||||
describe "ModuleEdit", ->
|
||||
beforeEach ->
|
||||
@@ -6,7 +6,8 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
id: "stub-id"
|
||||
|
||||
setFixtures """
|
||||
<li class="component" id="stub-id">
|
||||
<ul>
|
||||
<li class="component" id="stub-id" data-locator="stub-id">
|
||||
<div class="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
@@ -23,7 +24,10 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
<div id="stub-module-content"/>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="edit-xblock-modal"/>
|
||||
"""
|
||||
edit_helpers.installEditTemplates(true);
|
||||
spyOn($, 'ajax').andReturn(@moduleData)
|
||||
|
||||
@moduleEdit = new ModuleEdit(
|
||||
@@ -55,11 +59,13 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
describe "render", ->
|
||||
beforeEach ->
|
||||
spyOn(@moduleEdit, 'loadDisplay')
|
||||
spyOn(@moduleEdit, 'loadEdit')
|
||||
spyOn(@moduleEdit, 'delegateEvents')
|
||||
spyOn($.fn, 'append')
|
||||
spyOn($, 'getScript').andReturn($.Deferred().resolve().promise())
|
||||
|
||||
window.MockXBlock = (runtime, element) ->
|
||||
return { }
|
||||
|
||||
window.loadedXBlockResources = undefined
|
||||
|
||||
@moduleEdit.render()
|
||||
@@ -75,6 +81,9 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
]
|
||||
)
|
||||
|
||||
afterEach ->
|
||||
window.MockXBlock = null
|
||||
|
||||
it "loads the module preview via ajax on the view element", ->
|
||||
expect($.ajax).toHaveBeenCalledWith(
|
||||
url: "/xblock/#{@moduleEdit.model.id}/student_view"
|
||||
@@ -92,10 +101,10 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
success: jasmine.any(Function)
|
||||
)
|
||||
expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
|
||||
expect(@moduleEdit.loadEdit).not.toHaveBeenCalled()
|
||||
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
|
||||
|
||||
it "loads the editing view via ajax on demand", ->
|
||||
edit_helpers.installEditTemplates(true);
|
||||
expect($.ajax).not.toHaveBeenCalledWith(
|
||||
url: "/xblock/#{@moduleEdit.model.id}/studio_view"
|
||||
type: "GET"
|
||||
@@ -103,12 +112,13 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
Accept: 'application/json'
|
||||
success: jasmine.any(Function)
|
||||
)
|
||||
expect(@moduleEdit.loadEdit).not.toHaveBeenCalled()
|
||||
|
||||
@moduleEdit.clickEditButton({'preventDefault': jasmine.createSpy('event.preventDefault')})
|
||||
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore')
|
||||
|
||||
$.ajax.mostRecentCall.args[0].success(
|
||||
html: '<div>Response html</div>'
|
||||
html: mockXBlockEditorHtml
|
||||
resources: [
|
||||
['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}],
|
||||
['hash2', {kind: 'url', mimetype: 'text/css', data: 'css-url'}],
|
||||
@@ -126,7 +136,6 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
|
||||
Accept: 'application/json'
|
||||
success: jasmine.any(Function)
|
||||
)
|
||||
expect(@moduleEdit.loadEdit).toHaveBeenCalled()
|
||||
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
|
||||
|
||||
it "loads inline css from fragments", ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_sinon", "js/base", "date", "jquery.timepicker"],
|
||||
define ["js/views/overview", "js/views/feedback_notification", "js/spec_helpers/create_sinon", "js/base", "date", "jquery.timepicker"],
|
||||
(Overview, Notification, create_sinon) ->
|
||||
|
||||
describe "Course Overview", ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["js/models/section", "js/views/section_show", "js/views/section_edit", "js/spec/create_sinon"], (Section, SectionShow, SectionEdit, create_sinon) ->
|
||||
define ["js/models/section", "js/views/section_show", "js/views/section_edit", "js/spec_helpers/create_sinon"], (Section, SectionShow, SectionEdit, create_sinon) ->
|
||||
|
||||
describe "SectionShow", ->
|
||||
describe "Basic", ->
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course",
|
||||
"js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks",
|
||||
"js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification",
|
||||
"js/spec/create_sinon", "jasmine-stealth"],
|
||||
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, create_sinon) ->
|
||||
"js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers", "jasmine-stealth"],
|
||||
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, create_sinon, modal_helpers) ->
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
beforeEach ->
|
||||
@@ -272,8 +272,8 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
|
||||
tpl = readFixtures("edit-chapter.underscore")
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-chapter-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
modal_helpers.installModalTemplates()
|
||||
appendSetFixtures($("<script>", {id: "edit-chapter-tpl", type: "text/template"}).text(tpl))
|
||||
@model = new Chapter
|
||||
name: "Chapter 1"
|
||||
asset_path: "/ch1.pdf"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/create_sinon"], (FileUpload, UploadDialog, Chapter, create_sinon) ->
|
||||
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers"], (FileUpload, UploadDialog, Chapter, create_sinon, modal_helpers) ->
|
||||
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
@@ -6,8 +6,8 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
tpl = readFixtures("upload-dialog.underscore")
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
modal_helpers.installModalTemplates()
|
||||
appendSetFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
|
||||
@model = new FileUpload(
|
||||
@@ -37,11 +37,10 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
if (@view && modal_helpers.isShowingModal(@view))
|
||||
@view.hide()
|
||||
|
||||
describe "Basic", ->
|
||||
it "should be shown by default", ->
|
||||
expect(@view.options.shown).toBeTruthy()
|
||||
|
||||
it "should render without a file selected", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
@@ -65,18 +64,6 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
expect(@view.$el).toContain("#upload_error")
|
||||
expect(@view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "adds body class on show()", ->
|
||||
@view.show()
|
||||
expect(@view.options.shown).toBeTruthy()
|
||||
# can't test: this blows up the spec runner
|
||||
# expect($("body")).toHaveClass("dialog-is-shown")
|
||||
|
||||
it "removes body class on hide()", ->
|
||||
@view.hide()
|
||||
expect(@view.options.shown).toBeFalsy()
|
||||
# can't test: this blows up the spec runner
|
||||
# expect($("body")).not.toHaveClass("dialog-is-shown")
|
||||
|
||||
describe "Uploads", ->
|
||||
beforeEach ->
|
||||
@clock = sinon.useFakeTimers()
|
||||
@@ -87,6 +74,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
it "can upload correctly", ->
|
||||
requests = create_sinon["requests"](this)
|
||||
|
||||
@view.render()
|
||||
@view.upload()
|
||||
expect(@model.get("uploading")).toBeTruthy()
|
||||
expect(requests.length).toEqual(1)
|
||||
@@ -103,6 +91,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
it "can handle upload errors", ->
|
||||
requests = create_sinon["requests"](this)
|
||||
|
||||
@view.render()
|
||||
@view.upload()
|
||||
requests[0].respond(500)
|
||||
expect(@model.get("title")).toMatch(/error/)
|
||||
@@ -111,9 +100,10 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
it "removes itself after two seconds on successful upload", ->
|
||||
requests = create_sinon["requests"](this)
|
||||
|
||||
@view.render()
|
||||
@view.upload()
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"response": "dummy_response"}')
|
||||
expect(@view.remove).not.toHaveBeenCalled()
|
||||
expect(modal_helpers.isShowingModal(@view)).toBeTruthy();
|
||||
@clock.tick(2001)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
expect(modal_helpers.isShowingModal(@view)).toBeFalsy();
|
||||
|
||||
@@ -1,67 +1,22 @@
|
||||
define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
|
||||
"js/views/xblock", "js/views/feedback_notification", "js/views/metadata", "js/collections/metadata"
|
||||
"js/utils/modal", "jquery.inputnumber"],
|
||||
($, _, gettext, XBlock, XBlockView, NotificationView, MetadataView, MetadataCollection, ModalUtils) ->
|
||||
"js/views/xblock", "js/views/modals/edit_xblock"],
|
||||
($, _, gettext, XBlock, XBlockView, EditXBlockModal) ->
|
||||
class ModuleEdit extends XBlockView
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
"click .mode a": 'clickModeButton'
|
||||
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@render()
|
||||
|
||||
$componentEditor: => @$el.find('.component-editor')
|
||||
$moduleEditor: => @$componentEditor().find('.module-editor')
|
||||
|
||||
loadDisplay: ->
|
||||
XBlock.initializeBlock(@$el.find('.xblock-student_view'))
|
||||
|
||||
loadEdit: ->
|
||||
@module = XBlock.initializeBlock(@$el.find('.xblock-studio_view'))
|
||||
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
|
||||
metadataEditor = @$el.find('.metadata_edit')
|
||||
metadataData = metadataEditor.data('metadata')
|
||||
models = [];
|
||||
for key of metadataData
|
||||
models.push(metadataData[key])
|
||||
@metadataEditor = new MetadataView.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new MetadataCollection(models)
|
||||
})
|
||||
|
||||
@module.setMetadataEditor(@metadataEditor) if @module.setMetadataEditor
|
||||
|
||||
# Need to update set "active" class on data editor if there is one.
|
||||
# If we are only showing settings, hide the data editor controls and update settings accordingly.
|
||||
if @hasDataEditor()
|
||||
@selectMode(@editorMode)
|
||||
else
|
||||
@hideDataEditor()
|
||||
|
||||
title = interpolate(gettext('<em>Editing:</em> %s'),
|
||||
[@metadataEditor.getDisplayName()])
|
||||
@$el.find('.component-name').html(title)
|
||||
|
||||
customMetadata: ->
|
||||
# Hack to support metadata fields that aren't part of the metadata editor (ie, LaTeX high level source).
|
||||
# Walk through the set of elements which have the 'data-metadata_name' attribute and
|
||||
# build up an object to pass back to the server on the subsequent POST.
|
||||
# Note that these values will always be sent back on POST, even if they did not actually change.
|
||||
_metadata = {}
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', @$componentEditor())
|
||||
return _metadata
|
||||
|
||||
changedMetadata: ->
|
||||
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
|
||||
|
||||
createItem: (parent, payload, callback=->) ->
|
||||
payload.parent_locator = parent
|
||||
$.postJSON(
|
||||
@@ -89,72 +44,10 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
|
||||
@delegateEvents()
|
||||
)
|
||||
|
||||
clickSaveButton: (event) =>
|
||||
event.preventDefault()
|
||||
data = @module.save()
|
||||
|
||||
analytics.track "Saved Module",
|
||||
course: course_location_analytics
|
||||
id: _this.model.id
|
||||
|
||||
data.metadata = _.extend(data.metadata || {}, @changedMetadata())
|
||||
ModalUtils.hideModalCover()
|
||||
saving = new NotificationView.Mini
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
@model.save(data).done( =>
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
saving.hide()
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.removeClass('editing')
|
||||
@$componentEditor().slideUp(150)
|
||||
ModalUtils.hideModalCover()
|
||||
|
||||
clickEditButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.addClass('editing')
|
||||
ModalUtils.showModalCover(true)
|
||||
@loadView('studio_view', @$moduleEditor(), =>
|
||||
@$componentEditor().slideDown(150)
|
||||
@loadEdit()
|
||||
@delegateEvents()
|
||||
)
|
||||
|
||||
clickModeButton: (event) ->
|
||||
event.preventDefault()
|
||||
if not @hasDataEditor()
|
||||
return
|
||||
@selectMode(event.currentTarget.parentElement.id)
|
||||
|
||||
hasDataEditor: =>
|
||||
return @$el.find('.wrapper-comp-editor').length > 0
|
||||
|
||||
selectMode: (mode) =>
|
||||
dataEditor = @$el.find('.wrapper-comp-editor')
|
||||
settingsEditor = @$el.find('.wrapper-comp-settings')
|
||||
editorModeButton = @$el.find('#editor-mode').find("a")
|
||||
settingsModeButton = @$el.find('#settings-mode').find("a")
|
||||
|
||||
if mode == @editorMode
|
||||
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
|
||||
# we have to use a class of is-inactive instead of is-active.
|
||||
dataEditor.removeClass('is-inactive')
|
||||
editorModeButton.addClass('is-set')
|
||||
settingsEditor.removeClass('is-active')
|
||||
settingsModeButton.removeClass('is-set')
|
||||
else
|
||||
dataEditor.addClass('is-inactive')
|
||||
editorModeButton.removeClass('is-set')
|
||||
settingsEditor.addClass('is-active')
|
||||
settingsModeButton.addClass('is-set')
|
||||
|
||||
hideDataEditor: =>
|
||||
editorModeButtonParent = @$el.find('#editor-mode')
|
||||
editorModeButtonParent.addClass('inactive-mode')
|
||||
editorModeButtonParent.removeClass('active-mode')
|
||||
@$el.find('.wrapper-comp-settings').addClass('is-active')
|
||||
@$el.find('#settings-mode').find("a").addClass('is-set')
|
||||
modal = new EditXBlockModal({
|
||||
el: $('.edit-xblock-modal'),
|
||||
view: 'student_view'
|
||||
});
|
||||
modal.edit(this.$el, self.model, { refresh: _.bind(@render, this) })
|
||||
|
||||
@@ -9,8 +9,10 @@ define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) {
|
||||
"category": null,
|
||||
"is_draft": null,
|
||||
"is_container": null,
|
||||
"data": null,
|
||||
"metadata" : null,
|
||||
"children": []
|
||||
}
|
||||
});
|
||||
return XBlockInfo;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec/create_sinon", "jquery"],
|
||||
define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_helpers/create_sinon", "jquery"],
|
||||
function (ContentDragger, Notification, create_sinon, $) {
|
||||
describe("Overview drag and drop functionality", function () {
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore', 'js/spec/create_sinon', 'squire'
|
||||
'jquery', 'underscore', 'js/spec_helpers/create_sinon', 'squire'
|
||||
],
|
||||
function ($, _, create_sinon, Squire) {
|
||||
'use strict';
|
||||
|
||||
89
cms/static/js/spec/views/modals/base_modal_spec.js
Normal file
89
cms/static/js/spec/views/modals/base_modal_spec.js
Normal file
@@ -0,0 +1,89 @@
|
||||
define(["jquery", "underscore", "js/views/modals/base_modal", "js/spec_helpers/modal_helpers"],
|
||||
function ($, _, BaseModal, modal_helpers) {
|
||||
|
||||
describe("BaseModal", function() {
|
||||
var MockModal, modal, showMockModal;
|
||||
|
||||
MockModal = BaseModal.extend({
|
||||
getContentHtml: function() {
|
||||
return readFixtures('mock/mock-modal.underscore');
|
||||
}
|
||||
});
|
||||
|
||||
showMockModal = function() {
|
||||
modal = new MockModal({
|
||||
title: "Mock Modal"
|
||||
});
|
||||
modal.show();
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
modal_helpers.installModalTemplates();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
modal_helpers.hideModalIfShowing(modal);
|
||||
});
|
||||
|
||||
describe("Single Modal", function() {
|
||||
it('is visible after show is called', function () {
|
||||
showMockModal();
|
||||
expect(modal_helpers.isShowingModal(modal)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is removed after hide is called', function () {
|
||||
showMockModal();
|
||||
modal.hide();
|
||||
expect(modal_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is removed after cancel is clicked', function () {
|
||||
showMockModal();
|
||||
modal_helpers.cancelModal(modal);
|
||||
expect(modal_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Nested Modal", function() {
|
||||
var nestedModal, showNestedModal;
|
||||
|
||||
showNestedModal = function() {
|
||||
showMockModal();
|
||||
nestedModal = new MockModal({
|
||||
title: "Nested Modal",
|
||||
parent: modal
|
||||
});
|
||||
nestedModal.show();
|
||||
};
|
||||
|
||||
afterEach(function() {
|
||||
if (nestedModal && modal_helpers.isShowingModal(nestedModal)) {
|
||||
nestedModal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
it('is visible after show is called', function () {
|
||||
showNestedModal();
|
||||
expect(modal_helpers.isShowingModal(nestedModal)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is removed after hide is called', function () {
|
||||
showNestedModal();
|
||||
nestedModal.hide();
|
||||
expect(modal_helpers.isShowingModal(nestedModal)).toBeFalsy();
|
||||
|
||||
// Verify that the parent modal is still showing
|
||||
expect(modal_helpers.isShowingModal(modal)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is removed after cancel is clicked', function () {
|
||||
showNestedModal();
|
||||
modal_helpers.cancelModal(nestedModal);
|
||||
expect(modal_helpers.isShowingModal(nestedModal)).toBeFalsy();
|
||||
|
||||
// Verify that the parent modal is still showing
|
||||
expect(modal_helpers.isShowingModal(modal)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
166
cms/static/js/spec/views/modals/edit_xblock_spec.js
Normal file
166
cms/static/js/spec/views/modals/edit_xblock_spec.js
Normal file
@@ -0,0 +1,166 @@
|
||||
define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/modals/edit_xblock", "js/models/xblock_info"],
|
||||
function ($, _, create_sinon, edit_helpers, EditXBlockModal, XBlockInfo) {
|
||||
|
||||
describe("EditXBlockModal", function() {
|
||||
var model, modal, showModal;
|
||||
|
||||
showModal = function(requests, mockHtml) {
|
||||
var xblockElement = $('.xblock');
|
||||
return edit_helpers.showEditModal(requests, xblockElement, model, mockHtml);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installEditTemplates();
|
||||
appendSetFixtures('<div class="xblock" data-locator="mock-xblock" data-display-name="Mock XBlock"></div>');
|
||||
model = new XBlockInfo({
|
||||
id: 'testCourse/branch/draft/block/verticalFFF',
|
||||
display_name: 'Test Unit',
|
||||
category: 'vertical'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
describe("XBlock Editor", function() {
|
||||
var mockXBlockEditorHtml;
|
||||
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
});
|
||||
|
||||
it('can show itself', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXBlockEditorHtml);
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeTruthy();
|
||||
edit_helpers.cancelModal(modal);
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows the correct title', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXBlockEditorHtml);
|
||||
expect(modal.$('.modal-window-title').text()).toBe('Editing: Component');
|
||||
});
|
||||
|
||||
it('does not show any editor mode buttons', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXBlockEditorHtml);
|
||||
expect(modal.$('.editor-modes a').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("XModule Editor", function() {
|
||||
var mockXModuleEditorHtml;
|
||||
|
||||
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
// Mock the VerticalDescriptor so that the module can be rendered
|
||||
window.VerticalDescriptor = XModule.Descriptor;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
window.VerticalDescriptor = null;
|
||||
});
|
||||
|
||||
it('can render itself', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeTruthy();
|
||||
edit_helpers.cancelModal(modal);
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows the correct title', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(modal.$('.modal-window-title').text()).toBe('Editing: Component');
|
||||
});
|
||||
|
||||
it('shows the correct default buttons', function() {
|
||||
var requests = create_sinon.requests(this),
|
||||
editorButton,
|
||||
settingsButton;
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(modal.$('.editor-modes a').length).toBe(2);
|
||||
editorButton = modal.$('.editor-button');
|
||||
settingsButton = modal.$('.settings-button');
|
||||
expect(editorButton.length).toBe(1);
|
||||
expect(editorButton).toHaveClass('is-set');
|
||||
expect(settingsButton.length).toBe(1);
|
||||
expect(settingsButton).not.toHaveClass('is-set');
|
||||
});
|
||||
|
||||
it('can switch tabs', function() {
|
||||
var requests = create_sinon.requests(this),
|
||||
editorButton,
|
||||
settingsButton;
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(modal.$('.editor-modes a').length).toBe(2);
|
||||
editorButton = modal.$('.editor-button');
|
||||
settingsButton = modal.$('.settings-button');
|
||||
expect(modal.$('.metadata_edit')).toHaveClass('is-inactive');
|
||||
settingsButton.click();
|
||||
expect(modal.$('.metadata_edit')).toHaveClass('is-active');
|
||||
editorButton.click();
|
||||
expect(modal.$('.metadata_edit')).toHaveClass('is-inactive');
|
||||
});
|
||||
|
||||
describe("Custom Tabs", function() {
|
||||
var mockCustomTabsHtml;
|
||||
|
||||
mockCustomTabsHtml = readFixtures('mock/mock-xmodule-editor-with-custom-tabs.underscore');
|
||||
|
||||
it('hides the modal\'s header', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockCustomTabsHtml);
|
||||
expect(modal.$('.modal-header')).toBeHidden();
|
||||
});
|
||||
|
||||
it('shows the correct title', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockCustomTabsHtml);
|
||||
expect(modal.$('.component-name').text()).toBe('Editing: Component');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("XModule Editor (settings only)", function() {
|
||||
var mockXModuleEditorHtml;
|
||||
|
||||
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
// Mock the VerticalDescriptor so that the module can be rendered
|
||||
window.VerticalDescriptor = XModule.Descriptor;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
window.VerticalDescriptor = null;
|
||||
});
|
||||
|
||||
it('can render itself', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeTruthy();
|
||||
edit_helpers.cancelModal(modal);
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does not show any mode buttons', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(modal.$('.editor-modes li').length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
157
cms/static/js/spec/views/pages/container_spec.js
Normal file
157
cms/static/js/spec/views/pages/container_spec.js
Normal file
@@ -0,0 +1,157 @@
|
||||
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/pages/container", "js/models/xblock_info"],
|
||||
function ($, create_sinon, edit_helpers, ContainerPage, XBlockInfo) {
|
||||
|
||||
describe("ContainerPage", function() {
|
||||
var model, containerPage, respondWithMockXBlockEditorFragment, mockContainerPage;
|
||||
|
||||
mockContainerPage = readFixtures('mock/mock-container-page.underscore');
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installEditTemplates();
|
||||
appendSetFixtures(mockContainerPage);
|
||||
|
||||
model = new XBlockInfo({
|
||||
id: 'testCourse/branch/draft/block/verticalFFF',
|
||||
display_name: 'Test Unit',
|
||||
category: 'vertical'
|
||||
});
|
||||
containerPage = new ContainerPage({
|
||||
model: model,
|
||||
el: $('#content')
|
||||
});
|
||||
});
|
||||
|
||||
respondWithMockXBlockEditorFragment = function(requests, response) {
|
||||
var requestIndex = requests.length - 1;
|
||||
create_sinon.respondWithJson(requests, response, requestIndex);
|
||||
};
|
||||
|
||||
describe("Basic display", function() {
|
||||
var mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore');
|
||||
|
||||
it('can render itself', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
containerPage.render();
|
||||
respondWithMockXBlockEditorFragment(requests, {
|
||||
html: mockContainerXBlockHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
expect(containerPage.$el.select('.xblock-header')).toBeTruthy();
|
||||
expect(containerPage.$('.wrapper-xblock')).not.toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.no-container-content')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('shows a loading indicator', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
containerPage.render();
|
||||
expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden');
|
||||
respondWithMockXBlockEditorFragment(requests, {
|
||||
html: mockContainerXBlockHtml,
|
||||
resources: []
|
||||
});
|
||||
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Editing an xblock", function() {
|
||||
var mockContainerXBlockHtml,
|
||||
mockXBlockEditorHtml,
|
||||
newDisplayName = 'New Display Name';
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock({
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: newDisplayName
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore');
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
it('can show an edit modal for a child xblock', function() {
|
||||
var requests = create_sinon.requests(this),
|
||||
editButtons;
|
||||
containerPage.render();
|
||||
respondWithMockXBlockEditorFragment(requests, {
|
||||
html: mockContainerXBlockHtml,
|
||||
resources: []
|
||||
});
|
||||
editButtons = containerPage.$('.edit-button');
|
||||
// The container renders six mock xblocks, so there should be an equal number of edit buttons
|
||||
expect(editButtons.length).toBe(6);
|
||||
editButtons.first().click();
|
||||
// Make sure that the correct xblock is requested to be edited
|
||||
expect(requests[requests.length - 1].url).toBe('/xblock/locator-component-A1/studio_view');
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXBlockEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
expect(edit_helpers.isShowingModal()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can save changes to settings', function() {
|
||||
var requests, editButtons, modal, mockUpdatedXBlockHtml;
|
||||
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
|
||||
requests = create_sinon.requests(this);
|
||||
containerPage.render();
|
||||
respondWithMockXBlockEditorFragment(requests, {
|
||||
html: mockContainerXBlockHtml,
|
||||
resources: []
|
||||
});
|
||||
editButtons = containerPage.$('.edit-button');
|
||||
// The container renders six mock xblocks, so there should be an equal number of edit buttons
|
||||
expect(editButtons.length).toBe(6);
|
||||
editButtons.first().click();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXBlockEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
modal = $('.edit-xblock-modal');
|
||||
// Click on the settings tab
|
||||
modal.find('.settings-button').click();
|
||||
// Change the display name's text
|
||||
modal.find('.setting-input').text("Mock Update");
|
||||
// Press the save button
|
||||
modal.find('.action-save').click();
|
||||
// Respond to the save
|
||||
create_sinon.respondWithJson(requests, {
|
||||
id: model.id
|
||||
});
|
||||
// Respond to the request to refresh
|
||||
respondWithMockXBlockEditorFragment(requests, {
|
||||
html: mockUpdatedXBlockHtml,
|
||||
resources: []
|
||||
});
|
||||
// Verify that the xblock was updated
|
||||
expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Empty container", function() {
|
||||
var mockContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore');
|
||||
|
||||
it('shows the "no children" message', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
containerPage.render();
|
||||
respondWithMockXBlockEditorFragment(requests, {
|
||||
html: mockContainerXBlockHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
expect(containerPage.$('.no-container-content')).not.toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.wrapper-xblock')).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
define([ "jquery", "js/spec/create_sinon", "URI",
|
||||
define([ "jquery", "js/spec_helpers/create_sinon", "URI",
|
||||
"js/views/paging", "js/views/paging_header", "js/views/paging_footer",
|
||||
"js/models/asset", "js/collections/asset" ],
|
||||
function ($, create_sinon, URI, PagingView, PagingHeader, PagingFooter, AssetModel, AssetCollection) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon", "js/views/feedback_notification",
|
||||
define(["coffee/src/views/unit", "js/models/module_info", "js/spec_helpers/create_sinon", "js/views/feedback_notification",
|
||||
"jasmine-stealth"],
|
||||
function (UnitEditView, ModuleModel, create_sinon, NotificationView) {
|
||||
var verifyJSON = function (requests, json) {
|
||||
|
||||
124
cms/static/js/spec/views/xblock_editor_spec.js
Normal file
124
cms/static/js/spec/views/xblock_editor_spec.js
Normal file
@@ -0,0 +1,124 @@
|
||||
define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/xblock_editor", "js/models/xblock_info"],
|
||||
function ($, _, create_sinon, edit_helpers, XBlockEditorView, XBlockInfo) {
|
||||
|
||||
describe("XBlockEditorView", function() {
|
||||
var model, editor, testDisplayName, mockSaveResponse;
|
||||
|
||||
testDisplayName = 'Test Display Name';
|
||||
mockSaveResponse = {
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: testDisplayName
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installEditTemplates();
|
||||
model = new XBlockInfo({
|
||||
id: 'testCourse/branch/draft/block/verticalFFF',
|
||||
display_name: 'Test Unit',
|
||||
category: 'vertical'
|
||||
});
|
||||
editor = new XBlockEditorView({
|
||||
model: model
|
||||
});
|
||||
});
|
||||
|
||||
describe("Editing an xblock", function() {
|
||||
var mockXBlockEditorHtml;
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock(mockSaveResponse);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
});
|
||||
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
it('can render itself', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
editor.render();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXBlockEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
expect(editor.$el.select('.xblock-header')).toBeTruthy();
|
||||
expect(editor.getMode()).toEqual('settings');
|
||||
});
|
||||
|
||||
it('saves any custom metadata', function() {
|
||||
var requests = create_sinon.requests(this), request, response;
|
||||
editor.render();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXBlockEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
editor.save();
|
||||
request = requests[requests.length - 1];
|
||||
response = JSON.parse(request.requestBody);
|
||||
expect(response.metadata.display_name).toBe(testDisplayName);
|
||||
expect(response.metadata.custom_field).toBe('Custom Value');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Editing an xmodule", function() {
|
||||
var mockXModuleEditorHtml;
|
||||
|
||||
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
edit_helpers.installMockXModule(mockSaveResponse);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
edit_helpers.uninstallMockXModule();
|
||||
});
|
||||
|
||||
it('can render itself', function() {
|
||||
var requests = create_sinon.requests(this);
|
||||
editor.render();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXModuleEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
expect(editor.$el.select('.xblock-header')).toBeTruthy();
|
||||
expect(editor.getMode()).toEqual('editor');
|
||||
});
|
||||
|
||||
it('saves any custom metadata', function() {
|
||||
var requests = create_sinon.requests(this), request, response;
|
||||
editor.render();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXModuleEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
// Give the mock xblock a save method...
|
||||
editor.xblock.save = window.MockDescriptor.save;
|
||||
editor.save();
|
||||
request = requests[requests.length - 1];
|
||||
response = JSON.parse(request.requestBody);
|
||||
expect(response.metadata.display_name).toBe(testDisplayName);
|
||||
expect(response.metadata.custom_field).toBe('Custom Value');
|
||||
});
|
||||
|
||||
it('can render a module with only settings', function() {
|
||||
var requests = create_sinon.requests(this), mockXModuleEditorHtml;
|
||||
mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore');
|
||||
|
||||
editor.render();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXModuleEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
expect(editor.$el.select('.xblock-header')).toBeTruthy();
|
||||
expect(editor.getMode()).toEqual('settings');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/xblock_info",
|
||||
define([ "jquery", "js/spec_helpers/create_sinon", "URI", "js/views/xblock", "js/models/xblock_info",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function ($, create_sinon, URI, XBlockView, XBlockInfo) {
|
||||
|
||||
@@ -7,7 +7,7 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/
|
||||
|
||||
beforeEach(function () {
|
||||
model = new XBlockInfo({
|
||||
id: 'testCourse/branch/published/block/verticalFFF',
|
||||
id: 'testCourse/branch/draft/block/verticalFFF',
|
||||
display_name: 'Test Unit',
|
||||
category: 'vertical'
|
||||
});
|
||||
@@ -28,7 +28,7 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/
|
||||
xblockView.render();
|
||||
respondWithMockXBlockFragment(requests, {
|
||||
html: mockXBlockHtml,
|
||||
"resources": []
|
||||
resources: []
|
||||
});
|
||||
|
||||
expect(xblockView.$el.select('.xblock-header')).toBeTruthy();
|
||||
|
||||
79
cms/static/js/spec_helpers/edit_helpers.js
Normal file
79
cms/static/js/spec_helpers/edit_helpers.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Provides helper methods for invoking Studio editors in Jasmine tests.
|
||||
*/
|
||||
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers", "js/views/modals/edit_xblock",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function($, create_sinon, modal_helpers, EditXBlockModal) {
|
||||
|
||||
var editorTemplate = readFixtures('metadata-editor.underscore'),
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore'),
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore'),
|
||||
editXBlockModalTemplate = readFixtures('edit-xblock-modal.underscore'),
|
||||
editorModeButtonTemplate = readFixtures('editor-mode-button.underscore'),
|
||||
installMockXBlock,
|
||||
uninstallMockXBlock,
|
||||
hasSavedMockXBlock,
|
||||
installMockXModule,
|
||||
uninstallMockXModule,
|
||||
hasSavedMockXModule,
|
||||
installEditTemplates,
|
||||
showEditModal;
|
||||
|
||||
installMockXBlock = function(mockResult) {
|
||||
window.MockXBlock = function(runtime, element) {
|
||||
return {
|
||||
save: function() {
|
||||
return mockResult;
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
uninstallMockXBlock = function() {
|
||||
window.MockXBlock = null;
|
||||
};
|
||||
|
||||
installMockXModule = function(mockResult) {
|
||||
window.MockDescriptor = _.extend(XModule.Descriptor, {
|
||||
save: function() {
|
||||
return mockResult;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
uninstallMockXModule = function() {
|
||||
window.MockDescriptor = null;
|
||||
};
|
||||
|
||||
installEditTemplates = function(append) {
|
||||
modal_helpers.installModalTemplates(append);
|
||||
|
||||
// Add templates needed by the edit XBlock modal
|
||||
appendSetFixtures($("<script>", { id: "edit-xblock-modal-tpl", type: "text/template" }).text(editXBlockModalTemplate));
|
||||
appendSetFixtures($("<script>", { id: "editor-mode-button-tpl", type: "text/template" }).text(editorModeButtonTemplate));
|
||||
|
||||
// Add templates needed by the settings editor
|
||||
appendSetFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate));
|
||||
};
|
||||
|
||||
showEditModal = function(requests, xblockElement, model, mockHtml) {
|
||||
var modal = new EditXBlockModal({});
|
||||
modal.edit(xblockElement, model);
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockHtml,
|
||||
"resources": []
|
||||
});
|
||||
return modal;
|
||||
};
|
||||
|
||||
return $.extend(modal_helpers, {
|
||||
'installMockXBlock': installMockXBlock,
|
||||
'uninstallMockXBlock': uninstallMockXBlock,
|
||||
'installMockXModule': installMockXModule,
|
||||
'uninstallMockXModule': uninstallMockXModule,
|
||||
'installEditTemplates': installEditTemplates,
|
||||
'showEditModal': showEditModal
|
||||
});
|
||||
});
|
||||
68
cms/static/js/spec_helpers/modal_helpers.js
Normal file
68
cms/static/js/spec_helpers/modal_helpers.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
|
||||
*/
|
||||
define(["jquery"],
|
||||
function($) {
|
||||
var basicModalTemplate = readFixtures('basic-modal.underscore'),
|
||||
modalButtonTemplate = readFixtures('modal-button.underscore'),
|
||||
feedbackTemplate = readFixtures('system-feedback.underscore'),
|
||||
installModalTemplates,
|
||||
getModalElement,
|
||||
isShowingModal,
|
||||
hideModalIfShowing,
|
||||
cancelModal,
|
||||
cancelModalIfShowing;
|
||||
|
||||
installModalTemplates = function(append) {
|
||||
if (append) {
|
||||
appendSetFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTemplate));
|
||||
} else {
|
||||
setFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTemplate));
|
||||
}
|
||||
appendSetFixtures($("<script>", { id: "basic-modal-tpl", type: "text/template" }).text(basicModalTemplate));
|
||||
appendSetFixtures($("<script>", { id: "modal-button-tpl", type: "text/template" }).text(modalButtonTemplate));
|
||||
};
|
||||
|
||||
getModalElement = function(modal) {
|
||||
var modalElement;
|
||||
if (modal) {
|
||||
modalElement = modal.$('.wrapper-modal-window');
|
||||
} else {
|
||||
modalElement = $('.wrapper-modal-window');
|
||||
}
|
||||
return modalElement;
|
||||
};
|
||||
|
||||
isShowingModal = function(modal) {
|
||||
var modalElement = getModalElement(modal);
|
||||
return modalElement.length > 0;
|
||||
};
|
||||
|
||||
hideModalIfShowing = function(modal) {
|
||||
if (isShowingModal(modal)) {
|
||||
modal.hide();
|
||||
}
|
||||
};
|
||||
|
||||
cancelModal = function(modal) {
|
||||
var modalElement, cancelButton;
|
||||
modalElement = getModalElement(modal);
|
||||
cancelButton = modalElement.find('.action-cancel');
|
||||
expect(cancelButton.length).toBe(1);
|
||||
cancelButton.click();
|
||||
};
|
||||
|
||||
cancelModalIfShowing = function(modal) {
|
||||
if (isShowingModal(modal)) {
|
||||
cancelModal(modal);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
'installModalTemplates': installModalTemplates,
|
||||
'isShowingModal': isShowingModal,
|
||||
'hideModalIfShowing': hideModalIfShowing,
|
||||
'cancelModal': cancelModal,
|
||||
'cancelModalIfShowing': cancelModalIfShowing
|
||||
};
|
||||
});
|
||||
@@ -2,7 +2,7 @@ define(["js/views/baseview", "underscore", "gettext", "js/views/feedback_prompt"
|
||||
function(BaseView, _, gettext, PromptView, NotificationView) {
|
||||
var AssetView = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#asset-tpl").text());
|
||||
this.template = this.loadTemplate("asset");
|
||||
this.listenTo(this.model, "change:locked", this.updateLockState);
|
||||
},
|
||||
tagName: "tr",
|
||||
|
||||
@@ -11,7 +11,7 @@ define(["jquery", "underscore", "gettext", "js/views/paging", "js/views/asset",
|
||||
initialize : function() {
|
||||
PagingView.prototype.initialize.call(this);
|
||||
var collection = this.collection;
|
||||
this.template = _.template($("#asset-library-tpl").text());
|
||||
this.template = this.loadTemplate("asset-library");
|
||||
this.listenTo(collection, 'destroy', this.handleDestroy);
|
||||
this.registerSortableColumn('js-asset-name-col', gettext('Name'), 'display_name', 'asc');
|
||||
this.registerSortableColumn('js-asset-date-col', gettext('Date Added'), 'date_added', 'desc');
|
||||
|
||||
@@ -19,9 +19,9 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
|
||||
constructor: function(options) {
|
||||
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
|
||||
var _this = this;
|
||||
this.render = _.wrap(this.render, function (render) {
|
||||
this.render = _.wrap(this.render, function (render, options) {
|
||||
_this.beforeRender();
|
||||
render();
|
||||
render(options);
|
||||
_this.afterRender();
|
||||
return _this;
|
||||
});
|
||||
@@ -43,9 +43,13 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
|
||||
|
||||
toggleExpandCollapse: function(event) {
|
||||
var target = $(event.target);
|
||||
// Don't propagate the event as it is possible that two views will both contain
|
||||
// this element, e.g. clicking on the element of a child view container in a parent.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
target.closest('.expand-collapse').toggleClass('expand').toggleClass('collapse');
|
||||
target.closest('.is-collapsible, .window').toggleClass('collapsed');
|
||||
target.closest('.is-collapsible').children('article').slideToggle();
|
||||
},
|
||||
|
||||
showLoadingIndicator: function() {
|
||||
@@ -54,6 +58,20 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
|
||||
|
||||
hideLoadingIndicator: function() {
|
||||
$('.ui-loading').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the named template from the page, or logs an error if it fails.
|
||||
* @param name The name of the template.
|
||||
* @returns The loaded template.
|
||||
*/
|
||||
loadTemplate: function(name) {
|
||||
var templateSelector = "#" + name + "-tpl",
|
||||
templateText = $(templateSelector).text();
|
||||
if (!templateText) {
|
||||
console.error("Failed to load " + name + " template");
|
||||
}
|
||||
return _.template(templateText);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ define(["js/views/baseview", "underscore", "jquery"], function(BaseView, _, $) {
|
||||
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
this.template = _.template($("#checklist-tpl").text());
|
||||
this.template = this.loadTemplate('checklist');
|
||||
this.collection.fetch({
|
||||
reset: true,
|
||||
complete: function() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
define(["js/views/baseview", "underscore", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"],
|
||||
function(BaseView, _, CodeMirror, NotificationView, CourseInfoHelper, ModalUtils) {
|
||||
define(["js/views/baseview", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"],
|
||||
function(BaseView, CodeMirror, NotificationView, CourseInfoHelper, ModalUtils) {
|
||||
|
||||
// the handouts view is dumb right now; it needs tied to a model and all that jazz
|
||||
var CourseInfoHandoutsView = BaseView.extend({
|
||||
@@ -11,7 +11,7 @@ define(["js/views/baseview", "underscore", "codemirror", "js/views/feedback_noti
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = _.template($("#course_info_handouts-tpl").text());
|
||||
this.template = this.loadTemplate('course_info_handouts');
|
||||
var self = this;
|
||||
this.model.fetch({
|
||||
complete: function() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(["js/views/baseview", "underscore", "codemirror", "js/models/course_update",
|
||||
define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
"js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"],
|
||||
function(BaseView, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper, ModalUtils) {
|
||||
function(BaseView, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper, ModalUtils) {
|
||||
|
||||
var CourseInfoUpdateView = BaseView.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
@@ -13,7 +13,7 @@ define(["js/views/baseview", "underscore", "codemirror", "js/models/course_updat
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = _.template($("#course_info_update-tpl").text());
|
||||
this.template = this.loadTemplate('course_info_update');
|
||||
this.render();
|
||||
// when the client refetches the updates as a whole, re-render them
|
||||
this.listenTo(this.collection, 'reset', this.render);
|
||||
|
||||
@@ -3,7 +3,7 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gette
|
||||
_.str = str; // used in template
|
||||
var EditChapter = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#edit-chapter-tpl").text());
|
||||
this.template = this.loadTemplate('edit-chapter');
|
||||
this.listenTo(this.model, "change", this.render);
|
||||
},
|
||||
tagName: "li",
|
||||
@@ -54,7 +54,7 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gette
|
||||
var msg = new FileUploadModel({
|
||||
title: _.template(gettext("Upload a new PDF to “<%= name %>”"),
|
||||
{name: course.escape('name')}),
|
||||
message: "Files must be in PDF format.",
|
||||
message: gettext("Please select a PDF file to upload."),
|
||||
mimeTypes: ['application/pdf']
|
||||
});
|
||||
var that = this;
|
||||
@@ -69,7 +69,7 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gette
|
||||
that.model.set(options);
|
||||
}
|
||||
});
|
||||
$(".wrapper-view").after(view.show().el);
|
||||
view.show();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ define(["js/views/baseview", "underscore", "jquery", "js/views/edit_chapter", "j
|
||||
function(BaseView, _, $, EditChapterView, NotificationView) {
|
||||
var EditTextbook = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#edit-textbook-tpl").text());
|
||||
this.template = this.loadTemplate('edit-textbook');
|
||||
this.listenTo(this.model, "invalid", this.render);
|
||||
var chapters = this.model.get('chapters');
|
||||
this.listenTo(chapters, "add", this.addOne);
|
||||
|
||||
@@ -48,11 +48,7 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery"], funct
|
||||
throw "SystemFeedback: intent required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
var tpl = $("#system-feedback-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load system-feedback template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.template = this.loadTemplate("system-feedback");
|
||||
this.setElement($("#page-"+this.options.type));
|
||||
// handle single "secondary" action
|
||||
if (this.options.actions && this.options.actions.secondary &&
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
define(["js/views/baseview", "underscore", "jquery", "js/views/edit_textbook", "js/views/show_textbook"],
|
||||
function(BaseView, _, $, EditTextbookView, ShowTextbookView) {
|
||||
define(["js/views/baseview", "jquery", "js/views/edit_textbook", "js/views/show_textbook"],
|
||||
function(BaseView, $, EditTextbookView, ShowTextbookView) {
|
||||
var ListTextbooks = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.emptyTemplate = _.template($("#no-textbooks-tpl").text());
|
||||
this.emptyTemplate = this.loadTemplate('no-textbooks');
|
||||
this.listenTo(this.collection, 'all', this.render);
|
||||
this.listenTo(this.collection, 'destroy', this.handleDestroy);
|
||||
},
|
||||
|
||||
@@ -11,11 +11,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList, VideoTranslation
|
||||
|
||||
// Model is CMS.Models.MetadataCollection,
|
||||
initialize : function() {
|
||||
var tpl = $("#metadata-editor-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load metadata editor template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.template = this.loadTemplate('metadata-editor');
|
||||
|
||||
this.$el.html(this.template({numEntries: this.collection.length}));
|
||||
var counter = 0;
|
||||
|
||||
127
cms/static/js/views/modals/base_modal.js
Normal file
127
cms/static/js/views/modals/base_modal.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* This is a base modal implementation that provides common utilities.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/baseview"],
|
||||
function($, _, gettext, BaseView) {
|
||||
var BaseModal = BaseView.extend({
|
||||
events : {
|
||||
'click .action-cancel': 'cancel'
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseView.prototype.options, {
|
||||
type: 'prompt',
|
||||
closeIcon: false,
|
||||
icon: false,
|
||||
modalName: 'basic',
|
||||
modalType: 'generic',
|
||||
modalSize: 'lg',
|
||||
title: ''
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
var parent = this.options.parent,
|
||||
parentElement = this.options.parentElement;
|
||||
this.modalTemplate = this.loadTemplate('basic-modal');
|
||||
this.buttonTemplate = this.loadTemplate('modal-button');
|
||||
if (parent) {
|
||||
parentElement = parent.$el;
|
||||
} else if (!parentElement) {
|
||||
parentElement = this.$el.closest('.modal-window');
|
||||
if (parentElement.length === 0) {
|
||||
parentElement = $('body');
|
||||
}
|
||||
}
|
||||
this.parentElement = parentElement;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.modalTemplate({
|
||||
name: this.options.modalName,
|
||||
type: this.options.modalType,
|
||||
size: this.options.modalSize,
|
||||
title: this.options.title
|
||||
}));
|
||||
this.addActionButtons();
|
||||
this.renderContents();
|
||||
this.parentElement.append(this.$el);
|
||||
},
|
||||
|
||||
renderContents: function() {
|
||||
var contentHtml = this.getContentHtml();
|
||||
this.$('.modal-content').html(contentHtml);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the content to be shown in the modal.
|
||||
*/
|
||||
getContentHtml: function() {
|
||||
return '';
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.render();
|
||||
this.resize();
|
||||
$(window).resize(_.bind(this.resize, this));
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
// Completely remove the modal from the DOM
|
||||
this.undelegateEvents();
|
||||
this.$el.html('');
|
||||
},
|
||||
|
||||
cancel: function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // Make sure parent modals don't see the click
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the action buttons to the modal.
|
||||
*/
|
||||
addActionButtons: function() {
|
||||
if (this.options.addSaveButton) {
|
||||
this.addActionButton('save', gettext('Save'), true);
|
||||
}
|
||||
this.addActionButton('cancel', gettext('Cancel'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new action button to the modal.
|
||||
* @param type The type of the action.
|
||||
* @param name The action's name.
|
||||
* @param isPrimary True if this button is the primary one.
|
||||
*/
|
||||
addActionButton: function(type, name, isPrimary) {
|
||||
var html = this.buttonTemplate({
|
||||
type: type,
|
||||
name: name,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
this.$('.modal-actions ul').append(html);
|
||||
},
|
||||
|
||||
resize: function() {
|
||||
var top, left, modalWindow, modalWidth, modalHeight,
|
||||
availableWidth, availableHeight, maxWidth, maxHeight;
|
||||
|
||||
modalWindow = this.$('.modal-window');
|
||||
availableWidth = $(window).width();
|
||||
availableHeight = $(window).height();
|
||||
maxWidth = availableWidth * 0.80;
|
||||
maxHeight = availableHeight * 0.80;
|
||||
modalWidth = Math.min(modalWindow.outerWidth(), maxWidth);
|
||||
modalHeight = Math.min(modalWindow.outerHeight(), maxHeight);
|
||||
|
||||
left = (availableWidth - modalWidth) / 2;
|
||||
top = (availableHeight - modalHeight) / 2;
|
||||
|
||||
modalWindow.css({
|
||||
top: top + $(window).scrollTop(),
|
||||
left: left + $(window).scrollLeft()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return BaseModal;
|
||||
});
|
||||
165
cms/static/js/views/modals/edit_xblock.js
Normal file
165
cms/static/js/views/modals/edit_xblock.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* The EditXBlockModal is a Backbone view that shows an xblock editor in a modal window.
|
||||
* It is invoked using the edit method which is passed an existing rendered xblock,
|
||||
* and upon save an optional refresh function can be invoked to update the display.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
|
||||
"js/models/xblock_info", "js/views/xblock_editor"],
|
||||
function($, _, gettext, BaseModal, XBlockInfo, XBlockEditorView) {
|
||||
var EditXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
"click .action-save": "save",
|
||||
"click .action-modes a": "changeMode"
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'edit-xblock',
|
||||
addSaveButton: true
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.events = _.extend({}, BaseModal.prototype.events, this.events);
|
||||
this.template = this.loadTemplate('edit-xblock-modal');
|
||||
this.editorModeButtonTemplate = this.loadTemplate('editor-mode-button');
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an edit modal for the specified xblock
|
||||
* @param xblockElement The element that contains the xblock to be edited.
|
||||
* @param rootXBlockInfo An XBlockInfo model that describes the root xblock on the page.
|
||||
* @param options A standard options object.
|
||||
*/
|
||||
edit: function(xblockElement, rootXBlockInfo, options) {
|
||||
this.xblockElement = xblockElement;
|
||||
this.xblockInfo = this.findXBlockInfo(xblockElement, rootXBlockInfo);
|
||||
this.options.modalType = this.xblockInfo.get('category');
|
||||
this.editOptions = options;
|
||||
this.render();
|
||||
this.show();
|
||||
// Display the xblock after the modal is shown as there are some xblocks
|
||||
// that depend upon being visible when they initialize, e.g. the problem xmodule.
|
||||
this.displayXBlock();
|
||||
},
|
||||
|
||||
getContentHtml: function() {
|
||||
return this.template({
|
||||
xblockInfo: this.xblockInfo
|
||||
});
|
||||
},
|
||||
|
||||
displayXBlock: function() {
|
||||
this.editorView = new XBlockEditorView({
|
||||
el: this.$('.xblock-editor'),
|
||||
model: this.xblockInfo
|
||||
});
|
||||
this.editorView.render({
|
||||
success: _.bind(this.onDisplayXBlock, this)
|
||||
});
|
||||
},
|
||||
|
||||
onDisplayXBlock: function() {
|
||||
var editorView = this.editorView,
|
||||
title = this.getTitle();
|
||||
if (editorView.hasCustomTabs()) {
|
||||
// Hide the modal's header as the custom editor provides its own
|
||||
this.$('.modal-header').hide();
|
||||
|
||||
// Update the custom editor's title
|
||||
editorView.$('.component-name').text(title);
|
||||
} else {
|
||||
this.$('.modal-window-title').text(title);
|
||||
if (editorView.getDataEditor() && editorView.getMetadataEditor()) {
|
||||
this.addDefaultModes();
|
||||
this.selectMode(editorView.mode);
|
||||
}
|
||||
}
|
||||
this.resize();
|
||||
},
|
||||
|
||||
getTitle: function() {
|
||||
var displayName = this.xblockElement.find('.xblock-header .header-details').text().trim();
|
||||
// If not found, try the old unit page style rendering
|
||||
if (!displayName) {
|
||||
displayName = this.xblockElement.find('.component-header').text().trim();
|
||||
if (!displayName) {
|
||||
displayName = gettext('Component');
|
||||
}
|
||||
}
|
||||
return interpolate(gettext("Editing: %(title)s"), { title: displayName }, true);
|
||||
},
|
||||
|
||||
addDefaultModes: function() {
|
||||
var defaultModes, i, mode;
|
||||
defaultModes = this.editorView.getDefaultModes();
|
||||
for (i = 0; i < defaultModes.length; i++) {
|
||||
mode = defaultModes[i];
|
||||
this.addModeButton(mode.id, mode.name);
|
||||
}
|
||||
},
|
||||
|
||||
changeMode: function(event) {
|
||||
var parent = $(event.target.parentElement),
|
||||
mode = parent.data('mode');
|
||||
event.preventDefault();
|
||||
this.selectMode(mode);
|
||||
},
|
||||
|
||||
selectMode: function(mode) {
|
||||
var editorView = this.editorView,
|
||||
buttonSelector;
|
||||
editorView.selectMode(mode);
|
||||
this.$('.editor-modes a').removeClass('is-set');
|
||||
if (mode) {
|
||||
buttonSelector = '.' + mode + '-button';
|
||||
this.$(buttonSelector).addClass('is-set');
|
||||
}
|
||||
},
|
||||
|
||||
save: function(event) {
|
||||
var self = this,
|
||||
xblockInfo = this.xblockInfo,
|
||||
refresh = self.editOptions.refresh;
|
||||
event.preventDefault();
|
||||
this.editorView.save({
|
||||
success: function() {
|
||||
self.hide();
|
||||
if (refresh) {
|
||||
refresh(xblockInfo);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
BaseModal.prototype.hide.call(this);
|
||||
|
||||
// Completely clear the contents of the modal
|
||||
this.undelegateEvents();
|
||||
this.$el.html("");
|
||||
},
|
||||
|
||||
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
|
||||
var xblockInfo = defaultXBlockInfo,
|
||||
xblockElement;
|
||||
if (xblockWrapperElement.length > 0) {
|
||||
xblockElement = xblockWrapperElement.find('.xblock');
|
||||
xblockInfo = new XBlockInfo({
|
||||
id: xblockWrapperElement.data('locator'),
|
||||
category: xblockElement.data('block-type')
|
||||
});
|
||||
}
|
||||
return xblockInfo;
|
||||
},
|
||||
|
||||
addModeButton: function(mode, displayName) {
|
||||
var buttonPanel = this.$('.editor-modes');
|
||||
buttonPanel.append(this.editorModeButtonTemplate({
|
||||
mode: mode,
|
||||
displayName: displayName
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return EditXBlockModal;
|
||||
});
|
||||
@@ -37,7 +37,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
|
||||
|
||||
|
||||
var closeModalNew = function () {
|
||||
$('body').removeClass('dialog-is-shown');
|
||||
$('body').removeClass('modal-window-is-shown');
|
||||
$('.edit-section-publish-settings').removeClass('is-shown');
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
|
||||
$modal.find('.save-button').hide();
|
||||
}
|
||||
$modal.find('.section-name').html('"' + $(this).closest('.courseware-section').find('.section-name-span').text() + '"');
|
||||
$('body').addClass('dialog-is-shown');
|
||||
$('body').addClass('modal-window-is-shown');
|
||||
$('.edit-section-publish-settings').addClass('is-shown');
|
||||
};
|
||||
|
||||
|
||||
95
cms/static/js/views/pages/container.js
Normal file
95
cms/static/js/views/pages/container.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* XBlockContainerView is used to display an xblock which has children, and allows the
|
||||
* user to interact with the children.
|
||||
*/
|
||||
define(["jquery", "underscore", "js/views/baseview", "js/views/xblock", "js/views/modals/edit_xblock"],
|
||||
function ($, _, BaseView, XBlockView, EditXBlockModal) {
|
||||
|
||||
var XBlockContainerView = BaseView.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
view: 'container_preview',
|
||||
|
||||
initialize: function() {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.noContentElement = this.$('.no-container-content');
|
||||
this.xblockView = new XBlockView({
|
||||
el: this.$('.wrapper-xblock'),
|
||||
model: this.model,
|
||||
view: this.view
|
||||
});
|
||||
},
|
||||
|
||||
render: function(options) {
|
||||
var self = this,
|
||||
noContentElement = this.noContentElement,
|
||||
xblockView = this.xblockView,
|
||||
loadingElement = this.$('.ui-loading');
|
||||
loadingElement.removeClass('is-hidden');
|
||||
|
||||
// Hide both blocks until we know which one to show
|
||||
noContentElement.addClass('is-hidden');
|
||||
xblockView.$el.addClass('is-hidden');
|
||||
|
||||
// Add actions to any top level buttons, e.g. "Edit" of the container itself
|
||||
self.addButtonActions(this.$el);
|
||||
|
||||
// Render the xblock
|
||||
xblockView.render({
|
||||
success: function(xblock) {
|
||||
if (xblockView.hasChildXBlocks()) {
|
||||
xblockView.$el.removeClass('is-hidden');
|
||||
self.addButtonActions(xblockView.$el);
|
||||
} else {
|
||||
noContentElement.removeClass('is-hidden');
|
||||
}
|
||||
loadingElement.addClass('is-hidden');
|
||||
self.delegateEvents();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
findXBlockElement: function(target) {
|
||||
return $(target).closest('[data-locator]');
|
||||
},
|
||||
|
||||
addButtonActions: function(element) {
|
||||
var self = this;
|
||||
element.find('.edit-button').click(function(event) {
|
||||
var modal,
|
||||
target = event.target,
|
||||
xblockElement = self.findXBlockElement(target);
|
||||
event.preventDefault();
|
||||
modal = new EditXBlockModal({ });
|
||||
modal.edit(xblockElement, self.model,
|
||||
{
|
||||
refresh: function(xblockInfo) {
|
||||
self.refreshXBlock(xblockInfo, xblockElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
refreshXBlock: function(xblockInfo, xblockElement) {
|
||||
var self = this,
|
||||
temporaryView;
|
||||
// There is only one Backbone view created on the container page, which is
|
||||
// for the container xblock itself. Any child xblocks rendered inside the
|
||||
// container do not get a Backbone view. Thus, create a temporary XBlock
|
||||
// around the child element so that it can be refreshed.
|
||||
temporaryView = new XBlockView({
|
||||
el: xblockElement,
|
||||
model: xblockInfo,
|
||||
view: this.view
|
||||
});
|
||||
temporaryView.render({
|
||||
success: function() {
|
||||
temporaryView.unbind(); // Remove the temporary view
|
||||
self.addButtonActions(xblockElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return XBlockContainerView;
|
||||
}); // end define();
|
||||
@@ -1,6 +1,6 @@
|
||||
define(["backbone", "underscore"], function(Backbone, _) {
|
||||
define(["underscore", "js/views/baseview"], function(_, BaseView) {
|
||||
|
||||
var PagingFooter = Backbone.View.extend({
|
||||
var PagingFooter = BaseView.extend({
|
||||
events : {
|
||||
"click .next-page-link": "nextPage",
|
||||
"click .previous-page-link": "previousPage",
|
||||
@@ -11,7 +11,7 @@ define(["backbone", "underscore"], function(Backbone, _) {
|
||||
var view = options.view,
|
||||
collection = view.collection;
|
||||
this.view = view;
|
||||
this.template = _.template($("#paging-footer-tpl").text());
|
||||
this.template = this.loadTemplate('paging-footer');
|
||||
collection.bind('add', _.bind(this.render, this));
|
||||
collection.bind('remove', _.bind(this.render, this));
|
||||
collection.bind('reset', _.bind(this.render, this));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
|
||||
define(["underscore", "gettext", "js/views/baseview"], function(_, gettext, BaseView) {
|
||||
|
||||
var PagingHeader = Backbone.View.extend({
|
||||
var PagingHeader = BaseView.extend({
|
||||
events : {
|
||||
"click .next-page-link": "nextPage",
|
||||
"click .previous-page-link": "previousPage"
|
||||
@@ -10,7 +10,7 @@ define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
|
||||
var view = options.view,
|
||||
collection = view.collection;
|
||||
this.view = view;
|
||||
this.template = _.template($("#paging-header-tpl").text());
|
||||
this.template = this.loadTemplate('paging-header');
|
||||
collection.bind('add', _.bind(this.render, this));
|
||||
collection.bind('remove', _.bind(this.render, this));
|
||||
collection.bind('reset', _.bind(this.render, this));
|
||||
@@ -42,16 +42,16 @@ define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
|
||||
fmts = gettext('Showing %(current_span)s%(start)s-%(end)s%(end_span)s out of %(total_span)s%(total)s total%(end_span)s, sorted by %(order_span)s%(sort_order)s%(end_span)s %(sort_direction)s');
|
||||
|
||||
return '<p>' + interpolate(fmts, {
|
||||
start: Math.min(start + 1, end),
|
||||
end: end,
|
||||
total: total,
|
||||
sort_order: sortName,
|
||||
sort_direction: sortDirectionName,
|
||||
current_span: '<span class="count-current-shown">',
|
||||
total_span: '<span class="count-total">',
|
||||
order_span: '<span class="sort-order">',
|
||||
end_span: '</span>'
|
||||
}, true) + "</p>";
|
||||
start: Math.min(start + 1, end),
|
||||
end: end,
|
||||
total: total,
|
||||
sort_order: sortName,
|
||||
sort_direction: sortDirectionName,
|
||||
current_span: '<span class="count-current-shown">',
|
||||
total_span: '<span class="count-total">',
|
||||
order_span: '<span class="sort-order">',
|
||||
end_span: '</span>'
|
||||
}, true) + "</p>";
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
|
||||
@@ -9,7 +9,7 @@ define(["js/views/baseview", "underscore", "js/views/feedback_prompt", "js/views
|
||||
return this;
|
||||
},
|
||||
initialize: function() {
|
||||
this.template = _.template($("#section-name-edit-tpl").text());
|
||||
this.template = this.loadTemplate('section-name-edit');
|
||||
this.listenTo(this.model, "invalid", this.showInvalidMessage);
|
||||
this.render();
|
||||
},
|
||||
|
||||
@@ -276,7 +276,7 @@ var DetailsView = ValidatingView.extend({
|
||||
$('#course-image').attr('src', self.model.get('course_image_asset_path'));
|
||||
}
|
||||
});
|
||||
$('.wrapper-view').after(modal.show().el);
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,109 +1,114 @@
|
||||
define(["js/views/baseview", "underscore", "jquery", "jquery.form"],
|
||||
function(BaseView, _, $) {
|
||||
var UploadDialog = BaseView.extend({
|
||||
options: {
|
||||
shown: true,
|
||||
successMessageTimeout: 2000 // 2 seconds
|
||||
},
|
||||
initialize: function() {
|
||||
this.template = _.template($("#upload-dialog-tpl").text());
|
||||
this.listenTo(this.model, "change", this.render);
|
||||
},
|
||||
render: function() {
|
||||
var isValid = this.model.isValid();
|
||||
var selectedFile = this.model.get('selectedFile');
|
||||
var oldInput = this.$("input[type=file]").get(0);
|
||||
this.$el.html(this.template({
|
||||
shown: this.options.shown,
|
||||
url: this.options.url || CMS.URL.UPLOAD_ASSET,
|
||||
title: this.model.escape('title'),
|
||||
message: this.model.escape('message'),
|
||||
selectedFile: selectedFile,
|
||||
uploading: this.model.get('uploading'),
|
||||
uploadedBytes: this.model.get('uploadedBytes'),
|
||||
totalBytes: this.model.get('totalBytes'),
|
||||
finished: this.model.get('finished'),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
// Ideally, we'd like to tell the browser to pre-populate the
|
||||
// <input type="file"> with the selectedFile if we have one -- but
|
||||
// browser security prohibits that. So instead, we'll swap out the
|
||||
// new input (that has no file selected) with the old input (that
|
||||
// already has the selectedFile selected). However, we only want to do
|
||||
// this if the selected file is valid: if it isn't, we want to render
|
||||
// a blank input to prompt the user to upload a different (valid) file.
|
||||
if (selectedFile && isValid) {
|
||||
$(oldInput).removeClass("error");
|
||||
this.$('input[type=file]').replaceWith(oldInput);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"change input[type=file]": "selectFile",
|
||||
"click .action-cancel": "hideAndRemove",
|
||||
"click .action-upload": "upload"
|
||||
},
|
||||
selectFile: function(e) {
|
||||
this.model.set({
|
||||
selectedFile: e.target.files[0] || null
|
||||
});
|
||||
},
|
||||
show: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.options.shown = true;
|
||||
$("body").addClass('dialog-is-shown');
|
||||
return this.render();
|
||||
},
|
||||
hide: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.options.shown = false;
|
||||
$("body").removeClass('dialog-is-shown');
|
||||
return this.render();
|
||||
},
|
||||
hideAndRemove: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
return this.hide().remove();
|
||||
},
|
||||
upload: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('uploading', true);
|
||||
this.$("form").ajaxSubmit({
|
||||
success: _.bind(this.success, this),
|
||||
error: _.bind(this.error, this),
|
||||
uploadProgress: _.bind(this.progress, this),
|
||||
data: {
|
||||
// don't show the generic error notification; we're in a modal,
|
||||
// and we're better off modifying it instead.
|
||||
notifyOnError: false
|
||||
define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "jquery.form"],
|
||||
function($, _, gettext, BaseModal) {
|
||||
var UploadDialog = BaseModal.extend({
|
||||
events: {
|
||||
"change input[type=file]": "selectFile",
|
||||
"click .action-upload": "upload"
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'assetupload',
|
||||
modalSize: 'med',
|
||||
successMessageTimeout: 2000 // 2 seconds
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.events = _.extend({}, BaseModal.prototype.events, this.events);
|
||||
this.template = this.loadTemplate("upload-dialog");
|
||||
this.listenTo(this.model, "change", this.renderContents);
|
||||
this.options.title = this.model.get('title');
|
||||
},
|
||||
|
||||
addActionButtons: function() {
|
||||
this.addActionButton('upload', gettext("Upload"), true);
|
||||
BaseModal.prototype.addActionButtons.call(this);
|
||||
},
|
||||
|
||||
renderContents: function() {
|
||||
var isValid = this.model.isValid(),
|
||||
selectedFile = this.model.get('selectedFile'),
|
||||
oldInput = this.$("input[type=file]").get(0);
|
||||
BaseModal.prototype.renderContents.call(this);
|
||||
// Ideally, we'd like to tell the browser to pre-populate the
|
||||
// <input type="file"> with the selectedFile if we have one -- but
|
||||
// browser security prohibits that. So instead, we'll swap out the
|
||||
// new input (that has no file selected) with the old input (that
|
||||
// already has the selectedFile selected). However, we only want to do
|
||||
// this if the selected file is valid: if it isn't, we want to render
|
||||
// a blank input to prompt the user to upload a different (valid) file.
|
||||
if (selectedFile && isValid) {
|
||||
$(oldInput).removeClass("error");
|
||||
this.$('input[type=file]').replaceWith(oldInput);
|
||||
this.$('.action-upload').removeClass('disabled');
|
||||
} else {
|
||||
this.$('.action-upload').addClass('disabled');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
getContentHtml: function() {
|
||||
return this.template({
|
||||
url: this.options.url || CMS.URL.UPLOAD_ASSET,
|
||||
message: this.model.escape('message'),
|
||||
selectedFile: this.model.get('selectedFile'),
|
||||
uploading: this.model.get('uploading'),
|
||||
uploadedBytes: this.model.get('uploadedBytes'),
|
||||
totalBytes: this.model.get('totalBytes'),
|
||||
finished: this.model.get('finished'),
|
||||
error: this.model.validationError
|
||||
});
|
||||
},
|
||||
|
||||
selectFile: function(e) {
|
||||
this.model.set({
|
||||
selectedFile: e.target.files[0] || null
|
||||
});
|
||||
},
|
||||
|
||||
upload: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('uploading', true);
|
||||
this.$("form").ajaxSubmit({
|
||||
success: _.bind(this.success, this),
|
||||
error: _.bind(this.error, this),
|
||||
uploadProgress: _.bind(this.progress, this),
|
||||
data: {
|
||||
// don't show the generic error notification; we're in a modal,
|
||||
// and we're better off modifying it instead.
|
||||
notifyOnError: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
progress: function(event, position, total) {
|
||||
this.model.set({
|
||||
"uploadedBytes": position,
|
||||
"totalBytes": total
|
||||
});
|
||||
},
|
||||
|
||||
success: function(response, statusText, xhr, form) {
|
||||
this.model.set({
|
||||
uploading: false,
|
||||
finished: true
|
||||
});
|
||||
if (this.options.onSuccess) {
|
||||
this.options.onSuccess(response, statusText, xhr, form);
|
||||
}
|
||||
var that = this;
|
||||
this.removalTimeout = setTimeout(function() {
|
||||
that.hide();
|
||||
}, this.options.successMessageTimeout);
|
||||
},
|
||||
|
||||
error: function() {
|
||||
this.model.set({
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"title": gettext("We're sorry, there was an error")
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
progress: function(event, position, total, percentComplete) {
|
||||
this.model.set({
|
||||
"uploadedBytes": position,
|
||||
"totalBytes": total
|
||||
});
|
||||
},
|
||||
success: function(response, statusText, xhr, form) {
|
||||
this.model.set({
|
||||
uploading: false,
|
||||
finished: true
|
||||
});
|
||||
if(this.options.onSuccess) {
|
||||
this.options.onSuccess(response, statusText, xhr, form);
|
||||
}
|
||||
var that = this;
|
||||
this.removalTimeout = setTimeout(function() {
|
||||
that.hide().remove();
|
||||
}, this.options.successMessageTimeout);
|
||||
},
|
||||
error: function() {
|
||||
this.model.set({
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"title": gettext("We're sorry, there was an error")
|
||||
});
|
||||
}
|
||||
});
|
||||
return UploadDialog;
|
||||
}); // end define()
|
||||
return UploadDialog;
|
||||
}); // end define()
|
||||
|
||||
@@ -20,7 +20,7 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
this.collection = new MetadataCollection(models);
|
||||
|
||||
// initialize MetadataView.Editor
|
||||
this.metadataEditor = new MetadataView.Editor({
|
||||
this.settingsView = new MetadataView.Editor({
|
||||
el: this.$el,
|
||||
collection: this.collection
|
||||
});
|
||||
@@ -72,7 +72,7 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
syncBasicTab: function (metadataCollection, metadataView) {
|
||||
var result = [],
|
||||
getField = Utils.getField,
|
||||
component_locator = this.$el.closest('.component').data('locator'),
|
||||
component_locator = this.$el.closest('[data-locator]').data('locator'),
|
||||
subs = getField(metadataCollection, 'sub'),
|
||||
values = {},
|
||||
videoUrl, metadata, modifiedValues;
|
||||
|
||||
@@ -31,7 +31,7 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
|
||||
initialize: function () {
|
||||
_.bindAll(this);
|
||||
|
||||
this.component_locator = this.$el.closest('.component').data('locator');
|
||||
this.component_locator = this.$el.closest('[data-locator]').data('locator');
|
||||
|
||||
this.fileUploader = new FileUploader({
|
||||
el: this.$el,
|
||||
|
||||
@@ -46,7 +46,7 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
|
||||
_.debounce(_.bind(this.inputHandler, this), this.inputDelay)
|
||||
);
|
||||
|
||||
this.component_locator = this.$el.closest('.component').data('locator');
|
||||
this.component_locator = this.$el.closest('[data-locator]').data('locator');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
@@ -55,7 +55,7 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
|
||||
.apply(this, arguments);
|
||||
|
||||
var self = this,
|
||||
component_locator = this.$el.closest('.component').data('locator'),
|
||||
component_locator = this.$el.closest('[data-locator]').data('locator'),
|
||||
videoList = this.getVideoObjectsList(),
|
||||
|
||||
showServerError = function (response) {
|
||||
|
||||
@@ -145,12 +145,13 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
target = $(event.currentTarget),
|
||||
lang = target.data('lang'),
|
||||
model = new FileUpload({
|
||||
title: gettext('Upload translation.'),
|
||||
title: gettext('Upload translation'),
|
||||
fileFormats: ['srt']
|
||||
}),
|
||||
view = new VideoUploadDialog({
|
||||
model: model,
|
||||
url: self.model.get('urlRoot') + '/' + lang,
|
||||
parentElement: target.closest('.xblock-editor'),
|
||||
onSuccess: function (response) {
|
||||
if (!response['filename']) { return; }
|
||||
|
||||
@@ -161,7 +162,7 @@ function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
}
|
||||
});
|
||||
|
||||
$('.wrapper-view').after(view.show().el);
|
||||
view.show();
|
||||
},
|
||||
|
||||
enableAdd: function() {
|
||||
|
||||
@@ -9,27 +9,49 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
|
||||
this.view = this.options.view;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
render: function(options) {
|
||||
var self = this,
|
||||
view = this.view;
|
||||
view = this.view,
|
||||
xblockInfo = this.model,
|
||||
xblockUrl = xblockInfo.url();
|
||||
return $.ajax({
|
||||
url: decodeURIComponent(this.model.url()) + "/" + view,
|
||||
url: decodeURIComponent(xblockUrl) + "/" + view,
|
||||
type: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
headers: { Accept: 'application/json' },
|
||||
success: function(fragment) {
|
||||
var wrapper = self.$el,
|
||||
xblock;
|
||||
self.renderXBlockFragment(fragment, wrapper).done(function() {
|
||||
xblock = self.$('.xblock').first();
|
||||
XBlock.initializeBlock(xblock);
|
||||
self.delegateEvents();
|
||||
});
|
||||
self.handleXBlockFragment(fragment, options);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleXBlockFragment: function(fragment, options) {
|
||||
var wrapper = this.$el,
|
||||
xblockElement,
|
||||
success = options ? options.success : null,
|
||||
xblock;
|
||||
this.renderXBlockFragment(fragment, wrapper);
|
||||
xblockElement = this.$('.xblock').first();
|
||||
xblock = XBlock.initializeBlock(xblockElement);
|
||||
this.xblock = xblock;
|
||||
this.xblockReady(xblock);
|
||||
if (success) {
|
||||
success(xblock);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is called upon successful rendering of an xblock.
|
||||
*/
|
||||
xblockReady: function(xblock) {
|
||||
// Do nothing
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the specified xblock has children.
|
||||
*/
|
||||
hasChildXBlocks: function() {
|
||||
return this.$('.wrapper-xblock').length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders an xblock fragment into the specified element. The fragment has two attributes:
|
||||
|
||||
168
cms/static/js/views/xblock_editor.js
Normal file
168
cms/static/js/views/xblock_editor.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* XBlockEditorView displays the authoring view of an xblock, and allows the user to switch between
|
||||
* the available modes.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/xblock",
|
||||
"js/views/metadata", "js/collections/metadata", "jquery.inputnumber"],
|
||||
function ($, _, gettext, NotificationView, XBlockView, MetadataView, MetadataCollection) {
|
||||
|
||||
var XBlockEditorView = XBlockView.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
options: $.extend({}, XBlockView.prototype.options, {
|
||||
view: 'studio_view'
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
XBlockView.prototype.initialize.call(this);
|
||||
this.view = this.options.view;
|
||||
},
|
||||
|
||||
xblockReady: function(xblock) {
|
||||
XBlockView.prototype.xblockReady.call(this, xblock);
|
||||
this.initializeEditors();
|
||||
},
|
||||
|
||||
initializeEditors: function() {
|
||||
var metadataEditor,
|
||||
defaultMode = 'editor';
|
||||
metadataEditor = this.createMetadataEditor();
|
||||
this.metadataEditor = metadataEditor;
|
||||
if (!this.hasCustomTabs()) {
|
||||
if (this.getDataEditor()) {
|
||||
defaultMode = 'editor';
|
||||
} else if (metadataEditor) {
|
||||
defaultMode = 'settings';
|
||||
}
|
||||
this.selectMode(defaultMode);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultModes: function() {
|
||||
return [
|
||||
{ id: 'editor', name: gettext("Editor")},
|
||||
{ id: 'settings', name: gettext("Settings")}
|
||||
];
|
||||
},
|
||||
|
||||
hasCustomTabs: function() {
|
||||
return this.$('.editor-with-tabs').length > 0;
|
||||
},
|
||||
|
||||
createMetadataEditor: function() {
|
||||
var metadataEditor,
|
||||
metadataData,
|
||||
models = [],
|
||||
key,
|
||||
xblock = this.xblock,
|
||||
metadataView = null;
|
||||
metadataEditor = this.$('.metadata_edit');
|
||||
if (metadataEditor.length === 1) {
|
||||
metadataData = metadataEditor.data('metadata');
|
||||
for (key in metadataData) {
|
||||
if (metadataData.hasOwnProperty(key)) {
|
||||
models.push(metadataData[key]);
|
||||
}
|
||||
}
|
||||
metadataView = new MetadataView.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new MetadataCollection(models)
|
||||
});
|
||||
if (xblock.setMetadataEditor) {
|
||||
xblock.setMetadataEditor(metadataView);
|
||||
}
|
||||
}
|
||||
return metadataView;
|
||||
},
|
||||
|
||||
getDataEditor: function() {
|
||||
var editor = this.$('.wrapper-comp-editor');
|
||||
return editor.length === 1 ? editor : null;
|
||||
},
|
||||
|
||||
getMetadataEditor: function() {
|
||||
return this.metadataEditor;
|
||||
},
|
||||
|
||||
save: function(options) {
|
||||
var xblockInfo = this.model,
|
||||
data,
|
||||
saving;
|
||||
data = this.getXBlockData();
|
||||
saving = new NotificationView.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
return xblockInfo.save(data).done(function() {
|
||||
var success = options.success;
|
||||
saving.hide();
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getXBlockData: function() {
|
||||
var xblock = this.xblock,
|
||||
metadataEditor = this.getMetadataEditor(),
|
||||
data;
|
||||
data = xblock.save();
|
||||
if (metadataEditor) {
|
||||
data.metadata = _.extend(data.metadata || {}, this.getChangedMetadata());
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the metadata that has changed in the editor. This is a combination of the metadata
|
||||
* modified in the "Settings" editor, as well as any custom metadata provided by the component.
|
||||
*/
|
||||
getChangedMetadata: function() {
|
||||
var metadataEditor = this.getMetadataEditor();
|
||||
return _.extend(metadataEditor.getModifiedMetadataValues(), this.getCustomMetadata());
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns custom metadata defined by a particular xmodule that aren't part of the metadata editor.
|
||||
* In particular, this is used for LaTeX high level source.
|
||||
*/
|
||||
getCustomMetadata: function() {
|
||||
// Walk through the set of elements which have the 'data-metadata_name' attribute and
|
||||
// build up an object to pass back to the server on the subsequent POST.
|
||||
// Note that these values will always be sent back on POST, even if they did not actually change.
|
||||
var metadata, metadataNameElements, i, element, metadataName;
|
||||
metadata = {};
|
||||
metadataNameElements = this.$('[data-metadata-name]');
|
||||
for (i = 0; i < metadataNameElements.length; i++) {
|
||||
element = metadataNameElements[i];
|
||||
metadataName = $(element).data("metadata-name");
|
||||
metadata[metadataName] = element.value;
|
||||
}
|
||||
return metadata;
|
||||
},
|
||||
|
||||
getMode: function() {
|
||||
return this.mode;
|
||||
},
|
||||
|
||||
selectMode: function(mode) {
|
||||
var showEditor = mode === 'editor',
|
||||
dataEditor = this.getDataEditor(),
|
||||
metadataEditor = this.getMetadataEditor();
|
||||
if (dataEditor) {
|
||||
this.setEditorActivation(dataEditor, showEditor);
|
||||
}
|
||||
if (metadataEditor) {
|
||||
this.setEditorActivation(metadataEditor.$el, !showEditor);
|
||||
}
|
||||
this.mode = mode;
|
||||
},
|
||||
|
||||
setEditorActivation: function(editor, isActive) {
|
||||
editor.removeClass('is-active').removeClass('is-inactive');
|
||||
editor.addClass(isActive ? 'is-active' : 'is-inactive');
|
||||
}
|
||||
});
|
||||
|
||||
return XBlockEditorView;
|
||||
}); // end define();
|
||||
@@ -754,10 +754,10 @@ hr.divide {
|
||||
.tooltip {
|
||||
@include font-size(12);
|
||||
@include transition(opacity $tmg-f3 ease-out 0s);
|
||||
@extend %ui-depth5;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
padding: 0 10px;
|
||||
border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// studio - elements - editing dialog
|
||||
// ========================
|
||||
|
||||
body.course.feature-edit-dialog {
|
||||
|
||||
// dialog
|
||||
.wrapper-dialog {
|
||||
@extend %ui-depth5;
|
||||
@include transition(all $tmg-f2 ease-in-out);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: $black-t2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -0.25em; /* Adjusts for spacing */
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: $baseline*23;
|
||||
box-shadow: 0px 0px 7px $shadow-d1;
|
||||
border-radius: ($baseline/5);
|
||||
background-color: $gray-l4;
|
||||
padding: 7px;
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
@extend %t-title5;
|
||||
margin-bottom: ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend %t-copy-sub2;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 0;
|
||||
|
||||
.form-content {
|
||||
box-shadow: 0 0 3px $shadow-d1;
|
||||
padding: ($baseline*1.5);
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
label {
|
||||
@include font-size(14);
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
@extend %t-copy-sub2;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: ($baseline*0.75) $baseline ($baseline/2) $baseline;
|
||||
|
||||
.action-item {
|
||||
@extend %t-action4;
|
||||
display: inline-block;
|
||||
margin-right: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(12); // needed due to bad button mixins for now
|
||||
border-color: $blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dialog set-up
|
||||
.wrapper-dialog {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.dialog {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// dialog showing/hiding
|
||||
&.dialog-is-shown {
|
||||
|
||||
.wrapper-dialog.is-shown {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
|
||||
.dialog {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,11 @@ textarea.text {
|
||||
// forms - additional UI
|
||||
form {
|
||||
|
||||
input[type=file] {
|
||||
@include font-size(14);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.note {
|
||||
@include box-sizing(border-box);
|
||||
|
||||
|
||||
317
cms/static/sass/elements/_modal-window.scss
Normal file
317
cms/static/sass/elements/_modal-window.scss
Normal file
@@ -0,0 +1,317 @@
|
||||
// studio - elements - modal-window
|
||||
// ========================
|
||||
|
||||
// start with the view/body
|
||||
[class*="view-"] {
|
||||
|
||||
// basic modal content
|
||||
.modal-window {
|
||||
@extend %ui-depth3;
|
||||
@include box-sizing(border-box);
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
box-shadow: 0px 0px 7px $shadow-d1;
|
||||
border-radius: ($baseline/5);
|
||||
background-color: $gray-l4;
|
||||
padding: 7px;
|
||||
text-align: left;
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
box-shadow: 0 0 3px $shadow-d1;
|
||||
background-color: $white;
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %t-title5;
|
||||
margin: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend %t-copy-sub1;
|
||||
margin: 0 0 $baseline 0;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
padding: 0;
|
||||
|
||||
&.error {
|
||||
border: 0;
|
||||
background-color: $white;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.success {
|
||||
border: 0;
|
||||
background-color: $white;
|
||||
color: $green-d1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-chin,
|
||||
.modal-actions {
|
||||
padding: ($baseline*0.75) 2% ($baseline/2) 2%;
|
||||
|
||||
.action-item {
|
||||
@extend %t-action3;
|
||||
display: inline-block;
|
||||
margin-right: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(14); // needed due to bad button mixins for now
|
||||
border-color: $blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// small modals - quick editors and dialogs
|
||||
.modal-sm {
|
||||
width: 30%;
|
||||
min-width: ($baseline*15);
|
||||
|
||||
.modal-content {
|
||||
padding: 5% 4%;
|
||||
}
|
||||
}
|
||||
|
||||
// medium modals - forms and interactives
|
||||
.modal-med {
|
||||
width: 40%;
|
||||
min-width: ($baseline*18);
|
||||
|
||||
.modal-content {
|
||||
padding: 4%;
|
||||
}
|
||||
}
|
||||
|
||||
// large modals - component editors and interactives
|
||||
.modal-lg {
|
||||
width: 75%;
|
||||
min-width: ($baseline*27.5);
|
||||
height: auto;
|
||||
|
||||
&.modal-editor {
|
||||
|
||||
.modal-header {
|
||||
margin: ($baseline/4) ($baseline/2);
|
||||
|
||||
.title {
|
||||
width: 47%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.editor-modes {
|
||||
width: 48%;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
|
||||
.action-item {
|
||||
display: inline-block;
|
||||
margin-left: ($baseline/2);
|
||||
|
||||
.editor-button,
|
||||
.settings-button {
|
||||
@extend %btn-secondary-gray;
|
||||
@extend %t-copy-sub1;
|
||||
border: 0;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
}
|
||||
|
||||
&.is-set {
|
||||
background-color: $gray-d1;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
height: 435px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// specific modal overrides
|
||||
|
||||
// upload modal
|
||||
.assetupload-modal {
|
||||
|
||||
.status-upload {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// component editor
|
||||
.modal-window {
|
||||
|
||||
.CodeMirror {
|
||||
height: 365px;
|
||||
}
|
||||
|
||||
.wrapper-comp-settings {
|
||||
|
||||
.list-input {
|
||||
|
||||
&.settings-list {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// special overrides for video module editor/hidden tab editors
|
||||
.modal-lg.modal-type-video {
|
||||
|
||||
.modal-content {
|
||||
box-shadow: none;
|
||||
height: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
// modal within a modal
|
||||
.wrapper-modal-window-assetupload {
|
||||
|
||||
.modal-window {
|
||||
top: 10% !important;
|
||||
left: 10% !important;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin: ($baseline/2) 2%;
|
||||
|
||||
.title {
|
||||
width: auto;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
}
|
||||
|
||||
.editor-modes {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 2%;
|
||||
|
||||
.message {
|
||||
margin: 0 ($baseline/4) ($baseline/4) ($baseline/4);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
padding: ($baseline/2) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_edit.xmodule_VideoDescriptor .editor-with-tabs {
|
||||
|
||||
.edit-header {
|
||||
border: 0;
|
||||
background-color: $gray-l4;
|
||||
padding: ($baseline/2);
|
||||
|
||||
.component-name {
|
||||
@extend %t-title5;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 48%;
|
||||
margin-left: ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
|
||||
em {
|
||||
color: inherit;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-tabs {
|
||||
display: inline-block;
|
||||
width: 48%;
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
padding: 0;
|
||||
text-align: right;
|
||||
|
||||
.inner_tab_wrap {
|
||||
padding: 0;
|
||||
|
||||
a.tab {
|
||||
@extend %btn-secondary-gray;
|
||||
@extend %t-copy-sub1;
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
|
||||
&.current {
|
||||
background-color: $gray-d1;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
height: ($baseline*24);
|
||||
overflow-y: scroll;
|
||||
border: 1px solid $gray-l2;
|
||||
|
||||
.component-tab {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-window-overlay {
|
||||
@extend %ui-depth3;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0.5;
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
// studio - elements - uploads
|
||||
// ========================
|
||||
|
||||
body.course.feature-upload {
|
||||
|
||||
// dialog
|
||||
.wrapper-dialog {
|
||||
@extend %ui-depth5;
|
||||
@include transition(all 0.05s ease-in-out);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: $black-t2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -0.25em; /* Adjusts for spacing */
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@include box-sizing(border-box);
|
||||
box-shadow: 0px 0px 7px $shadow-d1;
|
||||
border-radius: ($baseline/5);
|
||||
background-color: $gray-l4;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: $baseline*23;
|
||||
padding: 7px;
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
@extend %t-title5;
|
||||
margin-bottom: ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend %t-copy-sub2;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 0;
|
||||
|
||||
.form-content {
|
||||
box-shadow: 0 0 3px $shadow-d1;
|
||||
padding: ($baseline*1.5);
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@extend %t-copy-sub2;
|
||||
}
|
||||
|
||||
.status-upload {
|
||||
height: 30px;
|
||||
margin-top: $baseline;
|
||||
|
||||
.wrapper-progress {
|
||||
box-shadow: inset 0 0 3px $shadow-d1;
|
||||
display: block;
|
||||
border-radius: ($baseline*0.75);
|
||||
background-color: $gray-l5;
|
||||
padding: 1px 8px 2px 8px;
|
||||
height: 25px;
|
||||
|
||||
progress {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: ($baseline*0.75);
|
||||
background-color: $gray-l5;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: $pink;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: $pink;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.message-status {
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include font-size(14);
|
||||
display: none;
|
||||
border-bottom: 2px solid $yellow;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
background: $paleYellow;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $red-d2;
|
||||
background: $red-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
border-color: $green-d2;
|
||||
background: $green-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: ($baseline*0.75) $baseline ($baseline/2) $baseline;
|
||||
|
||||
|
||||
|
||||
.action-item {
|
||||
@extend %t-action4;
|
||||
display: inline-block;
|
||||
margin-right: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(12); // needed due to bad button mixins for now
|
||||
border-color: $blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// js enabled
|
||||
.js {
|
||||
|
||||
// dialog set-up
|
||||
.wrapper-dialog {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.dialog {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// dialog showing/hiding
|
||||
&.dialog-is-shown {
|
||||
|
||||
.wrapper-dialog {
|
||||
-webkit-filter: blur(2px) grayscale(25%);
|
||||
filter: blur(2px) grayscale(25%);
|
||||
}
|
||||
|
||||
.wrapper-dialog.is-shown {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
|
||||
.dialog {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// extends - UI archetypes - xblock rendering
|
||||
%wrap-xblock {
|
||||
margin: $baseline;
|
||||
margin: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
border-radius: ($baseline/5);
|
||||
background: $white;
|
||||
@@ -23,12 +23,12 @@
|
||||
border-radius: ($baseline/5) ($baseline/5) 0 0;
|
||||
min-height: ($baseline*2.5);
|
||||
background-color: $gray-l6;
|
||||
padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline);
|
||||
|
||||
.header-details {
|
||||
@extend %cont-truncated;
|
||||
@extend %ui-justify-left-flex;
|
||||
@include ui-flexbox();
|
||||
padding-left: flex-gutter();
|
||||
width: flex-grid(6,12);
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -40,11 +40,6 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: xblock render
|
||||
.xblock-render {
|
||||
@extend %anim-fadeIn;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
@@ -64,14 +59,20 @@
|
||||
.expand-collapse {
|
||||
@extend %expand-collapse;
|
||||
margin: 0 ($baseline/4);
|
||||
}
|
||||
height: ($baseline*1.25);
|
||||
width: $baseline;
|
||||
|
||||
&.collapsed .xblock-render {
|
||||
display: none;
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-view {
|
||||
|
||||
.action-button {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.action-button-text {
|
||||
padding-right: ($baseline/5);
|
||||
padding-left: 0;
|
||||
|
||||
@@ -37,5 +37,4 @@
|
||||
@import 'elements/system-help'; // help UI
|
||||
@import 'elements/modal'; // interstitial UI, dialogs, modal windows
|
||||
@import 'elements/vendor'; // overrides to vendor-provided styling
|
||||
@import 'elements/uploads';
|
||||
@import 'elements/edit_dialog';
|
||||
@import 'elements/modal-window';
|
||||
|
||||
@@ -14,6 +14,14 @@ body.view-container {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.wrapper-mast .mast.has-navigation .nav-actions {
|
||||
bottom: -($baseline*.75);
|
||||
|
||||
.nav-item .button {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
@@ -22,6 +30,25 @@ body.view-container {
|
||||
.content-primary {
|
||||
margin-right: flex-gutter();
|
||||
width: flex-grid(9,12);
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
|
||||
.no-container-content {
|
||||
@extend %ui-well;
|
||||
padding: ($baseline*2);
|
||||
background-color: $gray-l4;
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
|
||||
.new-button {
|
||||
@include font-size(14);
|
||||
margin-left: $baseline;
|
||||
|
||||
[class^="icon-"] {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
@@ -35,10 +62,35 @@ body.view-container {
|
||||
margin-bottom: $baseline;
|
||||
border-top: 5px solid $blue;
|
||||
background-color: $white;
|
||||
padding: ($baseline*.75) ($baseline*.75) ($baseline) ($baseline*.75);
|
||||
|
||||
.pub-status {
|
||||
@extend %t-title6;
|
||||
display: block;
|
||||
background-color: $blue-l2;
|
||||
padding: ($baseline/2) ($baseline*.75);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.published {
|
||||
border-top: 5px solid $blue;
|
||||
|
||||
.pub-status {
|
||||
background-color: $blue-t0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.draft {
|
||||
border-top: 5px solid $gray-l1;
|
||||
|
||||
.pub-status {
|
||||
background-color: $gray-l4;
|
||||
}
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend %t-copy-sub1;
|
||||
padding: ($baseline*.75) ($baseline*.75) ($baseline) ($baseline*.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,13 +113,13 @@ body.view-container .content-primary {
|
||||
|
||||
// CASE: nesting level xblock rendering
|
||||
&.level-nesting {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
border: none;
|
||||
padding-bottom: $baseline;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
background-color: $gray-l7;
|
||||
background-color: $gray-l6;
|
||||
box-shadow: 0 0 1px $shadow-d2 inset;
|
||||
}
|
||||
|
||||
@@ -79,7 +131,7 @@ body.view-container .content-primary {
|
||||
}
|
||||
|
||||
.xblock-render {
|
||||
margin: 0 $baseline $baseline $baseline;
|
||||
margin: ($baseline/2);
|
||||
}
|
||||
|
||||
// STATE: nesting level xblock is collapsed
|
||||
@@ -92,10 +144,10 @@ body.view-container .content-primary {
|
||||
|
||||
// CASE: element level xblock rendering
|
||||
&.level-element {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
border-color: $blue;
|
||||
}
|
||||
|
||||
@@ -108,7 +160,8 @@ body.view-container .content-primary {
|
||||
}
|
||||
|
||||
.xblock-render {
|
||||
margin: $baseline;
|
||||
margin: ($baseline/2);
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
|
||||
// STATE: xBlock containers styled as rows.
|
||||
@@ -145,3 +198,4 @@ body.view-container .content-primary {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -508,7 +508,56 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
// modal to edit section publish settings
|
||||
// basic non-backbone modal-window set-up
|
||||
.wrapper-modal-window {
|
||||
@extend %ui-depth4;
|
||||
@include transition(all $tmg-f2 ease-in-out);
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
overflow: scroll;
|
||||
background: $black-t2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -0.25em; /* Adjusts for spacing */
|
||||
}
|
||||
|
||||
.modal-window {
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// modal-window showing/hiding
|
||||
&.modal-window-is-shown {
|
||||
overflow: hidden;
|
||||
|
||||
.wrapper-modal-window.is-shown {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
display: block;
|
||||
|
||||
.modal-window {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-section-publish-settings {
|
||||
|
||||
.picker {
|
||||
@@ -517,9 +566,10 @@
|
||||
|
||||
.field {
|
||||
float: left;
|
||||
margin-right: ($baseline/2);
|
||||
margin: 0 ($baseline/2) ($baseline/2);
|
||||
|
||||
label, input {
|
||||
label,
|
||||
input {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -527,6 +577,11 @@
|
||||
label {
|
||||
@extend %t-copy-sub1;
|
||||
margin-bottom: ($baseline/4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
height: ($baseline*1.5);
|
||||
border-radius: 3px;
|
||||
color: $gray-l1;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue;
|
||||
|
||||
@@ -633,13 +633,14 @@ body.course.unit,.view-unit {
|
||||
|
||||
//component-setting-entry
|
||||
.field.comp-setting-entry {
|
||||
background-color: $white;
|
||||
padding: $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.7;
|
||||
margin: 0 $baseline;
|
||||
border-top: 1px solid $gray-l4;
|
||||
background-color: $white;
|
||||
padding: $baseline ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
//margin-bottom: 0;
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
//no required component settings currently
|
||||
@@ -1085,6 +1086,7 @@ body.unit {
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
@@ -1311,17 +1313,18 @@ body.unit {
|
||||
// ====================
|
||||
|
||||
// Latex Compiler
|
||||
|
||||
.wrapper-comp-editor.latex-problem {
|
||||
margin-top: ($baseline*2.5);
|
||||
}
|
||||
|
||||
.launch-latex-compiler {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: $white;
|
||||
padding: $baseline/2 0 $baseline/2 $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.8;
|
||||
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
// hides latex compiler button if settings mode is-active
|
||||
@@ -1340,7 +1343,6 @@ body.unit .xblock-type-container {
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
@include transition(all $tmg-f2 linear 0s);
|
||||
border-color: $blue;
|
||||
|
||||
.container-drag {
|
||||
@@ -1350,7 +1352,6 @@ body.unit .xblock-type-container {
|
||||
}
|
||||
|
||||
.xblock-header {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
border-radius: ($baseline/5);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<ul class="component-actions">
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button">
|
||||
<i class="icon-edit"></i>
|
||||
<i class="icon-pencil"></i>
|
||||
<span class="action-button-text">${_("Edit")}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<%!
|
||||
import json
|
||||
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
from contentstore.utils import PublishState
|
||||
from contentstore.views.helpers import xblock_studio_url, EDITING_TEMPLATES
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<%block name="title">${_("Container")}</%block>
|
||||
@@ -12,27 +13,33 @@ from django.utils.translation import ugettext as _
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in EDITING_TEMPLATES:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<%
|
||||
xblock_info = {
|
||||
main_xblock_info = {
|
||||
'id': str(xblock_locator),
|
||||
'display-name': xblock.display_name_with_default,
|
||||
'display_name': xblock.display_name_with_default,
|
||||
'category': xblock.category,
|
||||
};
|
||||
%>
|
||||
<script type='text/javascript'>
|
||||
require(["domReady!", "jquery", "js/models/xblock_info", "js/views/xblock",
|
||||
"js/models/module_info", "coffee/src/views/unit",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1",
|
||||
"js/views/metadata", "js/collections/metadata"],
|
||||
function(doc, $, XBlockInfo, XBlockView) {
|
||||
var model,
|
||||
view;
|
||||
model = new XBlockInfo(${json.dumps(xblock_info) | n});
|
||||
view = new XBlockView({
|
||||
el: $('.wrapper-xblock.level-page').first(),
|
||||
model: model,
|
||||
view: 'container_preview'
|
||||
require(["domReady!", "jquery", "js/models/xblock_info", "js/views/pages/container",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function(doc, $, XBlockInfo, ContainerPage) {
|
||||
var view, mainXBlockInfo;
|
||||
|
||||
mainXBlockInfo = new XBlockInfo(${json.dumps(main_xblock_info) | n});
|
||||
|
||||
view = new ContainerPage({
|
||||
el: $('#content'),
|
||||
model: mainXBlockInfo
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
@@ -43,7 +50,7 @@ xblock_info = {
|
||||
<%block name="content">
|
||||
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
|
||||
<header class="mast has-actions has-navigation">
|
||||
<h1 class="page-header">
|
||||
<small class="navigation navigation-parents">
|
||||
@@ -63,9 +70,6 @@ xblock_info = {
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
<li class="sr nav-item">
|
||||
${_("No Actions")}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -76,22 +80,50 @@ xblock_info = {
|
||||
<section class="content-area">
|
||||
|
||||
<article class="content-primary window">
|
||||
<section class="wrapper-xblock level-page" data-locator="${xblock_locator}"/>
|
||||
<section class="wrapper-xblock level-page is-hidden" data-locator="${xblock_locator}">
|
||||
</section>
|
||||
<div class="no-container-content is-hidden">
|
||||
<p>${_("This page has no content yet.")}</p>
|
||||
</div>
|
||||
<div class="ui-loading">
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">${_("Loading...")}</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit-publishing">
|
||||
<h3 class="title-3">${_("Publishing Status")}</h3>
|
||||
<p class="copy">${_('This content is published with unit {unit_name}.').format(
|
||||
unit_name=u'<a href="{unit_address}">{unit_display_name}</a>'.format(
|
||||
unit_address=xblock_studio_url(unit),
|
||||
unit_display_name=unit.display_name_with_default,
|
||||
)
|
||||
)}</p>
|
||||
</div>
|
||||
% if unit:
|
||||
% if unit_publish_state == PublishState.public:
|
||||
<div class="bit-publishing published">
|
||||
<h3 class="title pub-status"><span class="sr">${_("Publishing Status")} </span>${_("Published")}</h3>
|
||||
<p class="copy">
|
||||
<%
|
||||
unit_link=u'<a href="{unit_address}">{unit_display_name}</a>'.format(
|
||||
unit_address=xblock_studio_url(unit),
|
||||
unit_display_name=unit.display_name_with_default,
|
||||
)
|
||||
%>
|
||||
${_('To make changes to the content of this page, you need to edit unit {unit_link} as a draft.'
|
||||
).format(unit_link=unit_link)}
|
||||
</p>
|
||||
</div>
|
||||
% else:
|
||||
<div class="bit-publishing draft">
|
||||
<h3 class="title pub-status"><span class="sr">${_("Publishing Status")} </span>${_("Draft")}</h3>
|
||||
<p class="copy">
|
||||
<%
|
||||
unit_link=u'<a href="{unit_address}">{unit_display_name}</a>'.format(
|
||||
unit_address=xblock_studio_url(unit),
|
||||
unit_display_name=unit.display_name_with_default,
|
||||
)
|
||||
%>
|
||||
${_('You can edit the content of this page, and your changes will be published with unit {unit_link}.').format(unit_link=unit_link)}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<ul class="list-details">
|
||||
<li class="item-detail">${_("You can view course components that contain other components on this page. In the case of experiment blocks, this allows you to confirm that you have properly configured your experiment groups.")}</li>
|
||||
<li class="item-detail">${_("You can view and edit course components that contain other components on this page. In the case of experiment blocks, this allows you to confirm that you have properly configured your experiment groups and make changes to existing content.")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -99,4 +131,6 @@ xblock_info = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-xblock-modal"/>
|
||||
|
||||
</%block>
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
<%block name="title">${_("Pages")}</%block>
|
||||
<%block name="bodyclass">is-signedin course view-static-pages</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type='text/javascript'>
|
||||
require(["js/models/explicit_url", "coffee/src/views/tabs",
|
||||
@@ -170,4 +178,6 @@
|
||||
<span class="label">${_("close modal")}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="edit-xblock-modal"/>
|
||||
</%block>
|
||||
|
||||
22
cms/templates/js/basic-modal.underscore
Normal file
22
cms/templates/js/basic-modal.underscore
Normal file
@@ -0,0 +1,22 @@
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-<%= name %>"
|
||||
aria-describedby="modal-window-description"
|
||||
aria-labelledby="modal-window-title"
|
||||
aria-hidden=""
|
||||
role="dialog">
|
||||
<div class="modal-window-overlay"></div>
|
||||
<div class="modal-window confirm modal-editor modal-<%= size %> modal-type-<%= type %>">
|
||||
<div class="<%= name %>-modal" action="#">
|
||||
<div class="modal-header">
|
||||
<h2 class="title modal-window-title"><%= title %></h2>
|
||||
<ul class="editor-modes action-list action-modes">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<h3 class="sr"><%= gettext("Actions") %></h3>
|
||||
<ul></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
1
cms/templates/js/edit-xblock-modal.underscore
Normal file
1
cms/templates/js/edit-xblock-modal.underscore
Normal file
@@ -0,0 +1 @@
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>"></div>
|
||||
3
cms/templates/js/editor-mode-button.underscore
Normal file
3
cms/templates/js/editor-mode-button.underscore
Normal file
@@ -0,0 +1,3 @@
|
||||
<li class="action-item" data-mode="<%= mode %>">
|
||||
<a href="#" class="<%= mode %>-button"><%= displayName %></a>
|
||||
</li>
|
||||
42
cms/templates/js/mock/mock-container-page.underscore
Normal file
42
cms/templates/js/mock/mock-container-page.underscore
Normal file
@@ -0,0 +1,42 @@
|
||||
<div id="content">
|
||||
|
||||
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
|
||||
<header class="mast has-actions has-navigation">
|
||||
<h1 class="page-header">
|
||||
<small class="navigation navigation-parents">
|
||||
|
||||
<a href="/unit/TestCourse/branch/draft/block/vertical8eb" class="navigation-link navigation-parent">Unit 1</a>
|
||||
<a href="#" class="navigation-link navigation-current">Nested Vertical Test</a>
|
||||
</small>
|
||||
</h1>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<section class="content-area">
|
||||
|
||||
<article class="content-primary window">
|
||||
<section class="wrapper-xblock level-page" data-locator="TestCourse/branch/draft/block/vertical131">
|
||||
</section>
|
||||
<div class="no-container-content is-hidden">
|
||||
<p>This page has no content yet.</p>
|
||||
</div>
|
||||
<div class="ui-loading is-hidden">
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-notification"></div>
|
||||
</div>
|
||||
127
cms/templates/js/mock/mock-container-xblock.underscore
Normal file
127
cms/templates/js/mock/mock-container-xblock.underscore
Normal file
@@ -0,0 +1,127 @@
|
||||
<header class="xblock-header"></header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical">
|
||||
<div class="vert-mod">
|
||||
<div class="vert vert-0">
|
||||
|
||||
<div class="xblock" data-block-type="vertical">
|
||||
<div class="vert-mod">
|
||||
<div class="vert vert-0">
|
||||
|
||||
<section class="wrapper-xblock level-nesting" data-locator="locator-group-A">
|
||||
<header class="xblock-header"></header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical">
|
||||
<div class="vert-mod">
|
||||
<div class="vert vert-0">
|
||||
|
||||
<section class="wrapper-xblock level-element" data-locator="locator-component-A1">
|
||||
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit"><a href="#" class="edit-button action-button"></a></li>
|
||||
<li class="action-item action-duplicate"><a href="#" class="duplicate-button action-button"></a></li>
|
||||
<li class="action-item action-delete"><a href="#" class="delete-button action-button"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render"></article>
|
||||
</section>
|
||||
|
||||
<section class="wrapper-xblock level-element" data-locator="locator-component-A2">
|
||||
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit"><a href="#" class="edit-button action-button"></a></li>
|
||||
<li class="action-item action-duplicate"><a href="#" class="duplicate-button action-button"></a></li>
|
||||
<li class="action-item action-delete"><a href="#" class="delete-button action-button"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render"></article>
|
||||
</section>
|
||||
|
||||
<section class="wrapper-xblock level-element" data-locator="locator-component-A3">
|
||||
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit"><a href="#" class="edit-button action-button"></a></li>
|
||||
<li class="action-item action-duplicate"><a href="#" class="duplicate-button action-button"></a></li>
|
||||
<li class="action-item action-delete"><a href="#" class="delete-button action-button"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render"></article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="wrapper-xblock level-nesting" data-locator="locator-group-B">
|
||||
<header class="xblock-header"></header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical">
|
||||
<div class="vert-mod">
|
||||
<div class="vert vert-0">
|
||||
|
||||
<section class="wrapper-xblock level-element" data-locator="locator-component-B1">
|
||||
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit"><a href="#" class="edit-button action-button"></a></li>
|
||||
<li class="action-item action-duplicate"><a href="#" class="duplicate-button action-button"></a></li>
|
||||
<li class="action-item action-delete"><a href="#" class="delete-button action-button"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render"></article>
|
||||
</section>
|
||||
|
||||
<section class="wrapper-xblock level-element" data-locator="locator-component-B2">
|
||||
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit"><a href="#" class="edit-button action-button"></a></li>
|
||||
<li class="action-item action-duplicate"><a href="#" class="duplicate-button action-button"></a></li>
|
||||
<li class="action-item action-delete"><a href="#" class="delete-button action-button"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render"></article>
|
||||
</section>
|
||||
|
||||
<section class="wrapper-xblock level-element" data-locator="locator-component-B3">
|
||||
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit"><a href="#" class="edit-button action-button"></a></li>
|
||||
<li class="action-item action-duplicate"><a href="#" class="duplicate-button action-button"></a></li>
|
||||
<li class="action-item action-delete"><a href="#" class="delete-button action-button"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render"></article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
22
cms/templates/js/mock/mock-empty-container-xblock.underscore
Normal file
22
cms/templates/js/mock/mock-empty-container-xblock.underscore
Normal file
@@ -0,0 +1,22 @@
|
||||
<header class="xblock-header">
|
||||
<div class="header-details">
|
||||
<a href="#" data-tooltip="Expand or Collapse" class="action expand-collapse collapse">
|
||||
<i class="icon-caret-down ui-toggle-expansion"></i>
|
||||
<span class="sr">Expand or Collapse</span>
|
||||
</a>
|
||||
<span>Empty Vertical Test</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="sr action-item">No Actions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock xblock-student_view xmodule_display xmodule_VerticalModule xblock-initialized" data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_vertical;_131a499ddaa3474194c1aa2eced34455" data-type="None" data-block-type="vertical">
|
||||
<div class="vert-mod">
|
||||
<div class="vert vert-0" data-id="i4x://AndyA/ABT101/vertical/2758bbc495dd40d59050da15b40bd9a5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
1
cms/templates/js/mock/mock-modal.underscore
Normal file
1
cms/templates/js/mock/mock-modal.underscore
Normal file
@@ -0,0 +1 @@
|
||||
<div class="xblock-editor"></div>
|
||||
17
cms/templates/js/mock/mock-updated-xblock.underscore
Normal file
17
cms/templates/js/mock/mock-updated-xblock.underscore
Normal file
@@ -0,0 +1,17 @@
|
||||
<header class="xblock-header">
|
||||
<div class="header-details">
|
||||
<span>Mock XBlock</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="sr action-item">No Actions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock xblock-student_view xmodule_display xmodule_VerticalModule"
|
||||
data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1"
|
||||
data-type="None">
|
||||
<div class="mock-updated-content">Mock Update</div>
|
||||
</div>
|
||||
</article>
|
||||
10
cms/templates/js/mock/mock-xblock-editor.underscore
Normal file
10
cms/templates/js/mock/mock-xblock-editor.underscore
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-block-type="mock" tabindex="0">
|
||||
|
||||
<div class="mock-xblock">
|
||||
<h3>Mock XBlock Editor</h3>
|
||||
<div class="metadata_edit"></div>
|
||||
<textarea data-metadata-name="custom_field">Custom Value</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" data-block-type="wrapper" tabindex="0">
|
||||
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
|
||||
<section class="editor-with-tabs">
|
||||
<div class="edit-header">
|
||||
<span class="component-name"></span>
|
||||
<ul class="editor-tabs">
|
||||
<li class="inner_tab_wrap"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-0" class="tab current">Basic</a></li>
|
||||
<li class="inner_tab_wrap"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1" class="tab">Advanced</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="component-tab" id="tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-0">
|
||||
<p>Basic editor</p>
|
||||
</div>
|
||||
<div class="component-tab" id="tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1">
|
||||
<p>Advanced editor</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
30
cms/templates/js/mock/mock-xmodule-editor.underscore
Normal file
30
cms/templates/js/mock/mock-xmodule-editor.underscore
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="MockDescriptor" data-block-type="wrapper" tabindex="0">
|
||||
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
|
||||
</div>
|
||||
<section class="sequence-edit">
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<ul class="list-input settings-list">
|
||||
<% _.each(_.range(numEntries), function() { %>
|
||||
<li class="field comp-setting-entry metadata_entry">
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
|
||||
</script>
|
||||
|
||||
<script id="metadata-string-entry" type="text/template">
|
||||
<div class="wrapper-comp-setting">
|
||||
\t<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
|
||||
\t<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
|
||||
\t<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{"display_name": {"default_value": null, "explicitly_set": true, "display_name": "Display Name", "help": "This name appears in the horizontal navigation at the top of the page.", "type": "Generic", "value": "Poll Question", "field_name": "display_name", "options": []}, "due": {"default_value": null, "explicitly_set": false, "display_name": "due", "help": "Date that this problem is due by", "type": "Generic", "value": null, "field_name": "due", "options": []}}'/>
|
||||
<textarea data-metadata-name="custom_field">Custom Value</textarea>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" data-block-type="wrapper" tabindex="0">
|
||||
<section class="sequence-edit">
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<ul class="list-input settings-list">
|
||||
<% _.each(_.range(numEntries), function() { %>
|
||||
<li class="field comp-setting-entry metadata_entry">
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
|
||||
</script>
|
||||
|
||||
<script id="metadata-string-entry" type="text/template">
|
||||
<div class="wrapper-comp-setting">
|
||||
\t<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
|
||||
\t<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
|
||||
\t<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{"display_name": {"default_value": null, "explicitly_set": true, "display_name": "Display Name", "help": "This name appears in the horizontal navigation at the top of the page.", "type": "Generic", "value": "Poll Question", "field_name": "display_name", "options": []}, "due": {"default_value": null, "explicitly_set": false, "display_name": "due", "help": "Date that this problem is due by", "type": "Generic", "value": null, "field_name": "due", "options": []}}'/>
|
||||
</section>
|
||||
</div>
|
||||
3
cms/templates/js/modal-button.underscore
Normal file
3
cms/templates/js/modal-button.underscore
Normal file
@@ -0,0 +1,3 @@
|
||||
<li class="action-item">
|
||||
<a href="#" class="button <%= isPrimary ? 'action-primary' : '' %> action-<%= type %>"><%= name %></a>
|
||||
</li>
|
||||
@@ -1,55 +1,31 @@
|
||||
<div id="dialog-assetupload"
|
||||
class="wrapper wrapper-dialog wrapper-dialog-assetupload <% if(shown) { print('is-shown') } %>"
|
||||
aria-describedby="dialog-assetupload-description"
|
||||
aria-labelledby="dialog-assetupload-title"
|
||||
aria-hidden="<%= !shown %>"
|
||||
role="dialog">
|
||||
<div class="dialog confirm">
|
||||
|
||||
<form class="upload-dialog" method="POST" action="<%= url %>" enctype="multipart/form-data">
|
||||
<div class="form-content">
|
||||
<h2 class="title"><%= title %></h2>
|
||||
<% if(error) {%>
|
||||
<div id="upload_error" class="message message-status message-status error is-shown" name="upload_error">
|
||||
<p><%= error.message %></p>
|
||||
<p id="dialog-assetupload-description" class="message"><%= message %></p>
|
||||
<input type="file" name="file" <% if(error && error.attributes && error.attributes.selectedFile) {%>class="error"<% } %> />
|
||||
|
||||
<div class="status-upload">
|
||||
|
||||
<% if(uploading) { %>
|
||||
<div class="wrapper-progress">
|
||||
<% if (uploadedBytes && totalBytes) { %>
|
||||
<progress value="<%= uploadedBytes %>" max="<%= totalBytes %>"><%= uploadedBytes/totalBytes*100 %>%</progress>
|
||||
<% } else { %>
|
||||
<progress></progress>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if(error) {%>
|
||||
<div id="upload_error" class="message message-status error is-shown" name="upload_error">
|
||||
<p><%= error.message %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if(finished) { %>
|
||||
<div id="upload_confirm" class="message message-status confirm success is-shown" name="upload_confirm">
|
||||
<p><%= gettext("File upload succeeded") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<p id="dialog-assetupload-description" class="message"><%= message %></p>
|
||||
<input type="file" name="file" <% if(error && error.attributes && error.attributes.selectedFile) {%>class="error"<% } %> />
|
||||
|
||||
<div class="status-upload">
|
||||
|
||||
<% if(uploading) { %>
|
||||
<div class="wrapper-progress">
|
||||
<% if (uploadedBytes && totalBytes) { %>
|
||||
<progress value="<%= uploadedBytes %>" max="<%= totalBytes %>"><%= uploadedBytes/totalBytes*100 %>%</progress>
|
||||
<% } else { %>
|
||||
<progress></progress>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if(finished) { %>
|
||||
<div id="upload_confirm" class="message message-status message-status confirm is-shown" name="upload_confirm">
|
||||
<p><%= gettext("Success!") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<h3 class="sr"><%= gettext('Form Actions') %></h3>
|
||||
<ul>
|
||||
<li class="action-item">
|
||||
<a href="#" class="button action-primary action-upload <% if (!selectedFile || error) { %>disabled<% } %>"><%= gettext('Upload') %></a>
|
||||
</li>
|
||||
<li class="action-item">
|
||||
<a href="#" class="button action-secondary action-cancel"><%= gettext('Cancel') %></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
%>
|
||||
<%block name="title">${_("Course Outline")}</%block>
|
||||
<%block name="bodyclass">is-signedin course view-outline feature-edit-dialog</%block>
|
||||
<%block name="bodyclass">is-signedin course view-outline</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
@@ -277,14 +277,14 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
<footer></footer>
|
||||
|
||||
<div
|
||||
class="wrapper wrapper-dialog wrapper-dialog-edit-sectionrelease edit-section-publish-settings"
|
||||
class="wrapper wrapper-modal-window wrapper-dialog-edit-sectionrelease edit-section-publish-settings"
|
||||
aria-describedby="dialog-edit-sectionrelease-description"
|
||||
aria-labelledby="dialog-edit-sectionrelease-title"
|
||||
aria-hidden=""
|
||||
role="dialog">
|
||||
<div class="dialog confirm">
|
||||
<div class="modal-window modal-med confirm">
|
||||
<form class="edit-sectionrelease-dialog" action="#">
|
||||
<div class="form-content">
|
||||
<div class="modal-content">
|
||||
<h2 class="title dialog-edit-sectionrelease-title">${_("Section Release Date")}</h2>
|
||||
<p id="dialog-edit-sectionrelease-description" class="message">${_('On the date set below, this section - {name} - will be released to students. Any units marked private will only be visible to admins.').format(name='<strong class="section-name"></strong>')}</p>
|
||||
<ul class="list-input picker datepair">
|
||||
@@ -293,12 +293,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
|
||||
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
</li>
|
||||
<li class="field field-start-time">
|
||||
<label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">UTC</abbr>)</label>
|
||||
<label for="start_time">${_("Release Time")} in <abbr title="${_("Coordinated Universal Time")}">UTC</abbr></label>
|
||||
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="actions modal-actions">
|
||||
<h3 class="sr">${_("Form Actions")}</h3>
|
||||
<ul>
|
||||
<li class="action-item">
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["basic-modal", "modal-button", "upload-dialog"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
|
||||
<script type="text/template" id="upload-dialog-tpl">
|
||||
<%static:include path="js/upload-dialog.underscore" />
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.CMS = window.CMS || {};
|
||||
CMS.URL = CMS.URL || {};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
% if xblock.location != xblock_context['root_xblock'].location:
|
||||
% if xblock.has_children:
|
||||
<section class="wrapper-xblock level-nesting" data-locator="${locator}">
|
||||
<section class="wrapper-xblock level-nesting" data-locator="${locator}" data-display-name="${xblock.display_name_with_default | h}" data-category="${xblock.category | h}">
|
||||
% else:
|
||||
<section class="wrapper-xblock level-element" data-locator="${locator}">
|
||||
<section class="wrapper-xblock level-element" data-locator="${locator}" data-display-name="${xblock.display_name_with_default | h}" data-category="${xblock.category | h}">
|
||||
% endif
|
||||
% endif
|
||||
<header class="xblock-header">
|
||||
@@ -15,22 +15,10 @@
|
||||
% if not xblock_context['read_only']:
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button">
|
||||
<i class="icon-edit"></i>
|
||||
<i class="icon-pencil"></i>
|
||||
<span class="action-button-text">${_("Edit")}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
|
||||
<i class="icon-copy"></i>
|
||||
<span class="sr">${_("Duplicate this component")}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
|
||||
<i class="icon-trash"></i>
|
||||
<span class="sr">${_("Delete this component")}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%block name="title">${_("Textbooks")}</%block>
|
||||
<%block name="bodyclass">is-signedin course view-textbooks feature-upload</%block>
|
||||
<%block name="bodyclass">is-signedin course view-textbooks</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["edit-textbook", "show-textbook", "edit-chapter", "no-textbooks", "upload-dialog"]:
|
||||
% for template_name in ["edit-textbook", "show-textbook", "edit-chapter", "no-textbooks", "basic-modal", "modal-button", "upload-dialog"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from contentstore.views.helpers import EDITING_TEMPLATES
|
||||
from django.utils.translation import ugettext as _
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
%>
|
||||
@@ -9,6 +10,14 @@ from xmodule.modulestore.django import loc_mapper
|
||||
<%block name="title">${_("Individual Unit")}</%block>
|
||||
<%block name="bodyclass">is-signedin course unit view-unit feature-upload</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in EDITING_TEMPLATES:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type='text/javascript'>
|
||||
require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit", "jquery.ui",
|
||||
@@ -34,15 +43,8 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="image-modal-tpl">
|
||||
<%static:include path="js/imageModal.underscore" />
|
||||
</script>
|
||||
<script type="text/template" id="upload-dialog-tpl">
|
||||
<%static:include path="js/upload-dialog.underscore" />
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper edit-state-${unit_state}" data-locator="${unit_locator}">
|
||||
<div class="inner-wrapper">
|
||||
@@ -223,7 +225,5 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="edit-xblock-modal"></div>
|
||||
</%block>
|
||||
|
||||
@@ -36,10 +36,12 @@ from django.core.urlresolvers import reverse
|
||||
<section class="content-area">
|
||||
<article class="content-primary">
|
||||
|
||||
<div class="ui-loading">
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
|
||||
</div>
|
||||
|
||||
<div class="ui-loading">
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
|
||||
</div>
|
||||
<div class="no-container-content">
|
||||
<p>This page has no content yet.</p>
|
||||
</div>
|
||||
<section class="wrapper-xblock level-page">
|
||||
<header class="xblock-header">
|
||||
<div class="header-details">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user