MDL-47494 ddimageortext: Fix drag and drop on Android touch devices. #85108
[moodle.git] / question / type / ddimageortext / yui / build / moodle-qtype_ddimageortext-dd / moodle-qtype_ddimageortext-dd.js
blob57f2e21cf3e37387304e34b294818cb6b67711cc
1 YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
4 //
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.
9 //
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);
23 /**
24  * This is the base class for the question rendering and question editing form code.
25  */
26 Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
27     doc : null,
28     polltimer : null,
29     afterimageloaddone : false,
30     poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
31         if (this.afterimageloaddone) {
32             return;
33         }
34         var bgdone = this.doc.bg_img().get('complete');
35         if (waitforimageconstrain) {
36             bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
37         }
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'){
42                 return false;
43             }
44             var done = (dragitemhome.get('complete'));
45             if (waitforimageconstrain) {
46                 done = done && dragitemhome.hasClass('constrained');
47             }
48             return !done;
49         });
50         if (bgdone && alldragsloaded) {
51             if (this.polltimer !== null) {
52                 this.polltimer.cancel();
53                 this.polltimer = null;
54             }
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);
57             if (pause !== 0) {
58                 Y.later(pause, this, doafterwords);
59             } else {
60                 doafterwords.call(this);
61             }
62             this.afterimageloaddone = true;
63         } else if (this.polltimer === null) {
64             var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
65             this.polltimer =
66                         Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
67         }
68     },
69     /**
70      * Object to encapsulate operations on dd area.
71      */
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');
76         return {
77             top_node : function() {
78                 return topnode;
79             },
80             drag_items : function() {
81                 return dragitemsarea.all('.drag');
82             },
83             drop_zones : function() {
84                 return topnode.all('div.dropzones div.dropzone');
85             },
86             drop_zone_group : function(groupno) {
87                 return topnode.all('div.dropzones div.group' + groupno);
88             },
89             drag_items_cloned_from : function(dragitemno) {
90                 return dragitemsarea.all('.dragitems' + dragitemno);
91             },
92             drag_item : function(draginstanceno) {
93                 return dragitemsarea.one('.draginstance' + draginstanceno);
94             },
95             drag_items_in_group : function(groupno) {
96                 return dragitemsarea.all('.drag.group' + groupno);
97             },
98             drag_item_homes : function() {
99                 return dragitemsarea.all('.draghome');
100             },
101             bg_img : function() {
102                 return topnode.one('.dropbackground');
103             },
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');
107             },
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) {
114                     if (url) {
115                         dragitemsarea.append(imghtml);
116                     } else if (alt !== '') {
117                         dragitemsarea.append(divhtml);
118                     }
119                 } else {
120                     if (url) {
121                         dragitemsarea.insert(imghtml, oldhome);
122                     } else if (alt !== '') {
123                         dragitemsarea.insert(divhtml, oldhome);
124                     }
125                     oldhome.remove(true);
126                 }
127                 var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
128                 if (newlycreated !== null) {
129                     newlycreated.setData('groupno', group);
130                     newlycreated.setData('dragitemno', dragitemno);
131                 }
132             },
133             drag_item_home : function (dragitemno) {
134                 return dragitemsarea.one('.dragitemhomes' + dragitemno);
135             },
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]);
145                             return + match[0];
146                         }
147                     }
148                 }
149                 throw 'Prefix "' + prefix + '" not found in class names.';
150             },
151             clone_new_drag_item : function (draginstanceno, dragitemno) {
152                 var draghome = this.drag_item_home(dragitemno);
153                 if (draghome === null) {
154                     return null;
155                 }
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);
166                 return drag;
167             },
168             draggable_for_question : function (drag, group, choice) {
169                 new Y.DD.Drag({
170                     node: drag,
171                     dragMode: 'point',
172                     groups: [group]
173                 }).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
175                 drag.setData('group', group);
176                 drag.setData('choice', choice);
177             },
178             draggable_for_form : function (drag) {
179                 var dd = new Y.DD.Drag({
180                     node: drag,
181                     dragMode: 'point'
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');
188                     if (!gooddrop) {
189                         mainobj.reset_drag_xy(draginstanceno);
190                     } else {
191                         mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
192                     }
193                 }, this);
194                 dd.on('drag:start', function(e) {
195                     var drag = e.target;
196                     drag.get('node').setData('gooddrop', false);
197                 }, this);
199             }
201         };
202     },
204     update_padding_sizes_all : function () {
205         for (var groupno = 1; groupno <= 8; groupno++) {
206             this.update_padding_size_for_group(groupno);
207         }
208     },
209     update_padding_size_for_group : function (groupno) {
210         var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
211         if (groupitems.size() !== 0) {
212             var maxwidth = 0;
213             var maxheight = 0;
214             groupitems.each(function(item){
215                 maxwidth = Math.max(maxwidth, item.get('clientWidth'));
216                 maxheight = Math.max(maxheight, item.get('clientHeight'));
217             }, this);
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');
223             }, this);
224             this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
225                                                             'height': maxheight + 10});
226         }
227     },
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];
231     }
232 }, {
233     NAME : DDIMAGEORTEXTDDNAME,
234     ATTRS : {
235         drops : {value : null},
236         readonly : {value : false},
237         topnode : {value : null}
238     }
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.
250  */
251 Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
252     touchscrolldisable: null,
253     pendingid: '',
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);
264     },
266     /**
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
271      * draggable items.
272      */
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.
282             }
283             this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
284                 e = e || window.event;
285                 e.preventDefault();
286             });
287         }, this);
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;
294             }
295         }, this);
296     },
297     create_all_drag_and_drops : function () {
298         this.init_drops();
299         this.update_padding_sizes_all();
300         var i = 0;
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);
307             i++;
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);
313             }
314             if (dragnode.hasClass('infinite')) {
315                 var dragstocreate = groupsize - 1;
316                 while (dragstocreate > 0) {
317                     dragnode = this.doc.clone_new_drag_item(i, dragitemno);
318                     i++;
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);
324                     }
325                     dragstocreate--;
326                 }
327             }
328         }, this);
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(
333                 function(v){
334                     v.on('dragchange', this.drop_zone_key_press, this);
335                 }, this);
336         }
337         M.util.js_complete(this.pendingid);
338     },
339     drop_zone_key_press : function (e) {
340         switch (e.direction) {
341             case 'next' :
342                 this.place_next_drag_in(e.target);
343                 break;
344             case 'previous' :
345                 this.place_previous_drag_in(e.target);
346                 break;
347             case 'remove' :
348                 this.remove_drag_from_drop(e.target);
349                 break;
350         }
351         e.preventDefault();
352         this.reposition_drags_for_question();
353     },
354     place_next_drag_in : function (drop) {
355         this.search_for_unplaced_drop_choice(drop, 1);
356     },
357     place_previous_drag_in : function (drop) {
358         this.search_for_unplaced_drop_choice(drop, -1);
359     },
360     search_for_unplaced_drop_choice : function (drop, direction) {
361         var next;
362         var current = this.current_drag_in_drop(drop);
363         if ('' === current) {
364             if (direction === 1) {
365                 next = 1;
366             } else {
367                 next = 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'));
371                 }, this);
372             }
373         } else {
374             next = + current + direction;
375         }
376         var drag;
377         do {
378             if (this.get_choices_for_drop(next, drop).size() === 0){
379                 this.remove_drag_from_drop(drop);
380                 return;
381             } else {
382                 drag = this.get_unplaced_choice_for_drop(next, drop);
383             }
384             next = next + direction;
385         } while (drag === null);
386         this.place_drag_in_drop(drag, drop);
387     },
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');
392     },
393     remove_drag_from_drop : function (drop) {
394         this.place_drag_in_drop(null, drop);
395     },
396     place_drag_in_drop : function (drag, drop) {
397         var inputid = drop.getData('inputid');
398         var inputnode = Y.one('input#' + inputid);
399         if (drag !== null) {
400             inputnode.set('value', drag.getData('choice'));
401         } else {
402             inputnode.set('value', '');
403         }
404     },
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');
410             }
411         }, this);
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');
418             if (choice !== "") {
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');
427                         },this, input);
428                     }
429                 }
430             }
431         }, this);
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());
436             }
437         }, this);
438     },
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');
443     },
444     get_unplaced_choice_for_drop : function(choice, drop) {
445         var dragitems = this.get_choices_for_drop(choice, drop);
446         var dragitem = null;
447         dragitems.some(function (d) {
448             if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
449                 dragitem = d;
450                 return true;
451             } else {
452                 return false;
453             }
454         });
455         return dragitem;
456     },
457     init_drops : function () {
458         var dropareas = this.doc.top_node().one('div.dropzones');
459         var groupnodes = {};
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;
464         }
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);
470             }
471         };
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 + '">&nbsp;</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);
487         }
488     }
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',
498     _keys: {
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
505     },
507     _keyHandler: function (e, notifier) {
508         if (this._keys[e.keyCode]) {
509             e.direction = this._keys[e.keyCode];
510             notifier.fire(e);
511         }
512     },
514     on: function (node, sub, notifier) {
515         sub._detacher = node.on(this._event, this._keyHandler,
516                                 this, notifier);
517     }
520 M.qtype_ddimageortext.init_question = function(config) {
521     return new DDIMAGEORTEXT_QUESTION(config);
524 }, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});