d22e07c7e41afb3f87d15d3be0285391045c9974
[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     initializer : function() {
252         this.doc = this.doc_structure(this);
253         this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
254         this.doc.bg_img().after('load', this.poll_for_image_load, this,
255                                                 false, 0, this.create_all_drag_and_drops);
256         this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
257                                                 false, 0, this.create_all_drag_and_drops);
258         Y.later(500, this, this.reposition_drags_for_question, [], true);
259     },
260     create_all_drag_and_drops : function () {
261         this.init_drops();
262         this.update_padding_sizes_all();
263         var i = 0;
264         this.doc.drag_item_homes().each(function(dragitemhome){
265             var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
266             var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
267             var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
268             var groupsize = this.doc.drop_zone_group(group).size();
269             var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
270             i++;
271             if (!this.get('readonly')) {
272                 this.doc.draggable_for_question(dragnode, group, choice);
273             }
274             if (dragnode.hasClass('infinite')) {
275                 var dragstocreate = groupsize - 1;
276                 while (dragstocreate > 0) {
277                     dragnode = this.doc.clone_new_drag_item(i, dragitemno);
278                     i++;
279                     if (!this.get('readonly')) {
280                         this.doc.draggable_for_question(dragnode, group, choice);
281                     }
282                     dragstocreate--;
283                 }
284             }
285         }, this);
286         this.reposition_drags_for_question();
287         if (!this.get('readonly')) {
288             this.doc.drop_zones().set('tabIndex', 0);
289             this.doc.drop_zones().each(
290                 function(v){
291                     v.on('dragchange', this.drop_zone_key_press, this);
292                 }, this);
293         }
294     },
295     drop_zone_key_press : function (e) {
296         switch (e.direction) {
297             case 'next' :
298                 this.place_next_drag_in(e.target);
299                 break;
300             case 'previous' :
301                 this.place_previous_drag_in(e.target);
302                 break;
303             case 'remove' :
304                 this.remove_drag_from_drop(e.target);
305                 break;
306         }
307         e.preventDefault();
308         this.reposition_drags_for_question();
309     },
310     place_next_drag_in : function (drop) {
311         this.search_for_unplaced_drop_choice(drop, 1);
312     },
313     place_previous_drag_in : function (drop) {
314         this.search_for_unplaced_drop_choice(drop, -1);
315     },
316     search_for_unplaced_drop_choice : function (drop, direction) {
317         var next;
318         var current = this.current_drag_in_drop(drop);
319         if ('' === current) {
320             if (direction === 1) {
321                 next = 1;
322             } else {
323                 next = 1;
324                 var groupno = drop.getData('group');
325                 this.doc.drag_items_in_group(groupno).each(function(drag) {
326                     next = Math.max(next, drag.getData('choice'));
327                 }, this);
328             }
329         } else {
330             next = + current + direction;
331         }
332         var drag;
333         do {
334             if (this.get_choices_for_drop(next, drop).size() === 0){
335                 this.remove_drag_from_drop(drop);
336                 return;
337             } else {
338                 drag = this.get_unplaced_choice_for_drop(next, drop);
339             }
340             next = next + direction;
341         } while (drag === null);
342         this.place_drag_in_drop(drag, drop);
343     },
344     current_drag_in_drop : function (drop) {
345         var inputid = drop.getData('inputid');
346         var inputnode = Y.one('input#' + inputid);
347         return inputnode.get('value');
348     },
349     remove_drag_from_drop : function (drop) {
350         this.place_drag_in_drop(null, drop);
351     },
352     place_drag_in_drop : function (drag, drop) {
353         var inputid = drop.getData('inputid');
354         var inputnode = Y.one('input#' + inputid);
355         if (drag !== null) {
356             inputnode.set('value', drag.getData('choice'));
357         } else {
358             inputnode.set('value', '');
359         }
360     },
361     reposition_drags_for_question : function() {
362         this.doc.drag_items().removeClass('placed');
363         this.doc.drag_items().each (function (dragitem) {
364             if (dragitem.dd !== undefined) {
365                 dragitem.dd.detachAll('drag:start');
366             }
367         }, this);
368         this.doc.drop_zones().each(function(dropzone) {
369             var relativexy = dropzone.getData('xy');
370             dropzone.setXY(this.convert_to_window_xy(relativexy));
371             var inputcss = 'input#' + dropzone.getData('inputid');
372             var input = this.doc.top_node().one(inputcss);
373             var choice = input.get('value');
374             if (choice !== "") {
375                 var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
376                 if (dragitem !== null) {
377                     dragitem.setXY(dropzone.getXY());
378                     dragitem.addClass('placed');
379                     if (dragitem.dd !== undefined) {
380                         dragitem.dd.once('drag:start', function (e, input) {
381                             input.set('value', '');
382                             e.target.get('node').removeClass('placed');
383                         },this, input);
384                     }
385                 }
386             }
387         }, this);
388         this.doc.drag_items().each(function(dragitem) {
389             if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
390                 var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
391                 dragitem.setXY(dragitemhome.getXY());
392             }
393         }, this);
394     },
395     get_choices_for_drop : function(choice, drop) {
396         var group = drop.getData('group');
397         return this.doc.top_node().all(
398                 'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
399     },
400     get_unplaced_choice_for_drop : function(choice, drop) {
401         var dragitems = this.get_choices_for_drop(choice, drop);
402         var dragitem = null;
403         dragitems.some(function (d) {
404             if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
405                 dragitem = d;
406                 return true;
407             } else {
408                 return false;
409             }
410         });
411         return dragitem;
412     },
413     init_drops : function () {
414         var dropareas = this.doc.top_node().one('div.dropzones');
415         var groupnodes = {};
416         for (var groupno = 1; groupno <= 8; groupno++) {
417             var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
418             dropareas.append(groupnode);
419             groupnodes[groupno] = groupnode;
420         }
421         var drop_hit_handler = function(e) {
422             var drag = e.drag.get('node');
423             var drop = e.drop.get('node');
424             if (Number(drop.getData('group')) === drag.getData('group')){
425                 this.place_drag_in_drop(drag, drop);
426             }
427         };
428         for (var dropno in this.get('drops')) {
429             var drop = this.get('drops')[dropno];
430             var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
431             var title = drop.text.replace('"', '\"');
432             var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '">&nbsp;</div>';
433             var dropnode = Y.Node.create(dropnodehtml);
434             groupnodes[drop.group].append(dropnode);
435             dropnode.setStyles({'opacity': 0.5});
436             dropnode.setData('xy', drop.xy);
437             dropnode.setData('place', dropno);
438             dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
439             dropnode.setData('group', drop.group);
440             var dropdd = new Y.DD.Drop({
441                   node: dropnode, groups : [drop.group]});
442             dropdd.on('drop:hit', drop_hit_handler, this);
443         }
444     }
445 }, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
447 Y.Event.define('dragchange', {
448     // Webkit and IE repeat keydown when you hold down arrow keys.
449     // Opera links keypress to page scroll; others keydown.
450     // Firefox prevents page scroll via preventDefault() on either
451     // keydown or keypress.
452     _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
454     _keys: {
455         '32': 'next',     // Space
456         '37': 'previous', // Left arrow
457         '38': 'previous', // Up arrow
458         '39': 'next',     // Right arrow
459         '40': 'next',     // Down arrow
460         '27': 'remove'    // Escape
461     },
463     _keyHandler: function (e, notifier) {
464         if (this._keys[e.keyCode]) {
465             e.direction = this._keys[e.keyCode];
466             notifier.fire(e);
467         }
468     },
470     on: function (node, sub, notifier) {
471         sub._detacher = node.on(this._event, this._keyHandler,
472                                 this, notifier);
473     }
474 });
476 M.qtype_ddimageortext.init_question = function(config) {
477     return new DDIMAGEORTEXT_QUESTION(config);
478 };