Merge pull request #9525 from edx/clrux/ac-76
LMS: Course navigation revamp
This commit is contained in:
@@ -82,7 +82,7 @@ class CourseNavPage(PageObject):
|
||||
|
||||
# Click the section to ensure it's open (no harm in clicking twice if it's already open)
|
||||
# Add one to convert from list index to CSS index
|
||||
section_css = 'nav>div.chapter:nth-of-type({0})>h3>a'.format(sec_index + 1)
|
||||
section_css = '.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1)
|
||||
self.q(css=section_css).first.click()
|
||||
|
||||
# Get the subsection by index
|
||||
@@ -94,9 +94,10 @@ class CourseNavPage(PageObject):
|
||||
return
|
||||
|
||||
# Convert list indices (start at zero) to CSS indices (start at 1)
|
||||
subsection_css = "nav>div.chapter:nth-of-type({0})>ul>li:nth-of-type({1})>a".format(
|
||||
sec_index + 1, subsec_index + 1
|
||||
)
|
||||
subsection_css = (
|
||||
".course-navigation .chapter-content-container:nth-of-type({0}) "
|
||||
".menu-item:nth-of-type({1})"
|
||||
).format(sec_index + 1, subsec_index + 1)
|
||||
|
||||
# Click the subsection and ensure that the page finishes reloading
|
||||
self.q(css=subsection_css).first.click()
|
||||
@@ -130,7 +131,7 @@ class CourseNavPage(PageObject):
|
||||
"""
|
||||
Return a list of all section titles on the page.
|
||||
"""
|
||||
chapter_css = 'nav > div.chapter > h3 > a'
|
||||
chapter_css = '.course-navigation .chapter .group-heading'
|
||||
return self.q(css=chapter_css).map(lambda el: el.text.strip()).results
|
||||
|
||||
def _subsection_titles(self, section_index):
|
||||
@@ -140,7 +141,10 @@ class CourseNavPage(PageObject):
|
||||
"""
|
||||
# Retrieve the subsection title for the section
|
||||
# Add one to the list index to get the CSS index, which starts at one
|
||||
subsection_css = 'nav>div.chapter:nth-of-type({0})>ul>li>a>p:nth-of-type(1)'.format(section_index)
|
||||
subsection_css = (
|
||||
".course-navigation .chapter-content-container:nth-of-type({0}) "
|
||||
".menu-item a p:nth-of-type(1)"
|
||||
).format(section_index)
|
||||
|
||||
# If the element is visible, we can get its text directly
|
||||
# Otherwise, we need to get the HTML
|
||||
@@ -171,8 +175,8 @@ class CourseNavPage(PageObject):
|
||||
That's true right after we click the section/subsection, but not true in general
|
||||
(the user could go to a section, then expand another tab).
|
||||
"""
|
||||
current_section_list = self.q(css='nav>div.chapter.is-open>h3>a').text
|
||||
current_subsection_list = self.q(css='nav>div.chapter.is-open li.active>a>p').text
|
||||
current_section_list = self.q(css='.course-navigation .chapter.is-open .group-heading').text
|
||||
current_subsection_list = self.q(css='.course-navigation .chapter-content-container .menu-item.active a p').text
|
||||
|
||||
if len(current_section_list) == 0:
|
||||
self.warning("Could not find the current section")
|
||||
|
||||
@@ -14,7 +14,7 @@ class CoursewarePage(CoursePage):
|
||||
url_path = "courseware/"
|
||||
xblock_component_selector = '.vert .xblock'
|
||||
section_selector = '.chapter'
|
||||
subsection_selector = '.chapter ul li'
|
||||
subsection_selector = '.chapter-content-container a'
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='body.courseware').present
|
||||
@@ -102,7 +102,7 @@ class CoursewarePage(CoursePage):
|
||||
"""
|
||||
return the url of the active subsection in the left nav
|
||||
"""
|
||||
return self.q(css='.chapter ul li.active a').attrs('href')[0]
|
||||
return self.q(css='.chapter-content-container .menu-item.active a').attrs('href')[0]
|
||||
|
||||
@property
|
||||
def can_start_proctored_exam(self):
|
||||
|
||||
@@ -1121,7 +1121,7 @@ class EntranceExamTest(UniqueCourseTest):
|
||||
When I view the courseware that has an entrance exam
|
||||
Then there should be an "Entrance Exam" chapter.'
|
||||
"""
|
||||
entrance_exam_link_selector = 'div#accordion nav div h3 a'
|
||||
entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading'
|
||||
# visit courseware page and make sure there is not entrance exam chapter.
|
||||
self.courseware_page.visit()
|
||||
self.courseware_page.wait_for_page()
|
||||
|
||||
@@ -92,7 +92,7 @@ def when_i_navigate_to_a_section(step):
|
||||
world.disable_jquery_animations()
|
||||
|
||||
# Open the 2nd section
|
||||
world.css_click(css_selector='div.chapter', index=1)
|
||||
world.css_click(css_selector='.chapter', index=1)
|
||||
subsection_css = 'a[href*="Test_Subsection_2/"]'
|
||||
|
||||
# Click on the subsection to see the content
|
||||
|
||||
@@ -74,6 +74,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mixin import wrap_with_license
|
||||
from util.json_request import JsonResponse
|
||||
from util.model_utils import slugify
|
||||
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
|
||||
from util import milestones_helpers
|
||||
from verify_student.services import ReverificationService
|
||||
@@ -165,6 +166,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
|
||||
for chapter in chapters:
|
||||
# Only show required content, if there is required content
|
||||
# chapter.hide_from_toc is read-only (boo)
|
||||
display_id = slugify(chapter.display_name_with_default)
|
||||
local_hide_from_toc = False
|
||||
if required_content:
|
||||
if unicode(chapter.location) not in required_content:
|
||||
@@ -246,6 +248,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
|
||||
sections.append(section_context)
|
||||
toc_chapters.append({
|
||||
'display_name': chapter.display_name_with_default,
|
||||
'display_id': display_id,
|
||||
'url_name': chapter.url_name,
|
||||
'sections': sections,
|
||||
'active': chapter.url_name == active_chapter
|
||||
|
||||
@@ -155,7 +155,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
}
|
||||
],
|
||||
'url_name': u'Entrance_Exam_Section_-_Chapter_1',
|
||||
'display_name': u'Entrance Exam Section - Chapter 1'
|
||||
'display_name': u'Entrance Exam Section - Chapter 1',
|
||||
'display_id': u'entrance-exam-section-chapter-1',
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -182,19 +183,22 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
}
|
||||
],
|
||||
'url_name': u'Overview',
|
||||
'display_name': u'Overview'
|
||||
'display_name': u'Overview',
|
||||
'display_id': u'overview'
|
||||
},
|
||||
{
|
||||
'active': False,
|
||||
'sections': [],
|
||||
'url_name': u'Week_1',
|
||||
'display_name': u'Week 1'
|
||||
'display_name': u'Week 1',
|
||||
'display_id': u'week-1'
|
||||
},
|
||||
{
|
||||
'active': False,
|
||||
'sections': [],
|
||||
'url_name': u'Instructor',
|
||||
'display_name': u'Instructor'
|
||||
'display_name': u'Instructor',
|
||||
'display_id': u'instructor'
|
||||
},
|
||||
{
|
||||
'active': True,
|
||||
@@ -209,7 +213,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
}
|
||||
],
|
||||
'url_name': u'Entrance_Exam_Section_-_Chapter_1',
|
||||
'display_name': u'Entrance Exam Section - Chapter 1'
|
||||
'display_name': u'Entrance Exam Section - Chapter 1',
|
||||
'display_id': u'entrance-exam-section-chapter-1'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
@@ -633,6 +633,7 @@ class TestTOC(ModuleStoreTestCase):
|
||||
def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds):
|
||||
with self.store.default_store(default_ms):
|
||||
self.setup_modulestore(default_ms, setup_finds, setup_sends)
|
||||
|
||||
expected = ([{'active': True, 'sections':
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': None, 'active': False},
|
||||
@@ -642,11 +643,11 @@ class TestTOC(ModuleStoreTestCase):
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
'url_name': 'Overview', 'display_name': u'Overview', 'display_id': u'overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}])
|
||||
|
||||
course = self.store.get_course(self.toy_course.id, depth=2)
|
||||
with check_mongo_calls(toc_finds):
|
||||
@@ -682,11 +683,11 @@ class TestTOC(ModuleStoreTestCase):
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
'url_name': 'Overview', 'display_name': u'Overview', 'display_id': u'overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}])
|
||||
|
||||
with check_mongo_calls(toc_finds):
|
||||
actual = render.toc_for_course(
|
||||
|
||||
@@ -107,7 +107,6 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'chapter': 'Chrome',
|
||||
'section': displayname,
|
||||
}))
|
||||
self.assertEquals('open_close_accordion' in response.content, accordion)
|
||||
self.assertEquals('course-tabs' in response.content, tabs)
|
||||
|
||||
self.assertTabInactive('progress', response)
|
||||
|
||||
@@ -26,7 +26,6 @@ class RenderXBlockTestMixin(object):
|
||||
# DOM elements that appear in the LMS Courseware,
|
||||
# but are excluded from the xBlock-only rendering.
|
||||
COURSEWARE_CHROME_HTML_ELEMENTS = [
|
||||
'<header id="open_close_accordion"',
|
||||
'<ol class="course-tabs"',
|
||||
'<footer id="footer-openedx"',
|
||||
'<div class="window-wrap"',
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<div class="course-wrapper">
|
||||
<header id="open_close_accordion">
|
||||
<a href="#">close</a>
|
||||
</header>
|
||||
<div id="accordion"></div>
|
||||
</div>
|
||||
@@ -1,10 +1,5 @@
|
||||
describe 'Courseware', ->
|
||||
describe 'start', ->
|
||||
it 'create the navigation', ->
|
||||
spyOn(window, 'Navigation')
|
||||
Courseware.start()
|
||||
expect(window.Navigation).toHaveBeenCalled()
|
||||
|
||||
it 'binds the Logger', ->
|
||||
spyOn(Logger, 'bind')
|
||||
Courseware.start()
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
describe 'Navigation', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'coffee/fixtures/accordion.html'
|
||||
@navigation = new Navigation
|
||||
|
||||
describe 'constructor', ->
|
||||
describe 'when the #accordion exists', ->
|
||||
describe 'when there is an active section', ->
|
||||
beforeEach ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
|
||||
new Navigation
|
||||
|
||||
it 'activate the accordion with correct active section', ->
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
heightStyle: 'content'
|
||||
|
||||
describe 'when there is no active section', ->
|
||||
beforeEach ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
|
||||
new Navigation
|
||||
|
||||
it 'activate the accordian with no section as active', ->
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 0
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
heightStyle: 'content'
|
||||
|
||||
it 'binds the accordionchange event', ->
|
||||
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
|
||||
|
||||
it 'bind the navigation toggle', ->
|
||||
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
|
||||
|
||||
describe 'when the #accordion does not exists', ->
|
||||
beforeEach ->
|
||||
$('#accordion').remove()
|
||||
|
||||
it 'does not activate the accordion', ->
|
||||
spyOn $.fn, 'accordion'
|
||||
expect($('#accordion').accordion).wasNotCalled()
|
||||
|
||||
describe 'toggle', ->
|
||||
it 'toggle closed class on the wrapper', ->
|
||||
$('.course-wrapper').removeClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).toHaveClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).not.toHaveClass('closed')
|
||||
|
||||
describe 'log', ->
|
||||
beforeEach ->
|
||||
spyOn Logger, 'log'
|
||||
|
||||
it 'submit event log', ->
|
||||
@navigation.log {}, {
|
||||
newHeader:
|
||||
text: -> "new"
|
||||
oldHeader:
|
||||
text: -> "old"
|
||||
}
|
||||
|
||||
expect(Logger.log).toHaveBeenCalledWith 'accordion',
|
||||
newheader: 'new'
|
||||
oldheader: 'old'
|
||||
@@ -2,7 +2,6 @@ class @Courseware
|
||||
@prefix: ''
|
||||
|
||||
constructor: ->
|
||||
new Navigation
|
||||
Logger.bind()
|
||||
@render()
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
class @Navigation
|
||||
constructor: ->
|
||||
if $('#accordion').length
|
||||
# First look for an active section
|
||||
active = $('#accordion ul:has(li.active)').index('#accordion ul')
|
||||
# if we didn't find one, look for an active chapter
|
||||
if active < 0
|
||||
active = $('#accordion h3.active').index('#accordion h3')
|
||||
# if that didn't work either, default to 0
|
||||
if active < 0
|
||||
active = 0
|
||||
$('#accordion').bind('accordionchange', @log).accordion
|
||||
active: active
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
heightStyle: 'content'
|
||||
$('#accordion .ui-state-active').closest('.chapter').addClass('is-open')
|
||||
$('#open_close_accordion a').click @toggle
|
||||
$('#accordion').show()
|
||||
$('#accordion a').click @setChapter
|
||||
|
||||
log: (event, ui) ->
|
||||
Logger.log 'accordion',
|
||||
newheader: ui.newHeader.text()
|
||||
oldheader: ui.oldHeader.text()
|
||||
|
||||
toggle: ->
|
||||
$('.course-wrapper').toggleClass('closed')
|
||||
|
||||
setChapter: ->
|
||||
$('#accordion .is-open').removeClass('is-open')
|
||||
$(this).closest('.chapter').addClass('is-open')
|
||||
|
||||
5
lms/static/coffee/src/navigation.js
Normal file
5
lms/static/coffee/src/navigation.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// This file is intentionally blank as it was removed because it was not
|
||||
// longer necessary, but the pipeline requires it for whatever reason.
|
||||
// Until the pipeline issue is resolved this file is here. It shouldn't
|
||||
// conflict with anything.
|
||||
var nothingtoseehere;
|
||||
56
lms/static/js/fixtures/accordion.html
Normal file
56
lms/static/js/fixtures/accordion.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<div class="course-wrapper">
|
||||
<div class="accordion">
|
||||
<a href="accordion-menu-1" role="button" class="button-chapter chapter" aria-controls="accordion-menu-1">
|
||||
<span class="group-heading">
|
||||
1 Introduction Chapter
|
||||
</span>
|
||||
</a>
|
||||
<div class="chapter-content-container" id="accordion-menu-1" tabindex="-1" aria-expanded="true">
|
||||
<div class="chapter-menu">
|
||||
<div class="menu-item">
|
||||
<a href="#">
|
||||
<p>1 edX Homepage</p>
|
||||
<p class="subtitle">Ungraded</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item active">
|
||||
<a href="#">
|
||||
<p>1 The edX Blog</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item graded">
|
||||
<a href="#">
|
||||
<p>1 Courses Dashboard</p>
|
||||
<p class="subtitle">Graded</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="accordion-menu-2" role="button" class="button-chapter chapter" aria-controls="accordion-menu-2">
|
||||
<span class="group-heading">
|
||||
2 Another Chapter
|
||||
</span>
|
||||
</a>
|
||||
<div class="chapter-content-container" id="accordion-menu-2" tabindex="-1" aria-expanded="false">
|
||||
<div class="chapter-menu">
|
||||
<div class="menu-item">
|
||||
<a href="#">
|
||||
<p>2 edX Homepage</p>
|
||||
<p class="subtitle">Ungraded</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<a href="#">
|
||||
<p>2 The edX Blog</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<a class="graded" href="#">
|
||||
<p>2 Courses Dashboard</p>
|
||||
<p class="subtitle">Graded</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>]
|
||||
@@ -70,6 +70,7 @@
|
||||
'history': 'js/vendor/history',
|
||||
'js/staff_debug_actions': 'js/staff_debug_actions',
|
||||
'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit',
|
||||
'js/utils/navigation': 'js/utils/navigation',
|
||||
|
||||
// Backbone classes loaded explicitly until they are converted to use RequireJS
|
||||
'js/models/notification': 'js/models/notification',
|
||||
@@ -686,6 +687,7 @@
|
||||
'lms/include/js/spec/edxnotes/plugins/caret_navigation_spec.js',
|
||||
'lms/include/js/spec/edxnotes/collections/notes_spec.js',
|
||||
'lms/include/js/spec/search/search_spec.js',
|
||||
'lms/include/js/spec/navigation_spec.js',
|
||||
'lms/include/js/spec/discovery/collections/filters_spec.js',
|
||||
'lms/include/js/spec/discovery/models/course_card_spec.js',
|
||||
'lms/include/js/spec/discovery/models/course_directory_spec.js',
|
||||
|
||||
93
lms/static/js/spec/navigation_spec.js
Normal file
93
lms/static/js/spec/navigation_spec.js
Normal file
@@ -0,0 +1,93 @@
|
||||
define(['jquery', 'js/utils/navigation'], function($) {
|
||||
'use strict';
|
||||
|
||||
describe('Course Navigation Accordion', function() {
|
||||
var accordion, button, heading, chapterContent, chapterMenu;
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('js/fixtures/accordion.html');
|
||||
|
||||
accordion = $('.accordion');
|
||||
button = accordion.children('.button-chapter');
|
||||
heading = button.children('.group-heading');
|
||||
chapterContent = accordion.children('.chapter-content-container');
|
||||
chapterMenu = chapterContent.children('.chapter-menu');
|
||||
|
||||
spyOn($.fn, 'focus').andCallThrough();
|
||||
edx.util.navigation.init();
|
||||
});
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
describe('always', function() {
|
||||
|
||||
it('ensures accordion is present', function() {
|
||||
expect(accordion.length).toBe(1);
|
||||
});
|
||||
|
||||
it('ensures aria attributes are present', function() {
|
||||
expect(chapterContent).toHaveAttr({
|
||||
'aria-expanded': 'true'
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures only one active item', function() {
|
||||
expect(chapterMenu.find('.active').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('open section with mouse click', function() {
|
||||
|
||||
it('ensures new section is opened and previous section is closed', function() {
|
||||
button:eq(1).click();
|
||||
|
||||
expect(chapterContent:eq(0)).not.toHaveClass('is-open');
|
||||
expect(chapterContent:eq(1)).toHaveClass('is-open');
|
||||
|
||||
expect(button:eq(0)).not.toHaveClass('is-open');
|
||||
expect(button:eq(1)).toHaveClass('is-open');
|
||||
|
||||
expect(chapterContent:eq(1).focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ensure proper aria and attrs', function() {
|
||||
expect(chapterContent:eq(1)).toHaveAttr({
|
||||
'aria-expanded': 'false'
|
||||
});
|
||||
expect(chapterContent:eq(0)).toHaveAttr({
|
||||
'aria-expanded': 'true'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('open section with spacebar', function() {
|
||||
|
||||
function keyPressEvent(key) {
|
||||
return $.Event('keydown', { keyCode: key });
|
||||
}
|
||||
|
||||
it('ensures new section is opened and previous section is closed', function() {
|
||||
button:eq(1).focus();
|
||||
button.trigger(keyPressEvent(32)); // Spacebar
|
||||
|
||||
expect(chapterContent:eq(0)).not.toHaveClass('is-open');
|
||||
expect(chapterContent:eq(1)).toHaveClass('is-open');
|
||||
|
||||
expect(button:eq(0)).not.toHaveClass('is-open');
|
||||
expect(button:eq(1)).toHaveClass('is-open');
|
||||
|
||||
expect(chapterContent:eq(1).focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ensure proper aria and attrs', function() {
|
||||
expect(chapterContent:eq(1)).toHaveAttr({
|
||||
'aria-expanded': 'false'
|
||||
});
|
||||
expect(chapterContent:eq(0)).toHaveAttr({
|
||||
'aria-expanded': 'true'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
130
lms/static/js/utils/navigation.js
Normal file
130
lms/static/js/utils/navigation.js
Normal file
@@ -0,0 +1,130 @@
|
||||
var edx = edx || {},
|
||||
|
||||
Navigation = (function() {
|
||||
|
||||
var navigation = {
|
||||
|
||||
init: function() {
|
||||
if ($('.accordion').length) {
|
||||
navigation.loadAccordion();
|
||||
}
|
||||
},
|
||||
|
||||
loadAccordion: function() {
|
||||
navigation.checkForCurrent();
|
||||
navigation.listenForClick();
|
||||
navigation.listenForKeypress();
|
||||
},
|
||||
|
||||
getActiveIndex: function() {
|
||||
var index = $('.accordion .button-chapter:has(.active)').index('.accordion .button-chapter'),
|
||||
button = null;
|
||||
|
||||
if (index > -1) {
|
||||
button = $('.accordion .button-chapter:eq(' + index + ')');
|
||||
}
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
checkForCurrent: function() {
|
||||
var button = navigation.getActiveIndex();
|
||||
|
||||
navigation.closeAccordions();
|
||||
|
||||
if (button !== null) {
|
||||
navigation.setupCurrentAccordionSection(button);
|
||||
}
|
||||
},
|
||||
|
||||
listenForClick: function() {
|
||||
$('.accordion').on('click', '.button-chapter', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var button = $(event.currentTarget),
|
||||
section = button.next('.chapter-content-container');
|
||||
|
||||
navigation.closeAccordions(button, section);
|
||||
navigation.openAccordion(button, section);
|
||||
});
|
||||
},
|
||||
|
||||
listenForKeypress: function() {
|
||||
$('.accordion').on('keydown', '.button-chapter', function(event) {
|
||||
// because we're changing the role of the toggle from an 'a' to a 'button'
|
||||
// we need to ensure it has the same keyboard use cases as a real button.
|
||||
// this is useful for screenreader users primarily.
|
||||
if (event.which == 32) { // spacebar
|
||||
event.preventDefault();
|
||||
$(event.currentTarget).trigger('click');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeAccordions: function(button, section) {
|
||||
var menu = $(section).find('.chapter-menu'), toggle;
|
||||
|
||||
$('.accordion .button-chapter').each(function(index, element) {
|
||||
toggle = $(element);
|
||||
|
||||
toggle
|
||||
.removeClass('is-open')
|
||||
.attr('aria-expanded', 'false');
|
||||
|
||||
toggle
|
||||
.children('.group-heading')
|
||||
.removeClass('active')
|
||||
.find('.icon')
|
||||
.addClass('fa-caret-right')
|
||||
.removeClass('fa-caret-down');
|
||||
|
||||
toggle
|
||||
.next('.chapter-content-container')
|
||||
.removeClass('is-open')
|
||||
.find('.chapter-menu').not(menu)
|
||||
.removeClass('is-open')
|
||||
.slideUp();
|
||||
});
|
||||
},
|
||||
|
||||
setupCurrentAccordionSection: function(button) {
|
||||
var section = $(button).next('.chapter-content-container');
|
||||
|
||||
navigation.openAccordion(button, section);
|
||||
},
|
||||
|
||||
openAccordion: function(button, section) {
|
||||
var sectionEl = $(section),
|
||||
firstLink = sectionEl.find('.menu-item').first(),
|
||||
buttonEl = $(button);
|
||||
|
||||
buttonEl
|
||||
.addClass('is-open')
|
||||
.attr('aria-expanded', 'true');
|
||||
|
||||
buttonEl
|
||||
.children('.group-heading')
|
||||
.addClass('active')
|
||||
.find('.icon')
|
||||
.removeClass('fa-caret-right')
|
||||
.addClass('fa-caret-down');
|
||||
|
||||
sectionEl
|
||||
.addClass('is-open')
|
||||
.find('.chapter-menu')
|
||||
.addClass('is-open')
|
||||
.slideDown();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: navigation.init
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
edx.util = edx.util || {};
|
||||
edx.util.navigation = Navigation;
|
||||
edx.util.navigation.init();
|
||||
@@ -671,7 +671,7 @@ div.course-wrapper {
|
||||
}
|
||||
}
|
||||
|
||||
div#accordion {
|
||||
.accordion {
|
||||
visibility: hidden;
|
||||
width: 10px;
|
||||
padding: 0;
|
||||
@@ -679,11 +679,6 @@ div.course-wrapper {
|
||||
nav {
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
|
||||
ul {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,234 +1,171 @@
|
||||
.course-index {
|
||||
@extend .sidebar;
|
||||
@extend .tran;
|
||||
@include border-right(1px solid $border-color-2);
|
||||
@include border-radius(3px, 0, 0, 3px);
|
||||
@include transition( all .2s $ease-in-out-quad 0s);
|
||||
@include border-right(1px solid $border-color-2);
|
||||
@include border-radius(3px, 0, 0, 3px);
|
||||
display: table-cell; // needed to extend the sidebar the full height of the area
|
||||
|
||||
#open_close_accordion {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header {
|
||||
max-height: 47px;
|
||||
|
||||
h2 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
div#accordion {
|
||||
width: auto;
|
||||
font-size: 14px;
|
||||
|
||||
h3 {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
font-size: 16px;
|
||||
|
||||
&:first-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.ui-state-hover, &.ui-state-focus {
|
||||
a {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-accordion-header {
|
||||
border-bottom: none;
|
||||
// provides sufficient contrast for all screen reader-only elements
|
||||
.sr {
|
||||
color: $black;
|
||||
|
||||
a {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
@include padding-left(19px);
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
@extend .active;
|
||||
border-bottom: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
span.ui-icon {
|
||||
@include left(0);
|
||||
opacity: 0.3;
|
||||
background-image: url("/static/images/ui-icons_222222_256x240.png"); // jQuery UI sprite
|
||||
|
||||
&.ui-icon-triangle-1-e {
|
||||
|
||||
// CASE: left to right layout
|
||||
@include ltr {
|
||||
background-position: -32px -16px; // jQuery UI east arrow position
|
||||
}
|
||||
|
||||
// CASE: right to left layout
|
||||
@include rtl {
|
||||
background-position: -96px -16px; // jQuery UI west arrow position
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
width: 100% !important;
|
||||
@include box-sizing(border-box);
|
||||
padding: 11px 14px;
|
||||
@include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom);
|
||||
background-color: $sidebar-chapter-bg;
|
||||
box-shadow: 0 1px 0 $white inset, 0 -1px 0 $shadow-l1 inset;
|
||||
@include transition(background-color .1s linear 0s);
|
||||
|
||||
&.is-open {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 3px 0 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 0 3px;
|
||||
box-shadow: 0 1px 0 $white inset;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
ul.ui-accordion-content {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
padding: 9px 0 9px 9px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
li {
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
margin-bottom: ($baseline/5);
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
@include padding( ($baseline/4), ($baseline*1.5), ($baseline/4), ($baseline/2));
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
p {
|
||||
font-weight: bold;
|
||||
font-family: $sans-serif;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.3;
|
||||
|
||||
&.subtitle {
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-weight2;
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
color: $gray-d1;
|
||||
margin: 0;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// definitions for proctored exam attempt status indicators
|
||||
i.verified {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
i.rejected {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
i.error {
|
||||
color: $alert-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: $shadow-l1;
|
||||
|
||||
> a p {
|
||||
color: $gray-d3;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: inset 0 1px 14px 0 $shadow-l1;
|
||||
|
||||
&:after {
|
||||
opacity: 1.0;
|
||||
right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@extend %t-weight5;
|
||||
|
||||
&:after {
|
||||
content: '›';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
margin-top: -13px;
|
||||
font-size: 30px;
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
opacity: 0;
|
||||
@include transition(none);
|
||||
}
|
||||
|
||||
> a {
|
||||
border: 1px solid $border-color-1;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, .35) inset;
|
||||
background: $sidebar-active-image;
|
||||
|
||||
&:after {
|
||||
opacity: 1.0;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
span.subtitle {
|
||||
@extend %t-weight2;
|
||||
}
|
||||
}
|
||||
|
||||
&.graded {
|
||||
> a {
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.active > a {
|
||||
background: linear-gradient(to bottom, #e6e6e6, #d6d6d6);
|
||||
}
|
||||
}
|
||||
}
|
||||
// reseting bolded fonts for the course index
|
||||
.group-heading {
|
||||
@extend %t-regular;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we're targetting the .class now, instead of the #id
|
||||
.accordion {
|
||||
@extend %t-copy-sub1;
|
||||
|
||||
.course-navigation {
|
||||
|
||||
.button-chapter {
|
||||
@include transition(all $tmg-s3 ease-in-out);
|
||||
@include box-sizing(border-box);
|
||||
@include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom);
|
||||
@include transition(background-color .1s linear 0s);
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-shadow: 0 1px 0 $white inset, 0 -1px 0 $shadow-l1 inset;
|
||||
background-color: $sidebar-chapter-bg;
|
||||
color: $link-color;
|
||||
-webkit-font-smoothing: subpixel-antialiased; // this brings back our nice, dark blue
|
||||
|
||||
.group-heading {
|
||||
@extend %t-title6;
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: ($baseline*.75) $baseline ($baseline*.75) ($baseline*2);
|
||||
@include text-align(left);
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
@include left($baseline);
|
||||
top: $baseline;
|
||||
font-size: $body-font-size;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
background: $white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
||||
.group-heading {
|
||||
@extend %t-strong;
|
||||
color: $base-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-content-container {
|
||||
background: $white;
|
||||
|
||||
&.is-open {
|
||||
border-bottom: 1px solid $shadow-l1;
|
||||
}
|
||||
|
||||
.chapter-menu {
|
||||
display: none;
|
||||
padding: 0 14px;
|
||||
overflow: hidden;
|
||||
|
||||
.menu-item {
|
||||
@extend %t-strong;
|
||||
margin: ($baseline/5) 0;
|
||||
background: inherit;
|
||||
|
||||
a {
|
||||
@extend %t-action3;
|
||||
@extend %t-strong;
|
||||
position: relative;
|
||||
display: block;
|
||||
@include padding(($baseline/4) ($baseline/2));
|
||||
border-radius: ($baseline/4);
|
||||
font-family: $sans-serif;
|
||||
color: $base-font-color;
|
||||
|
||||
p {
|
||||
|
||||
&.subtitle {
|
||||
@extend %t-action4;
|
||||
@extend %t-regular;
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-d1;
|
||||
|
||||
&:empty {
|
||||
// hide empty subtitles
|
||||
display: none;
|
||||
}
|
||||
|
||||
// definitions for proctored exam attempt status indicators
|
||||
.verified {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
.rejected {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $alert-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $base-font-color;
|
||||
background: $gray-l5;
|
||||
|
||||
.subtitle {
|
||||
color: $gray-d1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.graded {
|
||||
|
||||
.menu-icon {
|
||||
@include right($baseline/2);
|
||||
position: absolute;
|
||||
bottom: ($baseline/2);
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
||||
a {
|
||||
@extend %t-ultrastrong;
|
||||
background: $gray-l4;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,73 +6,45 @@
|
||||
%>
|
||||
|
||||
<%def name="make_chapter(chapter)">
|
||||
<div class="chapter">
|
||||
<%
|
||||
if chapter.get('active'):
|
||||
aria_label = _('{chapter}, current chapter').format(chapter=chapter['display_name'])
|
||||
active_class = ' class="active"'
|
||||
else:
|
||||
aria_label = chapter['display_name']
|
||||
active_class = ''
|
||||
%>
|
||||
<h3 ${active_class} aria-label="${aria_label}">
|
||||
<a href="#">
|
||||
${chapter['display_name']}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
% for section in chapter['sections']:
|
||||
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
|
||||
<%
|
||||
if chapter.get('active'):
|
||||
aria_label = _('{chapter} current chapter').format(chapter=chapter['display_name'])
|
||||
active_class = 'active'
|
||||
else:
|
||||
aria_label = chapter['display_name']
|
||||
active_class = ''
|
||||
%>
|
||||
<a href="#${chapter['display_id']}-child" role="button" class="button-chapter chapter ${active_class}" id="${chapter['display_id']}-parent" aria-controls="${chapter['display_id']}-child" aria-expanded="false">
|
||||
<span class="group-heading ${active_class}" aria-label="${aria_label}">
|
||||
<span class="icon fa fa-caret-right" aria-hidden="true"></span>
|
||||
${chapter['display_name']}
|
||||
</span>
|
||||
</a>
|
||||
<div class="chapter-content-container" id="${chapter['display_id']}-child" tabindex="-1" role="group" aria-label="${chapter['display_name']} submenu">
|
||||
<div class="chapter-menu">
|
||||
% for section in chapter['sections']:
|
||||
<div class="menu-item ${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
|
||||
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
|
||||
<p>${section['display_name']} ${'<span class="sr">, current section</span>' if 'active' in section and section['active'] else ''}</p>
|
||||
<%
|
||||
<p>${section['display_name']} ${'<span class="sr"> current section</span>' if 'active' in section and section['active'] else ''}</p>
|
||||
<%
|
||||
if section.get('due') is None:
|
||||
due_date = ''
|
||||
else:
|
||||
formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=settings.TIME_ZONE_DISPLAYED_FOR_DEADLINES)
|
||||
due_date = '' if len(formatted_string)==0 else _('due {date}').format(date=formatted_string)
|
||||
%>
|
||||
|
||||
## There is behavior differences between
|
||||
## rending of sections which have proctoring/timed examinations
|
||||
## and those that do not.
|
||||
##
|
||||
## Proctoring exposes a exam status message field as well as
|
||||
## a status icon
|
||||
|
||||
% if section['format'] or due_date or 'proctoring' in section:
|
||||
<p class="subtitle">
|
||||
% if 'proctoring' in section:
|
||||
## Display the proctored exam status icon and status message
|
||||
<i class="fa ${section['proctoring'].get('suggested_icon', 'fa-lock')} ${section['proctoring'].get('status', 'eligible')}"></i>
|
||||
<span class="subtitle-name">${section['proctoring'].get('short_description', '')}
|
||||
</span>
|
||||
## completed proctored exam statuses should not show the due date
|
||||
## since the exam has already been submitted by the user
|
||||
% if not section['proctoring'].get('in_completed_state', False):
|
||||
<span class="subtitle-name">${due_date}</span>
|
||||
% endif
|
||||
% else:
|
||||
## non-proctored section, we just show the exam format and the due date
|
||||
## this is the standard case in edx-platform
|
||||
<span class="subtitle-name">${section['format']} ${due_date}
|
||||
</span>
|
||||
%>
|
||||
<p class="subtitle">${section['format']} ${due_date}</p>
|
||||
% if 'graded' in section and section['graded']:
|
||||
<span class="menu-icon icon fa fa-pencil-square-o" aria-hidden="true"></span>
|
||||
<span class="sr">This content is graded</span>
|
||||
% endif
|
||||
</p>
|
||||
% endif
|
||||
|
||||
% if 'graded' in section and section['graded']:
|
||||
## sections that are graded should indicate this through an icon
|
||||
<i class="icon fa fa-pencil-square-o" aria-hidden="true" data-tooltip="${_("This section is graded.")}"></i>
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
% for chapter in toc:
|
||||
${make_chapter(chapter)}
|
||||
% endfor
|
||||
% endfor
|
||||
@@ -165,10 +165,6 @@ ${fragment.foot_html()}
|
||||
|
||||
% if disable_accordion is UNDEFINED or not disable_accordion:
|
||||
<div class="course-index">
|
||||
<header id="open_close_accordion">
|
||||
<a href="#">${_("close")}</a>
|
||||
</header>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
<div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course">
|
||||
<form>
|
||||
@@ -186,8 +182,8 @@ ${fragment.foot_html()}
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div id="accordion" style="display: none">
|
||||
<nav aria-label="${_('Course Navigation')}">
|
||||
<div class="accordion">
|
||||
<nav class="course-navigation" aria-label="${_('Course')}">
|
||||
% if accordion.strip():
|
||||
${accordion}
|
||||
% else:
|
||||
|
||||
@@ -204,11 +204,11 @@ import pytz
|
||||
activeHeader: "ui-icon-carat-1-s"
|
||||
};
|
||||
var act = 0;
|
||||
$("#accordion").accordion(
|
||||
$(".accordion").accordion(
|
||||
{
|
||||
heightStyle: 'content',
|
||||
activate: function(event, ui) {
|
||||
var active = jQuery("#accordion").accordion('option', 'active');
|
||||
var active = jQuery(".accordion").accordion('option', 'active');
|
||||
$.cookie('saved_index', null);
|
||||
$.cookie('saved_index', active);
|
||||
$('#error-msg').val('');
|
||||
|
||||
@@ -130,6 +130,7 @@ from branding import api as branding_api
|
||||
|
||||
<%block name="js_extra"/>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/noreferrer.js')}" charset="utf-8"></script>
|
||||
<script type="text/javascript" src="${static.url('js/utils/navigation.js')}" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -71,9 +71,6 @@ $("#open_close_accordion a").click(function(){
|
||||
<div class="book-wrapper">
|
||||
|
||||
<section aria-label="${_('Textbook Navigation')}" class="book-sidebar">
|
||||
<header id="open_close_accordion">
|
||||
<a href="#">close</a>
|
||||
</header>
|
||||
|
||||
<ul id="booknav" class="treeview-booknav">
|
||||
<%def name="print_entry(entry)">
|
||||
|
||||
Reference in New Issue
Block a user