Files
edx-platform/xmodule/js/src/sequence/display.js
2023-05-09 13:53:54 +05:00

457 lines
19 KiB
JavaScript

/* eslint-disable no-underscore-dangle */
/* globals Logger, interpolate */
(function() {
'use strict';
this.Sequence = (function() {
function Sequence(element, runtime) {
var self = this;
this.removeBookmarkIconFromActiveNavItem = function(event) {
return Sequence.prototype.removeBookmarkIconFromActiveNavItem.apply(self, [event]);
};
this.addBookmarkIconToActiveNavItem = function(event) {
return Sequence.prototype.addBookmarkIconToActiveNavItem.apply(self, [event]);
};
this._change_sequential = function(direction, event) {
return Sequence.prototype._change_sequential.apply(self, [direction, event]);
};
this.selectPrevious = function(event) {
return Sequence.prototype.selectPrevious.apply(self, [event]);
};
this.selectNext = function(event) {
return Sequence.prototype.selectNext.apply(self, [event]);
};
this.goto = function(event) {
return Sequence.prototype.goto.apply(self, [event]);
};
this.toggleArrows = function() {
return Sequence.prototype.toggleArrows.apply(self);
};
this.addToUpdatedProblems = function(problemId, newContentState, newState) {
return Sequence.prototype.addToUpdatedProblems.apply(self, [problemId, newContentState, newState]);
};
this.hideTabTooltip = function(event) {
return Sequence.prototype.hideTabTooltip.apply(self, [event]);
};
this.displayTabTooltip = function(event) {
return Sequence.prototype.displayTabTooltip.apply(self, [event]);
};
this.arrowKeys = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
};
this.updatedProblems = {};
this.requestToken = $(element).data('request-token');
this.el = $(element).find('.sequence');
this.path = $('.path');
this.contents = this.$('.seq_contents');
this.content_container = this.$('#seq_content');
this.sr_container = this.$('.sr-is-focusable');
this.num_contents = this.contents.length;
this.id = this.el.data('id');
this.getCompletionUrl = runtime.handlerUrl(element, 'get_completion');
this.gotoPositionUrl = runtime.handlerUrl(element, 'goto_position');
this.nextUrl = this.el.data('next-url');
this.prevUrl = this.el.data('prev-url');
this.savePosition = this.el.data('save-position');
this.showCompletion = this.el.data('show-completion');
this.keydownHandler($(element).find('#sequence-list .tab'));
this.base_page_title = ($('title').data('base-title') || '').trim();
this.bind();
this.render(parseInt(this.el.data('position'), 10));
}
Sequence.prototype.$ = function(selector) {
return $(selector, this.el);
};
Sequence.prototype.bind = function() {
this.$('#sequence-list .nav-item').click(this.goto);
this.$('#sequence-list .nav-item').keypress(this.keyDownHandler);
this.el.on('bookmark:add', this.addBookmarkIconToActiveNavItem);
this.el.on('bookmark:remove', this.removeBookmarkIconFromActiveNavItem);
this.$('#sequence-list .nav-item').on('focus mouseenter', this.displayTabTooltip);
this.$('#sequence-list .nav-item').on('blur mouseleave', this.hideTabTooltip);
};
Sequence.prototype.previousNav = function(focused, index) {
var $navItemList,
$sequenceList = $(focused).parent().parent();
if (index === 0) {
$navItemList = $sequenceList.find('li').last();
} else {
$navItemList = $sequenceList.find('li:eq(' + index + ')').prev();
}
$sequenceList.find('.tab').removeClass('visited').removeClass('focused');
$navItemList.find('.tab').addClass('focused').focus();
};
Sequence.prototype.nextNav = function(focused, index, total) {
var $navItemList,
$sequenceList = $(focused).parent().parent();
if (index === total) {
$navItemList = $sequenceList.find('li').first();
} else {
$navItemList = $sequenceList.find('li:eq(' + index + ')').next();
}
$sequenceList.find('.tab').removeClass('visited').removeClass('focused');
$navItemList.find('.tab').addClass('focused').focus();
};
Sequence.prototype.keydownHandler = function(element) {
var self = this;
element.keydown(function(event) {
var key = event.keyCode,
$focused = $(event.currentTarget),
$sequenceList = $focused.parent().parent(),
index = $sequenceList.find('li')
.index($focused.parent()),
total = $sequenceList.find('li')
.size() - 1;
switch (key) {
case self.arrowKeys.LEFT:
event.preventDefault();
self.previousNav($focused, index);
break;
case self.arrowKeys.RIGHT:
event.preventDefault();
self.nextNav($focused, index, total);
break;
// no default
}
});
};
Sequence.prototype.displayTabTooltip = function(event) {
$(event.currentTarget).find('.sequence-tooltip').removeClass('sr');
};
Sequence.prototype.hideTabTooltip = function(event) {
$(event.currentTarget).find('.sequence-tooltip').addClass('sr');
};
Sequence.prototype.updatePageTitle = function() {
// update the page title to include the current section
var currentUnitTitle,
newPageTitle,
positionLink = this.link_for(this.position);
if (positionLink && positionLink.data('page-title')) {
currentUnitTitle = positionLink.data('page-title');
newPageTitle = currentUnitTitle + ' | ' + this.base_page_title;
if (newPageTitle !== document.title) {
document.title = newPageTitle;
}
// Update the title section of the breadcrumb
$('.nav-item-sequence').text(currentUnitTitle);
}
};
Sequence.prototype.hookUpContentStateChangeEvent = function() {
var self = this;
return $('.problems-wrapper').bind('contentChanged', function(event, problemId, newContentState, newState) {
return self.addToUpdatedProblems(problemId, newContentState, newState);
});
};
Sequence.prototype.addToUpdatedProblems = function(problemId, newContentState, newState) {
/**
* Used to keep updated problem's state temporarily.
* params:
* 'problem_id' is problem id.
* 'new_content_state' is the updated content of the problem.
* 'new_state' is the updated state of the problem.
*/
// initialize for the current sequence if there isn't any updated problem for this position.
if (!this.anyUpdatedProblems(this.position)) {
this.updatedProblems[this.position] = {};
}
// Now, put problem content and score against problem id for current active sequence.
this.updatedProblems[this.position][problemId] = [newContentState, newState];
};
Sequence.prototype.anyUpdatedProblems = function(position) {
/**
* check for the updated problems for given sequence position.
* params:
* 'position' can be any sequence position.
*/
return typeof this.updatedProblems[position] !== 'undefined';
};
Sequence.prototype.enableButton = function(buttonClass, buttonAction) {
this.$(buttonClass)
.removeClass('disabled')
.removeAttr('disabled')
.click(buttonAction);
};
Sequence.prototype.disableButton = function(buttonClass) {
this.$(buttonClass).addClass('disabled').attr('disabled', true);
};
Sequence.prototype.updateButtonState = function(buttonClass, buttonAction, isAtBoundary, boundaryUrl) {
if (isAtBoundary && boundaryUrl === 'None') {
this.disableButton(buttonClass);
} else {
this.enableButton(buttonClass, buttonAction);
}
};
Sequence.prototype.toggleArrows = function() {
var isFirstTab, isLastTab, nextButtonClass, previousButtonClass;
this.$('.sequence-nav-button').unbind('click');
// previous button
isFirstTab = this.position === 1;
previousButtonClass = '.sequence-nav-button.button-previous';
this.updateButtonState(previousButtonClass, this.selectPrevious, isFirstTab, this.prevUrl);
// next button
// use inequality in case contents.length is 0 and position is 1.
isLastTab = this.position >= this.contents.length;
nextButtonClass = '.sequence-nav-button.button-next';
this.updateButtonState(nextButtonClass, this.selectNext, isLastTab, this.nextUrl);
};
Sequence.prototype.render = function(newPosition) {
var bookmarked, currentTab, sequenceLinks,
self = this;
if (this.position !== newPosition) {
if (this.position) {
this.mark_visited(this.position);
if (this.showCompletion) {
this.update_completion(this.position);
}
if (this.savePosition) {
$.postWithPrefix(this.gotoPositionUrl, JSON.stringify({
position: newPosition
}));
}
}
// On Sequence change, fire custom event 'sequence:change' on element.
// Added for aborting video bufferization, see ../video/10_main.js
this.el.trigger('sequence:change');
this.mark_active(newPosition);
currentTab = this.contents.eq(newPosition - 1);
bookmarked = this.el.find('.active .bookmark-icon').hasClass('bookmarked');
// update the data-attributes with latest contents only for updated problems.
this.content_container
.html(currentTab.text()) // xss-lint: disable=javascript-jquery-html
.attr('aria-labelledby', currentTab.attr('aria-labelledby'))
.data('bookmarked', bookmarked);
if (this.anyUpdatedProblems(newPosition)) {
$.each(this.updatedProblems[newPosition], function(problemId, latestData) {
var latestContent, latestResponse;
latestContent = latestData[0];
latestResponse = latestData[1];
self.content_container
.find("[data-problem-id='" + problemId + "']")
.data('content', latestContent)
.data('problem-score', latestResponse.current_score)
.data('problem-total-possible', latestResponse.total_possible)
.data('attempts-used', latestResponse.attempts_used);
});
}
XBlock.initializeBlocks(this.content_container, this.requestToken);
// For embedded circuit simulator exercises in 6.002x
if (window.hasOwnProperty('update_schematics')) {
window.update_schematics();
}
this.position = newPosition;
this.toggleArrows();
this.hookUpContentStateChangeEvent();
this.updatePageTitle();
sequenceLinks = this.content_container.find('a.seqnav');
sequenceLinks.click(this.goto);
this.sr_container.focus();
}
};
Sequence.prototype.goto = function(event) {
var alertTemplate, alertText, isBottomNav, newPosition, widgetPlacement;
event.preventDefault();
// Links from courseware <a class='seqnav' href='n'>...</a>, was .target_tab
if ($(event.currentTarget).hasClass('seqnav')) {
newPosition = $(event.currentTarget).attr('href');
} else if ($(event.currentTarget).data('href') !== undefined) {
location.href = $(event.currentTarget).data('href');
return true;
// Tab links generated by backend template
} else {
newPosition = $(event.currentTarget).data('element');
}
if ((newPosition >= 1) && (newPosition <= this.num_contents)) {
isBottomNav = $(event.target).closest('nav[class="sequence-bottom"]').length > 0;
if (isBottomNav) {
widgetPlacement = 'bottom';
} else {
widgetPlacement = 'top';
}
// Formerly known as seq_goto
Logger.log('edx.ui.lms.sequence.tab_selected', {
current_tab: this.position,
target_tab: newPosition,
tab_count: this.num_contents,
id: this.id,
widget_placement: widgetPlacement
});
// On Sequence change, destroy any existing polling thread
// for queued submissions, see ../capa/display.js
if (window.queuePollerID) {
window.clearTimeout(window.queuePollerID);
delete window.queuePollerID;
}
this.render(newPosition);
} else {
alertTemplate = gettext('Sequence error! Cannot navigate to %(tab_name)s in the current SequenceModule. Please contact the course staff.'); // eslint-disable-line max-len
alertText = interpolate(alertTemplate, {
tab_name: newPosition
}, true);
alert(alertText); // eslint-disable-line no-alert
}
};
Sequence.prototype.selectNext = function(event) {
this._change_sequential('next', event);
};
Sequence.prototype.selectPrevious = function(event) {
this._change_sequential('previous', event);
};
// `direction` can be 'previous' or 'next'
Sequence.prototype._change_sequential = function(direction, event) {
var analyticsEventName, isBottomNav, newPosition, offset, targetUrl, widgetPlacement;
// silently abort if direction is invalid.
if (direction !== 'previous' && direction !== 'next') {
return;
}
event.preventDefault();
analyticsEventName = 'edx.ui.lms.sequence.' + direction + '_selected';
isBottomNav = $(event.target).closest('nav[class="sequence-bottom"]').length > 0;
if (isBottomNav) {
widgetPlacement = 'bottom';
} else {
widgetPlacement = 'top';
}
if ((direction === 'next') && (this.position >= this.contents.length)) {
targetUrl = this.nextUrl;
} else if ((direction === 'previous') && (this.position === 1 || this.contents.length === 0)) {
targetUrl = this.prevUrl;
}
// Formerly known as seq_next and seq_prev
Logger.log(analyticsEventName, {
id: this.id,
current_tab: this.position,
tab_count: this.num_contents,
widget_placement: widgetPlacement
}).always(function() {
if (targetUrl) {
// Wait to load the new page until we've attempted to log the event
window.location.href = targetUrl;
}
});
// If we're staying on the page, no need to wait for the event logging to finish
if (!targetUrl) {
// If the bottom nav is used, scroll to the top of the page on change.
if (isBottomNav) {
$.scrollTo(0, 150);
}
offset = {
next: 1,
previous: -1
};
newPosition = this.position + offset[direction];
this.render(newPosition);
}
};
Sequence.prototype.link_for = function(position) {
return this.$('#sequence-list .nav-item[data-element=' + position + ']');
};
Sequence.prototype.mark_visited = function(position) {
// Don't overwrite class attribute to avoid changing Progress class
var element = this.link_for(position);
element.attr({tabindex: '-1', 'aria-selected': 'false', 'aria-expanded': 'false'})
.removeClass('inactive')
.removeClass('active')
.removeClass('focused')
.addClass('visited');
};
Sequence.prototype.update_completion = function(position) {
var element = this.link_for(position);
var usageKey = element[0].attributes['data-id'].value;
var completionIndicators = element.find('.check-circle');
if (completionIndicators.length) {
$.postWithPrefix(this.getCompletionUrl, JSON.stringify({
usage_key: usageKey
}), function(data) {
if (data.complete === true) {
completionIndicators.removeClass('is-hidden');
}
});
}
// Reload progress bar
this.$('#progress-frame').attr('src', this.$('#progress-frame').attr('src'));
};
Sequence.prototype.mark_active = function(position) {
// Don't overwrite class attribute to avoid changing Progress class
var element = this.link_for(position);
element.attr({tabindex: '0', 'aria-selected': 'true', 'aria-expanded': 'true'})
.removeClass('inactive')
.removeClass('visited')
.removeClass('focused')
.addClass('active');
this.$('.sequence-list-wrapper').focus();
};
Sequence.prototype.addBookmarkIconToActiveNavItem = function(event) {
event.preventDefault();
this.el.find('.nav-item.active .bookmark-icon').removeClass('is-hidden').addClass('bookmarked');
this.el.find('.nav-item.active .bookmark-icon-sr').text(gettext('Bookmarked'));
};
Sequence.prototype.removeBookmarkIconFromActiveNavItem = function(event) {
event.preventDefault();
this.el.find('.nav-item.active .bookmark-icon').removeClass('bookmarked').addClass('is-hidden');
this.el.find('.nav-item.active .bookmark-icon-sr').text('');
};
return Sequence;
}());
}).call(this);