Merge branch 'MDL-47494-ddimageortext' into MDL-47494

This commit is contained in:
Tim Hunt 2015-09-18 07:51:36 +01:00
commit 2ce6a9c64f
43 changed files with 6749 additions and 0 deletions

View file

@ -0,0 +1,22 @@
A drag-and-drop question type
You can use text and / or images as drag items onto defined drop zones on a
background image.
This question type requires that gapselect question type
https://github.com/moodleou/moodle-qtype_gapselect/
to be installed in order to work.
This question type was written by Jamie Pratt (http://jamiep.org/).
This version of this question type is compatible with Moodle 2.5+. There are
other versions available for Moodle 2.3+.
To install using git, type this command in the root of your Moodle install
git clone git@github.com:moodleou/moodle-qtype_ddimageortext.git question/type/ddimageortext
Then add question/type/ddimageortext to your git ignore.
Alternatively, download the zip from
https://github.com/moodleou/moodle-qtype_ddimageortext/zipball/master
unzip it into the question/type folder, and then rename the new folder to ddimageortext.

View file

@ -0,0 +1,102 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Backup code for qtype_ddimageortext.
*
* @package qtype_ddimageortext
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Provides the information to backup ddimageortext questions.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_qtype_ddimageortext_plugin extends backup_qtype_plugin {
/**
* Returns the question type this is.
* @return string question type name, like 'ddimageortext'.
*/
protected static function qtype_name() {
return 'ddimageortext';
}
/**
* Returns the qtype information to attach to question element.
*/
protected function define_question_plugin_structure() {
$qtype = self::qtype_name();
$plugin = $this->get_plugin_element(null, '../../qtype', $qtype);
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
$plugin->add_child($pluginwrapper);
$dds = new backup_nested_element($qtype, array('id'), array(
'shuffleanswers', 'correctfeedback', 'correctfeedbackformat',
'partiallycorrectfeedback', 'partiallycorrectfeedbackformat',
'incorrectfeedback', 'incorrectfeedbackformat', 'shownumcorrect'));
$pluginwrapper->add_child($dds);
$drags = new backup_nested_element('drags');
$drag = new backup_nested_element('drag', array('id'),
array('no', 'draggroup', 'infinite', 'label'));
$drops = new backup_nested_element('drops');
$drop = new backup_nested_element('drop', array('id'),
array('no', 'xleft', 'ytop', 'choice', 'label'));
$dds->set_source_table("qtype_{$qtype}",
array('questionid' => backup::VAR_PARENTID));
$pluginwrapper->add_child($drags);
$drags->add_child($drag);
$pluginwrapper->add_child($drops);
$drops->add_child($drop);
$drag->set_source_table("qtype_{$qtype}_drags",
array('questionid' => backup::VAR_PARENTID));
$drop->set_source_table("qtype_{$qtype}_drops",
array('questionid' => backup::VAR_PARENTID));
return $plugin;
}
/**
* Returns one array with filearea => mappingname elements for the qtype
*
* Used by {@link get_components_and_fileareas} to know about all the qtype
* files to be processed both in backup and restore.
*/
public static function get_qtype_fileareas() {
$qtype = self::qtype_name();
return array(
'correctfeedback' => 'question_created',
'partiallycorrectfeedback' => 'question_created',
'incorrectfeedback' => 'question_created',
'bgimage' => 'question_created',
'dragimage' => "qtype_{$qtype}_drags");
}
}

View file

@ -0,0 +1,154 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Restore code for qtype_ddimageortext.
* @package qtype_ddimageortext
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Restore plugin class for the ddimageortext question type.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_qtype_ddimageortext_plugin extends restore_qtype_plugin {
protected static function qtype_name() {
return 'ddimageortext';
}
/**
* Returns the paths to be handled by the plugin at question level.
*/
protected function define_question_plugin_structure() {
$paths = array();
// Add own qtype stuff.
$elename = 'dds';
$elepath = $this->get_pathfor('/'.self::qtype_name());
$paths[] = new restore_path_element($elename, $elepath);
$elename = 'drag';
$elepath = $this->get_pathfor('/drags/drag');
$paths[] = new restore_path_element($elename, $elepath);
$elename = 'drop';
$elepath = $this->get_pathfor('/drops/drop');
$paths[] = new restore_path_element($elename, $elepath);
return $paths; // And we return the interesting paths.
}
/**
* Process the qtype/{qtypename} element.
*/
public function process_dds($data) {
global $DB;
$prefix = 'qtype_'.self::qtype_name();
$data = (object)$data;
$oldid = $data->id;
// Detect if the question is created or mapped.
$oldquestionid = $this->get_old_parentid('question');
$newquestionid = $this->get_new_parentid('question');
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
// If the question has been created by restore,
// we need to create its qtype_ddimageortext too.
if ($questioncreated) {
// Adjust some columns.
$data->questionid = $newquestionid;
// Insert record.
$newitemid = $DB->insert_record($prefix, $data);
// Create mapping (needed for decoding links).
$this->set_mapping($prefix, $oldid, $newitemid);
}
}
/**
* Process the qtype/drags/drag element.
*/
public function process_drag($data) {
global $DB;
$prefix = 'qtype_'.self::qtype_name();
$data = (object)$data;
$oldid = $data->id;
// Detect if the question is created or mapped.
$oldquestionid = $this->get_old_parentid('question');
$newquestionid = $this->get_new_parentid('question');
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
if ($questioncreated) {
$data->questionid = $newquestionid;
// Insert record.
$newitemid = $DB->insert_record("{$prefix}_drags", $data);
// Create mapping (there are files and states based on this).
$this->set_mapping("{$prefix}_drags", $oldid, $newitemid);
}
}
/**
* Process the qtype/drags/drag element.
*/
public function process_drop($data) {
global $DB;
$prefix = 'qtype_'.self::qtype_name();
$data = (object)$data;
$oldid = $data->id;
// Detect if the question is created or mapped.
$oldquestionid = $this->get_old_parentid('question');
$newquestionid = $this->get_new_parentid('question');
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
if ($questioncreated) {
$data->questionid = $newquestionid;
// Insert record.
$newitemid = $DB->insert_record("{$prefix}_drops", $data);
// Create mapping (there are files and states based on this).
$this->set_mapping("{$prefix}_drops", $oldid, $newitemid);
}
}
/**
* Return the contents of this qtype to be processed by the links decoder.
*/
public static function define_decode_contents() {
$prefix = 'qtype_'.self::qtype_name();
$contents = array();
$fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
$contents[] =
new restore_decode_content($prefix, $fields, $prefix);
return $contents;
}
}

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="question/type/ddimageortext/db" VERSION="20110627" COMMENT="XMLDB file for Moodle question/type/ddimageortext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="qtype_ddimageortext" COMMENT="Defines drag and drop (text or images onto a background image) questions" NEXT="qtype_ddimageortext_drops">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="questionid"/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="shuffleanswers"/>
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="questionid" NEXT="correctfeedback"/>
<FIELD NAME="correctfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any correct response." PREVIOUS="shuffleanswers" NEXT="correctfeedbackformat"/>
<FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="correctfeedback" NEXT="partiallycorrectfeedback"/>
<FIELD NAME="partiallycorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any partially correct response." PREVIOUS="correctfeedbackformat" NEXT="partiallycorrectfeedbackformat"/>
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="partiallycorrectfeedback" NEXT="incorrectfeedback"/>
<FIELD NAME="incorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response." PREVIOUS="partiallycorrectfeedbackformat" NEXT="incorrectfeedbackformat"/>
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="incorrectfeedback" NEXT="shownumcorrect"/>
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="incorrectfeedbackformat"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="questionid"/>
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/>
</KEYS>
</TABLE>
<TABLE NAME="qtype_ddimageortext_drops" COMMENT="Drop boxes" PREVIOUS="qtype_ddimageortext" NEXT="qtype_ddimageortext_drags">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="questionid"/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="no"/>
<FIELD NAME="no" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" COMMENT="drop number" PREVIOUS="questionid" NEXT="xleft"/>
<FIELD NAME="xleft" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="no" NEXT="ytop"/>
<FIELD NAME="ytop" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="xleft" NEXT="choice"/>
<FIELD NAME="choice" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="ytop" NEXT="label"/>
<FIELD NAME="label" TYPE="text" LENGTH="medium" NOTNULL="true" SEQUENCE="false" COMMENT="Alt label for drop box" PREVIOUS="choice"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="questionid"/>
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/>
</KEYS>
</TABLE>
<TABLE NAME="qtype_ddimageortext_drags" COMMENT="Images to drag. Actual file names are not stored here we use the file names as found in the file storage area." PREVIOUS="qtype_ddimageortext_drops">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="questionid"/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="no"/>
<FIELD NAME="no" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" COMMENT="drag no" PREVIOUS="questionid" NEXT="draggroup"/>
<FIELD NAME="draggroup" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="no" NEXT="infinite"/>
<FIELD NAME="infinite" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="draggroup" NEXT="label"/>
<FIELD NAME="label" TYPE="text" LENGTH="medium" NOTNULL="true" SEQUENCE="false" COMMENT="Alt text label for drag-able image." PREVIOUS="infinite"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="questionid"/>
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>

View file

@ -0,0 +1,287 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines the editing form for the drag-and-drop images onto images question type.
*
* @package qtype_ddimageortext
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/ddimageortext/edit_ddtoimage_form_base.php');
/**
* Drag-and-drop images onto images editing form definition.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_edit_form extends qtype_ddtoimage_edit_form_base {
public function qtype() {
return 'ddimageortext';
}
public function data_preprocessing($question) {
$question = parent::data_preprocessing($question);
$question = $this->data_preprocessing_combined_feedback($question, true);
$question = $this->data_preprocessing_hints($question, true, true);
$dragids = array(); // Drag no -> dragid.
if (!empty($question->options)) {
$question->shuffleanswers = $question->options->shuffleanswers;
$question->drags = array();
foreach ($question->options->drags as $drag) {
$dragindex = $drag->no - 1;
$question->drags[$dragindex] = array();
$question->draglabel[$dragindex] = $drag->label;
$question->drags[$dragindex]['infinite'] = $drag->infinite;
$question->drags[$dragindex]['draggroup'] = $drag->draggroup;
$dragids[$dragindex] = $drag->id;
}
$question->drops = array();
foreach ($question->options->drops as $drop) {
$question->drops[$drop->no - 1] = array();
$question->drops[$drop->no - 1]['choice'] = $drop->choice;
$question->drops[$drop->no - 1]['droplabel'] = $drop->label;
$question->drops[$drop->no - 1]['xleft'] = $drop->xleft;
$question->drops[$drop->no - 1]['ytop'] = $drop->ytop;
}
}
// Initialise file picker for bgimage.
$draftitemid = file_get_submitted_draft_itemid('bgimage');
file_prepare_draft_area($draftitemid, $this->context->id, 'qtype_ddimageortext',
'bgimage', !empty($question->id) ? (int) $question->id : null,
self::file_picker_options());
$question->bgimage = $draftitemid;
// Initialise file picker for dragimages.
list(, $imagerepeats) = $this->get_drag_item_repeats();
$draftitemids = optional_param_array('dragitem', array(), PARAM_INT);
for ($imageindex = 0; $imageindex < $imagerepeats; $imageindex++) {
$draftitemid = isset($draftitemids[$imageindex]) ? $draftitemids[$imageindex] : 0;
// Numbers not allowed in filearea name.
$itemid = isset($dragids[$imageindex]) ? $dragids[$imageindex] : null;
file_prepare_draft_area($draftitemid, $this->context->id, 'qtype_ddimageortext',
'dragimage', $itemid, self::file_picker_options());
$question->dragitem[$imageindex] = $draftitemid;
}
if (!empty($question->options)) {
foreach ($question->options->drags as $drag) {
$dragindex = $drag->no - 1;
if (!isset($question->dragitem[$dragindex])) {
$fileexists = false;
} else {
$fileexists = self::file_uploaded($question->dragitem[$dragindex]);
}
$labelexists = (trim($question->draglabel[$dragindex]) != '');
if ($labelexists && !$fileexists) {
$question->drags[$dragindex]['dragitemtype'] = 'word';
} else {
$question->drags[$dragindex]['dragitemtype'] = 'image';
}
}
}
$this->js_call();
return $question;
}
public function js_call() {
global $PAGE;
$maxsizes = new stdClass();
$maxsizes->bgimage = new stdClass();
$maxsizes->bgimage->width = QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH;
$maxsizes->bgimage->height = QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT;
$maxsizes->dragimage = new stdClass();
$maxsizes->dragimage->width = QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH;
$maxsizes->dragimage->height = QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT;
$params = array('maxsizes' => $maxsizes,
'topnode' => 'fieldset#id_previewareaheader');
$PAGE->requires->yui_module('moodle-qtype_ddimageortext-form',
'M.qtype_ddimageortext.init_form',
array($params));
}
// Drag items.
protected function definition_draggable_items($mform, $itemrepeatsatstart) {
$mform->addElement('header', 'draggableitemheader',
get_string('draggableitems', 'qtype_ddimageortext'));
$mform->addElement('advcheckbox', 'shuffleanswers', ' ',
get_string('shuffleimages', 'qtype_'.$this->qtype()));
$mform->setDefault('shuffleanswers', 0);
$this->repeat_elements($this->draggable_item($mform), $itemrepeatsatstart,
$this->draggable_items_repeated_options(),
'noitems', 'additems', self::ADD_NUM_ITEMS,
get_string('addmoreimages', 'qtype_ddimageortext'), true);
}
protected function draggable_item($mform) {
$draggableimageitem = array();
$grouparray = array();
$dragitemtypes = array('image' => get_string('draggableimage', 'qtype_ddimageortext'),
'word' => get_string('draggableword', 'qtype_ddimageortext'));
$grouparray[] = $mform->createElement('select', 'dragitemtype',
get_string('draggableitemtype', 'qtype_ddimageortext'),
$dragitemtypes,
array('class' => 'dragitemtype'));
$options = array();
for ($i = 1; $i <= self::MAX_GROUPS; $i += 1) {
$options[$i] = $i;
}
$grouparray[] = $mform->createElement('select', 'draggroup',
get_string('group', 'qtype_gapselect'),
$options,
array('class' => 'draggroup'));
$grouparray[] = $mform->createElement('advcheckbox', 'infinite', ' ',
get_string('infinite', 'qtype_ddimageortext'));
$draggableimageitem[] = $mform->createElement('group', 'drags',
get_string('draggableitemheader', 'qtype_ddimageortext', '{no}'), $grouparray);
$draggableimageitem[] = $mform->createElement('filepicker', 'dragitem', '', null,
self::file_picker_options());
$draggableimageitem[] = $mform->createElement('text', 'draglabel',
get_string('label', 'qtype_ddimageortext'),
array('size' => 30, 'class' => 'tweakcss'));
$mform->setType('draglabel', PARAM_RAW); // These are validated manually.
return $draggableimageitem;
}
protected function draggable_items_repeated_options() {
$repeatedoptions = array();
$repeatedoptions['draggroup']['default'] = '1';
return $repeatedoptions;
}
// Drop zones.
protected function drop_zone($mform, $imagerepeats) {
$dropzoneitem = array();
$grouparray = array();
$grouparray[] = $mform->createElement('text', 'xleft',
get_string('xleft', 'qtype_ddimageortext'),
array('size' => 5, 'class' => 'tweakcss'));
$grouparray[] = $mform->createElement('text', 'ytop',
get_string('ytop', 'qtype_ddimageortext'),
array('size' => 5, 'class' => 'tweakcss'));
$options = array();
$options[0] = '';
for ($i = 1; $i <= $imagerepeats; $i += 1) {
$options[$i] = $i;
}
$grouparray[] = $mform->createElement('select', 'choice',
get_string('draggableitem', 'qtype_ddimageortext'), $options);
$grouparray[] = $mform->createElement('text', 'droplabel',
get_string('label', 'qtype_ddimageortext'),
array('size' => 10, 'class' => 'tweakcss'));
$mform->setType('droplabel', PARAM_NOTAGS);
$dropzone = $mform->createElement('group', 'drops',
get_string('dropzone', 'qtype_ddimageortext', '{no}'), $grouparray);
return array($dropzone);
}
protected function drop_zones_repeated_options() {
$repeatedoptions = array();
// The next two are PARAM_RAW becuase we need to distinguish 0 and ''.
// We do the necessary validation in the validation method.
$repeatedoptions['drops[xleft]']['type'] = PARAM_RAW;
$repeatedoptions['drops[ytop]']['type'] = PARAM_RAW;
$repeatedoptions['drops[droplabel]']['type'] = PARAM_RAW;
$repeatedoptions['choice']['default'] = '0';
return $repeatedoptions;
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (!self::file_uploaded($data['bgimage'])) {
$errors["bgimage"] = get_string('formerror_nobgimage', 'qtype_'.$this->qtype());
}
$allchoices = array();
for ($i = 0; $i < $data['nodropzone']; $i++) {
$ytoppresent = (trim($data['drops'][$i]['ytop']) !== '');
$xleftpresent = (trim($data['drops'][$i]['xleft']) !== '');
$ytopisint = (string) clean_param($data['drops'][$i]['ytop'], PARAM_INT) === trim($data['drops'][$i]['ytop']);
$xleftisint = (string) clean_param($data['drops'][$i]['xleft'], PARAM_INT) === trim($data['drops'][$i]['xleft']);
$labelpresent = (trim($data['drops'][$i]['droplabel']) !== '');
$choice = $data['drops'][$i]['choice'];
$imagechoicepresent = ($choice !== '0');
if ($imagechoicepresent) {
if (!$ytoppresent) {
$errors["drops[$i]"] = get_string('formerror_noytop', 'qtype_ddimageortext');
} else if (!$ytopisint) {
$errors["drops[$i]"] = get_string('formerror_notintytop', 'qtype_ddimageortext');
}
if (!$xleftpresent) {
$errors["drops[$i]"] = get_string('formerror_noxleft', 'qtype_ddimageortext');
} else if (!$xleftisint) {
$errors["drops[$i]"] = get_string('formerror_notintxleft', 'qtype_ddimageortext');
}
if ($data['drags'][$choice - 1]['dragitemtype'] != 'word' &&
!self::file_uploaded($data['dragitem'][$choice - 1])) {
$errors['dragitem['.($choice - 1).']'] =
get_string('formerror_nofile', 'qtype_ddimageortext', $i);
}
if (isset($allchoices[$choice]) && !$data['drags'][$choice - 1]['infinite']) {
$errors["drops[$i]"] =
get_string('formerror_multipledraginstance', 'qtype_ddimageortext', $choice);
$errors['drops['.($allchoices[$choice]).']'] =
get_string('formerror_multipledraginstance', 'qtype_ddimageortext', $choice);
$errors['drags['.($choice - 1).']'] =
get_string('formerror_multipledraginstance2', 'qtype_ddimageortext', $choice);
}
$allchoices[$choice] = $i;
} else {
if ($ytoppresent || $xleftpresent || $labelpresent) {
$errors["drops[$i]"] =
get_string('formerror_noimageselected', 'qtype_ddimageortext');
}
}
}
for ($dragindex = 0; $dragindex < $data['noitems']; $dragindex++) {
$label = $data['draglabel'][$dragindex];
if ($data['drags'][$dragindex]['dragitemtype'] == 'word') {
$allowedtags = '<br><sub><sup><b><i><strong><em>';
$errormessage = get_string('formerror_disallowedtags', 'qtype_ddimageortext');
} else {
$allowedtags = '';
$errormessage = get_string('formerror_noallowedtags', 'qtype_ddimageortext');
}
if ($label != strip_tags($label, $allowedtags)) {
$errors["drags[{$dragindex}]"] = $errormessage;
}
}
return $errors;
}
}

View file

@ -0,0 +1,145 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for editing form for the drag-and-drop images onto images question type.
*
* @package qtype
* @subpackage ddimageortext
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Base class for drag-and-drop onto images editing form definition.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class qtype_ddtoimage_edit_form_base extends question_edit_form {
const MAX_GROUPS = 8;
const START_NUM_ITEMS = 6;
const ADD_NUM_ITEMS = 3;
/**
*
* Options shared by all file pickers in the form.
*/
public static function file_picker_options() {
$filepickeroptions = array();
$filepickeroptions['accepted_types'] = array('web_image');
$filepickeroptions['maxbytes'] = 0;
$filepickeroptions['maxfiles'] = 1;
$filepickeroptions['subdirs'] = 0;
return $filepickeroptions;
}
/**
* definition_inner adds all specific fields to the form.
* @param MoodleQuickForm $mform (the form being built).
*/
protected function definition_inner($mform) {
$mform->addElement('header', 'previewareaheader',
get_string('previewareaheader', 'qtype_'.$this->qtype()));
$mform->setExpanded('previewareaheader');
$mform->addElement('static', 'previewarea', '',
get_string('previewareamessage', 'qtype_'.$this->qtype()));
$mform->registerNoSubmitButton('refresh');
$mform->addElement('submit', 'refresh', get_string('refresh', 'qtype_'.$this->qtype()));
$mform->addElement('filepicker', 'bgimage', get_string('bgimage', 'qtype_'.$this->qtype()),
null, self::file_picker_options());
$mform->closeHeaderBefore('dropzoneheader');
// Add the draggable image fields & drop zones to the form.
list($itemrepeatsatstart, $imagerepeats) = $this->get_drag_item_repeats();
$this->definition_draggable_items($mform, $itemrepeatsatstart);
$this->definition_drop_zones($mform, $imagerepeats);
$this->add_combined_feedback_fields(true);
$this->add_interactive_settings(true, true);
}
protected function definition_drop_zones($mform, $imagerepeats) {
$mform->addElement('header', 'dropzoneheader',
get_string('dropzoneheader', 'qtype_'.$this->qtype()));
$countdropzones = 0;
if (isset($this->question->id)) {
foreach ($this->question->options->drops as $drop) {
$countdropzones = max($countdropzones, $drop->no);
}
}
if (!$countdropzones) {
$countdropzones = self::START_NUM_ITEMS;
}
$dropzonerepeatsatstart = $countdropzones;
$this->repeat_elements($this->drop_zone($mform, $imagerepeats), $dropzonerepeatsatstart,
$this->drop_zones_repeated_options(),
'nodropzone', 'adddropzone', self::ADD_NUM_ITEMS,
get_string('addmoredropzones', 'qtype_ddimageortext'), true);
}
abstract protected function drop_zone($mform, $imagerepeats);
abstract protected function drop_zones_repeated_options();
abstract protected function definition_draggable_items($mform, $itemrepeatsatstart);
abstract protected function draggable_item($mform);
abstract protected function draggable_items_repeated_options();
protected function get_drag_item_repeats() {
$countimages = 0;
if (isset($this->question->id)) {
foreach ($this->question->options->drags as $drag) {
$countimages = max($countimages, $drag->no);
}
}
if (!$countimages) {
$countimages = self::START_NUM_ITEMS;
}
$itemrepeatsatstart = $countimages;
$imagerepeats = optional_param('noitems', $itemrepeatsatstart, PARAM_INT);
$addfields = optional_param('additems', false, PARAM_BOOL);
if ($addfields) {
$imagerepeats += self::ADD_NUM_ITEMS;
}
return array($itemrepeatsatstart, $imagerepeats);
}
abstract public function js_call();
public static function file_uploaded($draftitemid) {
$draftareafiles = file_get_drafarea_files($draftitemid);
do {
$draftareafile = array_shift($draftareafiles->list);
} while ($draftareafile !== null && $draftareafile->filename == '.');
if ($draftareafile === null) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
*
* @package qtype
* @subpackage ddimageortext
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addmoredropzones'] = 'Blanks for {no} more drop zones';
$string['addmoreimages'] = 'Blanks for {no} more draggable items';
$string['answer'] = 'Answer';
$string['bgimage'] = 'Background image';
$string['correctansweris'] = 'The correct answer is: {$a}';
$string['draggableimage'] = 'Draggable image';
$string['draggableitem'] = 'Draggable item';
$string['draggableitems'] = 'Draggable items';
$string['draggableitemheader'] = 'Draggable item {$a}';
$string['draggableitemtype'] = 'Type';
$string['draggableword'] = 'Draggable text';
$string['dropbackground'] = 'Background image for dragging markers onto';
$string['dropzone'] = 'Drop zone {$a}';
$string['dropzoneheader'] = 'Drop zones';
$string['formerror_disallowedtags'] = 'You have used html tags here that are not allowed in a draggable text drag item type.';
$string['formerror_noallowedtags'] = 'No html tags are allowed in this text which is the alt text for a draggable image';
$string['formerror_noytop'] = 'You must provide a value for the y coords for the top left corner of this drop area. You can drag and drop the drop area above to set the coordinates or enter them manually here.';
$string['formerror_noxleft'] = 'You must provide a value for the x coords for the top left corner of this drop area. You can drag and drop the drop area above to set the coordinates or enter them manually here.';
$string['formerror_nofile'] = 'You need to upload or select a file to use here.';
$string['formerror_nofile3'] = 'You need to select an image file here, or delete the associated label and uncheck the infinite checkbox.';
$string['formerror_notintytop'] = 'The y coords must be an integer.';
$string['formerror_notintxleft'] = 'The x coords must be an integer.';
$string['formerror_multipledraginstance'] = 'You have selected this image {$a} more than once as the correct choice for a drop zone but it is not marked as being an infinite drag item.';
$string['formerror_multipledraginstance2'] = 'You have selected this image more than once as the correct choice for a drop zone but it is not marked as being an infinite drag item.';
$string['formerror_noimageselected'] = 'You need to select a drag item to be the correct choice for this drop zone.';
$string['formerror_nobgimage'] = 'You need to select an image to use as the background for the drag and drop area.';
$string['infinite'] = 'Infinite';
$string['label'] = 'Text';
$string['nolabel'] = 'No label text';
$string['pleasedraganimagetoeachdropregion'] = 'Your answer is not complete, please drag an item to each drop region.';
$string['pluginname'] = 'Drag and drop onto image';
$string['pluginname_help'] = 'Select a background image file, select draggable images or enter text and define the drop zones on the background image to which they must be dragged.';
$string['pluginname_link'] = 'question/type/ddimageortext';
$string['pluginnameadding'] = 'Adding drag and drop onto image';
$string['pluginnameediting'] = 'Editing drag and drop onto image';
$string['pluginnamesummary'] = 'Images or text labels are dragged and dropped into drop zones on a background image.';
$string['previewareaheader'] = 'Preview';
$string['previewareamessage'] = 'Select a background image file and select draggable images or just enter text that will be made draggable. Then choose a drag item for each \'drop zone\', and drag the drag item to where the student should drag it to.';
$string['refresh'] = 'Refresh preview';
$string['shuffleimages'] = 'Shuffle drag items each time question is attempted';
$string['summarisechoice'] = '{$a->no}. {$a->text}';
$string['summariseplace'] = '{$a->no}. {$a->text}';
$string['summarisechoiceno'] = 'Item {$a}';
$string['summariseplaceno'] = 'Drop zone {$a}';
$string['xleft'] = 'Left';
$string['ytop'] = 'Top';

View file

@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Serve question type files
*
* @since 2.0
* @package qtype
* @subpackage ddimageortext
* @copyright Dongsheng Cai <dongsheng@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Checks file access for ddimageortext questions.
*/
function qtype_ddimageortext_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
question_pluginfile($course, $context, 'qtype_ddimageortext',
$filearea, $args, $forcedownload, $options);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Drag-and-drop onto image question definition class.
*
* @package qtype_ddimageortext
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php');
/**
* Represents a drag-and-drop onto image question.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_question extends qtype_ddtoimage_question_base {
}
/**
* Represents one of the choices (draggable images).
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_drag_item {
public $id;
public $text;
public $no;
public $group;
public $infinite;
public function __construct($alttextlabel, $no, $group = 1, $infinite = false, $id = 0) {
$this->id = $id;
$this->text = $alttextlabel;
$this->no = $no;
$this->group = $group;
$this->infinite = $infinite;
}
public function choice_group() {
return $this->group;
}
public function summarise() {
if (trim($this->text) != '') {
return get_string('summarisechoice', 'qtype_ddimageortext', $this);
} else {
return get_string('summarisechoiceno', 'qtype_ddimageortext', $this->no);
}
}
}
/**
* Represents one of the places (drop zones).
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_drop_zone {
public $no;
public $text;
public $group;
public $xy;
public function __construct($alttextlabel, $no, $group = 1, $x = '', $y = '') {
$this->no = $no;
$this->text = $alttextlabel;
$this->group = $group;
$this->xy = array($x, $y);
}
public function summarise() {
if (trim($this->text) != '') {
$summariseplace =
get_string('summariseplace', 'qtype_ddimageortext', $this);
} else {
$summariseplace =
get_string('summariseplaceno', 'qtype_ddimageortext', $this->no);
}
return $summariseplace;
}
}

View file

@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Drag-and-drop onto image question definition class.
*
* @package qtype_ddimageortext
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/gapselect/questionbase.php');
/**
* Represents a drag-and-drop onto image question.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddtoimage_question_base extends qtype_gapselect_question_base {
public function clear_wrong_from_response(array $response) {
foreach ($this->places as $place => $notused) {
if (array_key_exists($this->field($place), $response) &&
$response[$this->field($place)] != $this->get_right_choice_for($place)) {
$response[$this->field($place)] = '';
}
}
return $response;
}
public function get_right_choice_for($placeno) {
$place = $this->places[$placeno];
foreach ($this->choiceorder[$place->group] as $choicekey => $choiceid) {
if ($this->rightchoices[$placeno] == $choiceid) {
return $choicekey;
}
}
}
public function summarise_response(array $response) {
$allblank = true;
foreach ($this->places as $placeno => $place) {
$summariseplace = $place->summarise();
if (array_key_exists($this->field($placeno), $response) &&
$response[$this->field($placeno)]) {
$selected = $this->get_selected_choice($place->group,
$response[$this->field($placeno)]);
$summarisechoice = $selected->summarise();
$allblank = false;
} else {
$summarisechoice = '';
}
$choices[] = "$summariseplace -> {{$summarisechoice}}";
}
if ($allblank) {
return null;
}
return implode(' ', $choices);
}
public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
if ($filearea == 'bgimage' || $filearea == 'dragimage') {
$validfilearea = true;
} else {
$validfilearea = false;
}
if ($component == 'qtype_ddimageortext' && $validfilearea) {
$question = $qa->get_question();
$itemid = reset($args);
if ($filearea == 'bgimage') {
return $itemid == $question->id;
} else if ($filearea == 'dragimage') {
foreach ($question->choices as $group) {
foreach ($group as $drag) {
if ($drag->id == $itemid) {
return true;
}
}
}
return false;
}
} else {
return parent::check_file_access($qa, $options, $component,
$filearea, $args, $forcedownload);
}
}
public function get_validation_error(array $response) {
if ($this->is_complete_response($response)) {
return '';
}
return get_string('pleasedraganimagetoeachdropregion', 'qtype_ddimageortext');
}
public function classify_response(array $response) {
$parts = array();
foreach ($this->places as $placeno => $place) {
$group = $place->group;
if (!array_key_exists($this->field($placeno), $response) ||
!$response[$this->field($placeno)]) {
$parts[$placeno] = question_classified_response::no_response();
continue;
}
$fieldname = $this->field($placeno);
$choicekey = $this->choiceorder[$group][$response[$fieldname]];
$choice = $this->choices[$group][$choicekey];
$correct = $this->get_right_choice_for($placeno) == $response[$fieldname];
if ($correct) {
$grade = 1;
} else {
$grade = 0;
}
$parts[$placeno] = new question_classified_response($choice->no, $choice->summarise(), $grade);
}
return $parts;
}
public function get_random_guess_score() {
$accum = 0;
foreach ($this->places as $place) {
foreach ($this->choices[$place->group] as $choice) {
if ($choice->infinite) {
return null;
}
}
$accum += 1 / count($this->choices[$place->group]);
}
return $accum / count($this->places);
}
public function get_question_summary() {
$summary = '';
if (!html_is_blank($this->questiontext)) {
$question = $this->html_to_text($this->questiontext, $this->questiontextformat);
$summary .= $question . '; ';
}
$places = array();
foreach ($this->places as $place) {
$cs = array();
foreach ($this->choices[$place->group] as $choice) {
$cs[] = $choice->summarise();
}
$places[] = '[[' . $place->summarise() . ']] -> {' . implode(' / ', $cs) . '}';
}
$summary .= implode('; ', $places);
return $summary;
}
}

View file

@ -0,0 +1,284 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Question type class for the drag-and-drop onto image question type.
*
* @package qtype_ddimageortext
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/ddimageortext/questiontypebase.php');
define('QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH', 600);
define('QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT', 400);
define('QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH', 150);
define('QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT', 100);
/**
* The drag-and-drop onto image question type class.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext extends qtype_ddtoimage_base {
protected function make_choice($dragdata) {
return new qtype_ddimageortext_drag_item($dragdata->label, $dragdata->no,
$dragdata->draggroup, $dragdata->infinite, $dragdata->id);
}
protected function make_place($dropzonedata) {
return new qtype_ddimageortext_drop_zone($dropzonedata->label, $dropzonedata->no,
$dropzonedata->group,
$dropzonedata->xleft, $dropzonedata->ytop);
}
protected function make_hint($hint) {
return question_hint_with_parts::load_from_record($hint);
}
public function save_question_options($formdata) {
global $DB, $USER;
$context = $formdata->context;
$options = $DB->get_record('qtype_ddimageortext', array('questionid' => $formdata->id));
if (!$options) {
$options = new stdClass();
$options->questionid = $formdata->id;
$options->correctfeedback = '';
$options->partiallycorrectfeedback = '';
$options->incorrectfeedback = '';
$options->id = $DB->insert_record('qtype_ddimageortext', $options);
}
$options->shuffleanswers = !empty($formdata->shuffleanswers);
$options = $this->save_combined_feedback_helper($options, $formdata, $context, true);
$this->save_hints($formdata, true);
$DB->update_record('qtype_ddimageortext', $options);
$DB->delete_records('qtype_ddimageortext_drops', array('questionid' => $formdata->id));
foreach (array_keys($formdata->drops) as $dropno) {
if ($formdata->drops[$dropno]['choice'] == 0) {
continue;
}
$drop = new stdClass();
$drop->questionid = $formdata->id;
$drop->no = $dropno + 1;
$drop->xleft = $formdata->drops[$dropno]['xleft'];
$drop->ytop = $formdata->drops[$dropno]['ytop'];
$drop->choice = $formdata->drops[$dropno]['choice'];
$drop->label = $formdata->drops[$dropno]['droplabel'];
$DB->insert_record('qtype_ddimageortext_drops', $drop);
}
// An array of drag no -> drag id.
$olddragids = $DB->get_records_menu('qtype_ddimageortext_drags',
array('questionid' => $formdata->id),
'', 'no, id');
foreach (array_keys($formdata->drags) as $dragno) {
$info = file_get_draft_area_info($formdata->dragitem[$dragno]);
if ($info['filecount'] > 0 || (trim($formdata->draglabel[$dragno]) != '')) {
$draftitemid = $formdata->dragitem[$dragno];
$drag = new stdClass();
$drag->questionid = $formdata->id;
$drag->no = $dragno + 1;
$drag->draggroup = $formdata->drags[$dragno]['draggroup'];
$drag->infinite = empty($formdata->drags[$dragno]['infinite']) ? 0 : 1;
$drag->label = $formdata->draglabel[$dragno];
if (isset($olddragids[$dragno + 1])) {
$drag->id = $olddragids[$dragno + 1];
unset($olddragids[$dragno + 1]);
$DB->update_record('qtype_ddimageortext_drags', $drag);
} else {
$drag->id = $DB->insert_record('qtype_ddimageortext_drags', $drag);
}
if ($formdata->drags[$dragno]['dragitemtype'] == 'image') {
self::constrain_image_size_in_draft_area($draftitemid,
QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH,
QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT);
file_save_draft_area_files($draftitemid, $formdata->context->id,
'qtype_ddimageortext', 'dragimage', $drag->id,
array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
} else {
// Delete any existing files for draggable text item type.
$fs = get_file_storage();
$fs->delete_area_files($formdata->context->id, 'qtype_ddimageortext',
'dragimage', $drag->id);
}
}
}
if (!empty($olddragids)) {
list($sql, $params) = $DB->get_in_or_equal(array_values($olddragids));
$DB->delete_records_select('qtype_ddimageortext_drags', "id $sql", $params);
}
self::constrain_image_size_in_draft_area($formdata->bgimage,
QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH,
QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT);
file_save_draft_area_files($formdata->bgimage, $formdata->context->id,
'qtype_ddimageortext', 'bgimage', $formdata->id,
array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
}
public function move_files($questionid, $oldcontextid, $newcontextid) {
global $DB;
$fs = get_file_storage();
parent::move_files($questionid, $oldcontextid, $newcontextid);
$fs->move_area_files_to_new_context($oldcontextid,
$newcontextid, 'qtype_ddimageortext', 'bgimage', $questionid);
$dragids = $DB->get_records_menu('qtype_ddimageortext_drags',
array('questionid' => $questionid), 'id', 'id,1');
foreach ($dragids as $dragid => $notused) {
$fs->move_area_files_to_new_context($oldcontextid,
$newcontextid, 'qtype_ddimageortext', 'dragimage', $dragid);
}
$this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
}
/**
* Delete all the files belonging to this question.
* @param int $questionid the question being deleted.
* @param int $contextid the context the question is in.
*/
protected function delete_files($questionid, $contextid) {
global $DB;
$fs = get_file_storage();
parent::delete_files($questionid, $contextid);
$dragids = $DB->get_records_menu('qtype_ddimageortext_drags',
array('questionid' => $questionid), 'id', 'id,1');
foreach ($dragids as $dragid => $notused) {
$fs->delete_area_files($contextid, 'qtype_ddimageortext', 'dragimage', $dragid);
}
$this->delete_files_in_combined_feedback($questionid, $contextid);
$this->delete_files_in_hints($questionid, $contextid);
}
public function export_to_xml($question, qformat_xml $format, $extra = null) {
$fs = get_file_storage();
$contextid = $question->contextid;
$output = '';
if ($question->options->shuffleanswers) {
$output .= " <shuffleanswers/>\n";
}
$output .= $format->write_combined_feedback($question->options,
$question->id,
$question->contextid);
$files = $fs->get_area_files($contextid, 'qtype_ddimageortext', 'bgimage', $question->id);
$output .= " ".$this->write_files($files, 2)."\n";;
foreach ($question->options->drags as $drag) {
$files =
$fs->get_area_files($contextid, 'qtype_ddimageortext', 'dragimage', $drag->id);
$output .= " <drag>\n";
$output .= " <no>{$drag->no}</no>\n";
$output .= $format->writetext($drag->label, 3)."\n";
$output .= " <draggroup>{$drag->draggroup}</draggroup>\n";
if ($drag->infinite) {
$output .= " <infinite/>\n";
}
$output .= $this->write_files($files, 3);
$output .= " </drag>\n";
}
foreach ($question->options->drops as $drop) {
$output .= " <drop>\n";
$output .= $format->writetext($drop->label, 3);
$output .= " <no>{$drop->no}</no>\n";
$output .= " <choice>{$drop->choice}</choice>\n";
$output .= " <xleft>{$drop->xleft}</xleft>\n";
$output .= " <ytop>{$drop->ytop}</ytop>\n";
$output .= " </drop>\n";
}
return $output;
}
public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
if (!isset($data['@']['type']) || $data['@']['type'] != 'ddimageortext') {
return false;
}
$question = $format->import_headers($data);
$question->qtype = 'ddimageortext';
$question->shuffleanswers = array_key_exists('shuffleanswers',
$format->getpath($data, array('#'), array()));
$filexml = $format->getpath($data, array('#', 'file'), array());
$question->bgimage = $format->import_files_as_draft($filexml);
$drags = $data['#']['drag'];
$question->drags = array();
foreach ($drags as $dragxml) {
$dragno = $format->getpath($dragxml, array('#', 'no', 0, '#'), 0);
$dragindex = $dragno - 1;
$question->drags[$dragindex] = array();
$question->draglabel[$dragindex] =
$format->getpath($dragxml, array('#', 'text', 0, '#'), '', true);
$question->drags[$dragindex]['infinite'] = array_key_exists('infinite', $dragxml['#']);
$question->drags[$dragindex]['draggroup'] =
$format->getpath($dragxml, array('#', 'draggroup', 0, '#'), 1);
$filexml = $format->getpath($dragxml, array('#', 'file'), array());
$question->dragitem[$dragindex] = $format->import_files_as_draft($filexml);
if (count($filexml)) {
$question->drags[$dragindex]['dragitemtype'] = 'image';
} else {
$question->drags[$dragindex]['dragitemtype'] = 'word';
}
}
$drops = $data['#']['drop'];
$question->drops = array();
foreach ($drops as $dropxml) {
$dropno = $format->getpath($dropxml, array('#', 'no', 0, '#'), 0);
$dropindex = $dropno - 1;
$question->drops[$dropindex] = array();
$question->drops[$dropindex]['choice'] =
$format->getpath($dropxml, array('#', 'choice', 0, '#'), 0);
$question->drops[$dropindex]['droplabel'] =
$format->getpath($dropxml, array('#', 'text', 0, '#'), '', true);
$question->drops[$dropindex]['xleft'] =
$format->getpath($dropxml, array('#', 'xleft', 0, '#'), '');
$question->drops[$dropindex]['ytop'] =
$format->getpath($dropxml, array('#', 'ytop', 0, '#'), '');
}
$format->import_combined_feedback($question, $data, true);
$format->import_hints($question, $data, true, false,
$format->get_format($question->questiontextformat));
return $question;
}
}

