From f2e8a8a0fe00c9b3ad6c8047f99f9b9d578f254e Mon Sep 17 00:00:00 2001 From: Andrew Robert Nicols Date: Mon, 31 Dec 2012 11:21:26 +0000 Subject: [PATCH] Update to use YUI3 and simplified HTML structure --- dragdrop.js | 196 ------------------------ renderer.php | 320 ++++++++++++++++++--------------------- styles.css | 18 ++- yui/dragdrop/dragdrop.js | 156 +++++++++++++++++++ 4 files changed, 316 insertions(+), 374 deletions(-) delete mode 100644 dragdrop.js create mode 100644 yui/dragdrop/dragdrop.js diff --git a/dragdrop.js b/dragdrop.js deleted file mode 100644 index 885717a..0000000 --- a/dragdrop.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Javascript for drag-and-drop matching question. - * - * @copyright © 2007 Adriane Boyd - * @author adrianeboyd@gmail.com - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package aab_ddmatch - */ - -(function() { - -var Dom = YAHOO.util.Dom; -var DDM = YAHOO.util.DragDropMgr; - -// Override DDM moveToEl function to prevent it from repositioning items -// since MoodleDDMatchItem repositions them in the document. -DDM.moveToEl = function(srcEl, targetEl) { - return; -} - -MoodleDDMatchItem = function(id, sGroup, config, dragstring) { - - MoodleDDMatchItem.superclass.constructor.call(this, id, sGroup, config); - - var el = this.getDragEl(); - Dom.setStyle(el, "opacity", 0.67); // The proxy is slightly transparent - - this.sGroup = sGroup; - this.isTarget = false; - this.dragstring = dragstring; -}; - -YAHOO.extend(MoodleDDMatchItem, YAHOO.util.DDProxy, { - - startDrag: function(x, y) { - // make the proxy look like the source element - var dragEl = this.getDragEl(); - var clickEl = this.getEl(); - - dragEl.innerHTML = clickEl.innerHTML; - - Dom.addClass(dragEl, "matchdrag"); - }, - - endDrag: function(e) { - var proxy = this.getDragEl(); - - var proxyid = proxy.id; - var thisid = this.id; - - Dom.setStyle(proxyid, "visibility", "hidden"); - Dom.setStyle(thisid, "visibility", ""); - }, - - onDragDrop: function(e, id) { - // get the drag and drop object that was targeted - var oDD; - - if ("string" == typeof id) { - oDD = DDM.getDDById(id); - } else { - oDD = DDM.getBestMatch(id); - } - - var el = this.getEl(); - - // move the item into the target, deleting anything already in the slot - this.moveItem(el, oDD.getEl()); - - Dom.replaceClass(oDD.getEl(), "matchover", "matchdefault"); - }, - - onDragEnter: function(e, id) { - // get the drag and drop object that was targeted - var oDD; - - if ("string" == typeof id) { - oDD = DDM.getDDById(id); - } else { - oDD = DDM.getBestMatch(id); - } - - Dom.replaceClass(oDD.getEl(), "matchdefault", "matchover"); - }, - - onDragOut: function(e, id) { - // get the drag and drop object that was targeted - var oDD; - - if ("string" == typeof id) { - oDD = DDM.getDDById(id); - } else { - oDD = DDM.getBestMatch(id); - } - - Dom.replaceClass(oDD.getEl(), "matchover", "matchdefault"); - }, - - onInvalidDrop: function(e, id) { - var el = this.getEl(); - // if the item was dragged off a target, delete it - if (el.parentNode.id.match("target")) { - // add dragstring back to empty box - idparts = el.id.split("_"); - li = document.createElement("li"); - li.setAttribute("id", idparts[0] + "_0"); - li.appendChild(document.createTextNode(this.dragstring)); - el.parentNode.appendChild(li); - - Dom = YAHOO.util.Dom; - inputhidden = Dom.get(el.parentNode.getAttribute("name")); - inputhidden.setAttribute("value", 0); - - // delete the item - el.parentNode.removeChild(el); - } - }, - - moveItem: function(eldragged, eltargetul) { - eldraggedparent = eldragged.parentNode; - - // remove the item currently in the target - for (i = 0; i < eltargetul.childNodes.length; i++) { - eltargetul.removeChild(eltargetul.childNodes[0]); - } - - // if the item was moved from the origin, make a copy and move - if (eldraggedparent.id.match("origin")) { - el1copy = eldragged.cloneNode(true); - el1copy.setAttribute("id", ""); - el1id = Dom.generateId(el1copy, "_"); - el1copy.setAttribute("id", eldragged.id + el1id); - eltargetul.appendChild(el1copy); - new MoodleDDMatchItem(el1copy.id, this.sGroup, '', this.dragstring); - } - // else move item - else { - // add dragstring back to empty box - idparts = eldragged.id.split("_"); - li = document.createElement("li"); - li.setAttribute("id", idparts[0] + "_0"); - li.appendChild(document.createTextNode(this.dragstring)); - eldraggedparent.appendChild(li); - - // remove from origin - eldraggedparent.removeChild(eldragged); - - // add to target - eltargetul.appendChild(eldragged); - - Dom = YAHOO.util.Dom; - inputhidden = Dom.get(eldraggedparent.getAttribute("name")); - inputhidden.setAttribute("value", 0); - } - - Dom = YAHOO.util.Dom; - inputhidden = Dom.get(eltargetul.getAttribute("name")); - idparts = eldragged.id.split("_"); - inputhidden.setAttribute("value", idparts[1]); - } - -}); - -})(); - -M.ddmatch = {} - -// Replace the ablock menus with drag&drop enabled ablock and initialize the draggables -M.ddmatch.Init = function(vars) { - var id = vars.id; - var stems = vars.stems; - var choices = vars.choices; - var selectedids = vars.selectedids; - var readonly = vars.readonly; - var dragstring = vars.dragstring; - - var Dom = YAHOO.util.Dom; - var ablock = Dom.get("ablock_" + id); - ablock.innerHTML = vars.ablock; - var innerablock = ablock.firstChild; - ablock.parentNode.replaceChild(innerablock, ablock); - - if (!readonly) { - for (i in stems) { - new YAHOO.util.DDTarget("ultarget" + id + "_" + stems[i], id); - } - - for (i in choices) { - new MoodleDDMatchItem("drag" + id + "_" + i, id, "", dragstring); - } - - for (i in selectedids) { - new MoodleDDMatchItem(selectedids[i], id, "", dragstring); - } - } -} diff --git a/renderer.php b/renderer.php index e5f3f8e..034dc95 100644 --- a/renderer.php +++ b/renderer.php @@ -19,39 +19,46 @@ */ class qtype_ddmatch_renderer extends qtype_with_combined_feedback_renderer { - public function head_code(question_attempt $qa) { - global $PAGE; - - if ($this->can_use_drag_and_drop()) { - $PAGE->requires->js('/question/type/ddmatch/dragdrop.js'); - - $PAGE->requires->yui2_lib('yahoo'); - $PAGE->requires->yui2_lib('event'); - $PAGE->requires->yui2_lib('dom'); - $PAGE->requires->yui2_lib('dragdrop'); - $PAGE->requires->yui2_lib('animation'); - } - } - - public function formulation_and_controls(question_attempt $qa, - question_display_options $options) { + /** + * Generate the HTML required for a ddmatch question + * + * @param $qa question_attempt The question attempt + * @param $options question_display_options The options for display + */ + public function formulation_and_controls(question_attempt $qa, question_display_options $options) { + // We use the question quite a lot so store a reference to it once. + $question = $qa->get_question(); - $creator = new formulation_and_controls_select($this, $qa, $options); - $o = $creator->construct(); + // Put together the basic question text and answer block. + $output = ''; + $output .= $this->construct_questiontext($question->format_questiontext($qa)); + $output .= $this->construct_answerblock($qa, $question, $options); if ($this->can_use_drag_and_drop()) { - $creator = new formulation_and_controls_dragdrop($this, $qa, $options); - $creator->construct(); - - $initparams = json_encode($creator->get_ddmatch_init_params()); - $js = "YAHOO.util.Event.onDOMReady(function(){M.ddmatch.Init($initparams);});"; + $this->page->requires->string_for_js('draganswerhere', 'qtype_ddmatch'); + $this->page->requires->yui_module('moodle-qtype_ddmatch-dragdrop', + 'M.qtype.ddmatch.init_dragdrop', array(array( + 'questionid' => $qa->get_slot(), + 'readonly' => $options->readonly, + )) + ); + } - $o .= html_writer::script($js); + if ($qa->get_state() === question_state::$invalid) { + $output .= html_writer::nonempty_tag('div', + $question->get_validation_error($response), + array('class' => 'validationerror')); } - return $o; + return $output; } + /** + * Check whether drag and drop is supported + * + * exist somewhere) + * @return boolean Whether or not to generate the drag and drop content + */ protected function can_use_drag_and_drop() { global $USER; @@ -68,6 +75,11 @@ protected function can_use_drag_and_drop() { return true; } + /** + * Format the question choices for display + * + * @param question_attempt qa + */ public function format_choices(question_attempt $qa, $rawhtml=false) { $question = $qa->get_question(); $choices = array(); @@ -87,7 +99,10 @@ public function specific_feedback(question_attempt $qa) { } public function correct_response(question_attempt $qa) { - if ($qa->get_state()->is_correct()) return ''; + if ($qa->get_state()->is_correct()) { + // The answer was correct so we don't need to do anything further + return ''; + } $question = $qa->get_question(); $stemorder = $question->get_stem_order(); @@ -108,90 +123,74 @@ public function correct_response(question_attempt $qa) { return get_string('correctansweris', 'qtype_match', html_writer::table($table)); } - // needed for formulation_and_controls_* classes - public function feedback_class($fraction) { - return parent::feedback_class($fraction); - } - - public function feedback_image($fraction, $selected = true) { - return parent::feedback_image($fraction, $selected); - } -} - -abstract class formulation_and_controls_base { - protected $ddmatchrenderer; - protected $qa; - protected $options; - protected $question; - protected $choices; - protected $curfieldname; - - public function __construct(qtype_ddmatch_renderer $ddmatchrenderer, - question_attempt $qa, question_display_options $options) { - $this->ddmatchrenderer = $ddmatchrenderer; - $this->qa = $qa; - $this->options = $options; - $this->question = $qa->get_question(); - $this->init_choices(); + /** + * Construct the question text displayed to the user + * + * @param questiontext The question text to user + * @return String the rendered question text + */ + public function construct_questiontext($questiontext) { + return html_writer::tag('div', $questiontext, array( + 'class' => 'qtext', + )); } - public function construct() { - $response = $this->qa->get_last_qt_data(); - - $result = ''; - $result .= $this->construct_qtext(); - - $result .= $this->construct_ablock(); - - if ($this->qa->get_state() == question_state::$invalid) { - $result .= html_writer::nonempty_tag('div', - $this->question->get_validation_error($response), - array('class' => 'validationerror')); - } - - return $result; - } - - protected function construct_qtext() { - return html_writer::tag('div', $this->question->format_questiontext($this->qa), - array('class' => 'qtext')); - } - - protected function construct_ablock() { - $stemorder = $this->question->get_stem_order(); - $response = $this->qa->get_last_qt_data(); + /** + * Construct the answer block area + * + * @param question_attempt $qa + */ + public function construct_answerblock($qa, $question, $options) { + $stemorder = $question->get_stem_order(); + $response = $qa->get_last_qt_data(); + $selectchoices = $this->format_choices($qa); + $dragdropchoices = $this->format_choices($qa, true); - $o = html_writer::start_tag('div', array('id' => 'ablock_'.$this->question->id, 'class' => 'ablock')); + $o = html_writer::start_tag('div', array('class' => 'ablock')); $o .= html_writer::start_tag('table', array('class' => 'answer')); $o .= html_writer::start_tag('tbody'); $parity = 0; + $curfieldname = null; foreach ($stemorder as $key => $stemid) { $o .= html_writer::start_tag('tr', array('class' => 'r' . $parity)); - $o .= html_writer::tag('td', $this->construct_stem_cell($stemid), + $o .= html_writer::tag('td', $this->construct_stem_cell($qa, $question, $stemid), array('class' => 'text')); - $classes = 'control'; + $classes = array('control'); $feedbackimage = ''; - $this->curfieldname = $this->question->get_field_name($key); - if (array_key_exists($this->curfieldname, $response)) { - $selected = $response[$this->curfieldname]; + $curfieldname = $question->get_field_name($key); + if (array_key_exists($curfieldname, $response)) { + $selected = $response[$curfieldname]; } else { $selected = 0; } - $fraction = (int) ($selected && $selected == $this->question->get_right_choice_for($stemid)); + $fraction = (int) ($selected && $selected == $question->get_right_choice_for($stemid)); - if ($this->options->correctness && $selected) { - $classes .= ' ' . $this->ddmatchrenderer->feedback_class($fraction); - $feedbackimage = $this->ddmatchrenderer->feedback_image($fraction); + if ($options->correctness && $selected) { + $classes[] = $this->feedback_class($fraction); + $feedbackimage = $this->feedback_image($fraction); + } + + if ($this->can_use_drag_and_drop()) { + $dragdropclasses = $classes; + $classes[] = 'hiddenifjs'; + $dragdropclasses[] = 'visibleifjs'; } $o .= html_writer::tag('td', - $this->construct_choice_cell($stemid, $selected) . - ' ' . $feedbackimage, array('class' => $classes)); + $this->construct_choice_cell_select($qa, $options, $selectchoices, $stemid, $curfieldname, $selected) . + ' ' . $feedbackimage, array('class' => implode(' ', $classes))); + + if ($this->can_use_drag_and_drop()) { + // Only add the dragdrop divs if drag drop is enabled + $o .= html_writer::tag('td', + $this->construct_choice_cell_dragdrop($qa, $options, $dragdropchoices, $stemid, $curfieldname, $selected) . + ' ' . $feedbackimage, array('class' => implode(' ', $dragdropclasses))); + } $o .= html_writer::end_tag('tr'); $parity = 1 - $parity; @@ -199,122 +198,91 @@ protected function construct_ablock() { $o .= html_writer::end_tag('tbody'); $o .= html_writer::end_tag('table'); - $o .= $this->construct_additional_controls(); + if ($this->can_use_drag_and_drop()) { + $o .= $this->construct_available_dragdrop_choices($qa, $question); + } $o .= html_writer::end_tag('div'); $o .= html_writer::tag('div', '', array('class' => 'clearer')); - - return $o; - } - - protected function init_choices() { - - } - protected function construct_stem_cell($stemid) { - return $this->question->format_text( - $this->question->stems[$stemid], $this->question->stemformat[$stemid], - $this->qa, 'qtype_ddmatch', 'subquestion', $stemid); + return $o; } - protected function construct_choice_cell($stemid, $selected) { - throw new coding_exception('construct_choice_cell must return at least empty string'); + private function construct_stem_cell($qa, $question, $stemid) { + return $question->format_text( + $question->stems[$stemid], $question->stemformat[$stemid], + $qa, 'qtype_ddmatch', 'subquestion', $stemid); } - protected function construct_additional_controls() { - return ''; + private function construct_choice_cell_select($qa, $options, $choices, $stemid, $curfieldname, $selected) { + return html_writer::select($choices, $qa->get_qt_field_name($curfieldname), $selected, + array('0' => 'choose'), array('disabled' => $options->readonly)); } -} -class formulation_and_controls_select extends formulation_and_controls_base { - protected function init_choices() { - $this->choices = $this->ddmatchrenderer->format_choices($this->qa); - } + private function construct_choice_cell_dragdrop($qa, $options, $choices, $stemid, $curfieldname, $selected) { - protected function construct_choice_cell($stemid, $selected) { - return html_writer::select($this->choices, $this->qa->get_qt_field_name($this->curfieldname), $selected, - array('0' => 'choose'), array('disabled' => $this->options->readonly)); - } -} + $placeholderclasses = array('placeholder'); + $li = ''; -class formulation_and_controls_dragdrop extends formulation_and_controls_base { - private $lastpostfix = 0; - private $selectedids = array(); - private $ablock; - - protected function construct_ablock() { - $this->ablock = parent::construct_ablock(); - - return $this->ablock; - } - - protected function init_choices() { - $this->choices = $this->ddmatchrenderer->format_choices($this->qa, true); - } + // Check whether an answer has already been selected + if ($selected !== 0) { + // An answer has already been selected, display it as well + $question = $qa->get_question(); + $choiceorder = $question->get_choice_order(); - protected function construct_choice_cell($stemid, $selected) { - if ($selected == 0) { - $li = html_writer::tag('li', get_string('draganswerhere', 'qtype_ddmatch')); - } - else { - $choiceorder = $this->question->get_choice_order(); - $this->lastpostfix++; - - $id = 'drag'.$this->question->id.'_'.$selected.'_'.$this->lastpostfix; - $this->selectedids[] = $id; $attributes = array( - 'id' => $id, - 'name' => $id, - 'class' => 'matchdrag'); - $li = html_writer::tag('li', $this->choices[$selected], $attributes); + 'data-id' => $selected, + 'class' => 'matchdrag copy'); + $li = html_writer::tag('li', $choices[$selected], $attributes); + + // Add the hidden placeholder class so that the placeholder is initially hidden + $placeholderclasses[] = 'hidden'; } + $placeholder = html_writer::tag('li', get_string('draganswerhere', 'qtype_ddmatch'), array( + 'class' => implode(' ', $placeholderclasses), + )); + $li = $placeholder . $li; + + $question = $qa->get_question(); + $attributes = array( - 'id' => 'ultarget'.$this->question->id.'_'.$stemid, - 'name' => $this->qa->get_qt_field_name($this->curfieldname), - 'class' => 'matchtarget matchdefault'); - $o = html_writer::tag('ul', $li, $attributes); - - $attributes = array( - 'type' => 'hidden', - 'id' => $this->qa->get_qt_field_name($this->curfieldname), - 'name' => $this->qa->get_qt_field_name($this->curfieldname), - 'value' => $selected); - $o .= html_writer::empty_tag('input', $attributes); - - return $o; + 'id' => 'ultarget'.$question->id.'_'.$stemid, + 'name' => $qa->get_qt_field_name($curfieldname), + 'class' => 'matchtarget matchdefault', + 'data-selectname' => $qa->get_qt_field_name($curfieldname), + ); + $output = html_writer::tag('ul', $li, $attributes); + + return $output; } - protected function construct_additional_controls() { - $choiceorder = $this->question->get_choice_order(); + /** + * Construct the list of available answers for use in the drag and drop + * interface. + * + * @param $question + * @return String + */ + public function construct_available_dragdrop_choices($qa, $question) { + $choiceorder = $question->get_choice_order(); + $choices = $this->format_choices($qa, true); + $uldata = ''; foreach ($choiceorder as $key => $choiceid) { $attributes = array( - 'id' => 'drag'.$this->question->id.'_'.$key, - 'name' => 'drag'.$this->question->id.'_'.$key, - 'class' => 'matchdrag'); - $li = html_writer::tag('li', $this->choices[$key], $attributes); + 'data-id' => $key, + 'class' => 'matchdrag' + ); + $li = html_writer::tag('li', $choices[$key], $attributes); $uldata .= $li; } $attributes = array( - 'id' => 'ulorigin'.$this->question->id, - 'class' => 'matchorigin'); + 'id' => 'ulorigin' . $question->id, + 'class' => 'matchorigin visibleifjs'); $o = html_writer::tag('ul', $uldata, $attributes); return $o; } - - public function get_ddmatch_init_params() { - $initparams = new stdClass(); - $initparams->id = $this->question->id; - $initparams->stems = $this->question->get_stem_order(); - $initparams->choices = $this->question->get_choice_order(); - $initparams->selectedids = $this->selectedids; - $initparams->readonly = $this->options->readonly; - $initparams->dragstring = get_string('draganswerhere', 'qtype_ddmatch'); - $initparams->ablock = $this->ablock; - - return $initparams; - } } diff --git a/styles.css b/styles.css index 07bfce1..34a19ec 100644 --- a/styles.css +++ b/styles.css @@ -19,7 +19,7 @@ list-style:none; } -.ddmatch ul.matchtarget { +.ddmatch .matchtarget { border: 1px solid black; list-style: none; min-height: 30px; @@ -53,4 +53,18 @@ .ddmatch .correctanswertable { margin: 10px; -} \ No newline at end of file +} + +.yui3-dd-drop-active-valid { + border: 2px dotted red; +} +.ddmatch ul.yui3-dd-drop-over { + border: 2px dotted red; +} +.yui3-dd-drop-active-invalid { + border: 2px solid red; +} + +.matchtarget .placeholder.hidden { + display: none; +} diff --git a/yui/dragdrop/dragdrop.js b/yui/dragdrop/dragdrop.js new file mode 100644 index 0000000..91b20c5 --- /dev/null +++ b/yui/dragdrop/dragdrop.js @@ -0,0 +1,156 @@ +YUI.add('moodle-qtype_ddmatch-dragdrop', function(Y, NAME) { + + var DDMATCHNAME = 'qtype_ddmatch-dragdrop'; + + var DDMATCH = function() { + DDMATCH.superclass.constructor.apply(this, arguments); + }; + + Y.extend(DDMATCH, Y.Base, { + container : null, + delegation : null, + + initializer : function() { + if (this.get('readonly')) { + // Don't apply any of the drag and drop magic if this form is readonly + return; + } + + var containerid = 'div#q' + this.get('questionid'), + group = containerid + ' .matchtarget'; + + // Set the container - we use this in various places + this.container = Y.one(containerid); + if (typeof this.container === 'null') { + // If we can't find a valid question exit and leave the form in readonly state + return; + } + + this.delegation = new Y.DD.Delegate({ + container: this.container, + nodes: 'li.matchdrag' + }); + + this.delegation.dd.addToGroup(group); + + // Add the DDProxy so we only show the outline and can ensure + // that the element isn't actually moved + this.delegation.dd.plug(Y.Plugin.DDProxy, { + moveOnEnd: false + }); + + // Constrain the drag action to just this question + this.delegation.dd.plug(Y.Plugin.DDConstrained, { + constrain2node: this.container + }); + + this.container.all('.matchtarget').each(function(curNode) { + // Add drop targets to each matchtarget + var drop = new Y.DD.Drop({ + node: curNode, + groups: [ group ] + }); + }); + + this.delegation.dd.on('drag:drophit', this.handleHit, this); + this.delegation.dd.on('drag:dropmiss', this.handleMiss, this); + }, + handleHit : function(thisevent) { + // Local variables + var drag = thisevent.drag, + drop = thisevent.drop, + dragNode = drag.get('node'), + dropNode = drop.get('node').ancestor('ul.matchtarget', true), + copy, ancestor; + + if (dragNode.hasClass('copy')) { + // This node is a copy, just move it + copy = dragNode; + + // Unhide the old node's placeholder + ancestor = dragNode.ancestor(); + ancestor.one('li.placeholder').removeClass('hidden'); + + // Unset the input element value on the old ancestor + this.setValue(ancestor); + } else { + // Create a copy of the element being dragged element + copy = dragNode.cloneNode(true); + copy.addClass('copy'); + } + + // Remove any other matchdrag elements + dropNode.all('li.matchdrag').remove(); + + // Append it to the target + dropNode.appendChild(copy); + + // Hide the placeholder elemend on the drop node + dropNode.one('li.placeholder').addClass('hidden'); + + // Set the input element value on the dropNode + this.setValue(dropNode, dragNode.getData('id')); + + // Resync the targets + this.delegation.syncTargets(); + }, + handleMiss : function(thisevent) { + // Local variables + var dragNode = thisevent.target.get('node'), + ancestor; + + // We only need to handle misses where a copied node was being dragged + // Originals are returned by DDProxy + if (dragNode.hasClass('copy')) { + // Fetch the ancestor now - we'll need it later + ancestor = dragNode.ancestor('ul.matchtarget'); + + // Remove the node + dragNode.remove(); + this.delegation.syncTargets(); + + // Show the placeholder again + ancestor.one('li.placeholder').removeClass('hidden'); + + // Now clear the value for this ancestor + this.setValue(ancestor); + } + }, + setValue : function(targetNode, value) { + var selectname = targetNode.getData('selectname'), + selectelement, + selectoption; + + // Retrieve the element + selectelement = this.container.one('select[name=' + selectname + ']'); + if (value) { + // Attempt to set the value of the select to the relevant option + selectoption = selectelement.one('option[value=' + value + ']'); + selectoption.set('selected', true); + } else { + // Attempt to set the value of the select to the first option + selectelement.one('option').set('selected', true); + } + selectelement.get('value'); + } + }, + { + NAME : DDMATCHNAME, + ATTRS : { + questionid : { + 'type' : Number, + 'default' : null + } + } + }); + + M.qtype = M.qtype || {}; + M.qtype.ddmatch = M.qtype.ddmatch || {}; + M.qtype.ddmatch.init_dragdrop = function(config) { + if (config.readonly === true) { + // Don't instantiate the drag/drop if this form is readonly + return {}; + } + return new DDMATCH(config); + }; +}, '@VERSION@', {requires:['dd-delegate', 'dd-drop-plugin', 'dd-proxy', 'dd-constrain', 'selector-css3']});