mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
Merge branch 'MDL-78662-master' of https://github.com/NashTechOpenUniversity/moodle
This commit is contained in:
commit
10d1a28dda
@ -1134,6 +1134,20 @@ abstract class question_utils {
|
||||
|
||||
return $editoroptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format question fragment string and apply filtering,
|
||||
*
|
||||
* @param string $text current text that we want to be apply filters.
|
||||
* @param context $context of the page question are in.
|
||||
* @return string result has been modified by filters.
|
||||
*/
|
||||
public static function format_question_fragment(string $text, context $context): string {
|
||||
global $PAGE;
|
||||
$filtermanager = \filter_manager::instance();
|
||||
$filtermanager->setup_page_for_filters($PAGE, $context);
|
||||
return $filtermanager->filter_string($text, $context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -226,4 +226,32 @@ class questionutils_test extends \advanced_testcase {
|
||||
$this->assertSame(-1.5, question_utils::clean_param_mark('-1.5'));
|
||||
$this->assertSame(-1.5, question_utils::clean_param_mark('-1,5'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the content is being filtered by filters.
|
||||
*
|
||||
* @covers ::format_question_fragment
|
||||
*/
|
||||
public function test_format_question_fragment(): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/filterlib.php');
|
||||
$this->resetAfterTest();
|
||||
// Set few filters on.
|
||||
filter_set_global_state('multilang', TEXTFILTER_ON);
|
||||
filter_set_global_state('mathjaxloader', TEXTFILTER_ON);
|
||||
filter_set_applies_to_strings('multilang', 1);
|
||||
filter_set_applies_to_strings('mathjaxloader', 1);
|
||||
|
||||
$systemcontext = \context_system::instance();
|
||||
$input = 'Some inline math \\( y = x^2 \\) and multi lang with html tag
|
||||
<span lang="en" class="multilang"><b>English</b></span><span lang="fr" class="multilang">Français</span>';
|
||||
|
||||
$expected = question_utils::format_question_fragment($input, $systemcontext);
|
||||
|
||||
// The data should only be filtered by mathjax and multi lang filter. HTML tags should not be affeacted.
|
||||
$this->assertStringContainsString('<span class="filter_mathjaxloader_equation">Some inline math', $expected);
|
||||
$this->assertStringContainsString('<span class="nolink">\( y = x^2 \)</span>', $expected);
|
||||
$this->assertStringNotContainsString('<span lang="en" class="multilang">', $expected);
|
||||
$this->assertStringContainsString('<b>English</b>', $expected);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -24,12 +24,14 @@ define([
|
||||
'jquery',
|
||||
'core/dragdrop',
|
||||
'core/key_codes',
|
||||
'core_form/changechecker'
|
||||
'core_form/changechecker',
|
||||
'core_filters/events',
|
||||
], function(
|
||||
$,
|
||||
dragDrop,
|
||||
keys,
|
||||
FormChangeChecker
|
||||
FormChangeChecker,
|
||||
filterEvent
|
||||
) {
|
||||
|
||||
"use strict";
|
||||
@ -45,6 +47,7 @@ define([
|
||||
function DragDropOntoImageQuestion(containerId, readOnly, places) {
|
||||
this.containerId = containerId;
|
||||
this.questionAnswer = {};
|
||||
this.questionDragDropWidthHeight = [];
|
||||
M.util.js_pending('qtype_ddimageortext-init-' + this.containerId);
|
||||
this.places = places;
|
||||
this.allImagesLoaded = false;
|
||||
@ -61,6 +64,82 @@ define([
|
||||
this.waitForAllImagesToBeLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change all the drags and drops related to the item that has been changed by filter to correct size and content.
|
||||
*
|
||||
* @param {object} filteredElement the element has been modified by filter.
|
||||
*/
|
||||
DragDropOntoImageQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) {
|
||||
let currentFilteredItem = $(filteredElement);
|
||||
const parentIsDD = currentFilteredItem.parent().closest('div').hasClass('placed') ||
|
||||
currentFilteredItem.parent().hasClass('draghome');
|
||||
const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome');
|
||||
// The filtered element or parent element should a drag or drop item.
|
||||
if (!parentIsDD && !isDD) {
|
||||
return;
|
||||
}
|
||||
if (parentIsDD) {
|
||||
currentFilteredItem = currentFilteredItem.parent().closest('div');
|
||||
}
|
||||
if (this.getRoot().find(currentFilteredItem).length <= 0) {
|
||||
// If the DD item doesn't belong to this question
|
||||
// In case we have multiple questions in the same page.
|
||||
return;
|
||||
}
|
||||
const group = this.getGroup(currentFilteredItem),
|
||||
choice = this.getChoice(currentFilteredItem);
|
||||
let listOfModifiedDragDrop = [];
|
||||
// Get the list of drag and drop item within the same group and choice.
|
||||
this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) {
|
||||
// Same modified item, skip it.
|
||||
if ($(node).get(0) === currentFilteredItem.get(0)) {
|
||||
return;
|
||||
}
|
||||
const originalClass = $(node).attr('class');
|
||||
const originalStyle = $(node).attr('style');
|
||||
// We want to keep all the handler and event for filtered item, so using clone is the only choice.
|
||||
const filteredDragDropClone = currentFilteredItem.clone();
|
||||
// Sometimes, for the question that has a lot of input groups and unlimited draggable items,
|
||||
// this 'clone' process takes longer than usual,it will not add the eventHandler for this cloned drag.
|
||||
// We need to make sure to add the eventHandler for the cloned drag too.
|
||||
questionManager.addEventHandlersToDrag(filteredDragDropClone);
|
||||
// Replace the class and style of the drag drop item we want to replace for the clone.
|
||||
filteredDragDropClone.attr('class', originalClass);
|
||||
filteredDragDropClone.attr('style', originalStyle);
|
||||
// Insert into DOM.
|
||||
$(node).before(filteredDragDropClone);
|
||||
// Add the item has been replaced to a list so we can remove it later.
|
||||
listOfModifiedDragDrop.push(node);
|
||||
});
|
||||
|
||||
listOfModifiedDragDrop.forEach(function(node) {
|
||||
$(node).remove();
|
||||
});
|
||||
// Save the current height and width.
|
||||
const currentHeight = currentFilteredItem.height();
|
||||
const currentWidth = currentFilteredItem.width();
|
||||
// Set to auto, so we can get the real height and width of the filtered item.
|
||||
currentFilteredItem.height('auto');
|
||||
currentFilteredItem.width('auto');
|
||||
// We need to set display block so we can get height and width.
|
||||
// Some browsers can't get the offsetWidth/Height if they are an inline element like span tag.
|
||||
if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) {
|
||||
filteredElement.classList.add('d-block');
|
||||
}
|
||||
if (this.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) ||
|
||||
this.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) {
|
||||
// Remove the d-block class before calculation.
|
||||
filteredElement.classList.remove('d-block');
|
||||
// Now resize all the items in the same group if we have new maximum width or height.
|
||||
this.resizeAllDragsAndDropsInGroup(group);
|
||||
} else {
|
||||
currentFilteredItem.height(currentHeight);
|
||||
currentFilteredItem.width(currentWidth);
|
||||
}
|
||||
// Remove the d-block class after resize.
|
||||
filteredElement.classList.remove('d-block');
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits until all images are loaded before calling setupQuestion().
|
||||
*
|
||||
@ -94,6 +173,12 @@ define([
|
||||
// We now have all images. Carry on, but only after giving the layout a chance to settle down.
|
||||
this.allImagesLoaded = true;
|
||||
thisQ.setupQuestion();
|
||||
// Wait for all dynamic content loaded by filter to be completed.
|
||||
document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {
|
||||
elements.detail.nodes.forEach((element) => {
|
||||
thisQ.changeAllDragsAndDropsToFilteredContent(element);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -135,7 +220,7 @@ define([
|
||||
var thisQ = this;
|
||||
this.getRoot().find('.draghomes > div').each(function(i, node) {
|
||||
thisQ.resizeAllDragsAndDropsInGroup(
|
||||
thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));
|
||||
thisQ.getClassnameNumericSuffix($(node), 'dragitemgroup'));
|
||||
});
|
||||
};
|
||||
|
||||
@ -146,7 +231,7 @@ define([
|
||||
*/
|
||||
DragDropOntoImageQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {
|
||||
var root = this.getRoot(),
|
||||
dragHomes = root.find('.dragitemgroup' + group + ' .draghome'),
|
||||
dragHomes = root.find(".draghome.group" + group),
|
||||
maxWidth = 0,
|
||||
maxHeight = 0;
|
||||
|
||||
@ -159,18 +244,11 @@ define([
|
||||
// The size we will want to set is a bit bigger than this.
|
||||
maxWidth += 10;
|
||||
maxHeight += 10;
|
||||
this.questionDragDropWidthHeight[group] = {maxWidth, maxHeight};
|
||||
|
||||
// Set each drag home to that size.
|
||||
dragHomes.each(function(i, drag) {
|
||||
var left = Math.round((maxWidth - drag.offsetWidth) / 2),
|
||||
top = Math.floor((maxHeight - drag.offsetHeight) / 2);
|
||||
// Set top and left padding so the item is centred.
|
||||
$(drag).css({
|
||||
'padding-left': left + 'px',
|
||||
'padding-right': (maxWidth - drag.offsetWidth - left) + 'px',
|
||||
'padding-top': top + 'px',
|
||||
'padding-bottom': (maxHeight - drag.offsetHeight - top) + 'px'
|
||||
});
|
||||
$(drag).width(maxWidth).height(maxHeight).css('lineHeight', maxHeight + 'px');
|
||||
});
|
||||
|
||||
// Create the drops and make them the right size.
|
||||
@ -186,9 +264,11 @@ define([
|
||||
if (label === '') {
|
||||
label = M.util.get_string('blank', 'qtype_ddimageortext');
|
||||
}
|
||||
root.find('.dropzones').append('<div class="dropzone active group' + place.group +
|
||||
' place' + i + '" tabindex="0">' +
|
||||
if (root.find('.dropzones .dropzone.group' + place.group + '.place' + i).length === 0) {
|
||||
root.find('.dropzones').append('<div class="dropzone active group' + place.group +
|
||||
' place' + i + '" tabindex="0">' +
|
||||
'<span class="accesshide">' + label + '</span> </div>');
|
||||
}
|
||||
root.find('.dropzone.place' + i).width(maxWidth - 2).height(maxHeight - 2);
|
||||
}
|
||||
};
|
||||
|
@ -52,7 +52,6 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere
|
||||
|
||||
public function formulation_and_controls(question_attempt $qa,
|
||||
question_display_options $options) {
|
||||
|
||||
$question = $qa->get_question();
|
||||
$response = $qa->get_last_qt_data();
|
||||
|
||||
@ -92,6 +91,7 @@ class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_rendere
|
||||
$classes[] = 'infinite';
|
||||
}
|
||||
if ($dragimageurl === null) {
|
||||
$dragimage->text = question_utils::format_question_fragment($dragimage->text, $this->page->context);
|
||||
$dragimagehomesgroup .= html_writer::div($dragimage->text, join(' ', $classes), ['src' => $dragimageurl]);
|
||||
} else {
|
||||
$dragimagehomesgroup .= html_writer::img($dragimageurl, $dragimage->text, ['class' => join(' ', $classes)]);
|
||||
|
@ -85,6 +85,10 @@ form.mform fieldset#id_previewareaheader .droppreview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.que.ddimageortext .MathJax_Display {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.que.ddimageortext .draghomes .draghome.dragplaceholder.active {
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
|
@ -18,8 +18,9 @@ Feature: Preview a drag-drop onto image question
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddimageortext | Drag onto image | xsection |
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddimageortext | Drag onto image | xsection |
|
||||
| Test questions | ddimageortext | Drag to mathjax equation | mathjax |
|
||||
|
||||
@javascript @_bug_phantomjs
|
||||
Scenario: Preview a question using the mouse.
|
||||
@ -53,3 +54,12 @@ Feature: Preview a drag-drop onto image question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Identify the features" question is shown as "Correct"
|
||||
And I should see "Mark 1.00 out of 1.00"
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a drag-drop into image question with mathjax question.
|
||||
Given the "mathjaxloader" filter is "on"
|
||||
And the "mathjaxloader" filter applies to "content and headings"
|
||||
And I am on the "Drag to mathjax equation" "core_question > preview" page logged in as teacher
|
||||
And I press "Fill in correct responses"
|
||||
When I press "Submit and finish"
|
||||
Then ".filter_mathjaxloader_equation" "css_element" should exist in the ".draghome" "css_element"
|
||||
|
@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class qtype_ddimageortext_test_helper extends question_test_helper {
|
||||
public function get_test_questions() {
|
||||
return array('fox', 'maths', 'xsection', 'mixedlang');
|
||||
return ['fox', 'maths', 'xsection', 'mixedlang', 'mathjax'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,6 +251,54 @@ class qtype_ddimageortext_test_helper extends question_test_helper {
|
||||
return $fromform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data required to save a drag-drop into text question where the the answer contain equation
|
||||
*
|
||||
*
|
||||
* @return stdClass data to create a ddwtos question.
|
||||
*/
|
||||
public function get_ddimageortext_question_form_data_mathjax() {
|
||||
global $CFG, $USER;
|
||||
$fromform = new stdClass();
|
||||
|
||||
$bgdraftitemid = 0;
|
||||
file_prepare_draft_area($bgdraftitemid, null, null, null, null);
|
||||
$fs = get_file_storage();
|
||||
$filerecord = new stdClass();
|
||||
$filerecord->contextid = context_user::instance($USER->id)->id;
|
||||
$filerecord->component = 'user';
|
||||
$filerecord->filearea = 'draft';
|
||||
$filerecord->itemid = $bgdraftitemid;
|
||||
$filerecord->filepath = '/';
|
||||
$filerecord->filename = 'oceanfloorbase.jpg';
|
||||
$fs->create_file_from_pathname($filerecord, $CFG->dirroot .
|
||||
'/question/type/ddimageortext/tests/fixtures/oceanfloorbase.jpg');
|
||||
$fromform->name = 'Drag-and-drop words into image question with equation';
|
||||
$fromform->questiontext = ['text' => 'Fill in the correct mathjax equation: y = 2, x =4', 'format' => FORMAT_HTML];
|
||||
$fromform->defaultmark = 1.0;
|
||||
$fromform->generalfeedback = ['text' => 'The right answer is: "y = x^2"', 'format' => FORMAT_HTML];
|
||||
$fromform->drags = [
|
||||
['dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'],
|
||||
['dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'],
|
||||
];
|
||||
$fromform->bgimage = $bgdraftitemid;
|
||||
$fromform->dragitem = [0, 0];
|
||||
$fromform->draglabel =
|
||||
[
|
||||
'$$ y = x^2 $$',
|
||||
'$$ y = x^5 $$',
|
||||
];
|
||||
$fromform->drops = [
|
||||
['xleft' => '53', 'ytop' => '17', 'choice' => '1', 'droplabel' => ''],
|
||||
['xleft' => '172', 'ytop' => '2', 'choice' => '2', 'droplabel' => ''],
|
||||
];
|
||||
test_question_maker::set_standard_combined_feedback_form_data($fromform);
|
||||
$fromform->shownumcorrect = 0;
|
||||
$fromform->penalty = 0.3333333;
|
||||
$fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
return $fromform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a test question where the drag items are a different language than the main question text.
|
||||
*
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -26,13 +26,15 @@ define([
|
||||
'core/dragdrop',
|
||||
'qtype_ddmarker/shapes',
|
||||
'core/key_codes',
|
||||
'core_form/changechecker'
|
||||
'core_form/changechecker',
|
||||
'core_filters/events',
|
||||
], function(
|
||||
$,
|
||||
dragDrop,
|
||||
Shapes,
|
||||
keys,
|
||||
FormChangeChecker
|
||||
FormChangeChecker,
|
||||
filterEvent
|
||||
) {
|
||||
|
||||
"use strict";
|
||||
@ -757,7 +759,7 @@ define([
|
||||
* a time-out, because image on-loads are allegedly unreliable.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.waitForAllImagesToBeLoaded = function() {
|
||||
|
||||
var thisQ = this;
|
||||
// This method may get called multiple times (via image on-loads or timeouts.
|
||||
// If we are already done, don't do it again.
|
||||
if (this.allImagesLoaded) {
|
||||
@ -784,6 +786,64 @@ define([
|
||||
this.cloneDrags();
|
||||
this.repositionDrags();
|
||||
this.drawDropzones();
|
||||
// Wait for all dynamic content loaded by filter to be completed.
|
||||
document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {
|
||||
elements.detail.nodes.forEach((element) => {
|
||||
thisQ.changeAllMakerToFilteredContent(element);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change all the maker related to the item that has been changed by filter to correct size and content.
|
||||
*
|
||||
* @param {object} filteredElement the element has been modified by filter.
|
||||
*/
|
||||
DragDropMarkersQuestion.prototype.changeAllMakerToFilteredContent = function(filteredElement) {
|
||||
let currentFilteredItem = $(filteredElement);
|
||||
const parentIsMarker = currentFilteredItem.parent().closest('span.marker');
|
||||
const isMarker = currentFilteredItem.hasClass('marker');
|
||||
const root = this.getRoot();
|
||||
// The filtered element or parent element should a drag or drop item.
|
||||
if (!parentIsMarker && !isMarker) {
|
||||
return;
|
||||
}
|
||||
if (parentIsMarker) {
|
||||
currentFilteredItem = currentFilteredItem.parent().closest('span.marker');
|
||||
}
|
||||
if (root.find(currentFilteredItem).length <= 0) {
|
||||
// If the maker doesn't belong to this question
|
||||
// In case we have multiple questions in the same page.
|
||||
return;
|
||||
}
|
||||
const dragNo = this.getDragNo(currentFilteredItem);
|
||||
const choiceNo = this.getChoiceNoFromElement(currentFilteredItem);
|
||||
const listOfContainerToBeModifed = [
|
||||
'div.draghomes .marker:not(.dragplaceholder).dragno' + dragNo + '.choice' + choiceNo,
|
||||
'div.droparea .marker:not(.dragplaceholder).dragno' + dragNo + '.choice' + choiceNo,
|
||||
'div.draghomes .marker:not(.dragplaceholder).infinite.choice' + choiceNo,
|
||||
'div.droparea .marker:not(.dragplaceholder).infinite.choice' + choiceNo
|
||||
];
|
||||
let listOfModifiedDragDrop = [];
|
||||
// We want to keep all the handler and event for filtered item, so using clone is the only choice.
|
||||
const filteredDragDropClone = currentFilteredItem.clone();
|
||||
listOfContainerToBeModifed.forEach(function(selector) {
|
||||
root.find(selector).each(function(i, node) {
|
||||
const originalClass = $(node).attr('class');
|
||||
const originalStyle = $(node).attr('style');
|
||||
// Replace the class and style of the maker we want to replace for the clone.
|
||||
filteredDragDropClone.attr('class', originalClass);
|
||||
filteredDragDropClone.attr('style', originalStyle);
|
||||
// Add event for the clone.
|
||||
questionManager.addEventHandlersToMarker(filteredDragDropClone);
|
||||
// Insert into DOM.
|
||||
$(node).before(filteredDragDropClone);
|
||||
listOfModifiedDragDrop.push(node);
|
||||
});
|
||||
});
|
||||
listOfModifiedDragDrop.forEach(function(node) {
|
||||
$(node).remove();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -68,6 +68,7 @@ class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
|
||||
|
||||
$orderedgroup = $question->get_ordered_choices(1);
|
||||
$hiddenfields = '';
|
||||
$dragitems = '';
|
||||
foreach ($orderedgroup as $choiceno => $drag) {
|
||||
$classes = ['marker', 'user-select-none', 'choice' . $choiceno];
|
||||
$attr = [];
|
||||
@ -81,14 +82,17 @@ class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
|
||||
}
|
||||
$dragoutput = html_writer::start_span(join(' ', $classes), $attr);
|
||||
$targeticonhtml = $this->output->image_icon('crosshairs', '', $componentname, ['class' => 'target']);
|
||||
$markertext = html_writer::span($drag->text, 'markertext');
|
||||
$markertext = html_writer::span(question_utils::format_question_fragment($drag->text, $this->page->context),
|
||||
'markertext');
|
||||
$dragoutput .= $targeticonhtml . $markertext;
|
||||
$dragoutput .= html_writer::end_span();
|
||||
$output .= $dragoutput;
|
||||
$dragitems .= $dragoutput;
|
||||
$hiddenfields .= $this->hidden_field_choice($qa, $choiceno, $drag->infinite, $drag->noofdrags);
|
||||
}
|
||||
|
||||
$output .= $dragitems;
|
||||
$output .= html_writer::end_div();
|
||||
// Add extra hidden drag items so we can make sure the filter will be applied.
|
||||
$output .= html_writer::div($dragitems, 'dd-original d-none');
|
||||
$output .= html_writer::end_div();
|
||||
|
||||
if ($question->showmisplaced && $qa->get_state()->is_finished()) {
|
||||
|
@ -18,8 +18,9 @@ Feature: Preview a drag-drop marker question
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddmarker | Drag markers | mkmap |
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddmarker | Drag markers | mkmap |
|
||||
| Test questions | ddmarker | Drag to mathjax equation | mathjax |
|
||||
|
||||
@javascript @_bug_phantomjs
|
||||
Scenario: Preview a question using the mouse
|
||||
@ -56,3 +57,12 @@ Feature: Preview a drag-drop marker question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Correct"
|
||||
And I should see "Mark 1.00 out of 1.00"
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a drag-drop marker question with mathjax question.
|
||||
Given the "mathjaxloader" filter is "on"
|
||||
And the "mathjaxloader" filter applies to "content and headings"
|
||||
And I am on the "Drag to mathjax equation" "core_question > preview" page logged in as teacher
|
||||
And I press "Fill in correct responses"
|
||||
When I press "Submit and finish"
|
||||
Then ".filter_mathjaxloader_equation" "css_element" should exist in the ".droparea" "css_element"
|
||||
|
@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class qtype_ddmarker_test_helper extends question_test_helper {
|
||||
public function get_test_questions() {
|
||||
return array('fox', 'maths', 'mkmap', 'zerodrag');
|
||||
return ['fox', 'maths', 'mkmap', 'zerodrag', 'mathjax'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,6 +198,50 @@ class qtype_ddmarker_test_helper extends question_test_helper {
|
||||
return $fromform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the test data needed by the question generator to create question tempplate.
|
||||
*
|
||||
* @return stdClass date to create a ddmarkers question.
|
||||
*/
|
||||
public function get_ddmarker_question_form_data_mathjax() {
|
||||
global $CFG, $USER;
|
||||
$fromform = new stdClass();
|
||||
|
||||
$bgdraftitemid = 0;
|
||||
file_prepare_draft_area($bgdraftitemid, null, null, null, null);
|
||||
$fs = get_file_storage();
|
||||
$filerecord = new stdClass();
|
||||
$filerecord->contextid = context_user::instance($USER->id)->id;
|
||||
$filerecord->component = 'user';
|
||||
$filerecord->filearea = 'draft';
|
||||
$filerecord->itemid = $bgdraftitemid;
|
||||
$filerecord->filepath = '/';
|
||||
$filerecord->filename = 'mkmap.png';
|
||||
$fs->create_file_from_pathname($filerecord, $CFG->dirroot .
|
||||
'/question/type/ddmarker/tests/fixtures/mkmap.png');
|
||||
|
||||
$fromform->name = 'Drag-and-drop into maker question with equation';
|
||||
$fromform->questiontext = ['text' => 'Fill in the correct mathjax equation: y = 2, x =4', 'format' => FORMAT_HTML];
|
||||
|
||||
$fromform->defaultmark = 1;
|
||||
$fromform->generalfeedback = ['text' => 'The right answer is: "y = x^2"', 'format' => FORMAT_HTML];
|
||||
$fromform->bgimage = $bgdraftitemid;
|
||||
$fromform->shuffleanswers = 0;
|
||||
$fromform->drags = [
|
||||
['label' => '$$ y = x^2 $$', 'noofdrags' => '1'],
|
||||
['label' => '$$ y = x^5 $$', 'noofdrags' => '1'],
|
||||
];
|
||||
$fromform->drops = [
|
||||
['shape' => 'circle', 'coords' => '322,213;10', 'choice' => 1],
|
||||
['shape' => 'circle', 'coords' => '144,84;10', 'choice' => 2],
|
||||
];
|
||||
test_question_maker::set_standard_combined_feedback_form_data($fromform);
|
||||
$fromform->penalty = '0.3333333';
|
||||
$fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
|
||||
return $fromform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the test data needed by the question generator (the data that
|
||||
* would come from saving the editing form).
|
||||
|
2
question/type/ddwtos/amd/build/ddwtos.min.js
vendored
2
question/type/ddwtos/amd/build/ddwtos.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -41,12 +41,14 @@ define([
|
||||
'jquery',
|
||||
'core/dragdrop',
|
||||
'core/key_codes',
|
||||
'core_form/changechecker'
|
||||
'core_form/changechecker',
|
||||
'core_filters/events',
|
||||
], function(
|
||||
$,
|
||||
dragDrop,
|
||||
keys,
|
||||
FormChangeChecker
|
||||
FormChangeChecker,
|
||||
filterEvent
|
||||
) {
|
||||
|
||||
"use strict";
|
||||
@ -59,14 +61,22 @@ define([
|
||||
* @constructor
|
||||
*/
|
||||
function DragDropToTextQuestion(containerId, readOnly) {
|
||||
const thisQ = this;
|
||||
this.containerId = containerId;
|
||||
this.questionAnswer = {};
|
||||
this.questionDragDropWidthHeight = [];
|
||||
if (readOnly) {
|
||||
this.getRoot().addClass('qtype_ddwtos-readonly');
|
||||
}
|
||||
this.resizeAllDragsAndDrops();
|
||||
this.cloneDrags();
|
||||
this.positionDrags();
|
||||
// Wait for all dynamic content loaded by filter to be completed.
|
||||
document.addEventListener(filterEvent.eventTypes.filterContentRenderingComplete, (elements) => {
|
||||
elements.detail.nodes.forEach((element) => {
|
||||
thisQ.changeAllDragsAndDropsToFilteredContent(element);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,12 +97,12 @@ define([
|
||||
*/
|
||||
DragDropToTextQuestion.prototype.resizeAllDragsAndDropsInGroup = function(group) {
|
||||
var thisQ = this,
|
||||
dragHomes = this.getRoot().find('.draggrouphomes' + group + ' span.draghome'),
|
||||
dragDropItems = this.getRoot().find('span.group' + group),
|
||||
maxWidth = 0,
|
||||
maxHeight = 0;
|
||||
|
||||
// Find the maximum size of any drag in this groups.
|
||||
dragHomes.each(function(i, drag) {
|
||||
dragDropItems.each(function(i, drag) {
|
||||
maxWidth = Math.max(maxWidth, Math.ceil(drag.offsetWidth));
|
||||
maxHeight = Math.max(maxHeight, Math.ceil(0 + drag.offsetHeight));
|
||||
});
|
||||
@ -100,16 +110,85 @@ define([
|
||||
// The size we will want to set is a bit bigger than this.
|
||||
maxWidth += 8;
|
||||
maxHeight += 2;
|
||||
|
||||
thisQ.questionDragDropWidthHeight[group] = {maxWidth: maxWidth, maxHeight: maxHeight};
|
||||
// Set each drag home to that size.
|
||||
dragHomes.each(function(i, drag) {
|
||||
dragDropItems.each(function(i, drag) {
|
||||
thisQ.setElementSize(drag, maxWidth, maxHeight);
|
||||
});
|
||||
};
|
||||
|
||||
// Set each drop to that size.
|
||||
this.getRoot().find('span.drop.group' + group).each(function(i, drop) {
|
||||
thisQ.setElementSize(drop, maxWidth, maxHeight);
|
||||
/**
|
||||
* Change all the drags and drops related to the item that has been changed by filter to correct size and content.
|
||||
*
|
||||
* @param {object} filteredElement the element has been modified by filter.
|
||||
*/
|
||||
DragDropToTextQuestion.prototype.changeAllDragsAndDropsToFilteredContent = function(filteredElement) {
|
||||
let currentFilteredItem = $(filteredElement);
|
||||
const parentIsDD = currentFilteredItem.parent().closest('span').hasClass('placed') ||
|
||||
currentFilteredItem.parent().closest('span').hasClass('draghome');
|
||||
const isDD = currentFilteredItem.hasClass('placed') || currentFilteredItem.hasClass('draghome');
|
||||
// The filtered element or parent element should a drag or drop item.
|
||||
if (!parentIsDD && !isDD) {
|
||||
return;
|
||||
}
|
||||
if (parentIsDD) {
|
||||
currentFilteredItem = currentFilteredItem.parent().closest('span');
|
||||
}
|
||||
const thisQ = this;
|
||||
if (thisQ.getRoot().find(currentFilteredItem).length <= 0) {
|
||||
// If the DD item doesn't belong to this question
|
||||
// In case we have multiple questions in the same page.
|
||||
return;
|
||||
}
|
||||
const group = thisQ.getGroup(currentFilteredItem),
|
||||
choice = thisQ.getChoice(currentFilteredItem);
|
||||
let listOfModifiedDragDrop = [];
|
||||
// Get the list of drag and drop item within the same group and choice.
|
||||
this.getRoot().find('.group' + group + '.choice' + choice).each(function(i, node) {
|
||||
// Same modified item, skip it.
|
||||
if ($(node).get(0) === currentFilteredItem.get(0)) {
|
||||
return;
|
||||
}
|
||||
const originalClass = $(node).attr('class');
|
||||
const originalStyle = $(node).attr('style');
|
||||
// We want to keep all the handler and event for filtered item, so using clone is the only choice.
|
||||
const filteredDragDropClone = currentFilteredItem.clone();
|
||||
// Replace the class and style of the drag drop item we want to replace for the clone.
|
||||
filteredDragDropClone.attr('class', originalClass);
|
||||
filteredDragDropClone.attr('style', originalStyle);
|
||||
// Insert into DOM.
|
||||
$(node).before(filteredDragDropClone);
|
||||
// Add the item has been replaced to a list so we can remove it later.
|
||||
listOfModifiedDragDrop.push(node);
|
||||
});
|
||||
|
||||
listOfModifiedDragDrop.forEach(function(node) {
|
||||
$(node).remove();
|
||||
});
|
||||
// Save the current height and width.
|
||||
const currentHeight = currentFilteredItem.height();
|
||||
const currentWidth = currentFilteredItem.width();
|
||||
// Set to auto so we can get the real height and width of the filtered item.
|
||||
currentFilteredItem.height('auto');
|
||||
currentFilteredItem.width('auto');
|
||||
// We need to set display block so we can get height and width.
|
||||
// Some browser can't get the offsetWidth/Height if they are an inline element like span tag.
|
||||
if (!filteredElement.offsetWidth || !filteredElement.offsetHeight) {
|
||||
filteredElement.classList.add('d-block');
|
||||
}
|
||||
if (thisQ.questionDragDropWidthHeight[group].maxWidth < Math.ceil(filteredElement.offsetWidth) ||
|
||||
thisQ.questionDragDropWidthHeight[group].maxHeight < Math.ceil(0 + filteredElement.offsetHeight)) {
|
||||
// Remove the d-block class before calculation.
|
||||
filteredElement.classList.remove('d-block');
|
||||
// Now resize all the items in the same group if we have new maximum width or height.
|
||||
thisQ.resizeAllDragsAndDropsInGroup(group);
|
||||
} else {
|
||||
// Return the original height and width in case the real height and width is not the maximum.
|
||||
currentFilteredItem.height(currentHeight);
|
||||
currentFilteredItem.width(currentWidth);
|
||||
}
|
||||
// Remove the d-block class after resize.
|
||||
filteredElement.classList.remove('d-block');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -107,11 +107,9 @@ class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_ren
|
||||
|
||||
protected function drag_boxes($qa, $group, $choices, question_display_options $options) {
|
||||
$boxes = '';
|
||||
foreach ($choices as $key => $choice) {
|
||||
// Bug 8632: long text entry causes bug in drag and drop field in IE.
|
||||
$content = str_replace('-', '‑', $choice->text);
|
||||
$content = str_replace(' ', ' ', $content);
|
||||
|
||||
foreach ($choices as $key => $choice) {
|
||||
$content = question_utils::format_question_fragment($choice->text, $this->page->context);
|
||||
$infinite = '';
|
||||
if ($choice->infinite) {
|
||||
$infinite = ' infinite';
|
||||
|
@ -126,3 +126,7 @@
|
||||
.que.ddwtos sub {
|
||||
bottom: -0.2em;
|
||||
}
|
||||
|
||||
.que.ddwtos .MathJax_Display {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -18,9 +18,10 @@ Feature: Preview a drag-drop into text question
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddwtos | Drag to text | fox |
|
||||
| Test questions | ddwtos | Drag to text infinite | infinite |
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddwtos | Drag to text | fox |
|
||||
| Test questions | ddwtos | Drag to text infinite | infinite |
|
||||
| Test questions | ddwtos | Drag to mathjax equation | mathjax |
|
||||
|
||||
@javascript @_bug_phantomjs
|
||||
Scenario: Preview a question using the mouse.
|
||||
@ -78,3 +79,12 @@ Feature: Preview a drag-drop into text question
|
||||
Then I should see "Option1" in the home area of drag and drop into text question
|
||||
And I should see "Option2" in the home area of drag and drop into text question
|
||||
And I should see "Option3" in the home area of drag and drop into text question
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a drag-drop into text question with mathjax question.
|
||||
Given the "mathjaxloader" filter is "on"
|
||||
And the "mathjaxloader" filter applies to "content and headings"
|
||||
And I am on the "Drag to mathjax equation" "core_question > preview" page logged in as teacher
|
||||
And I press "Fill in correct responses"
|
||||
When I press "Submit and finish"
|
||||
Then ".filter_mathjaxloader_equation" "css_element" should exist in the ".draghome" "css_element"
|
||||
|
@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class qtype_ddwtos_test_helper extends question_test_helper {
|
||||
public function get_test_questions() {
|
||||
return array('fox', 'maths', 'oddgroups', 'missingchoiceno', 'infinite');
|
||||
return ['fox', 'maths', 'oddgroups', 'missingchoiceno', 'infinite', 'mathjax'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,6 +156,30 @@ class qtype_ddwtos_test_helper extends question_test_helper {
|
||||
return $fromform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data required to save a drag-drop into text question where the the answer contain equation
|
||||
*
|
||||
*
|
||||
* @return stdClass data to create a ddwtos question.
|
||||
*/
|
||||
public function get_ddwtos_question_form_data_mathjax() {
|
||||
$fromform = new stdClass();
|
||||
|
||||
$fromform->name = 'Drag-and-drop words into sentences question with equation';
|
||||
$fromform->questiontext = ['text' => 'Fill in the correct mathjax equation: y = 2, x =4 : [[1]]', 'format' => FORMAT_HTML];
|
||||
$fromform->defaultmark = 1.0;
|
||||
$fromform->generalfeedback = ['text' => 'The right answer is: "y = x^2"', 'format' => FORMAT_HTML];
|
||||
$fromform->choices = [
|
||||
['answer' => '$$ y = x^2 $$', 'choicegroup' => '1'],
|
||||
['answer' => '$$ y = x^5 $$', 'choicegroup' => '1'],
|
||||
];
|
||||
test_question_maker::set_standard_combined_feedback_form_data($fromform);
|
||||
$fromform->shownumcorrect = 0;
|
||||
$fromform->penalty = 0.3333333;
|
||||
$fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
return $fromform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return qtype_ddwtos_question
|
||||
*/
|
||||
|
@ -88,6 +88,9 @@ This files describes API changes for code that uses the question API.
|
||||
.question-bank-table. This applies to the same styles to preview on the qbank_columnsortorder admin screen. It is important
|
||||
that the styles match on these pages so that the defaults have the expected result in the question bank.
|
||||
|
||||
10) A new utility function format_question_fragment is created.
|
||||
format_question_fragment is added so that question content can filter base on filters.
|
||||
|
||||
=== 4.2 ===
|
||||
|
||||
1) The question/qengine.js has been deprecated. We create core_question/question_engine
|
||||
|
Loading…
x
Reference in New Issue
Block a user