View file

@ -0,0 +1,199 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Question type class for the drag-and-drop onto image question type.
*
* @package qtype_ddimageortext
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/engine/lib.php');
require_once($CFG->dirroot . '/question/format/xml/format.php');
require_once($CFG->dirroot . '/question/type/gapselect/questiontypebase.php');
/**
* The drag-and-drop onto image question type class.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddtoimage_base extends question_type {
protected function choice_group_key() {
return 'draggroup';
}
public function get_question_options($question) {
global $DB;
$dbprefix = 'qtype_'.$this->name();
$question->options = $DB->get_record($dbprefix,
array('questionid' => $question->id), '*', MUST_EXIST);
$question->options->drags = $DB->get_records($dbprefix.'_drags',
array('questionid' => $question->id), 'no ASC', '*');
$question->options->drops = $DB->get_records($dbprefix.'_drops',
array('questionid' => $question->id), 'no ASC', '*');
parent::get_question_options($question);
}
protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
$question->shufflechoices = $questiondata->options->shuffleanswers;
$this->initialise_combined_feedback($question, $questiondata, true);
$question->choices = array();
$choiceindexmap = array();
// Store the choices in arrays by group.
// This code is weird. The first choice in each group gets key 1 in the
// $question->choices[$choice->choice_group()] array, and the others get
// key $choice->no. Therefore you need to think carefully whether you
// are using the key, or $choice->no. This is presumably a mistake, but
// one that is now essentially un-fixable, since many questions of this
// type have been attempted, and theys keys get stored in the attempt data.
foreach ($questiondata->options->drags as $dragdata) {
$choice = $this->make_choice($dragdata);
if (array_key_exists($choice->choice_group(), $question->choices)) {
$question->choices[$choice->choice_group()][$dragdata->no] = $choice;
} else {
$question->choices[$choice->choice_group()][1] = $choice;
}
end($question->choices[$choice->choice_group()]);
$choiceindexmap[$dragdata->no] = array($choice->choice_group(),
key($question->choices[$choice->choice_group()]));
}
$question->places = array();
$question->rightchoices = array();
$i = 1;
foreach ($questiondata->options->drops as $dropdata) {
list($group, $choiceindex) = $choiceindexmap[$dropdata->choice];
$dropdata->group = $group;
$question->places[$dropdata->no] = $this->make_place($dropdata);
$question->rightchoices[$dropdata->no] = $choiceindex;
}
}
public static function constrain_image_size_in_draft_area($draftitemid, $maxwidth, $maxheight) {
global $USER;
$usercontext = context_user::instance($USER->id);
$fs = get_file_storage();
$draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
if ($draftfiles) {
foreach ($draftfiles as $file) {
if ($file->is_directory()) {
continue;
}
$imageinfo = $file->get_imageinfo();
$width = $imageinfo['width'];
$height = $imageinfo['height'];
$mimetype = $imageinfo['mimetype'];
switch ($mimetype) {
case 'image/jpeg' :
$quality = 80;
break;
case 'image/png' :
$quality = 8;
break;
default :
$quality = null;
}
$newwidth = min($maxwidth, $width);
$newheight = min($maxheight, $height);
if ($newwidth != $width || $newheight != $height) {
$newimagefilename = $file->get_filename();
$newimagefilename =
preg_replace('!\.!', "_{$newwidth}x{$newheight}.", $newimagefilename, 1);
$newrecord = new stdClass();
$newrecord->contextid = $usercontext->id;
$newrecord->component = 'user';
$newrecord->filearea = 'draft';
$newrecord->itemid = $draftitemid;
$newrecord->filepath = '/';
$newrecord->filename = $newimagefilename;
$fs->convert_image($newrecord, $file, $newwidth, $newheight, true, $quality);
$file->delete();
}
}
}
}
/**
* Convert files into text output in the given format.
* This method is copied from qformat_default as a quick fix, as the method there is
* protected.
* @param array
* @param string encoding method
* @return string $string
*/
public function write_files($files, $indent) {
if (empty($files)) {
return '';
}
$string = '';
foreach ($files as $file) {
if ($file->is_directory()) {
continue;
}
$string .= str_repeat(' ', $indent);
$string .= '<file name="' . $file->get_filename() . '" encoding="base64">';
$string .= base64_encode($file->get_content());
$string .= "</file>\n";
}
return $string;
}
public function get_possible_responses($questiondata) {
$question = $this->make_question($questiondata);
$parts = array();
foreach ($question->places as $placeno => $place) {
$choices = array();
foreach ($question->choices[$place->group] as $i => $choice) {
$correct = $question->rightchoices[$placeno] == $i;
$choices[$choice->no] = new question_possible_response($choice->summarise(), $correct ? 1 : 0);
}
$choices[null] = question_possible_response::no_response();
$parts[$placeno] = $choices;
}
return $parts;
}
public function get_random_guess_score($questiondata) {
$question = $this->make_question($questiondata);
return $question->get_random_guess_score();
}
public function delete_question($questionid, $contextid) {
global $DB;
$DB->delete_records('qtype_'.$this->name(), array('questionid' => $questionid));
$DB->delete_records('qtype_'.$this->name().'_drags', array('questionid' => $questionid));
$DB->delete_records('qtype_'.$this->name().'_drops', array('questionid' => $questionid));
return parent::delete_question($questionid, $contextid);
}
}

View file

@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Drag-and-drop onto image question renderer class.
*
* @package qtype_ddimageortext
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/ddimageortext/rendererbase.php');
/**
* Generates the output for drag-and-drop onto image questions.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_renderer extends qtype_ddtoimage_renderer_base {
}

View file

@ -0,0 +1,180 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Drag-and-drop onto image question renderer class.
*
* @package qtype_ddimageortext
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Generates the output for drag-and-drop onto image questions.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_renderer {
public function clear_wrong(question_attempt $qa) {
$question = $qa->get_question();
$response = $qa->get_last_qt_data();
if (!empty($response)) {
$cleanresponse = $question->clear_wrong_from_response($response);
} else {
$cleanresponse = $response;
}
$cleanresponsehtml = '';
foreach ($cleanresponse as $fieldname => $value) {
list (, $html) = $this->hidden_field_for_qt_var($qa, $fieldname, $value);
$cleanresponsehtml .= $html;
}
return $cleanresponsehtml;
}
public function formulation_and_controls(question_attempt $qa,
question_display_options $options) {
global $PAGE;
$question = $qa->get_question();
$response = $qa->get_last_qt_data();
$questiontext = $question->format_questiontext($qa);
$output = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
$bgimage = self::get_url_for_image($qa, 'bgimage');
$img = html_writer::empty_tag('img', array(
'src' => $bgimage, 'class' => 'dropbackground',
'alt' => get_string('dropbackground', 'qtype_ddimageortext')));
$droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
$dragimagehomes = '';
foreach ($question->choices as $groupno => $group) {
$dragimagehomesgroup = '';
$orderedgroup = $question->get_ordered_choices($groupno);
foreach ($orderedgroup as $choiceno => $dragimage) {
$dragimageurl = self::get_url_for_image($qa, 'dragimage', $dragimage->id);
$classes = array("group{$groupno}",
'draghome',
"dragitemhomes{$dragimage->no}",
"choice{$choiceno}");
if ($dragimage->infinite) {
$classes[] = 'infinite';
}
if ($dragimageurl === null) {
$classes[] = 'yui3-cssfonts';
$dragimagehomesgroup .= html_writer::tag('div', $dragimage->text,
array('src' => $dragimageurl, 'class' => join(' ', $classes)));
} else {
$dragimagehomesgroup .= html_writer::empty_tag('img',
array('src' => $dragimageurl, 'alt' => $dragimage->text,
'class' => join(' ', $classes)));
}
}
$dragimagehomes .= html_writer::tag('div', $dragimagehomesgroup,
array('class' => 'dragitemgroup' . $groupno));
}
$dragitemsclass = 'dragitems';
if ($options->readonly) {
$dragitemsclass .= ' readonly';
}
$dragitems = html_writer::tag('div', $dragimagehomes, array('class' => $dragitemsclass));
$dropzones = html_writer::tag('div', '', array('class' => 'dropzones'));
$hiddens = '';
foreach ($question->places as $placeno => $place) {
$varname = $question->field($placeno);
list($fieldname, $html) = $this->hidden_field_for_qt_var($qa, $varname);
$hiddens .= $html;
$question->places[$placeno]->fieldname = $fieldname;
}
$output .= html_writer::tag('div',
$droparea . $dragitems . $dropzones . $hiddens, array('class' => 'ddarea'));
$topnode = 'div#q'.$qa->get_slot().' div.ddarea';
$params = array('drops' => $question->places,
'topnode' => $topnode,
'readonly' => $options->readonly);
$PAGE->requires->yui_module('moodle-qtype_ddimageortext-dd',
'M.qtype_ddimageortext.init_question',
array($params));
if ($qa->get_state() == question_state::$invalid) {
$output .= html_writer::nonempty_tag('div',
$question->get_validation_error($qa->get_last_qt_data()),
array('class' => 'validationerror'));
}
return $output;
}
protected static function get_url_for_image(question_attempt $qa, $filearea, $itemid = 0) {
$question = $qa->get_question();
$qubaid = $qa->get_usage_id();
$slot = $qa->get_slot();
$fs = get_file_storage();
if ($filearea == 'bgimage') {
$itemid = $question->id;
}
$componentname = $question->qtype->plugin_name();
$draftfiles = $fs->get_area_files($question->contextid, $componentname,
$filearea, $itemid, 'id');
if ($draftfiles) {
foreach ($draftfiles as $file) {
if ($file->is_directory()) {
continue;
}
$url = moodle_url::make_pluginfile_url($question->contextid, $componentname,
$filearea, "$qubaid/$slot/{$itemid}", '/',
$file->get_filename());
return $url->out();
}
}
return null;
}
protected function hidden_field_for_qt_var(question_attempt $qa, $varname, $value = null,
$classes = null) {
if ($value === null) {
$value = $qa->get_last_qt_var($varname);
}
$fieldname = $qa->get_qt_field_name($varname);
$attributes = array('type' => 'hidden',
'id' => str_replace(':', '_', $fieldname),
'name' => $fieldname,
'value' => $value);
if ($classes !== null) {
$attributes['class'] = join(' ', $classes);
}
return array($fieldname, html_writer::empty_tag('input', $attributes)."\n");
}
public function specific_feedback(question_attempt $qa) {
return $this->combined_feedback($qa);
}
public function correct_response(question_attempt $qa) {
return '';
}
}

View file

@ -0,0 +1,129 @@
.que.ddimageortext .qtext {
margin-bottom: 0.5em;
display: block;
}
.que.ddimageortext div.droparea img, form.mform fieldset#id_previewareaheader div.droparea img {
border: 1px solid #000000;
max-width: none;
}
.que.ddimageortext .draghome, form.mform fieldset#id_previewareaheader .draghome {
vertical-align: top;
margin: 5px;
visibility : hidden;
}
.que.ddimageortext div.draghome, form.mform fieldset#id_previewareaheader div.draghome {
border: 1px solid black;
cursor: move;
background-color: #B0C4DE;
display: -moz-inline-stack;
display:inline-block;
height: auto;
width: auto;
zoom: 1;
*display: inline;
}
.que.ddimageortext .group1, form.mform fieldset#id_previewareaheader .group1 {
background-color: #FFFFFF;
}
.que.ddimageortext .group2, form.mform fieldset#id_previewareaheader .group2 {
background-color: #B0C4DE;
}
.que.ddimageortext .group3, form.mform fieldset#id_previewareaheader .group3 {
background-color: #DCDCDC;
}
.que.ddimageortext .group4, form.mform fieldset#id_previewareaheader .group4 {
background-color: #D8BFD8;
}
.que.ddimageortext .group5, form.mform fieldset#id_previewareaheader .group5 {
background-color: #87CEFA;
}
.que.ddimageortext .group6, form.mform fieldset#id_previewareaheader .group6 {
background-color: #DAA520;
}
.que.ddimageortext .group7, form.mform fieldset#id_previewareaheader .group7 {
background-color: #FFD700;
}
.que.ddimageortext .group8, form.mform fieldset#id_previewareaheader .group8 {
background-color: #F0E68C;
}
.que.ddimageortext .drag, form.mform fieldset#id_previewareaheader .drag {
border: 1px solid black;
cursor: move;
z-index: 2;
}
.que.ddimageortext .dragitems.readonly .drag {
cursor: auto;
}
.que.ddimageortext div.ddarea, form.mform fieldset#id_previewareaheader div.ddarea {
text-align : center;
}
.que.ddimageortext .dropbackground, form.mform fieldset#id_previewareaheader .dropbackground {
margin:0 auto;
}
.que.ddimageortext .dropzone {
border: 1px solid black;
position: absolute;
z-index: 1;
}
.que.ddimageortext .dropzone.yui3-dd-drop-over.yui3-dd-drop-active-valid {
border-color: #0a0;
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
}
.que.ddimageortext div.dragitems div.draghome, .que.ddimageortext div.dragitems div.drag,
form.mform fieldset#id_previewareaheader div.draghome, form.mform fieldset#id_previewareaheader div.drag {
font:13px/1.231 arial,helvetica,clean,sans-serif;
*font-size:small; /* for IE */
*font:x-small; /* for IE in quirks mode */
}
form.mform fieldset#id_previewareaheader div.drag.yui3-dd-dragging,
.que.ddimageortext div.drag.yui3-dd-dragging {
z-index: 3;
-moz-box-shadow: 3px 3px 4px #000;
-webkit-box-shadow: 3px 3px 4px #000;
box-shadow: 3px 3px 4px #000;
/* For IE 8 */
-ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#000000')";
/* For IE 5.5 - 7 */
filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#000000');
}
/* Editing form. Style repeated elements*/
/*Top*/
body#page-question-type-ddimageortext div[id^=fgroup_id_][id*=drags_] {
background: #EEE;
margin-top: 0;
margin-bottom: 0;
padding-bottom: 5px;
padding-top: 5px;
border: 1px solid #BBB;
border-bottom: 0;
}
body#page-question-type-ddimageortext div[id^=fgroup_id_][id*=drags_] .fgrouplabel label {
font-weight: bold;
}
/* Middle */
body#page-question-type-ddimageortext div[id^=fitem_id_][id*=dragitem_] {
background: #EEE;
margin-bottom: 0;
margin-top: 0;
padding-bottom: 5px;
padding-top: 5px;
border: 1px solid #BBB;
border-top: 0;
border-bottom: 0;
}
/* Bottom */
body#page-question-type-ddimageortext div[id^=fitem_id_][id*=draglabel_] {
background: #EEE;
margin-bottom: 2em;
margin-top: 0;
padding-bottom: 5px;
padding-top: 5px;
border: 1px solid #BBB;
border-top: 0;
}

View file

@ -0,0 +1,96 @@
@ou @ou_vle @qtype @qtype_ddimageortext
Feature: Test creating a drag and drop onto image question
As a teacher
In order to test my students
I need to be able to create drag and drop onto image questions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Question bank" node in "Course administration"
@javascript
Scenario: Create a drag and drop onto image question
When I press "Create a new question ..."
And I set the field "Drag and drop onto image" to "1"
And I press "Add"
And I set the field "Question name" to "Drag and drop onto image 001"
And I set the field "Question text" to "Identify the features in this cross-section."
And I set the field "General feedback" to "The locations are now labelled on the diagram below."
And I upload "question/type/ddimageortext/tests/fixtures/oceanfloorbase.jpg" file to "Background image" filemanager
# Draggable items
And I follow "Draggable items"
And I press "Blanks for 3 more draggable items"
And I set the field "id_drags_0_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_0" to "island<br/>arc"
And I set the field "id_drags_1_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_1" to "mid-ocean<br/>ridge"
And I set the field "id_drags_2_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_2" to "abyssal<br/>plain"
And I set the field "id_drags_3_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_3" to "continental<br/>rise"
And I set the field "id_drags_4_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_4" to "ocean<br/>trench"
And I set the field "id_drags_5_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_5" to "continental<br/>slope"
And I set the field "id_drags_6_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_6" to "mountain<br/>belt"
And I set the field "id_drags_7_dragitemtype" to "Draggable text"
And I set the field "id_draglabel_7" to "continental<br/>shelf"
# Drop zones
And I follow "Drop zones"
And I press "Blanks for 3 more drop zones"
And I set the field "id_drops_0_xleft" to "53"
And I set the field "id_drops_0_ytop" to "17"
And I set the field "id_drops_0_choice" to "7"
And I set the field "id_drops_1_xleft" to "172"
And I set the field "id_drops_1_ytop" to "2"
And I set the field "id_drops_1_choice" to "8"
And I set the field "id_drops_2_xleft" to "363"
And I set the field "id_drops_2_ytop" to "31"
And I set the field "id_drops_2_choice" to "5"
And I set the field "id_drops_3_xleft" to "440"
And I set the field "id_drops_3_ytop" to "13"
And I set the field "id_drops_3_choice" to "3"
And I set the field "id_drops_4_xleft" to "115"
And I set the field "id_drops_4_ytop" to "74"
And I set the field "id_drops_4_choice" to "6"
And I set the field "id_drops_5_xleft" to "210"
And I set the field "id_drops_5_ytop" to "94"
And I set the field "id_drops_5_choice" to "4"
And I set the field "id_drops_6_xleft" to "310"
And I set the field "id_drops_6_ytop" to "87"
And I set the field "id_drops_6_choice" to "1"
And I set the field "id_drops_7_xleft" to "479"
And I set the field "id_drops_7_ytop" to "84"
And I set the field "id_drops_7_choice" to "2"
And I press "id_submitbutton"
Then I should see "Drag and drop onto image 001"

View file

@ -0,0 +1,101 @@
@ou @ou_vle @qtype @qtype_ddimageortext
Feature: Test duplicating a quiz containing a drag and drop onto image question
As a teacher
In order re-use my courses containing drag and drop onto image questions
I need to be able to backup and restore them
Background:
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | ddimageortext | Drag onto image | xsection |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And quiz "Test quiz" contains the following questions:
| Drag onto image | 1 |
And I log in as "admin"
And I am on site homepage
And I follow "Course 1"
@javascript
Scenario: Backup and restore a course containing a drag and drop onto image question
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
And I navigate to "Question bank" node in "Course administration"
And I click on "Edit" "link" in the "Drag onto image" "table_row"
Then the following fields match these values:
| Question name | Drag onto image |
| General feedback | <p>More information about the major features of the Earth's surface can be found in Block 3, Section 6.2.</p> |
| Default mark | 1 |
| Shuffle | 0 |
| id_drags_0_dragitemtype | Draggable text |
| id_drags_0_draggroup | 1 |
| id_draglabel_0 | island<br/>arc |
| id_drags_1_dragitemtype | Draggable text |
| id_drags_1_draggroup | 1 |
| id_draglabel_1 | mid-ocean<br/>ridge |
| id_drags_2_dragitemtype | Draggable text |
| id_drags_2_draggroup | 1 |
| id_draglabel_2 | abyssal<br/>plain |
| id_drags_3_dragitemtype | Draggable text |
| id_drags_3_draggroup | 1 |
| id_draglabel_3 | continental<br/>rise |
| id_drags_4_dragitemtype | Draggable text |
| id_drags_4_draggroup | 1 |
| id_draglabel_4 | ocean<br/>trench |
| id_drags_5_dragitemtype | Draggable text |
| id_drags_5_draggroup | 1 |
| id_draglabel_5 | continental<br/>slope |
| id_drags_6_dragitemtype | Draggable text |
| id_drags_6_draggroup | 1 |
| id_draglabel_6 | mountain<br/>belt |
| id_drags_7_dragitemtype | Draggable text |
| id_drags_7_draggroup | 1 |
| id_draglabel_7 | continental<br/>shelf |
| id_drops_0_xleft | 53 |
| id_drops_0_ytop | 17 |
| id_drops_0_choice | 7. mountainbelt |
| id_drops_1_xleft | 172 |
| id_drops_1_ytop | 2 |
| id_drops_1_choice | 8. continentalshelf |
| id_drops_2_xleft | 363 |
| id_drops_2_ytop | 31 |
| id_drops_2_choice | 5. oceantrench |
| id_drops_3_xleft | 440 |
| id_drops_3_ytop | 13 |
| id_drops_3_choice | 3. abyssalplain |
| id_drops_4_xleft | 115 |
| id_drops_4_ytop | 74 |
| id_drops_4_choice | 6. continentalslope |
| id_drops_5_xleft | 210 |
| id_drops_5_ytop | 94 |
| id_drops_5_choice | 4. continentalrise |
| id_drops_6_xleft | 310 |
| id_drops_6_ytop | 87 |
| id_drops_6_choice | 1. islandarc |
| id_drops_7_xleft | 479 |
| id_drops_7_ytop | 84 |
| id_drops_7_choice | 2. mid-oceanridge |
| For any correct response | Well done! |
| For any partially correct response | Parts, but only parts, of your response are correct. |
| id_shownumcorrect | 1 |
| For any incorrect response | That is not right at all. |
| Penalty for each incorrect try | 0.3333333 |
| Hint 1 | Incorrect placements will be removed. |
| id_hintclearwrong_0 | 1 |
| id_hintshownumcorrect_0 | 1 |
| id_hintclearwrong_1 | 0 |
| id_hintshownumcorrect_1 | 1 |
| Hint 3 | Incorrect placements will be removed. |
| id_hintclearwrong_2 | 1 |
| id_hintshownumcorrect_2 | 1 |
| id_hintclearwrong_3 | 0 |
| id_hintshownumcorrect_3 | 1 |

View file

@ -0,0 +1,88 @@
<?php
// This file is part of Stack - http://stack.bham.ac.uk/
//
// Stack is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Stack 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Stack. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat steps definitions for drag and drop onto image.
*
* @package qtype_ddimageortext
* @category test
* @copyright 2015 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
/**
* Steps definitions related with the drag and drop onto image question type.
*
* @copyright 2015 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_qtype_ddimageortext extends behat_base {
/**
* Get the xpath for a given drag item.
* @param string $dragitem the text of the item to drag.
* @return string the xpath expression.
*/
protected function drag_xpath($dragitem) {
return '//div[contains(@class, " drag ") and contains(normalize-space(.), "' . $this->escape($dragitem) . '")]';
}
/**
* Get the xpath for a given drop box.
* @param string $dragitem the number of the drop box.
* @return string the xpath expression.
*/
protected function drop_xpath($placenumber) {
return '//div[contains(@class, "dropzone ") and contains(@class, "place' . $placenumber . ' ")]';
}
/**
* Drag the drag item with the given text to the given space.
*
* @param string $dragitem the text of the item to drag.
* @param int $placenumber the number of the place to drop into.
*
* @Given /^I drag "(?P<drag_item>[^"]*)" to place "(?P<place_number>\d+)" in the drag and drop onto image question$/
*
*/
public function i_drag_to_place_in_the_drag_and_drop_onto_image_question($dragitem, $placenumber) {
$generalcontext = behat_context_helper::get('behat_general');
$generalcontext->i_drag_and_i_drop_it_in($this->drag_xpath($dragitem),
'xpath_element', $this->drop_xpath($placenumber), 'xpath_element');
}
/**
* Type some characters while focussed on a given drop box.
*
* @param string $keys the characters to type.
* @param int $placenumber the number of the place to drop into.
*
* @Given /^I type "(?P<keys>[^"]*)" on place "(?P<place_number>\d+)" in the drag and drop onto image question$/
*/
public function i_type_on_place_in_the_drag_and_drop_onto_image_question($keys, $placenumber) {
$node = $this->get_selected_node('xpath_element', $this->drop_xpath($placenumber));
$this->ensure_node_is_visible($node);
foreach (str_split($keys) as $key) {
$node->keyDown($key);
$node->keyPress($key);
$node->keyUp($key);
}
}
}

View file

@ -0,0 +1,33 @@
@ou @ou_vle @qtype @qtype_ddimageortext
Feature: Test editing a drag and drop onto image questions
As a teacher
In order to be able to update my drag and drop onto image questions
I need to edit them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | ddimageortext | Drag onto image | xsection |
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Question bank" node in "Course administration"
@javascript
Scenario: Edit a drag and drop onto image question
When I click on "Edit" "link" in the "Drag onto image" "table_row"
And I set the following fields to these values:
| Question name | Edited question name |
And I press "id_submitbutton"
Then I should see "Edited question name"

View file

@ -0,0 +1,37 @@
@ou @ou_vle @qtype @qtype_ddimageortext
Feature: Test exporting drag and drop onto image questions
As a teacher
In order to be able to reuse my drag and drop onto image questions
I need to export them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | ddimageortext | Drag onto image | xsection |
And I log in as "teacher1"
And I follow "Course 1"
@javascript
Scenario: Export a drag and drop onto image question
# Import sample file.
When I navigate to "Export" node in "Course administration > Question bank"
And I set the field "id_format_xml" to "1"
And I press "Export questions to file"
And following "click here" should download between "18500" and "19000" bytes
# If the download step is the last in the scenario then we can sometimes run
# into the situation where the download page causes a http redirect but behat
# has already conducted its reset (generating an error). By putting a logout
# step we avoid behat doing the reset until we are off that page.
And I log out

View file

@ -0,0 +1,30 @@
@ou @ou_vle @qtype @qtype_ddimageortext
Feature: Test importing drag and drop onto image questions
As a teacher
In order to reuse drag and drop onto image questions
I need to import them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I follow "Course 1"
@javascript @_file_upload
Scenario: import drag and drop onto image question.
When I navigate to "Import" node in "Course administration > Question bank"
And I set the field "id_format_xml" to "1"
And I upload "question/type/ddimageortext/tests/fixtures/testquestion.moodle.xml" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 1 questions from file"
And I should see "Identify the features in this cross-section by dragging the labels into the boxes."
And I press "Continue"
And I should see "Imported Drag and drop onto image 001"

View file

@ -0,0 +1,60 @@
@ou @ou_vle @qtype @qtype_ddimageortext
Feature: Preview a drag-drop onto image question
As a teacher
In order to check my drag-drop onto image questions will work for students
I need to preview them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | ddimageortext | Drag onto image | xsection |
Given I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Question bank" node in "Course administration"
@javascript
Scenario: Preview a question using the mouse.
When I click on "Preview" "link" in the "Drag onto image" "table_row"
And I switch to "questionpreview" window
# Odd, but the <br>s go to nothing, not a space.
And I drag "mountainbelt" to place "1" in the drag and drop onto image question
And I drag "continentalshelf" to place "2" in the drag and drop onto image question
And I drag "oceantrench" to place "3" in the drag and drop onto image question
And I drag "abyssalplain" to place "4" in the drag and drop onto image question
And I drag "continentalslope" to place "5" in the drag and drop onto image question
And I drag "continentalrise" to place "6" in the drag and drop onto image question
And I drag "islandarc" to place "7" in the drag and drop onto image question
And I drag "mid-oceanridge" to place "8" in the drag and 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"
And I switch to the main window
@javascript
Scenario: Preview a question using the keyboard.
When I click on "Preview" "link" in the "Drag onto image" "table_row"
And I switch to "questionpreview" window
And I type " " on place "1" in the drag and drop onto image question
And I type " " on place "2" in the drag and drop onto image question
And I type " " on place "3" in the drag and drop onto image question
And I type " " on place "4" in the drag and drop onto image question
And I type " " on place "5" in the drag and drop onto image question
And I type " " on place "6" in the drag and drop onto image question
And I type " " on place "7" in the drag and drop onto image question
And I type " " on place "8" in the drag and 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"
And I switch to the main window

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,242 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test helpers for the drag-and-drop onto image question type.
*
* @package qtype_ddimageortext
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test helper class for the drag-and-drop onto image question type.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_test_helper extends question_test_helper {
public function get_test_questions() {
return array('fox', 'maths', 'xsection');
}
/**
* @return qtype_ddimageortext_question
*/
public function make_ddimageortext_question_fox() {
question_bank::load_question_definition_classes('ddimageortext');
$dd = new qtype_ddimageortext_question();
test_question_maker::initialise_a_question($dd);
$dd->name = 'Drag-and-drop onto image question';
$dd->questiontext = 'The quick brown fox jumped over the lazy dog.';
$dd->generalfeedback = 'This sentence uses each letter of the alphabet.';
$dd->qtype = question_bank::get_qtype('ddimageortext');
$dd->shufflechoices = true;
test_question_maker::set_standard_combined_feedback_fields($dd);
$dd->choices = $this->make_choice_structure(array(
new qtype_ddimageortext_drag_item('quick', 1, 1),
new qtype_ddimageortext_drag_item('fox', 2, 1),
new qtype_ddimageortext_drag_item('lazy', 3, 2),
new qtype_ddimageortext_drag_item('dog', 4, 2)
));
$dd->places = $this->make_place_structure(array(
new qtype_ddimageortext_drop_zone('', 1, 1),
new qtype_ddimageortext_drop_zone('', 2, 1),
new qtype_ddimageortext_drop_zone('', 3, 2),
new qtype_ddimageortext_drop_zone('', 4, 2)
));
$dd->rightchoices = array(1 => 1, 2 => 2, 3 => 1, 4 => 4);
return $dd;
}
protected function make_choice_structure($choices) {
$choicestructure = array();
foreach ($choices as $choice) {
if (!isset($choicestructure[$choice->group])) {
$choicestructure[$choice->group][1] = $choice;
} else {
$choicestructure[$choice->group][$choice->no] = $choice;
}
}
return $choicestructure;
}
protected function make_place_structure($places) {
$placestructure = array();
foreach ($places as $place) {
$placestructure[$place->no] = $place;
}
return $placestructure;
}
/**
* @return qtype_ddimageortext_question
*/
public function make_ddimageortext_question_maths() {
question_bank::load_question_definition_classes('ddimageortext');
$dd = new qtype_ddimageortext_question();
test_question_maker::initialise_a_question($dd);
$dd->name = 'Drag-and-drop onto image question';
$dd->questiontext = 'Fill in the operators to make this equation work: ' .
'7 [[1]] 11 [[2]] 13 [[1]] 17 [[2]] 19 = 3';
$dd->generalfeedback = 'This sentence uses each letter of the alphabet.';
$dd->qtype = question_bank::get_qtype('ddimageortext');
$dd->shufflechoices = true;
test_question_maker::set_standard_combined_feedback_fields($dd);
$dd->choices = $this->make_choice_structure(array(
new qtype_ddimageortext_drag_item('+', 1, 1),
new qtype_ddimageortext_drag_item('-', 2, 1)
));
$dd->places = $this->make_place_structure(array(
new qtype_ddimageortext_drop_zone('', 1, 1),
new qtype_ddimageortext_drop_zone('', 2, 1),
new qtype_ddimageortext_drop_zone('', 3, 1),
new qtype_ddimageortext_drop_zone('', 4, 1)
));
$dd->rightchoices = array(1 => 1, 2 => 2, 3 => 1, 4 => 2);
return $dd;
}
/**
* @return stdClass date to create a ddimageortext question.
*/
public function get_ddimageortext_question_form_data_xsection() {
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 = 'Geography cross-section';
$fromform->questiontext = array(
'text' => '<p>Identify the features in this cross-section by dragging the labels into the boxes.</p>
<p><em>Use the mouse to drag the boxed words into the empty boxes. Alternatively, use the tab key to select an empty box, then use the space key to cycle through the options.</em></p>',
'format' => FORMAT_HTML,
);
$fromform->defaultmark = 1;
$fromform->generalfeedback = array(
'text' => '<p>More information about the major features of the Earth\'s surface can be found in Block 3, Section 6.2.</p>',
'format' => FORMAT_HTML,
);
$fromform->bgimage = $bgdraftitemid;
$fromform->shuffleanswers = 0;
$fromform->drags = array(
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
);
$fromform->dragitem = array(0, 0, 0, 0, 0, 0, 0, 0);
$fromform->draglabel =
array(
'island<br/>arc',
'mid-ocean<br/>ridge',
'abyssal<br/>plain',
'continental<br/>rise',
'ocean<br/>trench',
'continental<br/>slope',
'mountain<br/>belt',
'continental<br/>shelf',
);
$fromform->drops = array(
array('xleft' => '53', 'ytop' => '17', 'choice' => '7', 'droplabel' => ''),
array('xleft' => '172', 'ytop' => '2', 'choice' => '8', 'droplabel' => ''),
array('xleft' => '363', 'ytop' => '31', 'choice' => '5', 'droplabel' => ''),
array('xleft' => '440', 'ytop' => '13', 'choice' => '3', 'droplabel' => ''),
array('xleft' => '115', 'ytop' => '74', 'choice' => '6', 'droplabel' => ''),
array('xleft' => '210', 'ytop' => '94', 'choice' => '4', 'droplabel' => ''),
array('xleft' => '310', 'ytop' => '87', 'choice' => '1', 'droplabel' => ''),
array('xleft' => '479', 'ytop' => '84', 'choice' => '2', 'droplabel' => ''),
);
test_question_maker::set_standard_combined_feedback_form_data($fromform);
$fromform->penalty ='0.3333333';
$fromform->hint = array(
array(
'text' => '<p>Incorrect placements will be removed.</p>',
'format' => FORMAT_HTML,
),
array(
'text' => '<ul>
<li>The abyssal plain is a flat almost featureless expanse of ocean floor 4km to 6km below sea-level.</li>
<li>The continental rise is the gently sloping part of the ocean floor beyond the continental slope.</li>
<li>The continental shelf is the gently sloping ocean floor just offshore from the land.</li>
<li>The continental slope is the relatively steep part of the ocean floor beyond the continental shelf.</li>
<li>A mid-ocean ridge is a broad submarine ridge several kilometres high.</li>
<li>A mountain belt is a long range of mountains.</li>
<li>An island arc is a chain of volcanic islands.</li>
<li>An oceanic trench is a deep trough in the ocean floor.</li>
</ul>',
'format' => FORMAT_HTML,
),
array(
'text' => '<p>Incorrect placements will be removed.</p>',
'format' => FORMAT_HTML,
),
array(
'text' => '<ul>
<li>The abyssal plain is a flat almost featureless expanse of ocean floor 4km to 6km below sea-level.</li>
<li>The continental rise is the gently sloping part of the ocean floor beyond the continental slope.</li>
<li>The continental shelf is the gently sloping ocean floor just offshore from the land.</li>
<li>The continental slope is the relatively steep part of the ocean floor beyond the continental shelf.</li>
<li>A mid-ocean ridge is a broad submarine ridge several kilometres high.</li>
<li>A mountain belt is a long range of mountains.</li>
<li>An island arc is a chain of volcanic islands.</li>
<li>An oceanic trench is a deep trough in the ocean floor.</li>
</ul>',
'format' => FORMAT_HTML,
),
);
$fromform->hintclearwrong = array(1, 0, 1, 0);
$fromform->hintshownumcorrect = array(1, 1, 1, 1);
return $fromform;
}
}

View file

@ -0,0 +1,269 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for the drag-and-drop onto image question definition class.
*
* @package qtype_ddimageortext
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php');
/**
* Unit tests for the matching question definition class.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_question_test extends basic_testcase {
public function test_get_question_summary() {
$dd = test_question_maker::make_question('ddimageortext');
$this->assertEquals('The quick brown fox jumped over the lazy dog.; '.
'[[Drop zone 1]] -> {1. quick / 2. fox}; '.
'[[Drop zone 2]] -> {1. quick / 2. fox}; '.
'[[Drop zone 3]] -> {3. lazy / 4. dog}; '.
'[[Drop zone 4]] -> {3. lazy / 4. dog}',
$dd->get_question_summary());
}
public function test_get_question_summary_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$this->assertEquals('Fill in the operators to make this equation work: '.
'7 [[1]] 11 [[2]] 13 [[1]] 17 [[2]] 19 = 3; '.
'[[Drop zone 1]] -> {1. + / 2. -}; '.
'[[Drop zone 2]] -> {1. + / 2. -}; '.
'[[Drop zone 3]] -> {1. + / 2. -}; '.
'[[Drop zone 4]] -> {1. + / 2. -}',
$dd->get_question_summary());
}
public function test_summarise_response() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals('Drop zone 1 -> {1. quick} '.
'Drop zone 2 -> {1. quick} '.
'Drop zone 3 -> {3. lazy} '.
'Drop zone 4 -> {3. lazy}',
$dd->summarise_response(array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
}
public function test_summarise_response_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals('Drop zone 1 -> {1. +} '.
'Drop zone 2 -> {2. -} '.
'Drop zone 3 -> {1. +} '.
'Drop zone 4 -> {2. -}',
$dd->summarise_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
}
public function test_get_random_guess_score() {
$dd = test_question_maker::make_question('ddimageortext');
$this->assertEquals(0.5, $dd->get_random_guess_score());
}
public function test_get_random_guess_score_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$this->assertEquals(0.5, $dd->get_random_guess_score());
}
public function test_get_right_choice_for() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(1, $dd->get_right_choice_for(1));
$this->assertEquals(2, $dd->get_right_choice_for(2));
}
public function test_get_right_choice_for_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(1, $dd->get_right_choice_for(1));
$this->assertEquals(2, $dd->get_right_choice_for(2));
$this->assertEquals(1, $dd->get_right_choice_for(3));
$this->assertEquals(2, $dd->get_right_choice_for(4));
}
public function test_clear_wrong_from_response() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$initialresponse = array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1');
$this->assertEquals(array('p1' => '1', 'p2' => '', 'p3' => '1', 'p4' => ''),
$dd->clear_wrong_from_response($initialresponse));
}
public function test_get_num_parts_right() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array(2, 4),
$dd->get_num_parts_right(array('p1' => '1', 'p2' => '1', 'p3' => '2', 'p4' => '2')));
$this->assertEquals(array(4, 4),
$dd->get_num_parts_right(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
}
public function test_get_num_parts_right_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array(2, 4),
$dd->get_num_parts_right(array(
'p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
}
public function test_get_expected_data() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(
array('p1' => PARAM_INT, 'p2' => PARAM_INT, 'p3' => PARAM_INT, 'p4' => PARAM_INT),
$dd->get_expected_data()
);
}
public function test_get_correct_response() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'),
$dd->get_correct_response());
}
public function test_get_correct_response_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'),
$dd->get_correct_response());
}
public function test_is_same_response() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertTrue($dd->is_same_response(
array(),
array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => '')));
$this->assertFalse($dd->is_same_response(
array(),
array('p1' => '1', 'p2' => '', 'p3' => '', 'p4' => '')));
$this->assertFalse($dd->is_same_response(
array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => ''),
array('p1' => '1', 'p2' => '', 'p3' => '', 'p4' => '')));
$this->assertTrue($dd->is_same_response(
array('p1' => '1', 'p2' => '2', 'p3' => '3', 'p4' => '4'),
array('p1' => '1', 'p2' => '2', 'p3' => '3', 'p4' => '4')));
$this->assertFalse($dd->is_same_response(
array('p1' => '1', 'p2' => '2', 'p3' => '3', 'p4' => '4'),
array('p1' => '1', 'p2' => '2', 'p3' => '2', 'p4' => '4')));
}
public function test_is_complete_response() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertFalse($dd->is_complete_response(array()));
$this->assertFalse($dd->is_complete_response(
array('p1' => '1', 'p2' => '1', 'p3' => '', 'p4' => '')));
$this->assertFalse($dd->is_complete_response(array('p1' => '1')));
$this->assertTrue($dd->is_complete_response(
array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
}
public function test_is_gradable_response() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertFalse($dd->is_gradable_response(array()));
$this->assertFalse($dd->is_gradable_response(
array('p1' => '', 'p2' => '', 'p3' => '', 'p3' => '')));
$this->assertTrue($dd->is_gradable_response(
array('p1' => '1', 'p2' => '1', 'p3' => '')));
$this->assertTrue($dd->is_gradable_response(array('p1' => '1')));
$this->assertTrue($dd->is_gradable_response(
array('p1' => '1', 'p2' => '1', 'p3' => '1')));
}
public function test_grading() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array(1, question_state::$gradedright),
$dd->grade_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
$this->assertEquals(array(0.25, question_state::$gradedpartial),
$dd->grade_response(array('p1' => '1')));
$this->assertEquals(array(0, question_state::$gradedwrong),
$dd->grade_response(array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1')));
}
public function test_grading_maths() {
$dd = test_question_maker::make_question('ddimageortext', 'maths');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array(1, question_state::$gradedright),
$dd->grade_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
$this->assertEquals(array(0.5, question_state::$gradedpartial),
$dd->grade_response(array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
$this->assertEquals(array(0, question_state::$gradedwrong),
$dd->grade_response(array('p1' => '', 'p2' => '1', 'p3' => '2', 'p4' => '1')));
}
public function test_classify_response() {
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$dd->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array(
1 => new question_classified_response(1, '1. quick', 1),
2 => new question_classified_response(2, '2. fox', 1),
3 => new question_classified_response(3, '3. lazy', 1),
4 => new question_classified_response(4, '4. dog', 1)
), $dd->classify_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
$this->assertEquals(array(
1 => question_classified_response::no_response(),
2 => new question_classified_response(1, '1. quick', 0),
3 => new question_classified_response(4, '4. dog', 0),
4 => new question_classified_response(4, '4. dog', 1)
), $dd->classify_response(array('p1' => '', 'p2' => '1', 'p3' => '2', 'p4' => '2')));
}
}

View file

@ -0,0 +1,58 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for the drag-and-drop onto image question definition class.
*
* @package qtype_ddimageortext
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php');
/**
* Unit tests for the drag-and-drop onto image question definition class.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_test extends basic_testcase {
/** @var qtype_ddimageortext instance of the question type class to test. */
protected $qtype;
protected function setUp() {
$this->qtype = question_bank::get_qtype('ddimageortext');;
}
protected function tearDown() {
$this->qtype = null;
}
public function test_name() {
$this->assertEquals($this->qtype->name(), 'ddimageortext');
}
public function test_can_analyse_responses() {
$this->assertTrue($this->qtype->can_analyse_responses());
}
}

View file

@ -0,0 +1,858 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for the drag-and-drop onto image question type.
*
* @package qtype_ddimageortext
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php');
/**
* Unit tests for the drag-and-drop onto image question type.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ddimageortext_walkthrough_test extends qbehaviour_walkthrough_test_base {
/**
* Get an expectation that the output contains an item ready to drag.
* @param int $dragitemno the item number.
* @param int $choice which choice this is.
* @param int $group which drag group it belongs to.
* @return question_contains_tag_with_attributes the required expectation.
*/
protected function get_contains_drag_image_home_expectation($dragitemno, $choice, $group) {
$class = 'group' . $group;
$class .= ' draghome dragitemhomes' . $dragitemno. ' choice'.$choice.' yui3-cssfonts';
$expectedattrs = array();
$expectedattrs['class'] = $class;
return new question_contains_tag_with_attributes('div', $expectedattrs);
}
public function test_interactive_behaviour() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->hints = array(
new question_hint_with_parts(13, 'This is the first hint.', FORMAT_HTML, false, false),
new question_hint_with_parts(14, 'This is the second hint.', FORMAT_HTML, true, true),
);
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'interactive', 12);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the wrong answer.
$this->process_submission(array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 1),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Submit the wrong answer.
$this->process_submission(
array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 1),
$this->get_contains_try_again_button_expectation(true),
$this->get_contains_hint_expectation('This is the first hint'));
// Do try again.
$this->process_submission(array('-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', '2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', '1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', '2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', '1'),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
// Submit the right answer.
$this->process_submission(
array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(8);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', '1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', '2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', '1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', '2'),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation());
// Check regrading does not mess anything up.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(8);
}
public function test_deferred_feedback() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'deferredfeedback', 12);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_does_not_contain_feedback_expectation());
// Save a partial answer.
$this->process_submission(array('p1' => '2', 'p2' => '1'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', ''),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', ''),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Save the right answer.
$this->process_submission(
array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(12);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
$this->get_contains_correct_expectation());
// Change the right answer a bit.
$dd->rightchoices[2] = 1;
// Check regrading does not mess anything up.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedpartial);
$this->check_current_mark(9);
}
public function test_deferred_feedback_unanswered() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'deferredfeedback', 12);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
$this->check_step_count(1);
// Save a blank response.
$this->process_submission(array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => ''));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', ''),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', ''),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', ''),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', ''),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
$this->check_step_count(1);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2));
}
public function test_deferred_feedback_partial_answer() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'deferredfeedback', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Save a partial response.
$this->process_submission(array('p1' => '1', 'p2' => '2', 'p3' => '', 'p4' => ''));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 0),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedpartial);
$this->check_current_mark(1.5);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_partcorrect_expectation());
}
public function test_interactive_grading() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->hints = array(
new question_hint_with_parts(1, 'This is the first hint.',
FORMAT_MOODLE, true, true),
new question_hint_with_parts(2, 'This is the second hint.',
FORMAT_MOODLE, true, true),
);
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'interactive', 12);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->assertEquals('interactivecountback',
$this->quba->get_question_attempt($this->slot)->get_behaviour_name());
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_does_not_contain_num_parts_correct(),
$this->get_no_hint_visible_expectation());
// Submit an response with the first two parts right.
$this->process_submission(
array('p1' => '1', 'p2' => '2', 'p3' => '2', 'p4' => '1', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_hint_expectation('This is the first hint'),
$this->get_contains_num_parts_correct(2),
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 1));
// Check that extract responses will return the reset data.
$prefix = $this->quba->get_field_prefix($this->slot);
$this->assertEquals(array('p1' => '1', 'p2' => '2'),
$this->quba->extract_responses($this->slot,
array($prefix . 'p1' => '1', $prefix . 'p2' => '2', '-tryagain' => 1)));
// Do try again.
// keys p3 and p4 are extra hidden fields to clear data.
$this->process_submission(
array('p1' => '1', 'p2' => '2', 'p3' => '', 'p4' => '', '-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 0),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
// Submit an response with the first and last parts right.
$this->process_submission(
array('p1' => '1', 'p2' => '1', 'p3' => '2', 'p4' => '2', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_hint_expectation('This is the second hint'),
$this->get_contains_num_parts_correct(2),
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 2));
// Do try again.
$this->process_submission(
array('p1' => '1', 'p2' => '', 'p3' => '', 'p4' => '2', '-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(1),
$this->get_no_hint_visible_expectation());
// Submit the right answer.
$this->process_submission(
array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(7);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 1),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
$this->get_contains_submit_button_expectation(false),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation(),
$this->get_does_not_contain_num_parts_correct(),
$this->get_contains_standard_correct_combined_feedback_expectation());
}
public function test_interactive_correct_no_submit() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->hints = array(
new question_hint_with_parts(23, 'This is the first hint.',
FORMAT_MOODLE, false, false),
new question_hint_with_parts(24, 'This is the second hint.',
FORMAT_MOODLE, true, true),
);
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'interactive', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the right answer.
$this->process_submission(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'));
// Finish the attempt without clicking check.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation());
// Check regrading does not mess anything up.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
}
public function test_interactive_partial_no_submit() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->hints = array(
new question_hint_with_parts(23, 'This is the first hint.',
FORMAT_MOODLE, false, false),
new question_hint_with_parts(24, 'This is the second hint.',
FORMAT_MOODLE, true, true),
);
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'interactive', 4);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the a partially right answer.
$this->process_submission(array('p1' => '1', 'p2' => '1', 'p3' => '2', 'p4' => '1'));
// Finish the attempt without clicking check.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_partcorrect_expectation(),
$this->get_no_hint_visible_expectation());
// Check regrading does not mess anything up.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedpartial);
$this->check_current_mark(1);
}
public function test_interactive_no_right_clears() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$dd->hints = array(
new question_hint_with_parts(23, 'This is the first hint.', FORMAT_MOODLE, false, true),
new question_hint_with_parts(24, 'This is the second hint.', FORMAT_MOODLE, true, true),
);
$dd->shufflechoices = false;
$this->start_attempt_at_question($dd, 'interactive', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the a completely wrong answer.
$this->process_submission(
array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_hint_expectation('This is the first hint'));
// Do try again.
$this->process_submission(
array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => '', '-tryagain' => 1));
// Check that all the wrong answers have been cleared.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_drag_image_home_expectation(1, 1, 1),
$this->get_contains_drag_image_home_expectation(2, 2, 1),
$this->get_contains_drag_image_home_expectation(3, 1, 2),
$this->get_contains_drag_image_home_expectation(4, 2, 2),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4', 0),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
}
public function test_display_of_right_answer_when_shuffled() {
// Create a drag-and-drop question.
$dd = test_question_maker::make_question('ddimageortext');
$this->start_attempt_at_question($dd, 'deferredfeedback', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4'),
$this->get_does_not_contain_feedback_expectation());
// Save a partial answer.
$this->process_submission($dd->get_correct_response());
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p1',
$dd->get_right_choice_for(1)),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p2',
$dd->get_right_choice_for(2)),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p3',
$dd->get_right_choice_for(3)),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'p4',
$dd->get_right_choice_for(4)),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->displayoptions->rightanswer = question_display_options::VISIBLE;
$this->assertEquals('Drop zone 1 -> {1. quick} '.
'Drop zone 2 -> {2. fox} '.
'Drop zone 3 -> {3. lazy} '.
'Drop zone 4 -> {4. dog}',
$dd->get_right_answer_summary());
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
}
}

View file

@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Version information for the drag-and-drop onto image question type.
*
* @package qtype_ddimageortext
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2015051000;
$plugin->requires = 2013101800;
$plugin->cron = 0;
$plugin->component = 'qtype_ddimageortext';
$plugin->maturity = MATURITY_STABLE;
$plugin->release = '1.8 for Moodle 2.6+';
$plugin->dependencies = array(
'qtype_gapselect' => 2014111200,
);
$plugin->outestssufficient = true;

View file

