Commit 6189ac38 by Matjaz Gregoric

Implement tooltips/clarifications.

The idea is based on capa <clarification> nodes: https://github.com/edx/edx-platform/pull/6679

I decided to go with <span class="pb-clarification> instead of a custom <clarification>
node because supporting clarifications inside the html child block was a requirement,
but the html block is defined inside edx-platform and we aren't able to preprocess the contents
of the html block from within problem-builder.
Even if we were able to get a patch for html block to support <clarification> elements upstream,
I wouldn't feel very comfortable with that idea since the html block is supposed to contain
plain html and making it recognize and preprocess custom elements wouldn't feel right.

The best way to implement this would probably be custom DOM elements (document.registerElement),
but browser support is not there yet and there are no reliable polyfills.
parent 48fd5952
...@@ -123,7 +123,11 @@ class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock): ...@@ -123,7 +123,11 @@ class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock):
fragment = Fragment(u"<p>{}</p>".format(self.question)) fragment = Fragment(u"<p>{}</p>".format(self.question))
self.render_children(context, fragment, can_reorder=True, can_add=False) self.render_children(context, fragment, can_reorder=True, can_add=False)
fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {})) fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {}))
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/questionnaire-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/problem_builder.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/questionnaire_edit.js'))
fragment.initialize_js('QuestionnaireEdit')
return fragment return fragment
def validate_field_data(self, validation, data): def validate_field_data(self, validation, data):
...@@ -208,5 +212,9 @@ class RatingBlock(MCQBlock): ...@@ -208,5 +212,9 @@ class RatingBlock(MCQBlock):
})) }))
self.render_children(context, fragment, can_reorder=True, can_add=False) self.render_children(context, fragment, can_reorder=True, can_add=False)
fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {})) fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {}))
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/questionnaire-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/problem_builder.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/questionnaire_edit.js'))
fragment.initialize_js('QuestionnaireEdit')
return fragment return fragment
...@@ -314,8 +314,10 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -314,8 +314,10 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
'child_content': child_content, 'child_content': child_content,
'missing_dependency_url': self.has_missing_dependency and self.next_step_url, 'missing_dependency_url': self.has_missing_dependency and self.next_step_url,
})) }))
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/mentoring.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.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/problem_builder.js'))
js_file = 'public/js/mentoring_{}_view.js'.format('assessment' if self.is_assessment else 'standard') js_file = 'public/js/mentoring_{}_view.js'.format('assessment' if self.is_assessment else 'standard')
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_file)) fragment.add_javascript_url(self.runtime.local_resource_url(self, js_file))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))
...@@ -746,7 +748,9 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -746,7 +748,9 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', { fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', {
"url_name": self.url_name "url_name": self.url_name
})) }))
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/mentoring_edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring_edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/problem_builder.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_edit.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_edit.js'))
fragment.initialize_js('MentoringEditComponents') fragment.initialize_js('MentoringEditComponents')
return fragment return fragment
......
.pb-clarification span.clarification i {
font-style: normal;
}
.pb-clarification span.clarification i:hover {
color: rgb(0, 159, 230);
}
\ No newline at end of file
...@@ -113,6 +113,8 @@ function MentoringBlock(runtime, element) { ...@@ -113,6 +113,8 @@ function MentoringBlock(runtime, element) {
} }
} }
ProblemBuilder.transformClarifications(element);
if (data.mode === 'standard') { if (data.mode === 'standard') {
MentoringStandardView(runtime, element, mentoring); MentoringStandardView(runtime, element, mentoring);
} }
......
...@@ -17,5 +17,8 @@ function MentoringEditComponents(runtime, element) { ...@@ -17,5 +17,8 @@ function MentoringEditComponents(runtime, element) {
$(this).addClass('disabled'); $(this).addClass('disabled');
} }
}); });
ProblemBuilder.transformClarifications(element);
runtime.listenTo('deleted-child', updateButtons); runtime.listenTo('deleted-child', updateButtons);
} }
window.ProblemBuilder = {
transformClarifications: function(element) {
element = $(element);
var transformExisting = function(node) {
$('.pb-clarification', node).each(function() {
var item = $(this);
var content = item.html();
var clarification = $(
'<span class="clarification" tabindex="0" role="note" aria-label="Clarification">' +
'<i data-tooltip-show-on-click="true" class="fa fa-info-circle" aria-hidden="true"></i>' +
'<span class="sr"></span>' +
'</span>'
);
clarification.find('i').attr('data-tooltip', content);
clarification.find('span.sr').html(content);
item.empty().append(clarification);
});
};
// Transform all span.pb-clarifications already existing inside the element.
transformExisting(element);
// Transform all future span.pb-clarifications using mutation observer.
// It's only needed in the Studio when editing xblock children because the
// block's JS init function isn't called after edits in the Studio.
if (window.MutationObserver) {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
Array.prototype.forEach.call(mutation.addedNodes, function(node) {
transformExisting(node);
});
})
});
observer.observe(element[0], {childList: true, subtree: true});
}
}
};
function QuestionnaireEdit(runtime, element) {
'use strict';
ProblemBuilder.transformClarifications(element);
}
...@@ -173,7 +173,11 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc ...@@ -173,7 +173,11 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
""" """
fragment = super(QuestionnaireAbstractBlock, self).author_edit_view(context) fragment = super(QuestionnaireAbstractBlock, self).author_edit_view(context)
fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {})) fragment.add_content(loader.render_template('templates/html/questionnaire_add_buttons.html', {}))
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/questionnaire-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/problem_builder.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/questionnaire_edit.js'))
fragment.initialize_js('QuestionnaireEdit')
return fragment return fragment
def validate_field_data(self, validation, data): def validate_field_data(self, validation, data):
......
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
"""
Test that <span class="pb-clarification"> elements are transformed into LMS-like tooltips.
"""
# Imports ###########################################################
import ddt
from cgi import escape
from xblockutils.base_test import SeleniumXBlockTest
# Classes ###########################################################
@ddt.ddt
class ClarificationTest(SeleniumXBlockTest):
"""
Test that the content of span.pb-clarification elements is transformed into
tooltip elements.
"""
clarification_text = 'Let me clarify...'
mcq_template = """
<problem-builder>
<pb-mcq question="Who was your favorite character? {clarify}">
<pb-choice value="gaius">Gaius Baltar</pb-choice>
<pb-choice value="adama">Admiral William Adama {clarify}</pb-choice>
<pb-choice value="starbuck">Starbuck</pb-choice>
</pb-mcq>
</problem-builder>
"""
mrq_template = """
<problem-builder>
<pb-mrq question="What makes a great {clarify} MRQ {clarify}?">
<pb-choice value="1">Lots of choices</pb-choice>
<pb-choice value="2">Funny{clarify} choices</pb-choice>
<pb-choice value="3">Not sure {clarify}</pb-choice>
</pb-mrq>
</problem-builder>
"""
rating_template = """
<problem-builder>
<pb-rating name="rating_1_1" question="How do you rate {clarify} Battlestar Galactica?">
<pb-choice value="6">More than 5 stars {clarify}</pb-choice>
</pb-rating>
</problem-builder>
"""
long_answer_template = """
<problem-builder>
<pb-answer question="What did you think {clarify} of the ending?" />
</problem-builder>
"""
def prepare_xml_scenario(self, xml_template):
span = '<span class="pb-clarification">{}</span>'.format(self.clarification_text)
escaped_span = escape(span, quote=True)
return xml_template.format(clarify=escaped_span)
@ddt.data(
(mcq_template, 2),
(mrq_template, 4),
(rating_template, 2),
(long_answer_template, 1),
)
@ddt.unpack
def test_title(self, xml_template, tooltip_count):
self.set_scenario_xml(self.prepare_xml_scenario(xml_template))
pb_element = self.go_to_view()
clarifications = pb_element.find_elements_by_css_selector('span.pb-clarification')
self.assertEqual(len(clarifications), tooltip_count)
for clarification in clarifications:
tooltip = clarification.find_element_by_css_selector('i[data-tooltip]')
self.assertEqual(tooltip.get_attribute('data-tooltip'), self.clarification_text)
sr = clarification.find_element_by_css_selector('span.sr')
self.assertEqual(sr.text, self.clarification_text)
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