1 YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
19 var DDIMAGEORTEXT_DD = function() {
20 DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
24 * This is the base class for the question rendering and question editing form code.
26 Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
29 afterimageloaddone : false,
30 poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
31 if (this.afterimageloaddone) {
34 var bgdone = this.doc.bg_img().get('complete');
35 if (waitforimageconstrain) {
36 bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
38 var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
39 //in 'some' loop returning true breaks the loop and is passed as return value from
40 //'some' else returns false. Can be though of as equivalent to ||.
41 if (dragitemhome.get('tagName') !== 'IMG'){
44 var done = (dragitemhome.get('complete'));
45 if (waitforimageconstrain) {
46 done = done && dragitemhome.hasClass('constrained');
50 if (bgdone && alldragsloaded) {
51 if (this.polltimer !== null) {
52 this.polltimer.cancel();
53 this.polltimer = null;
55 this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
56 this.doc.bg_img().detach('load', this.poll_for_image_load);
58 Y.later(pause, this, doafterwords);
60 doafterwords.call(this);
62 this.afterimageloaddone = true;
63 } else if (this.polltimer === null) {
64 var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
66 Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
70 * Object to encapsulate operations on dd area.
72 doc_structure : function (mainobj) {
73 var topnode = Y.one(this.get('topnode'));
74 var dragitemsarea = topnode.one('div.dragitems');
75 var dropbgarea = topnode.one('div.droparea');
77 top_node : function() {
80 drag_items : function() {
81 return dragitemsarea.all('.drag');
83 drop_zones : function() {
84 return topnode.all('div.dropzones div.dropzone');
86 drop_zone_group : function(groupno) {
87 return topnode.all('div.dropzones div.group' + groupno);
89 drag_items_cloned_from : function(dragitemno) {
90 return dragitemsarea.all('.dragitems' + dragitemno);
92 drag_item : function(draginstanceno) {
93 return dragitemsarea.one('.draginstance' + draginstanceno);
95 drag_items_in_group : function(groupno) {
96 return dragitemsarea.all('.drag.group' + groupno);
98 drag_item_homes : function() {
99 return dragitemsarea.all('.draghome');
101 bg_img : function() {
102 return topnode.one('.dropbackground');
104 load_bg_img : function (url) {
105 dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
106 this.bg_img().on('load', this.on_image_load, this, 'bg_image');
108 add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
109 var oldhome = this.drag_item_home(dragitemno);
110 var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
111 var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
112 var divhtml = '<div class="' + classes + '">' + alt + '</div>';
113 if (oldhome === null) {
115 dragitemsarea.append(imghtml);
116 } else if (alt !== '') {
117 dragitemsarea.append(divhtml);
121 dragitemsarea.insert(imghtml, oldhome);
122 } else if (alt !== '') {
123 dragitemsarea.insert(divhtml, oldhome);
125 oldhome.remove(true);
127 var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
128 if (newlycreated !== null) {
129 newlycreated.setData('groupno', group);
130 newlycreated.setData('dragitemno', dragitemno);
133 drag_item_home : function (dragitemno) {
134 return dragitemsarea.one('.dragitemhomes' + dragitemno);
136 get_classname_numeric_suffix : function(node, prefix) {
137 var classes = node.getAttribute('class');
138 if (classes !== '') {
139 var classesarr = classes.split(' ');
140 for (var index = 0; index < classesarr.length; index++) {
141 var patt1 = new RegExp('^' + prefix + '([0-9])+$');
142 if (patt1.test(classesarr[index])) {
143 var patt2 = new RegExp('([0-9])+$');
144 var match = patt2.exec(classesarr[index]);
149 throw 'Prefix "' + prefix + '" not found in class names.';
151 clone_new_drag_item : function (draginstanceno, dragitemno) {
152 var draghome = this.drag_item_home(dragitemno);
153 if (draghome === null) {
156 var drag = draghome.cloneNode(true);
157 drag.removeClass('dragitemhomes' + dragitemno);
158 drag.addClass('dragitems' + dragitemno);
159 drag.addClass('draginstance' + draginstanceno);
160 drag.removeClass('draghome');
161 drag.addClass('drag');
162 drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
163 drag.setData('draginstanceno', draginstanceno);
164 drag.setData('dragitemno', dragitemno);
165 draghome.get('parentNode').appendChild(drag);
168 draggable_for_question : function (drag, group, choice) {
173 }).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
175 drag.setData('group', group);
176 drag.setData('choice', choice);
178 draggable_for_form : function (drag) {
179 var dd = new Y.DD.Drag({
182 }).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
183 dd.on('drag:end', function(e) {
184 var dragnode = e.target.get('node');
185 var draginstanceno = dragnode.getData('draginstanceno');
186 var gooddrop = dragnode.getData('gooddrop');
189 mainobj.reset_drag_xy(draginstanceno);
191 mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
194 dd.on('drag:start', function(e) {
196 drag.get('node').setData('gooddrop', false);
204 update_padding_sizes_all : function () {
205 for (var groupno = 1; groupno <= 8; groupno++) {
206 this.update_padding_size_for_group(groupno);
209 update_padding_size_for_group : function (groupno) {
210 var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
211 if (groupitems.size() !== 0) {
214 groupitems.each(function(item){
215 maxwidth = Math.max(maxwidth, item.get('clientWidth'));
216 maxheight = Math.max(maxheight, item.get('clientHeight'));
218 groupitems.each(function(item) {
219 var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
220 var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
221 item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
222 margintopbottom + 'px ' + marginleftright + 'px');
224 this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
225 'height': maxheight + 10});
228 convert_to_window_xy : function (bgimgxy) {
229 return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
230 Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
233 NAME : DDIMAGEORTEXTDDNAME,
235 drops : {value : null},
236 readonly : {value : false},
237 topnode : {value : null}
241 M.qtype_ddimageortext = M.qtype_ddimageortext || {};
242 M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
244 var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
245 var DDIMAGEORTEXT_QUESTION = function() {
246 DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
249 * This is the code for question rendering.
251 Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
252 touchscrolldisable: null,
254 initializer : function() {
255 this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
256 M.util.js_pending(this.pendingid);
257 this.doc = this.doc_structure(this);
258 this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
259 this.doc.bg_img().after('load', this.poll_for_image_load, this,
260 false, 0, this.create_all_drag_and_drops);
261 this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
262 false, 0, this.create_all_drag_and_drops);
263 Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
267 * prevent_touchmove_from_scrolling allows users of touch screen devices to
268 * use drag and drop and normal scrolling at the same time. I.e. when
269 * touching and dragging a draggable item, the screen does not scroll, but
270 * you can scroll by touching other area of the screen apart from the
273 prevent_touchmove_from_scrolling : function(drag) {
274 var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
275 var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
276 var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
278 // Disable scrolling when touching the draggable items.
279 drag.on(touchstart, function() {
280 if (this.touchscrolldisable) {
281 return; // Already disabled.
283 this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
284 e = e || window.event;
289 // Allow scrolling after releasing the draggable items.
290 drag.on(touchend, function() {
291 if (this.touchscrolldisable) {
292 this.touchscrolldisable.detach();
293 this.touchscrolldisable = null;
297 create_all_drag_and_drops : function () {
299 this.update_padding_sizes_all();
301 this.doc.drag_item_homes().each(function(dragitemhome){
302 var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
303 var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
304 var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
305 var groupsize = this.doc.drop_zone_group(group).size();
306 var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
308 if (!this.get('readonly')) {
309 this.doc.draggable_for_question(dragnode, group, choice);
311 // Prevent scrolling whilst dragging on Adroid devices.
312 this.prevent_touchmove_from_scrolling(dragnode);
314 if (dragnode.hasClass('infinite')) {
315 var dragstocreate = groupsize - 1;
316 while (dragstocreate > 0) {
317 dragnode = this.doc.clone_new_drag_item(i, dragitemno);
319 if (!this.get('readonly')) {
320 this.doc.draggable_for_question(dragnode, group, choice);
322 // Prevent scrolling whilst dragging on Adroid devices.
323 this.prevent_touchmove_from_scrolling(dragnode);
329 this.reposition_drags_for_question();
330 if (!this.get('readonly')) {
331 this.doc.drop_zones().set('tabIndex', 0);
332 this.doc.drop_zones().each(
334 v.on('dragchange', this.drop_zone_key_press, this);
337 M.util.js_complete(this.pendingid);
339 drop_zone_key_press : function (e) {
340 switch (e.direction) {
342 this.place_next_drag_in(e.target);
345 this.place_previous_drag_in(e.target);
348 this.remove_drag_from_drop(e.target);
352 this.reposition_drags_for_question();
354 place_next_drag_in : function (drop) {
355 this.search_for_unplaced_drop_choice(drop, 1);
357 place_previous_drag_in : function (drop) {
358 this.search_for_unplaced_drop_choice(drop, -1);
360 search_for_unplaced_drop_choice : function (drop, direction) {
362 var current = this.current_drag_in_drop(drop);
363 if ('' === current) {
364 if (direction === 1) {
368 var groupno = drop.getData('group');
369 this.doc.drag_items_in_group(groupno).each(function(drag) {
370 next = Math.max(next, drag.getData('choice'));
374 next = + current + direction;
378 if (this.get_choices_for_drop(next, drop).size() === 0){
379 this.remove_drag_from_drop(drop);
382 drag = this.get_unplaced_choice_for_drop(next, drop);
384 next = next + direction;
385 } while (drag === null);
386 this.place_drag_in_drop(drag, drop);
388 current_drag_in_drop : function (drop) {
389 var inputid = drop.getData('inputid');
390 var inputnode = Y.one('input#' + inputid);
391 return inputnode.get('value');
393 remove_drag_from_drop : function (drop) {
394 this.place_drag_in_drop(null, drop);
396 place_drag_in_drop : function (drag, drop) {
397 var inputid = drop.getData('inputid');
398 var inputnode = Y.one('input#' + inputid);
400 inputnode.set('value', drag.getData('choice'));
402 inputnode.set('value', '');
405 reposition_drags_for_question : function() {
406 this.doc.drag_items().removeClass('placed');
407 this.doc.drag_items().each (function (dragitem) {
408 if (dragitem.dd !== undefined) {
409 dragitem.dd.detachAll('drag:start');
412 this.doc.drop_zones().each(function(dropzone) {
413 var relativexy = dropzone.getData('xy');
414 dropzone.setXY(this.convert_to_window_xy(relativexy));
415 var inputcss = 'input#' + dropzone.getData('inputid');
416 var input = this.doc.top_node().one(inputcss);
417 var choice = input.get('value');
419 var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
420 if (dragitem !== null) {
421 dragitem.setXY(dropzone.getXY());
422 dragitem.addClass('placed');
423 if (dragitem.dd !== undefined) {
424 dragitem.dd.once('drag:start', function (e, input) {
425 input.set('value', '');
426 e.target.get('node').removeClass('placed');
432 this.doc.drag_items().each(function(dragitem) {
433 if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
434 var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
435 dragitem.setXY(dragitemhome.getXY());
439 get_choices_for_drop : function(choice, drop) {
440 var group = drop.getData('group');
441 return this.doc.top_node().all(
442 'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
444 get_unplaced_choice_for_drop : function(choice, drop) {
445 var dragitems = this.get_choices_for_drop(choice, drop);
447 dragitems.some(function (d) {
448 if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
457 init_drops : function () {
458 var dropareas = this.doc.top_node().one('div.dropzones');
460 for (var groupno = 1; groupno <= 8; groupno++) {
461 var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
462 dropareas.append(groupnode);
463 groupnodes[groupno] = groupnode;
465 var drop_hit_handler = function(e) {
466 var drag = e.drag.get('node');
467 var drop = e.drop.get('node');
468 if (Number(drop.getData('group')) === drag.getData('group')){
469 this.place_drag_in_drop(drag, drop);
472 for (var dropno in this.get('drops')) {
473 var drop = this.get('drops')[dropno];
474 var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
475 var title = drop.text.replace('"', '\"');
476 var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '"> </div>';
477 var dropnode = Y.Node.create(dropnodehtml);
478 groupnodes[drop.group].append(dropnode);
479 dropnode.setStyles({'opacity': 0.5});
480 dropnode.setData('xy', drop.xy);
481 dropnode.setData('place', dropno);
482 dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
483 dropnode.setData('group', drop.group);
484 var dropdd = new Y.DD.Drop({
485 node: dropnode, groups : [drop.group]});
486 dropdd.on('drop:hit', drop_hit_handler, this);
489 }, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
491 Y.Event.define('dragchange', {
492 // Webkit and IE repeat keydown when you hold down arrow keys.
493 // Opera links keypress to page scroll; others keydown.
494 // Firefox prevents page scroll via preventDefault() on either
495 // keydown or keypress.
496 _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
499 '32': 'next', // Space
500 '37': 'previous', // Left arrow
501 '38': 'previous', // Up arrow
502 '39': 'next', // Right arrow
503 '40': 'next', // Down arrow
504 '27': 'remove' // Escape
507 _keyHandler: function (e, notifier) {
508 if (this._keys[e.keyCode]) {
509 e.direction = this._keys[e.keyCode];
514 on: function (node, sub, notifier) {
515 sub._detacher = node.on(this._event, this._keyHandler,
520 M.qtype_ddimageortext.init_question = function(config) {
521 return new DDIMAGEORTEXT_QUESTION(config);
524 }, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});