@ -0,0 +1,524 @@
YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
var DDIMAGEORTEXT_DD = function() {
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the base class for the question rendering and question editing form code.
*/
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
doc : null,
polltimer : null,
afterimageloaddone : false,
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
if (this.afterimageloaddone) {
return;
}
var bgdone = this.doc.bg_img().get('complete');
if (waitforimageconstrain) {
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
}
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
//in 'some' loop returning true breaks the loop and is passed as return value from
//'some' else returns false. Can be though of as equivalent to ||.
if (dragitemhome.get('tagName') !== 'IMG'){
return false;
}
var done = (dragitemhome.get('complete'));
if (waitforimageconstrain) {
done = done && dragitemhome.hasClass('constrained');
}
return !done;
});
if (bgdone && alldragsloaded) {
if (this.polltimer !== null) {
this.polltimer.cancel();
this.polltimer = null;
}
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
this.doc.bg_img().detach('load', this.poll_for_image_load);
if (pause !== 0) {
Y.later(pause, this, doafterwords);
} else {
doafterwords.call(this);
}
this.afterimageloaddone = true;
} else if (this.polltimer === null) {
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
this.polltimer =
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
}
},
/**
* Object to encapsulate operations on dd area.
*/
doc_structure : function (mainobj) {
var topnode = Y.one(this.get('topnode'));
var dragitemsarea = topnode.one('div.dragitems');
var dropbgarea = topnode.one('div.droparea');
return {
top_node : function() {
return topnode;
},
drag_items : function() {
return dragitemsarea.all('.drag');
},
drop_zones : function() {
return topnode.all('div.dropzones div.dropzone');
},
drop_zone_group : function(groupno) {
return topnode.all('div.dropzones div.group' + groupno);
},
drag_items_cloned_from : function(dragitemno) {
return dragitemsarea.all('.dragitems' + dragitemno);
},
drag_item : function(draginstanceno) {
return dragitemsarea.one('.draginstance' + draginstanceno);
},
drag_items_in_group : function(groupno) {
return dragitemsarea.all('.drag.group' + groupno);
},
drag_item_homes : function() {
return dragitemsarea.all('.draghome');
},
bg_img : function() {
return topnode.one('.dropbackground');
},
load_bg_img : function (url) {
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
},
add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
var oldhome = this.drag_item_home(dragitemno);
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
if (oldhome === null) {
if (url) {
dragitemsarea.append(imghtml);
} else if (alt !== '') {
dragitemsarea.append(divhtml);
}
} else {
if (url) {
dragitemsarea.insert(imghtml, oldhome);
} else if (alt !== '') {
dragitemsarea.insert(divhtml, oldhome);
}
oldhome.remove(true);
}
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
if (newlycreated !== null) {
newlycreated.setData('groupno', group);
newlycreated.setData('dragitemno', dragitemno);
}
},
drag_item_home : function (dragitemno) {
return dragitemsarea.one('.dragitemhomes' + dragitemno);
},
get_classname_numeric_suffix : function(node, prefix) {
var classes = node.getAttribute('class');
if (classes !== '') {
var classesarr = classes.split(' ');
for (var index = 0; index < classesarr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesarr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesarr[index]);
return + match[0];
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
clone_new_drag_item : function (draginstanceno, dragitemno) {
var draghome = this.drag_item_home(dragitemno);
if (draghome === null) {
return null;
}
var drag = draghome.cloneNode(true);
drag.removeClass('dragitemhomes' + dragitemno);
drag.addClass('dragitems' + dragitemno);
drag.addClass('draginstance' + draginstanceno);
drag.removeClass('draghome');
drag.addClass('drag');
drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
drag.setData('draginstanceno', draginstanceno);
drag.setData('dragitemno', dragitemno);
draghome.get('parentNode').appendChild(drag);
return drag;
},
draggable_for_question : function (drag, group, choice) {
new Y.DD.Drag({
node: drag,
dragMode: 'point',
groups: [group]
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
drag.setData('group', group);
drag.setData('choice', choice);
},
draggable_for_form : function (drag) {
var dd = new Y.DD.Drag({
node: drag,
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
dd.on('drag:end', function(e) {
var dragnode = e.target.get('node');
var draginstanceno = dragnode.getData('draginstanceno');
var gooddrop = dragnode.getData('gooddrop');
if (!gooddrop) {
mainobj.reset_drag_xy(draginstanceno);
} else {
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
}
}, this);
dd.on('drag:start', function(e) {
var drag = e.target;
drag.get('node').setData('gooddrop', false);
}, this);
}
};
},
update_padding_sizes_all : function () {
for (var groupno = 1; groupno <= 8; groupno++) {
this.update_padding_size_for_group(groupno);
}
},
update_padding_size_for_group : function (groupno) {
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
groupitems.each(function(item){
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
maxheight = Math.max(maxheight, item.get('clientHeight'));
}, this);
groupitems.each(function(item) {
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
margintopbottom + 'px ' + marginleftright + 'px');
}, this);
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
'height': maxheight + 10});
}
},
convert_to_window_xy : function (bgimgxy) {
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
}
}, {
NAME : DDIMAGEORTEXTDDNAME,
ATTRS : {
drops : {value : null},
readonly : {value : false},
topnode : {value : null}
}
});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
var DDIMAGEORTEXT_QUESTION = function() {
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
};
/**
* This is the code for question rendering.
*/
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
touchscrolldisable: null,
pendingid: '',
initializer : function() {
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.doc = this.doc_structure(this);
this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
this.doc.bg_img().after('load', this.poll_for_image_load, this,
false, 0, this.create_all_drag_and_drops);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
false, 0, this.create_all_drag_and_drops);
Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
},
/**
* prevent_touchmove_from_scrolling allows users of touch screen devices to
* use drag and drop and normal scrolling at the same time. I.e. when
* touching and dragging a draggable item, the screen does not scroll, but
* you can scroll by touching other area of the screen apart from the
* draggable items.
*/
prevent_touchmove_from_scrolling : function(drag) {
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
// Disable scrolling when touching the draggable items.
drag.on(touchstart, function() {
if (this.touchscrolldisable) {
return; // Already disabled.
}
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
e = e || window.event;
e.preventDefault();
});
}, this);
// Allow scrolling after releasing the draggable items.
drag.on(touchend, function() {
if (this.touchscrolldisable) {
this.touchscrolldisable.detach();
this.touchscrolldisable = null;
}
}, this);
},
create_all_drag_and_drops : function () {
this.init_drops();
this.update_padding_sizes_all();
var i = 0;
this.doc.drag_item_homes().each(function(dragitemhome){
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
var groupsize = this.doc.drop_zone_group(group).size();
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
if (dragnode.hasClass('infinite')) {
var dragstocreate = groupsize - 1;
while (dragstocreate > 0) {
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
dragstocreate--;
}
}
}, this);
this.reposition_drags_for_question();
if (!this.get('readonly')) {
this.doc.drop_zones().set('tabIndex', 0);
this.doc.drop_zones().each(
function(v){
v.on('dragchange', this.drop_zone_key_press, this);
}, this);
}
M.util.js_complete(this.pendingid);
},
drop_zone_key_press : function (e) {
switch (e.direction) {
case 'next' :
this.place_next_drag_in(e.target);
break;
case 'previous' :
this.place_previous_drag_in(e.target);
break;
case 'remove' :
this.remove_drag_from_drop(e.target);
break;
}
e.preventDefault();
this.reposition_drags_for_question();
},
place_next_drag_in : function (drop) {
this.search_for_unplaced_drop_choice(drop, 1);
},
place_previous_drag_in : function (drop) {
this.search_for_unplaced_drop_choice(drop, -1);
},
search_for_unplaced_drop_choice : function (drop, direction) {
var next;
var current = this.current_drag_in_drop(drop);
if ('' === current) {
if (direction === 1) {
next = 1;
} else {
next = 1;
var groupno = drop.getData('group');
this.doc.drag_items_in_group(groupno).each(function(drag) {
next = Math.max(next, drag.getData('choice'));
}, this);
}
} else {
next = + current + direction;
}
var drag;
do {
if (this.get_choices_for_drop(next, drop).size() === 0){
this.remove_drag_from_drop(drop);
return;
} else {
drag = this.get_unplaced_choice_for_drop(next, drop);
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_drag_in_drop : function (drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
return inputnode.get('value');
},
remove_drag_from_drop : function (drop) {
this.place_drag_in_drop(null, drop);
},
place_drag_in_drop : function (drag, drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', drag.getData('choice'));
} else {
inputnode.set('value', '');
}
},
reposition_drags_for_question : function() {
this.doc.drag_items().removeClass('placed');
this.doc.drag_items().each (function (dragitem) {
if (dragitem.dd !== undefined) {
dragitem.dd.detachAll('drag:start');
}
}, this);
this.doc.drop_zones().each(function(dropzone) {
var relativexy = dropzone.getData('xy');
dropzone.setXY(this.convert_to_window_xy(relativexy));
var inputcss = 'input#' + dropzone.getData('inputid');
var input = this.doc.top_node().one(inputcss);
var choice = input.get('value');
if (choice !== "") {
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
if (dragitem !== null) {
dragitem.setXY(dropzone.getXY());
dragitem.addClass('placed');
if (dragitem.dd !== undefined) {
dragitem.dd.once('drag:start', function (e, input) {
input.set('value', '');
e.target.get('node').removeClass('placed');
},this, input);
}
}
}
}, this);
this.doc.drag_items().each(function(dragitem) {
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
dragitem.setXY(dragitemhome.getXY());
}
}, this);
},
get_choices_for_drop : function(choice, drop) {
var group = drop.getData('group');
return this.doc.top_node().all(
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
},
get_unplaced_choice_for_drop : function(choice, drop) {
var dragitems = this.get_choices_for_drop(choice, drop);
var dragitem = null;
dragitems.some(function (d) {
if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
dragitem = d;
return true;
} else {
return false;
}
});
return dragitem;
},
init_drops : function () {
var dropareas = this.doc.top_node().one('div.dropzones');
var groupnodes = {};
for (var groupno = 1; groupno <= 8; groupno++) {
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
dropareas.append(groupnode);
groupnodes[groupno] = groupnode;
}
var drop_hit_handler = function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (Number(drop.getData('group')) === drag.getData('group')){
this.place_drag_in_drop(drag, drop);
}
};
for (var dropno in this.get('drops')) {
var drop = this.get('drops')[dropno];
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
var title = drop.text.replace('"', '\"');
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">&nbsp;</div>';
var dropnode = Y.Node.create(dropnodehtml);
groupnodes[drop.group].append(dropnode);
dropnode.setStyles({'opacity': 0.5});
dropnode.setData('xy', drop.xy);
dropnode.setData('place', dropno);
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
dropnode.setData('group', drop.group);
var dropdd = new Y.DD.Drop({
node: dropnode, groups : [drop.group]});
dropdd.on('drop:hit', drop_hit_handler, this);
}
}
}, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
Y.Event.define('dragchange', {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
_keys: {
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next', // Down arrow
'27': 'remove' // Escape
},
_keyHandler: function (e, notifier) {
if (this._keys[e.keyCode]) {
e.direction = this._keys[e.keyCode];
notifier.fire(e);
}
},
on: function (node, sub, notifier) {
sub._detacher = node.on(this._event, this._keyHandler,
this, notifier);
}
});
M.qtype_ddimageortext.init_question = function(config) {
return new DDIMAGEORTEXT_QUESTION(config);
};
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,524 @@
YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
var DDIMAGEORTEXT_DD = function() {
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the base class for the question rendering and question editing form code.
*/
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
doc : null,
polltimer : null,
afterimageloaddone : false,
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
if (this.afterimageloaddone) {
return;
}
var bgdone = this.doc.bg_img().get('complete');
if (waitforimageconstrain) {
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
}
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
//in 'some' loop returning true breaks the loop and is passed as return value from
//'some' else returns false. Can be though of as equivalent to ||.
if (dragitemhome.get('tagName') !== 'IMG'){
return false;
}
var done = (dragitemhome.get('complete'));
if (waitforimageconstrain) {
done = done && dragitemhome.hasClass('constrained');
}
return !done;
});
if (bgdone && alldragsloaded) {
if (this.polltimer !== null) {
this.polltimer.cancel();
this.polltimer = null;
}
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
this.doc.bg_img().detach('load', this.poll_for_image_load);
if (pause !== 0) {
Y.later(pause, this, doafterwords);
} else {
doafterwords.call(this);
}
this.afterimageloaddone = true;
} else if (this.polltimer === null) {
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
this.polltimer =
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
}
},
/**
* Object to encapsulate operations on dd area.
*/
doc_structure : function (mainobj) {
var topnode = Y.one(this.get('topnode'));
var dragitemsarea = topnode.one('div.dragitems');
var dropbgarea = topnode.one('div.droparea');
return {
top_node : function() {
return topnode;
},
drag_items : function() {
return dragitemsarea.all('.drag');
},
drop_zones : function() {
return topnode.all('div.dropzones div.dropzone');
},
drop_zone_group : function(groupno) {
return topnode.all('div.dropzones div.group' + groupno);
},
drag_items_cloned_from : function(dragitemno) {
return dragitemsarea.all('.dragitems' + dragitemno);
},
drag_item : function(draginstanceno) {
return dragitemsarea.one('.draginstance' + draginstanceno);
},
drag_items_in_group : function(groupno) {
return dragitemsarea.all('.drag.group' + groupno);
},
drag_item_homes : function() {
return dragitemsarea.all('.draghome');
},
bg_img : function() {
return topnode.one('.dropbackground');
},
load_bg_img : function (url) {
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
},
add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
var oldhome = this.drag_item_home(dragitemno);
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
if (oldhome === null) {
if (url) {
dragitemsarea.append(imghtml);
} else if (alt !== '') {
dragitemsarea.append(divhtml);
}
} else {
if (url) {
dragitemsarea.insert(imghtml, oldhome);
} else if (alt !== '') {
dragitemsarea.insert(divhtml, oldhome);
}
oldhome.remove(true);
}
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
if (newlycreated !== null) {
newlycreated.setData('groupno', group);
newlycreated.setData('dragitemno', dragitemno);
}
},
drag_item_home : function (dragitemno) {
return dragitemsarea.one('.dragitemhomes' + dragitemno);
},
get_classname_numeric_suffix : function(node, prefix) {
var classes = node.getAttribute('class');
if (classes !== '') {
var classesarr = classes.split(' ');
for (var index = 0; index < classesarr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesarr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesarr[index]);
return + match[0];
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
clone_new_drag_item : function (draginstanceno, dragitemno) {
var draghome = this.drag_item_home(dragitemno);
if (draghome === null) {
return null;
}
var drag = draghome.cloneNode(true);
drag.removeClass('dragitemhomes' + dragitemno);
drag.addClass('dragitems' + dragitemno);
drag.addClass('draginstance' + draginstanceno);
drag.removeClass('draghome');
drag.addClass('drag');
drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
drag.setData('draginstanceno', draginstanceno);
drag.setData('dragitemno', dragitemno);
draghome.get('parentNode').appendChild(drag);
return drag;
},
draggable_for_question : function (drag, group, choice) {
new Y.DD.Drag({
node: drag,
dragMode: 'point',
groups: [group]
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
drag.setData('group', group);
drag.setData('choice', choice);
},
draggable_for_form : function (drag) {
var dd = new Y.DD.Drag({
node: drag,
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
dd.on('drag:end', function(e) {
var dragnode = e.target.get('node');
var draginstanceno = dragnode.getData('draginstanceno');
var gooddrop = dragnode.getData('gooddrop');
if (!gooddrop) {
mainobj.reset_drag_xy(draginstanceno);
} else {
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
}
}, this);
dd.on('drag:start', function(e) {
var drag = e.target;
drag.get('node').setData('gooddrop', false);
}, this);
}
};
},
update_padding_sizes_all : function () {
for (var groupno = 1; groupno <= 8; groupno++) {
this.update_padding_size_for_group(groupno);
}
},
update_padding_size_for_group : function (groupno) {
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
groupitems.each(function(item){
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
maxheight = Math.max(maxheight, item.get('clientHeight'));
}, this);
groupitems.each(function(item) {
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
margintopbottom + 'px ' + marginleftright + 'px');
}, this);
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
'height': maxheight + 10});
}
},
convert_to_window_xy : function (bgimgxy) {
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
}
}, {
NAME : DDIMAGEORTEXTDDNAME,
ATTRS : {
drops : {value : null},
readonly : {value : false},
topnode : {value : null}
}
});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
var DDIMAGEORTEXT_QUESTION = function() {
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
};
/**
* This is the code for question rendering.
*/
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
touchscrolldisable: null,
pendingid: '',
initializer : function() {
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.doc = this.doc_structure(this);
this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
this.doc.bg_img().after('load', this.poll_for_image_load, this,
false, 0, this.create_all_drag_and_drops);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
false, 0, this.create_all_drag_and_drops);
Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
},
/**
* prevent_touchmove_from_scrolling allows users of touch screen devices to
* use drag and drop and normal scrolling at the same time. I.e. when
* touching and dragging a draggable item, the screen does not scroll, but
* you can scroll by touching other area of the screen apart from the
* draggable items.
*/
prevent_touchmove_from_scrolling : function(drag) {
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
// Disable scrolling when touching the draggable items.
drag.on(touchstart, function() {
if (this.touchscrolldisable) {
return; // Already disabled.
}
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
e = e || window.event;
e.preventDefault();
});
}, this);
// Allow scrolling after releasing the draggable items.
drag.on(touchend, function() {
if (this.touchscrolldisable) {
this.touchscrolldisable.detach();
this.touchscrolldisable = null;
}
}, this);
},
create_all_drag_and_drops : function () {
this.init_drops();
this.update_padding_sizes_all();
var i = 0;
this.doc.drag_item_homes().each(function(dragitemhome){
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
var groupsize = this.doc.drop_zone_group(group).size();
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
if (dragnode.hasClass('infinite')) {
var dragstocreate = groupsize - 1;
while (dragstocreate > 0) {
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
dragstocreate--;
}
}
}, this);
this.reposition_drags_for_question();
if (!this.get('readonly')) {
this.doc.drop_zones().set('tabIndex', 0);
this.doc.drop_zones().each(
function(v){
v.on('dragchange', this.drop_zone_key_press, this);
}, this);
}
M.util.js_complete(this.pendingid);
},
drop_zone_key_press : function (e) {
switch (e.direction) {
case 'next' :
this.place_next_drag_in(e.target);
break;
case 'previous' :
this.place_previous_drag_in(e.target);
break;
case 'remove' :
this.remove_drag_from_drop(e.target);
break;
}
e.preventDefault();
this.reposition_drags_for_question();
},
place_next_drag_in : function (drop) {
this.search_for_unplaced_drop_choice(drop, 1);
},
place_previous_drag_in : function (drop) {
this.search_for_unplaced_drop_choice(drop, -1);
},
search_for_unplaced_drop_choice : function (drop, direction) {
var next;
var current = this.current_drag_in_drop(drop);
if ('' === current) {
if (direction === 1) {
next = 1;
} else {
next = 1;
var groupno = drop.getData('group');
this.doc.drag_items_in_group(groupno).each(function(drag) {
next = Math.max(next, drag.getData('choice'));
}, this);
}
} else {
next = + current + direction;
}
var drag;
do {
if (this.get_choices_for_drop(next, drop).size() === 0){
this.remove_drag_from_drop(drop);
return;
} else {
drag = this.get_unplaced_choice_for_drop(next, drop);
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_drag_in_drop : function (drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
return inputnode.get('value');
},
remove_drag_from_drop : function (drop) {
this.place_drag_in_drop(null, drop);
},
place_drag_in_drop : function (drag, drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', drag.getData('choice'));
} else {
inputnode.set('value', '');
}
},
reposition_drags_for_question : function() {
this.doc.drag_items().removeClass('placed');
this.doc.drag_items().each (function (dragitem) {
if (dragitem.dd !== undefined) {
dragitem.dd.detachAll('drag:start');
}
}, this);
this.doc.drop_zones().each(function(dropzone) {
var relativexy = dropzone.getData('xy');
dropzone.setXY(this.convert_to_window_xy(relativexy));
var inputcss = 'input#' + dropzone.getData('inputid');
var input = this.doc.top_node().one(inputcss);
var choice = input.get('value');
if (choice !== "") {
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
if (dragitem !== null) {
dragitem.setXY(dropzone.getXY());
dragitem.addClass('placed');
if (dragitem.dd !== undefined) {
dragitem.dd.once('drag:start', function (e, input) {
input.set('value', '');
e.target.get('node').removeClass('placed');
},this, input);
}
}
}
}, this);
this.doc.drag_items().each(function(dragitem) {
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
dragitem.setXY(dragitemhome.getXY());
}
}, this);
},
get_choices_for_drop : function(choice, drop) {
var group = drop.getData('group');
return this.doc.top_node().all(
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
},
get_unplaced_choice_for_drop : function(choice, drop) {
var dragitems = this.get_choices_for_drop(choice, drop);
var dragitem = null;
dragitems.some(function (d) {
if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
dragitem = d;
return true;
} else {
return false;
}
});
return dragitem;
},
init_drops : function () {
var dropareas = this.doc.top_node().one('div.dropzones');
var groupnodes = {};
for (var groupno = 1; groupno <= 8; groupno++) {
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
dropareas.append(groupnode);
groupnodes[groupno] = groupnode;
}
var drop_hit_handler = function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (Number(drop.getData('group')) === drag.getData('group')){
this.place_drag_in_drop(drag, drop);
}
};
for (var dropno in this.get('drops')) {
var drop = this.get('drops')[dropno];
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
var title = drop.text.replace('"', '\"');
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">&nbsp;</div>';
var dropnode = Y.Node.create(dropnodehtml);
groupnodes[drop.group].append(dropnode);
dropnode.setStyles({'opacity': 0.5});
dropnode.setData('xy', drop.xy);
dropnode.setData('place', dropno);
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
dropnode.setData('group', drop.group);
var dropdd = new Y.DD.Drop({
node: dropnode, groups : [drop.group]});
dropdd.on('drop:hit', drop_hit_handler, this);
}
}
}, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
Y.Event.define('dragchange', {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
_keys: {
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next', // Down arrow
'27': 'remove' // Escape
},
_keyHandler: function (e, notifier) {
if (this._keys[e.keyCode]) {
e.direction = this._keys[e.keyCode];
notifier.fire(e);
}
},
on: function (node, sub, notifier) {
sub._detacher = node.on(this._event, this._keyHandler,
this, notifier);
}
});
M.qtype_ddimageortext.init_question = function(config) {
return new DDIMAGEORTEXT_QUESTION(config);
};
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});

