Commit 5a0c5535 by Jacek Bzdak

Detach HTML elements from the DOM when hiding a step.

parent ff61a46d
...@@ -1027,6 +1027,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1027,6 +1027,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
})) }))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/lms_util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js'))
fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html")
......
(function () {
/**
* Manager for HTML XBlocks. These blocks are hid by detaching and shown
* by re-attaching them to the DOM.This is only generic way to generically
* handle things like video players (they should stop playing when removed from DOM).
*
* @param html an html xblock
*/
function HtmlManager(html) {
var $element = $(html.element);
var $anchor = $("<span>").addClass("sb-video-anchor").insertBefore($element);
this.show = function () {
$element.insertAfter($anchor);
};
this.hide = function () {
$element.detach()
};
}
/**
*
* Manager for HTML Video child. Videos are paused when hiding them, and re-sized when showing them.
* @param video an video xblock
*
*/
function VideoManager(video) {
this.show = function () {
if (typeof video.resizer === 'undefined'){
// This one is tricky: but it looks like resizer is undefined only if the video is on the
// step that is initially visible (and then no resizing is necessary)
return;
}
video.resizer.align();
};
this.hide = function () {
if (typeof video.videoPlayer === 'undefined'){
// If video player is undefined this means that the video isn't loaded yet, so no need to
// pause it.
return;
}
video.videoPlayer.pause();
};
}
/**
* Manager for Plot Xblocks. Handles updating a plot before displaying it.
* @param plot
*/
function PlotManager(plot) {
this.show = function () {
plot.update();
};
this.hide = function () {};
}
function ChildManager(xblock_element, runtime) {
var Managers = {
'video': VideoManager,
'sb-plot': PlotManager
};
var children = runtime.children(xblock_element);
/**
* A list of managers for children than need special care when showing or hiding.
*
* @type {show, hide}[]
*/
var managedChildren = [];
/***
* This is a workaround for issue where jquery.xblock.Runtime doesn't return HTML Xblocks when querying
* for children.
*
* This can be removed when:
*
* * We allow inclusion of Ooyala blocks inside ProblemBuilder and our clients migrate to Ooyala, in this case
* we may drop special handling of HTML blocks. See discussions in OC-1441.
* * We include HTML blocks in runtime.children for runtime of jquery.xblock, then just add
* `html: HtmlManager` to `Managers`, and remove this block.
*/
$("div.xblock.xblock-student_view.xmodule_HtmlModule", xblock_element).each(function (idx, element) {
managedChildren.push(new HtmlManager({element:element}));
});
for (var idx = 0; idx < children.length; idx++){
var child = children[idx];
// NOTE: While following assertion is true for e.g Video blocks:
// child.type == $(child.element).data('block-type') it is invalidated by for all sb-* blocks
var type = $(child.element).data('block-type');
var constructor = Managers[type];
if (typeof constructor === 'undefined'){
// This block does not requires special care, moving on
continue ;
}
managedChildren.push(new constructor(child));
}
this.show = function () {
for (var idx = 0; idx<managedChildren.length; idx++){
managedChildren[idx].show();
}
};
this.hide = function () {
for (var idx = 0; idx<managedChildren.length; idx++){
managedChildren[idx].hide();
}
};
}
window.ProblemBuilderUtilLMS = {
ChildManager: ChildManager
};
})();
...@@ -7,6 +7,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -7,6 +7,7 @@ function MentoringWithStepsBlock(runtime, element) {
} }
var children = runtime.children(element); var children = runtime.children(element);
var steps = []; var steps = [];
for (var i = 0; i < children.length; i++) { for (var i = 0; i < children.length; i++) {
...@@ -17,20 +18,57 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -17,20 +18,57 @@ function MentoringWithStepsBlock(runtime, element) {
} }
} }
var activeStep = $('.mentoring', element).data('active-step'); var activeStepIndex = $('.mentoring', element).data('active-step');
var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var attemptsTemplate = _.template($('#xblock-attempts-template').html());
var message = $('.sb-step-message', element); var message = $('.sb-step-message', element);
var checkmark, submitDOM, nextDOM, reviewButtonDOM, tryAgainDOM, var checkmark, submitDOM, nextDOM, reviewButtonDOM, tryAgainDOM,
gradeDOM, attemptsDOM, reviewLinkDOM, submitXHR; gradeDOM, attemptsDOM, reviewLinkDOM, submitXHR;
var reviewStepDOM = $("div.xblock[data-block-type=sb-review-step], div.xblock-v1[data-block-type=sb-review-step]", element); var reviewStepDOM = $("div.xblock[data-block-type=sb-review-step], div.xblock-v1[data-block-type=sb-review-step]", element);
var reviewStepAnchor = $("<span>").addClass("review-anchor").insertBefore(reviewStepDOM);
var hasAReviewStep = reviewStepDOM.length == 1; var hasAReviewStep = reviewStepDOM.length == 1;
/**
* Returns the active step
* @returns MentoringStepBlock
*/
function getActiveStep() {
return steps[activeStepIndex];
}
/**
* Calls a function for each registered step. The object passed to this function is a MentoringStepBlock.
*
* @param func single arg function.
*/
function forEachStep(func){
for (var idx=0; idx < steps.length; idx++) {
func(steps[idx]);
}
}
/**
* Displays the active step
*/
function showActiveStep() {
var step = getActiveStep();
step.show();
}
/**
* Hides all steps
*/
function hideAllSteps() {
forEachStep(function(step){
step.hide();
});
}
function isLastStep() { function isLastStep() {
return (activeStep === steps.length-1); return (activeStepIndex === steps.length-1);
} }
function atReviewStep() { function atReviewStep() {
return (activeStep === -1); return (activeStepIndex === -1);
} }
function someAttemptsLeft() { function someAttemptsLeft() {
...@@ -49,7 +87,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -49,7 +87,7 @@ function MentoringWithStepsBlock(runtime, element) {
} else { } else {
checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation');
} }
var step = steps[activeStep]; var step = getActiveStep();
if (typeof step.showFeedback == 'function') { if (typeof step.showFeedback == 'function') {
step.showFeedback(response); step.showFeedback(response);
} }
...@@ -78,14 +116,14 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -78,14 +116,14 @@ function MentoringWithStepsBlock(runtime, element) {
function submit() { function submit() {
submitDOM.attr('disabled', 'disabled'); // Disable the button until the results load. submitDOM.attr('disabled', 'disabled'); // Disable the button until the results load.
var submitUrl = runtime.handlerUrl(element, 'submit'); var submitUrl = runtime.handlerUrl(element, 'submit');
var activeStep = getActiveStep();
var hasQuestion = steps[activeStep].hasQuestion(); var hasQuestion = activeStep.hasQuestion();
var data = steps[activeStep].getSubmitData(); var data = activeStep.getSubmitData();
data["active_step"] = activeStep; data["active_step"] = activeStepIndex;
$.post(submitUrl, JSON.stringify(data)).success(function(response) { $.post(submitUrl, JSON.stringify(data)).success(function(response) {
showFeedback(response); showFeedback(response);
activeStep = response.active_step; activeStepIndex = response.active_step;
if (activeStep === -1) { if (activeStepIndex === -1) {
// We are now showing the review step / end // We are now showing the review step / end
// Update the number of attempts. // Update the number of attempts.
attemptsDOM.data('num_attempts', response.num_attempts); attemptsDOM.data('num_attempts', response.num_attempts);
...@@ -102,15 +140,14 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -102,15 +140,14 @@ function MentoringWithStepsBlock(runtime, element) {
} }
function getResults() { function getResults() {
var step = steps[activeStep]; getActiveStep().getResults(handleReviewResults);
step.getResults(handleReviewResults);
} }
function handleReviewResults(response) { function handleReviewResults(response) {
// Show step-level feedback // Show step-level feedback
showFeedback(response); showFeedback(response);
// Forward to active step to show answer level feedback // Forward to active step to show answer level feedback
var step = steps[activeStep]; var step = getActiveStep();
var results = response.results; var results = response.results;
var options = { var options = {
checkmark: checkmark checkmark: checkmark
...@@ -118,14 +155,11 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -118,14 +155,11 @@ function MentoringWithStepsBlock(runtime, element) {
step.handleReview(results, options); step.handleReview(results, options);
} }
function hideAllSteps() {
for (var i=0; i < steps.length; i++) {
$(steps[i].element).hide();
}
}
function clearSelections() { function clearSelections() {
$('input[type=radio], input[type=checkbox]', element).prop('checked', false); forEachStep(function (step) {
$('input[type=radio], input[type=checkbox]', step.element).prop('checked', false);
});
} }
function cleanAll() { function cleanAll() {
...@@ -139,7 +173,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -139,7 +173,7 @@ function MentoringWithStepsBlock(runtime, element) {
} }
function updateNextLabel() { function updateNextLabel() {
var step = steps[activeStep]; var step = getActiveStep();
nextDOM.attr('value', step.getStepLabel()); nextDOM.attr('value', step.getStepLabel());
} }
...@@ -164,7 +198,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -164,7 +198,7 @@ function MentoringWithStepsBlock(runtime, element) {
nextDOM.on('click', updateDisplay); nextDOM.on('click', updateDisplay);
reviewButtonDOM.on('click', showGrade); reviewButtonDOM.on('click', showGrade);
var step = steps[activeStep]; var step = getActiveStep();
if (step.hasQuestion()) { // Step includes one or more questions if (step.hasQuestion()) { // Step includes one or more questions
nextDOM.attr('disabled', 'disabled'); nextDOM.attr('disabled', 'disabled');
submitDOM.show(); submitDOM.show();
...@@ -217,11 +251,20 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -217,11 +251,20 @@ function MentoringWithStepsBlock(runtime, element) {
reviewButtonDOM.hide(); reviewButtonDOM.hide();
tryAgainDOM.show(); tryAgainDOM.show();
/**
* We detach review step from DOM, this is required to handle HTML
* blocks that can be added to the Review step.
*
* NOTE: This is handled differently than step js. As the html contents
* of review step are replaced with fresh contents in submit function.
*/
reviewStepDOM.insertBefore(reviewStepAnchor);
reviewStepDOM.show(); reviewStepDOM.show();
} }
function hideReviewStep() { function hideReviewStep() {
reviewStepDOM.hide(); reviewStepDOM.hide();
reviewStepDOM.detach();
} }
function getStepToReview(event) { function getStepToReview(event) {
...@@ -231,7 +274,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -231,7 +274,7 @@ function MentoringWithStepsBlock(runtime, element) {
} }
function jumpToReview(stepIndex) { function jumpToReview(stepIndex) {
activeStep = stepIndex; activeStepIndex = stepIndex;
cleanAll(); cleanAll();
showActiveStep(); showActiveStep();
updateNextLabel(); updateNextLabel();
...@@ -245,7 +288,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -245,7 +288,7 @@ function MentoringWithStepsBlock(runtime, element) {
nextDOM.show(); nextDOM.show();
nextDOM.removeAttr('disabled'); nextDOM.removeAttr('disabled');
} }
var step = steps[activeStep]; var step = getActiveStep();
tryAgainDOM.hide(); tryAgainDOM.hide();
if (step.hasQuestion()) { if (step.hasQuestion()) {
...@@ -269,11 +312,6 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -269,11 +312,6 @@ function MentoringWithStepsBlock(runtime, element) {
} // Don't show attempts if unlimited attempts available (max_attempts === 0) } // Don't show attempts if unlimited attempts available (max_attempts === 0)
} }
function showActiveStep() {
var step = steps[activeStep];
$(step.element).show();
step.updateChildren();
}
function onChange() { function onChange() {
// We do not allow users to modify answers belonging to a step after submitting them: // We do not allow users to modify answers belonging to a step after submitting them:
...@@ -286,7 +324,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -286,7 +324,7 @@ function MentoringWithStepsBlock(runtime, element) {
function validateXBlock() { function validateXBlock() {
var isValid = true; var isValid = true;
var step = steps[activeStep]; var step = getActiveStep();
if (step) { if (step) {
isValid = step.validate(); isValid = step.validate();
} }
...@@ -298,16 +336,14 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -298,16 +336,14 @@ function MentoringWithStepsBlock(runtime, element) {
} }
function initSteps(options) { function initSteps(options) {
for (var i=0; i < steps.length; i++) { forEachStep(function (step) {
var step = steps[i]; options.mentoring = {
var mentoring = {
setContent: setContent, setContent: setContent,
publish_event: publishEvent, publish_event: publishEvent,
is_step_builder: true is_step_builder: true
}; };
options.mentoring = mentoring;
step.initChildren(options); step.initChildren(options);
} });
} }
function setContent(dom, content) { function setContent(dom, content) {
...@@ -347,7 +383,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -347,7 +383,7 @@ function MentoringWithStepsBlock(runtime, element) {
} }
function reviewNextStep() { function reviewNextStep() {
jumpToReview(activeStep+1); jumpToReview(activeStepIndex+1);
} }
function handleTryAgain(result) { function handleTryAgain(result) {
...@@ -356,7 +392,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -356,7 +392,7 @@ function MentoringWithStepsBlock(runtime, element) {
// and interrupting their experience with the current unit // and interrupting their experience with the current unit
notify('navigation', {state: 'lock'}); notify('navigation', {state: 'lock'});
activeStep = result.active_step; activeStepIndex = result.active_step;
clearSelections(); clearSelections();
updateDisplay(); updateDisplay();
tryAgainDOM.hide(); tryAgainDOM.hide();
......
...@@ -4,6 +4,8 @@ function MentoringStepBlock(runtime, element) { ...@@ -4,6 +4,8 @@ function MentoringStepBlock(runtime, element) {
var submitXHR, resultsXHR, var submitXHR, resultsXHR,
message = $(element).find('.sb-step-message'); message = $(element).find('.sb-step-message');
var childManager = new ProblemBuilderUtilLMS.ChildManager(element, runtime);
function callIfExists(obj, fn) { function callIfExists(obj, fn) {
if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') { if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') {
...@@ -13,13 +15,6 @@ function MentoringStepBlock(runtime, element) { ...@@ -13,13 +15,6 @@ function MentoringStepBlock(runtime, element) {
} }
} }
function updateVideo(video) {
video.resizer.align();
}
function updatePlot(plot) {
plot.update();
}
return { return {
...@@ -56,7 +51,7 @@ function MentoringStepBlock(runtime, element) { ...@@ -56,7 +51,7 @@ function MentoringStepBlock(runtime, element) {
}, },
showFeedback: function(response) { showFeedback: function(response) {
// Called when user has just submitted an answer or is reviewing their answer durign extended feedback. // Called when user has just submitted an answer or is reviewing their answer during extended feedback.
if (message.length) { if (message.length) {
message.fadeIn(); message.fadeIn();
$(document).click(function() { $(document).click(function() {
...@@ -110,20 +105,15 @@ function MentoringStepBlock(runtime, element) { ...@@ -110,20 +105,15 @@ function MentoringStepBlock(runtime, element) {
return $('.sb-step', element).data('has-question'); return $('.sb-step', element).data('has-question');
}, },
updateChildren: function() { show: function () {
children.forEach(function(child) { $(element).show();
var type = $(child.element).data('block-type'); childManager.show();
switch (type) { },
case 'video':
updateVideo(child);
break;
case 'sb-plot':
updatePlot(child);
break;
}
});
}
hide: function () {
$(element).hide();
childManager.hide();
}
}; };
} }
...@@ -624,9 +624,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -624,9 +624,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
@data(True, False) @data(True, False)
def test_conditional_messages(self, include_messages): def test_conditional_messages(self, include_messages):
""" # Test that conditional messages in the review step are visible or not, as appropriate.
Test that conditional messages in the review step are visible or not, as appropriate.
"""
max_attempts = 3 max_attempts = 3
extended_feedback = False extended_feedback = False
params = { params = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment