MDL-47494 ddimageortext: Update dd qtype tests to use js_pending not fixed waits...
[moodle.git] / question / type / ddimageortext / yui / src / ddimageortext / js / ddimageortext.js
1 // This file is part of Moodle - http://moodle.org/
2 //
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.
7 //
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.
12 //
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);
19 };
21 /**
22  * This is the base class for the question rendering and question editing form code.
23  */
24 Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
25     doc : null,
26     polltimer : null,
27     afterimageloaddone : false,
28     poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
29         if (this.afterimageloaddone) {
30             return;
31         }
32         var bgdone = this.doc.bg_img().get('complete');
33         if (waitforimageconstrain) {
34             bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
35         }
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'){
40                 return false;
41             }
42             var done = (dragitemhome.get('complete'));
43             if (waitforimageconstrain) {
44                 done = done && dragitemhome.hasClass('constrained');
45             }
46             return !done;
47         });
48         if (bgdone && alldragsloaded) {
49             if (this.polltimer !== null) {
50                 this.polltimer.cancel();
51                 this.polltimer = null;
52             }
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);
55             if (pause !== 0) {
56                 Y.later(pause, this, doafterwords);
57             } else {
58                 doafterwords.call(this);
59             }
60             this.afterimageloaddone = true;
61         } else if (this.polltimer === null) {
62             var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
63             this.polltimer =
64                         Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
65         }
66     },
67     /**
68      * Object to encapsulate operations on dd area.
69      */
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');
74         return {
75             top_node : function() {
76                 return topnode;
77             },
78             drag_items : function() {
79                 return dragitemsarea.all('.drag');
80             },
81             drop_zones : function() {
82                 return topnode.all('div.dropzones div.dropzone');
83             },
84             drop_zone_group : function(groupno) {
85                 return topnode.all('div.dropzones div.group' + groupno);
86             },
87             drag_items_cloned_from : function(dragitemno) {
88                 return dragitemsarea.all('.dragitems' + dragitemno);
89             },
90             drag_item : function(draginstanceno) {
91                 return dragitemsarea.one('.draginstance' + draginstanceno);
92             },
93             drag_items_in_group : function(groupno) {
94                 return dragitemsarea.all('.drag.group' + groupno);
95             },
96             drag_item_homes : function() {
97                 return dragitemsarea.all('.draghome');
98             },
99             bg_img : function() {
100                 return topnode.one('.dropbackground');
101             },
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');
105             },
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) {
112                     if (url) {
113                         dragitemsarea.append(imghtml);
114                     } else if (alt !== '') {
115                         dragitemsarea.append(divhtml);
116                     }
117                 } else {
118                     if (url) {
119                         dragitemsarea.insert(imghtml, oldhome);
120                     } else if (alt !== '') {
121                         dragitemsarea.insert(divhtml, oldhome);
122                     }
123                     oldhome.remove(true);
124                 }
125                 var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
126                 if (newlycreated !== null) {
127                     newlycreated.setData('groupno', group);
128                     newlycreated.setData('dragitemno', dragitemno);
129                 }
130             },
131             drag_item_home : function (dragitemno) {
132                 return dragitemsarea.one('.dragitemhomes' + dragitemno);
133             },
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]);
143                             return + match[0];
144                         }
145                     }
146                 }
147                 throw 'Prefix "' + prefix + '" not found in class names.';
148             },
149             clone_new_drag_item : function (draginstanceno, dragitemno) {
150                 var draghome = this.drag_item_home(dragitemno);
151                 if (draghome === null) {
152                     return null;
153                 }
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);
164                 return drag;
165             },
166             draggable_for_question : function (drag, group, choice) {
167                 new Y.DD.Drag({
168                     node: drag,
169                     dragMode: 'point',
170                     groups: [group]
171                 }).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
173                 drag.setData('group', group);
174                 drag.setData('choice', choice);
176             },
177             draggable_for_form : function (drag) {
178                 var dd = new Y.DD.Drag({
179                     node: drag,
180                     dragMode: 'point'
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');
187                     if (!gooddrop) {
188                         mainobj.reset_drag_xy(draginstanceno);
189                     } else {
190                         mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
191                     }
192                 }, this);
193                 dd.on('drag:start', function(e) {
194                     var drag = e.target;
195                     drag.get('node').setData('gooddrop', false);
196                 }, this);
198             }
200         };
201     },
203     update_padding_sizes_all : function () {
204         for (var groupno = 1; groupno <= 8; groupno++) {
205             this.update_padding_size_for_group(groupno);
206         }
207     },
208     update_padding_size_for_group : function (groupno) {
209         var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
210         if (groupitems.size() !== 0) {
211             var maxwidth = 0;
212             var maxheight = 0;
213             groupitems.each(function(item){
214                 maxwidth = Math.max(maxwidth, item.get('clientWidth'));
215                 maxheight = Math.max(maxheight, item.get('clientHeight'));
216             }, this);
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');
222             }, this);
223             this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
224                                                             'height': maxheight + 10});
225         }
226     },
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];
230     }
231 }, {
232     NAME : DDIMAGEORTEXTDDNAME,
233     ATTRS : {
234         drops : {value : null},
235         readonly : {value : false},
236         topnode : {value : null}
237     }
238 });
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);
246 };
247 /**
248  * This is the code for question rendering.
249  */
250 Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
251     pendingid: '',
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);
262     },
263     create_all_drag_and_drops : function () {
264         this.init_drops();
265         this.update_padding_sizes_all();
266         var i = 0;
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);
273             i++;
274             if (!this.get('readonly')) {
275                 this.doc.draggable_for_question(dragnode, group, choice);
276             }
277             if (dragnode.hasClass('infinite')) {
278                 var dragstocreate = groupsize - 1;
279                 while (dragstocreate > 0) {
280                     dragnode = this.doc.clone_new_drag_item(i, dragitemno);
281                     i++;
282                     if (!this.get('readonly')) {
283                         this.doc.draggable_for_question(dragnode, group, choice);
284                     }
285                     dragstocreate--;
286                 }
287             }
288         }, this);
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(
293                 function(v){
294                     v.on('dragchange', this.drop_zone_key_press, this);
295                 }, this);
296         }
297         M.util.js_complete(this.pendingid);
298     },
299     drop_zone_key_press : function (e) {
300         switch (e.direction) {
301             case 'next' :
302                 this.place_next_drag_in(e.target);
303                 break;
304             case 'previous' :
305                 this.place_previous_drag_in(e.target);
306                 break;
307             case 'remove' :
308                 this.remove_drag_from_drop(e.target);
309                 break;
310         }
311         e.preventDefault();
312         this.reposition_drags_for_question();
313     },
314     place_next_drag_in : function (drop) {
315         this.search_for_unplaced_drop_choice(drop, 1);
316     },
317     place_previous_drag_in : function (drop) {
318         this.search_for_unplaced_drop_choice(drop, -1);
319     },
320     search_for_unplaced_drop_choice : function (drop, direction) {
321         var next;
322         var current = this.current_drag_in_drop(drop);
323         if ('' === current) {
324             if (direction === 1) {
325                 next = 1;
326             } else {
327                 next = 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'));
331                 }, this);
332             }
333         } else {
334             next = + current + direction;
335         }
336         var drag;
337         do {
338             if (this.get_choices_for_drop(next, drop).size() === 0){
339                 this.remove_drag_from_drop(drop);
340                 return;
341             } else {
342                 drag = this.get_unplaced_choice_for_drop(next, drop);
343             }
344             next = next + direction;
345         } while (drag === null);
346         this.place_drag_in_drop(drag, drop);
347     },
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');
352     },
353     remove_drag_from_drop : function (drop) {
354         this.place_drag_in_drop(null, drop);
355     },
356     place_drag_in_drop : function (drag, drop) {
357         var inputid = drop.getData('inputid');
358         var inputnode = Y.one('input#' + inputid);
359         if (drag !== null) {
360             inputnode.set('value', drag.getData('choice'));
361         } else {
362             inputnode.set('value', '');
363         }
364     },
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');
370             }
371         }, this);
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');
378             if (choice !== "") {
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');
387                         },this, input);
388                     }
389                 }
390             }
391         }, this);
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());
396             }
397         }, this);
398     },
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');
403     },
404     get_unplaced_choice_for_drop : function(choice, drop) {
405         var dragitems = this.get_choices_for_drop(choice, drop);
406         var dragitem = null;
407         dragitems.some(function (d) {
408             if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
409                 dragitem = d;
410                 return true;
411             } else {
412                 return false;
413             }
414         });
415         return dragitem;
416     },
417     init_drops : function () {
418         var dropareas = this.doc.top_node().one('div.dropzones');
419         var groupnodes = {};
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;
424         }
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);
430             }
431         };
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 + '">&nbsp;</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);
447         }
448     }
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',
458     _keys: {
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
465     },
467     _keyHandler: function (e, notifier) {
468         if (this._keys[e.keyCode]) {
469             e.direction = this._keys[e.keyCode];
470             notifier.fire(e);
471         }
472     },
474     on: function (node, sub, notifier) {
475         sub._detacher = node.on(this._event, this._keyHandler,
476                                 this, notifier);
477     }
478 });
480 M.qtype_ddimageortext.init_question = function(config) {
481     return new DDIMAGEORTEXT_QUESTION(config);
482 };