View file

@ -0,0 +1,359 @@
YUI.add('moodle-qtype_ddimageortext-form', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the question editing form code.
*/
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
var DDIMAGEORTEXT_FORM = function() {
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
};
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
pendingid: '',
fp : null,
initializer : function() {
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.fp = this.file_pickers();
var tn = Y.one(this.get('topnode'));
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
'<div class="dropzones"></div></div>');
this.doc = this.doc_structure(this);
this.draw_dd_area();
},
draw_dd_area : function() {
var bgimageurl = this.fp.file('bgimage').href;
this.stop_selector_events();
this.set_options_for_drag_item_selectors();
if (bgimageurl !== null) {
this.doc.load_bg_img(bgimageurl);
this.load_drag_homes();
var drop = new Y.DD.Drop({
node: this.doc.bg_img()
});
//Listen for a drop:hit on the background image
drop.on('drop:hit', function(e) {
e.drag.get('node').setData('gooddrop', true);
});
this.afterimageloaddone = false;
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
this.doc.drag_item_homes()
.on('load', this.constrain_image_size, this, 'dragimage');
this.doc.bg_img().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
} else {
this.setup_form_events();
M.util.js_complete(this.pendingid);
}
this.update_visibility_of_file_pickers();
},
after_all_images_loaded : function () {
this.update_padding_sizes_all();
this.update_drag_instances();
this.reposition_drags_for_form();
this.set_options_for_drag_item_selectors();
this.setup_form_events();
Y.later(500, this, this.reposition_drags_for_form, [], true);
},
constrain_image_size : function (e, imagetype) {
var maxsize = this.get('maxsizes')[imagetype];
var reduceby = Math.max(e.target.get('width') / maxsize.width,
e.target.get('height') / maxsize.height);
if (reduceby > 1) {
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
}
e.target.addClass('constrained');
e.target.detach('load', this.constrain_image_size);
},
load_drag_homes : function () {
// Set up drag items homes.
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
this.load_drag_home(i);
}
},
load_drag_home : function (dragitemno) {
var url = null;
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
}
this.doc.add_or_update_drag_item_home(dragitemno, url,
this.form.get_form_value('draglabel', [dragitemno]),
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
},
update_drag_instances : function () {
// Set up drop zones.
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
if (drag !== null) {
this.doc.draggable_for_form(drag);
}
}
}
},
set_options_for_drag_item_selectors : function () {
var dragitemsoptions = {0: ''};
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
var label = this.form.get_form_value('draglabel', [i]);
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
&& file.name !== null) {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
}
}
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var selector = Y.one('#id_drops_' + i + '_choice');
var selectedvalue = selector.get('value');
selector.all('option').remove(true);
for (var value in dragitemsoptions) {
value = + value;
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
selector.append(option);
var optionnode = selector.one('option[value="' + value + '"]');
if (value === + selectedvalue) {
optionnode.set('selected', true);
} else {
if (value !== 0) { // No item option is always selectable.
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
if (cbel && !cbel.get('checked')) {
Y.all('fieldset#id_dropzoneheader select').some(function (selector) {
if (Number(selector.get('value')) === value) {
optionnode.set('disabled', true);
return true; // Stop looping.
}
return false;
}, this);
}
}
}
}
}
},
stop_selector_events : function () {
Y.all('fieldset#id_dropzoneheader select').detachAll();
},
setup_form_events : function () {
// Events triggered by changes to form data.
// X and y coordinates.
Y.all('fieldset#id_dropzoneheader input').on('blur', function (e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
}, this);
// Change in selected item.
Y.all('fieldset#id_dropzoneheader select').on('change', function (e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var old = this.doc.drag_item(draginstanceno);
if (old !== null) {
old.remove(true);
}
this.draw_dd_area();
}, this);
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
// Change to group selector.
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on(
'change', function () {
this.doc.drag_items().remove(true);
this.draw_dd_area();
}, this);
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on(
'change', function () {
this.doc.drag_items().remove(true);
this.draw_dd_area();
}, this);
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
.on('blur', this.set_options_for_drag_item_selectors, this);
// Change to infinite checkbox.
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
.on('change', this.set_options_for_drag_item_selectors, this);
}
// Event on file picker new file selection.
Y.after(function (e) {
var name = this.fp.name(e.id);
if (name !== 'bgimage') {
this.doc.drag_items().remove(true);
}
this.draw_dd_area();
}, M.form_filepicker, 'callback', this);
},
update_visibility_of_file_pickers : function() {
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'block');
} else {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'none');
}
}
},
reposition_drags_for_form : function() {
this.doc.drag_items().each(function (drag) {
var draginstanceno = drag.getData('draginstanceno');
this.reposition_drag_for_form(draginstanceno);
}, this);
M.util.js_complete(this.pendingid);
},
reposition_drag_for_form : function (draginstanceno) {
var drag = this.doc.drag_item(draginstanceno);
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
if (fromform[0] === '' && fromform[1] === '') {
var dragitemno = drag.getData('dragitemno');
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
} else {
drag.setXY(this.convert_to_window_xy(fromform));
}
}
},
set_drag_xy : function (draginstanceno, xy) {
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
},
reset_drag_xy : function (draginstanceno) {
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
},
//make sure xy value is not out of bounds of bg image
constrain_xy : function (draginstanceno, bgimgxy) {
var drag = this.doc.drag_item(draginstanceno);
var xleftconstrained =
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
var ytopconstrained =
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
xleftconstrained = Math.max(xleftconstrained, 0);
ytopconstrained = Math.max(ytopconstrained, 0);
return [xleftconstrained, ytopconstrained];
},
convert_to_bg_img_xy : function (windowxy) {
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
},
/**
* Low level operations on form.
*/
form : {
to_name_with_index : function(name, indexes) {
var indexstring = name;
for (var i = 0; i < indexes.length; i++) {
indexstring = indexstring + '[' + indexes[i] + ']';
}
return indexstring;
},
get_el : function (name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.to_name_with_index(name, indexes)];
},
get_form_value : function(name, indexes) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
set_form_value : function(name, indexes, value) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
},
from_name_with_index : function(name) {
var toreturn = {};
toreturn.indexes = [];
var bracket = name.indexOf('[');
toreturn.name = name.substring(0, bracket);
while (bracket !== -1) {
var end = name.indexOf(']', bracket + 1);
toreturn.indexes.push(name.substring(bracket + 1, end));
bracket = name.indexOf('[', end + 1);
}
return toreturn;
}
},
file_pickers : function () {
var draftitemidstoname;
var nametoparentnode;
if (draftitemidstoname === undefined) {
draftitemidstoname = {};
nametoparentnode = {};
var filepickers = Y.all('form.mform input.filepickerhidden');
filepickers.each(function(filepicker) {
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
}, this);
}
var toreturn = {
file : function (name) {
var parentnode = nametoparentnode[name];
var fileanchor = parentnode.one('div.filepicker-filelist a');
if (fileanchor) {
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
} else {
return {href : null, name : null};
}
},
name : function (draftitemid) {
return draftitemidstoname[draftitemid];
}
};
return toreturn;
}
}, {NAME : DDIMAGEORTEXTFORMNAME, ATTRS : {maxsizes:{value:null}}});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.init_form = function(config) {
return new DDIMAGEORTEXT_FORM(config);
};
}, '@VERSION@', {"requires": ["moodle-qtype_ddimageortext-dd", "form_filepicker"]});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,359 @@
YUI.add('moodle-qtype_ddimageortext-form', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the question editing form code.
*/
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
var DDIMAGEORTEXT_FORM = function() {
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
};
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
pendingid: '',
fp : null,
initializer : function() {
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.fp = this.file_pickers();
var tn = Y.one(this.get('topnode'));
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
'<div class="dropzones"></div></div>');
this.doc = this.doc_structure(this);
this.draw_dd_area();
},
draw_dd_area : function() {
var bgimageurl = this.fp.file('bgimage').href;
this.stop_selector_events();
this.set_options_for_drag_item_selectors();
if (bgimageurl !== null) {
this.doc.load_bg_img(bgimageurl);
this.load_drag_homes();
var drop = new Y.DD.Drop({
node: this.doc.bg_img()
});
//Listen for a drop:hit on the background image
drop.on('drop:hit', function(e) {
e.drag.get('node').setData('gooddrop', true);
});
this.afterimageloaddone = false;
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
this.doc.drag_item_homes()
.on('load', this.constrain_image_size, this, 'dragimage');
this.doc.bg_img().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
} else {
this.setup_form_events();
M.util.js_complete(this.pendingid);
}
this.update_visibility_of_file_pickers();
},
after_all_images_loaded : function () {
this.update_padding_sizes_all();
this.update_drag_instances();
this.reposition_drags_for_form();
this.set_options_for_drag_item_selectors();
this.setup_form_events();
Y.later(500, this, this.reposition_drags_for_form, [], true);
},
constrain_image_size : function (e, imagetype) {
var maxsize = this.get('maxsizes')[imagetype];
var reduceby = Math.max(e.target.get('width') / maxsize.width,
e.target.get('height') / maxsize.height);
if (reduceby > 1) {
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
}
e.target.addClass('constrained');
e.target.detach('load', this.constrain_image_size);
},
load_drag_homes : function () {
// Set up drag items homes.
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
this.load_drag_home(i);
}
},
load_drag_home : function (dragitemno) {
var url = null;
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
}
this.doc.add_or_update_drag_item_home(dragitemno, url,
this.form.get_form_value('draglabel', [dragitemno]),
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
},
update_drag_instances : function () {
// Set up drop zones.
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
if (drag !== null) {
this.doc.draggable_for_form(drag);
}
}
}
},
set_options_for_drag_item_selectors : function () {
var dragitemsoptions = {0: ''};
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
var label = this.form.get_form_value('draglabel', [i]);
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
&& file.name !== null) {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
}
}
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var selector = Y.one('#id_drops_' + i + '_choice');
var selectedvalue = selector.get('value');
selector.all('option').remove(true);
for (var value in dragitemsoptions) {
value = + value;
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
selector.append(option);
var optionnode = selector.one('option[value="' + value + '"]');
if (value === + selectedvalue) {
optionnode.set('selected', true);
} else {
if (value !== 0) { // No item option is always selectable.
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
if (cbel && !cbel.get('checked')) {
Y.all('fieldset#id_dropzoneheader select').some(function (selector) {
if (Number(selector.get('value')) === value) {
optionnode.set('disabled', true);
return true; // Stop looping.
}
return false;
}, this);
}
}
}
}
}
},
stop_selector_events : function () {
Y.all('fieldset#id_dropzoneheader select').detachAll();
},
setup_form_events : function () {
// Events triggered by changes to form data.
// X and y coordinates.
Y.all('fieldset#id_dropzoneheader input').on('blur', function (e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
}, this);
// Change in selected item.
Y.all('fieldset#id_dropzoneheader select').on('change', function (e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var old = this.doc.drag_item(draginstanceno);
if (old !== null) {
old.remove(true);
}
this.draw_dd_area();
}, this);
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
// Change to group selector.
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on(
'change', function () {
this.doc.drag_items().remove(true);
this.draw_dd_area();
}, this);
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on(
'change', function () {
this.doc.drag_items().remove(true);
this.draw_dd_area();
}, this);
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
.on('blur', this.set_options_for_drag_item_selectors, this);
// Change to infinite checkbox.
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
.on('change', this.set_options_for_drag_item_selectors, this);
}
// Event on file picker new file selection.
Y.after(function (e) {
var name = this.fp.name(e.id);
if (name !== 'bgimage') {
this.doc.drag_items().remove(true);
}
this.draw_dd_area();
}, M.form_filepicker, 'callback', this);
},
update_visibility_of_file_pickers : function() {
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'block');
} else {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'none');
}
}
},
reposition_drags_for_form : function() {
this.doc.drag_items().each(function (drag) {
var draginstanceno = drag.getData('draginstanceno');
this.reposition_drag_for_form(draginstanceno);
}, this);
M.util.js_complete(this.pendingid);
},
reposition_drag_for_form : function (draginstanceno) {
var drag = this.doc.drag_item(draginstanceno);
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
if (fromform[0] === '' && fromform[1] === '') {
var dragitemno = drag.getData('dragitemno');
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
} else {
drag.setXY(this.convert_to_window_xy(fromform));
}
}
},
set_drag_xy : function (draginstanceno, xy) {
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
},
reset_drag_xy : function (draginstanceno) {
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
},
//make sure xy value is not out of bounds of bg image
constrain_xy : function (draginstanceno, bgimgxy) {
var drag = this.doc.drag_item(draginstanceno);
var xleftconstrained =
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
var ytopconstrained =
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
xleftconstrained = Math.max(xleftconstrained, 0);
ytopconstrained = Math.max(ytopconstrained, 0);
return [xleftconstrained, ytopconstrained];
},
convert_to_bg_img_xy : function (windowxy) {
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
},
/**
* Low level operations on form.
*/
form : {
to_name_with_index : function(name, indexes) {
var indexstring = name;
for (var i = 0; i < indexes.length; i++) {
indexstring = indexstring + '[' + indexes[i] + ']';
}
return indexstring;
},
get_el : function (name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.to_name_with_index(name, indexes)];
},
get_form_value : function(name, indexes) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
set_form_value : function(name, indexes, value) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
},
from_name_with_index : function(name) {
var toreturn = {};
toreturn.indexes = [];
var bracket = name.indexOf('[');
toreturn.name = name.substring(0, bracket);
while (bracket !== -1) {
var end = name.indexOf(']', bracket + 1);
toreturn.indexes.push(name.substring(bracket + 1, end));
bracket = name.indexOf('[', end + 1);
}
return toreturn;
}
},
file_pickers : function () {
var draftitemidstoname;
var nametoparentnode;
if (draftitemidstoname === undefined) {
draftitemidstoname = {};
nametoparentnode = {};
var filepickers = Y.all('form.mform input.filepickerhidden');
filepickers.each(function(filepicker) {
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
}, this);
}
var toreturn = {
file : function (name) {
var parentnode = nametoparentnode[name];
var fileanchor = parentnode.one('div.filepicker-filelist a');
if (fileanchor) {
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
} else {
return {href : null, name : null};
}
},
name : function (draftitemid) {
return draftitemidstoname[draftitemid];
}
};
return toreturn;
}
}, {NAME : DDIMAGEORTEXTFORMNAME, ATTRS : {maxsizes:{value:null}}});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.init_form = function(config) {
return new DDIMAGEORTEXT_FORM(config);
};
}, '@VERSION@', {"requires": ["moodle-qtype_ddimageortext-dd", "form_filepicker"]});

View file

@ -0,0 +1,10 @@
{
"name": "moodle-qtype_ddimageortext-dd",
"builds": {
"moodle-qtype_ddimageortext-dd": {
"jsfiles": [
"ddimageortext.js"
]
}
}
}

View file

