1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16 var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
17 var DDIMAGEORTEXT_DD = function() {
18 DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
22 * This is the base class for the question rendering and question editing form code.
24 Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
27 afterimageloaddone : false,
28 poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
29 if (this.afterimageloaddone) {
32 var bgdone = this.doc.bg_img().get('complete');
33 if (waitforimageconstrain) {
34 bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
36 var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
37 //in 'some' loop returning true breaks the loop and is passed as return value from
38 //'some' else returns false. Can be though of as equivalent to ||.
39 if (dragitemhome.get('tagName') !== 'IMG'){
42 var done = (dragitemhome.get('complete'));
43 if (waitforimageconstrain) {
44 done = done && dragitemhome.hasClass('constrained');
48 if (bgdone && alldragsloaded) {
49 if (this.polltimer !== null) {
50 this.polltimer.cancel();
51 this.polltimer = null;
53 this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
54 this.doc.bg_img().detach('load', this.poll_for_image_load);
56 Y.later(pause, this, doafterwords);
58 doafterwords.call(this);
60 this.afterimageloaddone = true;
61 } else if (this.polltimer === null) {
62 var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
64 Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
68 * Object to encapsulate operations on dd area.
70 doc_structure : function (mainobj) {
71 var topnode = Y.one(this.get('topnode'));
72 var dragitemsarea = topnode.one('div.dragitems');
73 var dropbgarea = topnode.one('div.droparea');
75 top_node : function() {
78 drag_items : function() {
79 return dragitemsarea.all('.drag');
81 drop_zones : function() {
82 return topnode.all('div.dropzones div.dropzone');
84 drop_zone_group : function(groupno) {
85 return topnode.all('div.dropzones div.group' + groupno);
87 drag_items_cloned_from : function(dragitemno) {
88 return dragitemsarea.all('.dragitems' + dragitemno);
90 drag_item : function(draginstanceno) {
91 return dragitemsarea.one('.draginstance' + draginstanceno);
93 drag_items_in_group : function(groupno) {
94 return dragitemsarea.all('.drag.group' + groupno);
96 drag_item_homes : function() {
97 return dragitemsarea.all('.draghome');
100 return topnode.one('.dropbackground');
102 load_bg_img : function (url) {
103 dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
104 this.bg_img().on('load', this.on_image_load, this, 'bg_image');
106 add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
107 var oldhome = this.drag_item_home(dragitemno);
108 var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
109 var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
110 var divhtml = '<div class="' + classes + '">' + alt + '</div>';
111 if (oldhome === null) {
113 dragitemsarea.append(imghtml);
114 } else if (alt !== '') {
115 dragitemsarea.append(divhtml);
119 dragitemsarea.insert(imghtml, oldhome);
120 } else if (alt !== '') {
121 dragitemsarea.insert(divhtml, oldhome);
123 oldhome.remove(true);
125 var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
126 if (newlycreated !== null) {
127 newlycreated.setData('groupno', group);
128 newlycreated.setData('dragitemno', dragitemno);
131 drag_item_home : function (dragitemno) {
132 return dragitemsarea.one('.dragitemhomes' + dragitemno);
134 get_classname_numeric_suffix : function(node, prefix) {
135 var classes = node.getAttribute('class');
136 if (classes !== '') {
137 var classesarr = classes.split(' ');
138 for (var index = 0; index < classesarr.length; index++) {
139 var patt1 = new RegExp('^' + prefix + '([0-9])+$');
140 if (patt1.test(classesarr[index])) {
141 var patt2 = new RegExp('([0-9])+$');
142 var match = patt2.exec(classesarr[index]);
147 throw 'Prefix "' + prefix + '" not found in class names.';
149 clone_new_drag_item : function (draginstanceno, dragitemno) {
150 var draghome = this.drag_item_home(dragitemno);
151 if (draghome === null) {
154 var drag = draghome.cloneNode(true);
155 drag.removeClass('dragitemhomes' + dragitemno);
156 drag.addClass('dragitems' + dragitemno);
157 drag.addClass('draginstance' + draginstanceno);
158 drag.removeClass('draghome');
159 drag.addClass('drag');
160 drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
161 drag.setData('draginstanceno', draginstanceno);
162 drag.setData('dragitemno', dragitemno);
163 draghome.get('parentNode').appendChild(drag);
166 draggable_for_question : function (drag, group, choice) {
171 }).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
173 drag.setData('group', group);
174 drag.setData('choice', choice);
177 draggable_for_form : function (drag) {
178 var dd = new Y.DD.Drag({
181 }).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
182 dd.on('drag:end', function(e) {
183 var dragnode = e.target.get('node');
184 var draginstanceno = dragnode.getData('draginstanceno');
185 var gooddrop = dragnode.getData('gooddrop');
188 mainobj.reset_drag_xy(draginstanceno);
190 mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
193 dd.on('drag:start', function(e) {
195 drag.get('node').setData('gooddrop', false);
203 update_padding_sizes_all : function () {
204 for (var groupno = 1; groupno <= 8; groupno++) {
205 this.update_padding_size_for_group(groupno);
208 update_padding_size_for_group : function (groupno) {
209 var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
210 if (groupitems.size() !== 0) {
213 groupitems.each(function(item){
214 maxwidth = Math.max(maxwidth, item.get('clientWidth'));
215 maxheight = Math.max(maxheight, item.get('clientHeight'));
217 groupitems.each(function(item) {
218 var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
219 var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
220 item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
221 margintopbottom + 'px ' + marginleftright + 'px');
223 this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
224 'height': maxheight + 10});
227 convert_to_window_xy : function (bgimgxy) {
228 return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
229 Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
232 NAME : DDIMAGEORTEXTDDNAME,
234 drops : {value : null},
235 readonly : {value : false},
236 topnode : {value : null}
240 M.qtype_ddimageortext = M.qtype_ddimageortext || {};
241 M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
243 var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
244 var DDIMAGEORTEXT_QUESTION = function() {
245 DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
248 * This is the code for question rendering.
250 Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
252 initializer : function() {
253 this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
254 M.util.js_pending(this.pendingid);
255 this.doc = this.doc_structure(this);
256 this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
257 this.doc.bg_img().after('load', this.poll_for_image_load, this,
258 false, 0, this.create_all_drag_and_drops);
259 this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
260 false, 0, this.create_all_drag_and_drops);
261 Y.later(500, this, this.reposition_drags_for_question, [pendingid], true);
263 create_all_drag_and_drops : function () {
265 this.update_padding_sizes_all();
267 this.doc.drag_item_homes().each(function(dragitemhome){
268 var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
269 var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
270 var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
271 var groupsize = this.doc.drop_zone_group(group).size();
272 var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
274 if (!this.get('readonly')) {
275 this.doc.draggable_for_question(dragnode, group, choice);
277 if (dragnode.hasClass('infinite')) {
278 var dragstocreate = groupsize - 1;
279 while (dragstocreate > 0) {
280 dragnode = this.doc.clone_new_drag_item(i, dragitemno);
282 if (!this.get('readonly')) {
283 this.doc.draggable_for_question(dragnode, group, choice);
289 this.reposition_drags_for_question();
290 if (!this.get('readonly')) {
291 this.doc.drop_zones().set('tabIndex', 0);
292 this.doc.drop_zones().each(
294 v.on('dragchange', this.drop_zone_key_press, this);
297 M.util.js_complete(this.pendingid);
299 drop_zone_key_press : function (e) {
300 switch (e.direction) {
302 this.place_next_drag_in(e.target);
305 this.place_previous_drag_in(e.target);
308 this.remove_drag_from_drop(e.target);
312 this.reposition_drags_for_question();
314 place_next_drag_in : function (drop) {
315 this.search_for_unplaced_drop_choice(drop, 1);
317 place_previous_drag_in : function (drop) {
318 this.search_for_unplaced_drop_choice(drop, -1);
320 search_for_unplaced_drop_choice : function (drop, direction) {
322 var current = this.current_drag_in_drop(drop);
323 if ('' === current) {
324 if (direction === 1) {
328 var groupno = drop.getData('group');
329 this.doc.drag_items_in_group(groupno).each(function(drag) {
330 next = Math.max(next, drag.getData('choice'));
334 next = + current + direction;
338 if (this.get_choices_for_drop(next, drop).size() === 0){
339 this.remove_drag_from_drop(drop);
342 drag = this.get_unplaced_choice_for_drop(next, drop);
344 next = next + direction;
345 } while (drag === null);
346 this.place_drag_in_drop(drag, drop);
348 current_drag_in_drop : function (drop) {
349 var inputid = drop.getData('inputid');
350 var inputnode = Y.one('input#' + inputid);
351 return inputnode.get('value');
353 remove_drag_from_drop : function (drop) {
354 this.place_drag_in_drop(null, drop);
356 place_drag_in_drop : function (drag, drop) {
357 var inputid = drop.getData('inputid');
358 var inputnode = Y.one('input#' + inputid);
360 inputnode.set('value', drag.getData('choice'));
362 inputnode.set('value', '');
365 reposition_drags_for_question : function() {
366 this.doc.drag_items().removeClass('placed');
367 this.doc.drag_items().each (function (dragitem) {
368 if (dragitem.dd !== undefined) {
369 dragitem.dd.detachAll('drag:start');
372 this.doc.drop_zones().each(function(dropzone) {
373 var relativexy = dropzone.getData('xy');
374 dropzone.setXY(this.convert_to_window_xy(relativexy));
375 var inputcss = 'input#' + dropzone.getData('inputid');
376 var input = this.doc.top_node().one(inputcss);
377 var choice = input.get('value');
379 var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
380 if (dragitem !== null) {
381 dragitem.setXY(dropzone.getXY());
382 dragitem.addClass('placed');
383 if (dragitem.dd !== undefined) {
384 dragitem.dd.once('drag:start', function (e, input) {
385 input.set('value', '');
386 e.target.get('node').removeClass('placed');
392 this.doc.drag_items().each(function(dragitem) {
393 if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
394 var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
395 dragitem.setXY(dragitemhome.getXY());
399 get_choices_for_drop : function(choice, drop) {
400 var group = drop.getData('group');
401 return this.doc.top_node().all(
402 'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
404 get_unplaced_choice_for_drop : function(choice, drop) {
405 var dragitems = this.get_choices_for_drop(choice, drop);
407 dragitems.some(function (d) {
408 if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
417 init_drops : function () {
418 var dropareas = this.doc.top_node().one('div.dropzones');
420 for (var groupno = 1; groupno <= 8; groupno++) {
421 var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
422 dropareas.append(groupnode);
423 groupnodes[groupno] = groupnode;
425 var drop_hit_handler = function(e) {
426 var drag = e.drag.get('node');
427 var drop = e.drop.get('node');
428 if (Number(drop.getData('group')) === drag.getData('group')){
429 this.place_drag_in_drop(drag, drop);
432 for (var dropno in this.get('drops')) {
433 var drop = this.get('drops')[dropno];
434 var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
435 var title = drop.text.replace('"', '\"');
436 var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '"> </div>';
437 var dropnode = Y.Node.create(dropnodehtml);
438 groupnodes[drop.group].append(dropnode);
439 dropnode.setStyles({'opacity': 0.5});
440 dropnode.setData('xy', drop.xy);
441 dropnode.setData('place', dropno);
442 dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
443 dropnode.setData('group', drop.group);
444 var dropdd = new Y.DD.Drop({
445 node: dropnode, groups : [drop.group]});
446 dropdd.on('drop:hit', drop_hit_handler, this);
449 }, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
451 Y.Event.define('dragchange', {
452 // Webkit and IE repeat keydown when you hold down arrow keys.
453 // Opera links keypress to page scroll; others keydown.
454 // Firefox prevents page scroll via preventDefault() on either
455 // keydown or keypress.
456 _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
459 '32': 'next', // Space
460 '37': 'previous', // Left arrow
461 '38': 'previous', // Up arrow
462 '39': 'next', // Right arrow
463 '40': 'next', // Down arrow
464 '27': 'remove' // Escape
467 _keyHandler: function (e, notifier) {
468 if (this._keys[e.keyCode]) {
469 e.direction = this._keys[e.keyCode];
474 on: function (node, sub, notifier) {
475 sub._detacher = node.on(this._event, this._keyHandler,
480 M.qtype_ddimageortext.init_question = function(config) {
481 return new DDIMAGEORTEXT_QUESTION(config);