@ -0,0 +1,520 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
var DDIMAGEORTEXT_DD = function() {
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
};
/**
* This is the base class for the question rendering and question editing form code.
*/
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
doc : null,
polltimer : null,
afterimageloaddone : false,
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
if (this.afterimageloaddone) {
return;
}
var bgdone = this.doc.bg_img().get('complete');
if (waitforimageconstrain) {
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
}
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
//in 'some' loop returning true breaks the loop and is passed as return value from
//'some' else returns false. Can be though of as equivalent to ||.
if (dragitemhome.get('tagName') !== 'IMG'){
return false;
}
var done = (dragitemhome.get('complete'));
if (waitforimageconstrain) {
done = done && dragitemhome.hasClass('constrained');
}
return !done;
});
if (bgdone && alldragsloaded) {
if (this.polltimer !== null) {
this.polltimer.cancel();
this.polltimer = null;
}
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
this.doc.bg_img().detach('load', this.poll_for_image_load);
if (pause !== 0) {
Y.later(pause, this, doafterwords);
} else {
doafterwords.call(this);
}
this.afterimageloaddone = true;
} else if (this.polltimer === null) {
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
this.polltimer =
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
}
},
/**
* Object to encapsulate operations on dd area.
*/
doc_structure : function (mainobj) {
var topnode = Y.one(this.get('topnode'));
var dragitemsarea = topnode.one('div.dragitems');
var dropbgarea = topnode.one('div.droparea');
return {
top_node : function() {
return topnode;
},
drag_items : function() {
return dragitemsarea.all('.drag');
},
drop_zones : function() {
return topnode.all('div.dropzones div.dropzone');
},
drop_zone_group : function(groupno) {
return topnode.all('div.dropzones div.group' + groupno);
},
drag_items_cloned_from : function(dragitemno) {
return dragitemsarea.all('.dragitems' + dragitemno);
},
drag_item : function(draginstanceno) {
return dragitemsarea.one('.draginstance' + draginstanceno);
},
drag_items_in_group : function(groupno) {
return dragitemsarea.all('.drag.group' + groupno);
},
drag_item_homes : function() {
return dragitemsarea.all('.draghome');
},
bg_img : function() {
return topnode.one('.dropbackground');
},
load_bg_img : function (url) {
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
},
add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
var oldhome = this.drag_item_home(dragitemno);
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
if (oldhome === null) {
if (url) {
dragitemsarea.append(imghtml);
} else if (alt !== '') {
dragitemsarea.append(divhtml);
}
} else {
if (url) {
dragitemsarea.insert(imghtml, oldhome);
} else if (alt !== '') {
dragitemsarea.insert(divhtml, oldhome);
}
oldhome.remove(true);
}
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
if (newlycreated !== null) {
newlycreated.setData('groupno', group);
newlycreated.setData('dragitemno', dragitemno);
}
},
drag_item_home : function (dragitemno) {
return dragitemsarea.one('.dragitemhomes' + dragitemno);
},
get_classname_numeric_suffix : function(node, prefix) {
var classes = node.getAttribute('class');
if (classes !== '') {
var classesarr = classes.split(' ');
for (var index = 0; index < classesarr.length; index++) {
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
if (patt1.test(classesarr[index])) {
var patt2 = new RegExp('([0-9])+$');
var match = patt2.exec(classesarr[index]);
return + match[0];
}
}
}
throw 'Prefix "' + prefix + '" not found in class names.';
},
clone_new_drag_item : function (draginstanceno, dragitemno) {
var draghome = this.drag_item_home(dragitemno);
if (draghome === null) {
return null;
}
var drag = draghome.cloneNode(true);
drag.removeClass('dragitemhomes' + dragitemno);
drag.addClass('dragitems' + dragitemno);
drag.addClass('draginstance' + draginstanceno);
drag.removeClass('draghome');
drag.addClass('drag');
drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
drag.setData('draginstanceno', draginstanceno);
drag.setData('dragitemno', dragitemno);
draghome.get('parentNode').appendChild(drag);
return drag;
},
draggable_for_question : function (drag, group, choice) {
new Y.DD.Drag({
node: drag,
dragMode: 'point',
groups: [group]
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
drag.setData('group', group);
drag.setData('choice', choice);
},
draggable_for_form : function (drag) {
var dd = new Y.DD.Drag({
node: drag,
dragMode: 'point'
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
dd.on('drag:end', function(e) {
var dragnode = e.target.get('node');
var draginstanceno = dragnode.getData('draginstanceno');
var gooddrop = dragnode.getData('gooddrop');
if (!gooddrop) {
mainobj.reset_drag_xy(draginstanceno);
} else {
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
}
}, this);
dd.on('drag:start', function(e) {
var drag = e.target;
drag.get('node').setData('gooddrop', false);
}, this);
}
};
},
update_padding_sizes_all : function () {
for (var groupno = 1; groupno <= 8; groupno++) {
this.update_padding_size_for_group(groupno);
}
},
update_padding_size_for_group : function (groupno) {
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
if (groupitems.size() !== 0) {
var maxwidth = 0;
var maxheight = 0;
groupitems.each(function(item){
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
maxheight = Math.max(maxheight, item.get('clientHeight'));
}, this);
groupitems.each(function(item) {
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
margintopbottom + 'px ' + marginleftright + 'px');
}, this);
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
'height': maxheight + 10});
}
},
convert_to_window_xy : function (bgimgxy) {
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
}
}, {
NAME : DDIMAGEORTEXTDDNAME,
ATTRS : {
drops : {value : null},
readonly : {value : false},
topnode : {value : null}
}
});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
var DDIMAGEORTEXT_QUESTION = function() {
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
};
/**
* This is the code for question rendering.
*/
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
touchscrolldisable: null,
pendingid: '',
initializer : function() {
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.doc = this.doc_structure(this);
this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
this.doc.bg_img().after('load', this.poll_for_image_load, this,
false, 0, this.create_all_drag_and_drops);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
false, 0, this.create_all_drag_and_drops);
Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
},
/**
* prevent_touchmove_from_scrolling allows users of touch screen devices to
* use drag and drop and normal scrolling at the same time. I.e. when
* touching and dragging a draggable item, the screen does not scroll, but
* you can scroll by touching other area of the screen apart from the
* draggable items.
*/
prevent_touchmove_from_scrolling : function(drag) {
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
// Disable scrolling when touching the draggable items.
drag.on(touchstart, function() {
if (this.touchscrolldisable) {
return; // Already disabled.
}
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
e = e || window.event;
e.preventDefault();
});
}, this);
// Allow scrolling after releasing the draggable items.
drag.on(touchend, function() {
if (this.touchscrolldisable) {
this.touchscrolldisable.detach();
this.touchscrolldisable = null;
}
}, this);
},
create_all_drag_and_drops : function () {
this.init_drops();
this.update_padding_sizes_all();
var i = 0;
this.doc.drag_item_homes().each(function(dragitemhome){
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
var groupsize = this.doc.drop_zone_group(group).size();
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
if (dragnode.hasClass('infinite')) {
var dragstocreate = groupsize - 1;
while (dragstocreate > 0) {
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
i++;
if (!this.get('readonly')) {
this.doc.draggable_for_question(dragnode, group, choice);
// Prevent scrolling whilst dragging on Adroid devices.
this.prevent_touchmove_from_scrolling(dragnode);
}
dragstocreate--;
}
}
}, this);
this.reposition_drags_for_question();
if (!this.get('readonly')) {
this.doc.drop_zones().set('tabIndex', 0);
this.doc.drop_zones().each(
function(v){
v.on('dragchange', this.drop_zone_key_press, this);
}, this);
}
M.util.js_complete(this.pendingid);
},
drop_zone_key_press : function (e) {
switch (e.direction) {
case 'next' :
this.place_next_drag_in(e.target);
break;
case 'previous' :
this.place_previous_drag_in(e.target);
break;
case 'remove' :
this.remove_drag_from_drop(e.target);
break;
}
e.preventDefault();
this.reposition_drags_for_question();
},
place_next_drag_in : function (drop) {
this.search_for_unplaced_drop_choice(drop, 1);
},
place_previous_drag_in : function (drop) {
this.search_for_unplaced_drop_choice(drop, -1);
},
search_for_unplaced_drop_choice : function (drop, direction) {
var next;
var current = this.current_drag_in_drop(drop);
if ('' === current) {
if (direction === 1) {
next = 1;
} else {
next = 1;
var groupno = drop.getData('group');
this.doc.drag_items_in_group(groupno).each(function(drag) {
next = Math.max(next, drag.getData('choice'));
}, this);
}
} else {
next = + current + direction;
}
var drag;
do {
if (this.get_choices_for_drop(next, drop).size() === 0){
this.remove_drag_from_drop(drop);
return;
} else {
drag = this.get_unplaced_choice_for_drop(next, drop);
}
next = next + direction;
} while (drag === null);
this.place_drag_in_drop(drag, drop);
},
current_drag_in_drop : function (drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
return inputnode.get('value');
},
remove_drag_from_drop : function (drop) {
this.place_drag_in_drop(null, drop);
},
place_drag_in_drop : function (drag, drop) {
var inputid = drop.getData('inputid');
var inputnode = Y.one('input#' + inputid);
if (drag !== null) {
inputnode.set('value', drag.getData('choice'));
} else {
inputnode.set('value', '');
}
},
reposition_drags_for_question : function() {
this.doc.drag_items().removeClass('placed');
this.doc.drag_items().each (function (dragitem) {
if (dragitem.dd !== undefined) {
dragitem.dd.detachAll('drag:start');
}
}, this);
this.doc.drop_zones().each(function(dropzone) {
var relativexy = dropzone.getData('xy');
dropzone.setXY(this.convert_to_window_xy(relativexy));
var inputcss = 'input#' + dropzone.getData('inputid');
var input = this.doc.top_node().one(inputcss);
var choice = input.get('value');
if (choice !== "") {
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
if (dragitem !== null) {
dragitem.setXY(dropzone.getXY());
dragitem.addClass('placed');
if (dragitem.dd !== undefined) {
dragitem.dd.once('drag:start', function (e, input) {
input.set('value', '');
e.target.get('node').removeClass('placed');
},this, input);
}
}
}
}, this);
this.doc.drag_items().each(function(dragitem) {
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
dragitem.setXY(dragitemhome.getXY());
}
}, this);
},
get_choices_for_drop : function(choice, drop) {
var group = drop.getData('group');
return this.doc.top_node().all(
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
},
get_unplaced_choice_for_drop : function(choice, drop) {
var dragitems = this.get_choices_for_drop(choice, drop);
var dragitem = null;
dragitems.some(function (d) {
if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
dragitem = d;
return true;
} else {
return false;
}
});
return dragitem;
},
init_drops : function () {
var dropareas = this.doc.top_node().one('div.dropzones');
var groupnodes = {};
for (var groupno = 1; groupno <= 8; groupno++) {
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
dropareas.append(groupnode);
groupnodes[groupno] = groupnode;
}
var drop_hit_handler = function(e) {
var drag = e.drag.get('node');
var drop = e.drop.get('node');
if (Number(drop.getData('group')) === drag.getData('group')){
this.place_drag_in_drop(drag, drop);
}
};
for (var dropno in this.get('drops')) {
var drop = this.get('drops')[dropno];
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
var title = drop.text.replace('"', '\"');
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">&nbsp;</div>';
var dropnode = Y.Node.create(dropnodehtml);
groupnodes[drop.group].append(dropnode);
dropnode.setStyles({'opacity': 0.5});
dropnode.setData('xy', drop.xy);
dropnode.setData('place', dropno);
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
dropnode.setData('group', drop.group);
var dropdd = new Y.DD.Drop({
node: dropnode, groups : [drop.group]});
dropdd.on('drop:hit', drop_hit_handler, this);
}
}
}, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
Y.Event.define('dragchange', {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
_keys: {
'32': 'next', // Space
'37': 'previous', // Left arrow
'38': 'previous', // Up arrow
'39': 'next', // Right arrow
'40': 'next', // Down arrow
'27': 'remove' // Escape
},
_keyHandler: function (e, notifier) {
if (this._keys[e.keyCode]) {
e.direction = this._keys[e.keyCode];
notifier.fire(e);
}
},
on: function (node, sub, notifier) {
sub._detacher = node.on(this._event, this._keyHandler,
this, notifier);
}
});
M.qtype_ddimageortext.init_question = function(config) {
return new DDIMAGEORTEXT_QUESTION(config);
};

View file

@ -0,0 +1,10 @@
{
"moodle-qtype_ddimageortext-dd": {
"requires": [
"node",
"dd",
"dd-drop",
"dd-constrain"
]
}
}

View file

@ -0,0 +1,10 @@
{
"name": "moodle-qtype_ddimageortext-form",
"builds": {
"moodle-qtype_ddimageortext-form": {
"jsfiles": [
"form.js"
]
}
}
}

View file

@ -0,0 +1,354 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the question editing form code.
*/
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
var DDIMAGEORTEXT_FORM = function() {
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
};
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
pendingid: '',
fp : null,
initializer : function() {
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
M.util.js_pending(this.pendingid);
this.fp = this.file_pickers();
var tn = Y.one(this.get('topnode'));
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
'<div class="dropzones"></div></div>');
this.doc = this.doc_structure(this);
this.draw_dd_area();
},
draw_dd_area : function() {
var bgimageurl = this.fp.file('bgimage').href;
this.stop_selector_events();
this.set_options_for_drag_item_selectors();
if (bgimageurl !== null) {
this.doc.load_bg_img(bgimageurl);
this.load_drag_homes();
var drop = new Y.DD.Drop({
node: this.doc.bg_img()
});
//Listen for a drop:hit on the background image
drop.on('drop:hit', function(e) {
e.drag.get('node').setData('gooddrop', true);
});
this.afterimageloaddone = false;
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
this.doc.drag_item_homes()
.on('load', this.constrain_image_size, this, 'dragimage');
this.doc.bg_img().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
true, 0, this.after_all_images_loaded);
} else {
this.setup_form_events();
M.util.js_complete(this.pendingid);
}
this.update_visibility_of_file_pickers();
},
after_all_images_loaded : function () {
this.update_padding_sizes_all();
this.update_drag_instances();
this.reposition_drags_for_form();
this.set_options_for_drag_item_selectors();
this.setup_form_events();
Y.later(500, this, this.reposition_drags_for_form, [], true);
},
constrain_image_size : function (e, imagetype) {
var maxsize = this.get('maxsizes')[imagetype];
var reduceby = Math.max(e.target.get('width') / maxsize.width,
e.target.get('height') / maxsize.height);
if (reduceby > 1) {
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
}
e.target.addClass('constrained');
e.target.detach('load', this.constrain_image_size);
},
load_drag_homes : function () {
// Set up drag items homes.
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
this.load_drag_home(i);
}
},
load_drag_home : function (dragitemno) {
var url = null;
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
}
this.doc.add_or_update_drag_item_home(dragitemno, url,
this.form.get_form_value('draglabel', [dragitemno]),
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
},
update_drag_instances : function () {
// Set up drop zones.
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
if (drag !== null) {
this.doc.draggable_for_form(drag);
}
}
}
},
set_options_for_drag_item_selectors : function () {
var dragitemsoptions = {0: ''};
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
var label = this.form.get_form_value('draglabel', [i]);
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
&& file.name !== null) {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
} else if (label !== '') {
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
}
}
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
var selector = Y.one('#id_drops_' + i + '_choice');
var selectedvalue = selector.get('value');
selector.all('option').remove(true);
for (var value in dragitemsoptions) {
value = + value;
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
selector.append(option);
var optionnode = selector.one('option[value="' + value + '"]');
if (value === + selectedvalue) {
optionnode.set('selected', true);
} else {
if (value !== 0) { // No item option is always selectable.
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
if (cbel && !cbel.get('checked')) {
Y.all('fieldset#id_dropzoneheader select').some(function (selector) {
if (Number(selector.get('value')) === value) {
optionnode.set('disabled', true);
return true; // Stop looping.
}
return false;
}, this);
}
}
}
}
}
},
stop_selector_events : function () {
Y.all('fieldset#id_dropzoneheader select').detachAll();
},
setup_form_events : function () {
// Events triggered by changes to form data.
// X and y coordinates.
Y.all('fieldset#id_dropzoneheader input').on('blur', function (e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
}, this);
// Change in selected item.
Y.all('fieldset#id_dropzoneheader select').on('change', function (e) {
var name = e.target.getAttribute('name');
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
var old = this.doc.drag_item(draginstanceno);
if (old !== null) {
old.remove(true);
}
this.draw_dd_area();
}, this);
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
// Change to group selector.
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on(
'change', function () {
this.doc.drag_items().remove(true);
this.draw_dd_area();
}, this);
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on(
'change', function () {
this.doc.drag_items().remove(true);
this.draw_dd_area();
}, this);
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
.on('blur', this.set_options_for_drag_item_selectors, this);
// Change to infinite checkbox.
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
.on('change', this.set_options_for_drag_item_selectors, this);
}
// Event on file picker new file selection.
Y.after(function (e) {
var name = this.fp.name(e.id);
if (name !== 'bgimage') {
this.doc.drag_items().remove(true);
}
this.draw_dd_area();
}, M.form_filepicker, 'callback', this);
},
update_visibility_of_file_pickers : function() {
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'block');
} else {
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
.setStyle('display', 'none');
}
}
},
reposition_drags_for_form : function() {
this.doc.drag_items().each(function (drag) {
var draginstanceno = drag.getData('draginstanceno');
this.reposition_drag_for_form(draginstanceno);
}, this);
M.util.js_complete(this.pendingid);
},
reposition_drag_for_form : function (draginstanceno) {
var drag = this.doc.drag_item(draginstanceno);
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
if (fromform[0] === '' && fromform[1] === '') {
var dragitemno = drag.getData('dragitemno');
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
} else {
drag.setXY(this.convert_to_window_xy(fromform));
}
}
},
set_drag_xy : function (draginstanceno, xy) {
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
},
reset_drag_xy : function (draginstanceno) {
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
},
//make sure xy value is not out of bounds of bg image
constrain_xy : function (draginstanceno, bgimgxy) {
var drag = this.doc.drag_item(draginstanceno);
var xleftconstrained =
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
var ytopconstrained =
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
xleftconstrained = Math.max(xleftconstrained, 0);
ytopconstrained = Math.max(ytopconstrained, 0);
return [xleftconstrained, ytopconstrained];
},
convert_to_bg_img_xy : function (windowxy) {
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
},
/**
* Low level operations on form.
*/
form : {
to_name_with_index : function(name, indexes) {
var indexstring = name;
for (var i = 0; i < indexes.length; i++) {
indexstring = indexstring + '[' + indexes[i] + ']';
}
return indexstring;
},
get_el : function (name, indexes) {
var form = document.getElementById('mform1');
return form.elements[this.to_name_with_index(name, indexes)];
},
get_form_value : function(name, indexes) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
return el.checked;
} else {
return el.value;
}
},
set_form_value : function(name, indexes, value) {
var el = this.get_el(name, indexes);
if (el.type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
},
from_name_with_index : function(name) {
var toreturn = {};
toreturn.indexes = [];
var bracket = name.indexOf('[');
toreturn.name = name.substring(0, bracket);
while (bracket !== -1) {
var end = name.indexOf(']', bracket + 1);
toreturn.indexes.push(name.substring(bracket + 1, end));
bracket = name.indexOf('[', end + 1);
}
return toreturn;
}
},
file_pickers : function () {
var draftitemidstoname;
var nametoparentnode;
if (draftitemidstoname === undefined) {
draftitemidstoname = {};
nametoparentnode = {};
var filepickers = Y.all('form.mform input.filepickerhidden');
filepickers.each(function(filepicker) {
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
}, this);
}
var toreturn = {
file : function (name) {
var parentnode = nametoparentnode[name];
var fileanchor = parentnode.one('div.filepicker-filelist a');
if (fileanchor) {
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
} else {
return {href : null, name : null};
}
},
name : function (draftitemid) {
return draftitemidstoname[draftitemid];
}
};
return toreturn;
}
}, {NAME : DDIMAGEORTEXTFORMNAME, ATTRS : {maxsizes:{value:null}}});
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
M.qtype_ddimageortext.init_form = function(config) {
return new DDIMAGEORTEXT_FORM(config);
};

View file

@ -0,0 +1,8 @@
{
"moodle-qtype_ddimageortext-form": {
"requires": [
"moodle-qtype_ddimageortext-dd",
"form_filepicker"
]
}
}