MDL-47494 ddmarker: Improve the drag-drop question Behat tests.
[moodle.git] / question / type / ddmarker / yui / dd / dd.js
blobf983bbd2ad1505a3cb08ccd47911d2d96d356f0d
1 YUI.add('moodle-qtype_ddmarker-dd', function(Y) {
2     var DDMARKERDDNAME = 'ddmarker_dd';
3     var DDMARKER_DD = function() {
4         DDMARKER_DD.superclass.constructor.apply(this, arguments);
5     };
6     /**
7      * This is the base class for the question rendering and question editing form code.
8      */
9     Y.extend(DDMARKER_DD, Y.Base, {
10         doc : null,
11         polltimer : null,
12         afterimageloaddone : false,
13         graphics : null,
14         poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
15             if (this.afterimageloaddone) {
16                 return;
17             }
18             var bgdone = this.doc.bg_img().get('complete');
19             if (waitforimageconstrain) {
20                 bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
21             }
22             if (bgdone) {
23                 if (this.polltimer !== null) {
24                     this.polltimer.cancel();
25                     this.polltimer = null;
26                 }
27                 this.doc.bg_img().detach('load', this.poll_for_image_load);
28                 if (pause !== 0) {
29                     Y.later(pause, this, doafterwords);
30                 } else {
31                     doafterwords.call(this);
32                 }
33                 this.afterimageloaddone = true;
34             } else if (this.polltimer === null) {
35                 var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
36                 this.polltimer =
37                             Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
38             }
39         },
41         /**
42          * Object to encapsulate operations on dd area.
43          */
44         doc_structure : function () {
45             var topnode = Y.one(this.get('topnode'));
46             var dragitemsarea = topnode.one('div.dragitems');
47             var dropbgarea = topnode.one('div.droparea');
48             return {
49                 top_node : function() {
50                     return topnode;
51                 },
52                 bg_img : function() {
53                     return topnode.one('.dropbackground');
54                 },
55                 load_bg_img : function (url) {
56                     dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
57                     this.bg_img().on('load', this.on_image_load, this, 'bg_image');
58                 },
59                 drag_items : function() {
60                     return dragitemsarea.all('.dragitem');
61                 },
62                 drag_items_for_choice : function(choiceno) {
63                     return dragitemsarea.all('span.dragitem.choice' + choiceno);
64                 },
65                 drag_item_for_choice : function(choiceno, itemno) {
66                     return dragitemsarea.one('span.dragitem.choice' + choiceno +
67                                             '.item' + itemno);
68                 },
69                 drag_item_being_dragged : function(choiceno) {
70                     return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
71                 },
72                 drag_item_home : function (choiceno) {
73                     return dragitemsarea.one('span.draghome.choice' + choiceno);
74                 },
75                 drag_item_homes : function() {
76                     return dragitemsarea.all('span.draghome');
77                 },
78                 get_classname_numeric_suffix : function(node, prefix) {
79                     var classes = node.getAttribute('class');
80                     if (classes !== '') {
81                         var classesarr = classes.split(' ');
82                         for (var index = 0; index < classesarr.length; index++) {
83                             var patt1 = new RegExp('^' + prefix + '([0-9])+$');
84                             if (patt1.test(classesarr[index])) {
85                                 var patt2 = new RegExp('([0-9])+$');
86                                 var match = patt2.exec(classesarr[index]);
87                                 return Number(match[0]);
88                             }
89                         }
90                     }
91                     return null;
92                 },
93                 inputs_for_choices : function () {
94                     return topnode.all('input.choices');
95                 },
96                 input_for_choice : function (choiceno) {
97                     return topnode.one('input.choice' + choiceno);
98                 },
99                 marker_texts : function () {
100                     return topnode.one('div.markertexts');
101                 }
102             };
103         },
105         colours : ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
106                    '#87CEFA','#DAA520', '#FFD700', '#F0E68C'],
107         nextcolourindex : 0,
108         restart_colours : function () {
109             this.nextcolourindex = 0;
110         },
111         get_next_colour : function () {
112             var colour = this.colours[this.nextcolourindex];
113             this.nextcolourindex++;
114             if (this.nextcolourindex === this.colours.length) {
115                 this.nextcolourindex = 0;
116             }
117             return colour;
118         },
119         convert_to_window_xy : function (bgimgxy) {
120             return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
121                     Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
122         },
123         shapes : [],
124         draw_drop_zone : function (dropzoneno, markertext, shape, coords, colour, link) {
125             var existingmarkertext;
126             if (link) {
127                 existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
128             } else {
129                 existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
130             }
132             if (existingmarkertext) {
133                 if (markertext !== '') {
134                     existingmarkertext.setContent(markertext);
135                 } else {
136                     existingmarkertext.remove(true);
137                 }
138             } else if (markertext !== '') {
139                 var classnames = 'markertext markertext' + dropzoneno;
140                 if (link) {
141                     this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
142                                                                         markertext + '</a></span>');
143                 } else {
144                     this.doc.marker_texts().append('<span class="' + classnames + '">' +
145                                                                         markertext + '</span>');
146                 }
147             }
148             var drawfunc = 'draw_shape_' + shape;
149             if (this[drawfunc] instanceof Function){
150                var xyfortext = this[drawfunc](dropzoneno, coords, colour);
151                if (xyfortext !== null) {
152                    var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
153                    if (markerspan !== null) {
154                        markerspan.setStyle('opacity', '0.6');
155                        xyfortext[0] -= markerspan.get('offsetWidth') / 2;
156                        xyfortext[1] -= markerspan.get('offsetHeight') / 2;
157                        markerspan.setXY(this.convert_to_window_xy(xyfortext));
158                        var markerspananchor = markerspan.one('a');
159                        if (markerspananchor !== null) {
160                            markerspananchor.once('click', function (e, dropzoneno) {
161                                var fill = this.shapes[dropzoneno].get('fill');
162                                fill.opacity = 1;
163                                this.shapes[dropzoneno].set('fill', fill);
164                                },
165                                this,
166                                dropzoneno
167                            );
168                            markerspananchor.set('tabIndex', 0);
169                        }
170                    }
171                }
172             }
173         },
174         draw_shape_circle : function (dropzoneno, coords, colour) {
175             var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
176             if (coordsparts && coordsparts.length === 4) {
177                 var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
178                 if (this.coords_in_img(xy)) {
179                     var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
180                     var shape = this.graphics.addShape({
181                             type: 'circle',
182                             width: widthheight[0],
183                             height: widthheight[1],
184                             fill: {
185                                 color: colour,
186                                 opacity: "0.5"
187                             },
188                             stroke: {
189                                 weight: 1,
190                                 color: "black"
191                             }
192                     });
193                     shape.setXY(this.convert_to_window_xy(xy));
194                     this.shapes[dropzoneno] = shape;
195                     return [Number(coordsparts[1]), Number(coordsparts[2])];
196                 }
197             }
198             return null;
199         },
200         draw_shape_rectangle : function (dropzoneno, coords, colour) {
201             var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
202             if (coordsparts && coordsparts.length === 5) {
203                 var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
204                 var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
205                 if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
206                     var shape = this.graphics.addShape({
207                             type: 'rect',
208                             width: widthheight[0],
209                             height: widthheight[1],
210                             fill: {
211                                 color: colour,
212                                 opacity: "0.5"
213                             },
214                             stroke: {
215                                 weight: 1,
216                                 color: "black"
217                             }
218                     });
219                     shape.setXY(this.convert_to_window_xy(xy));
220                     this.shapes[dropzoneno] = shape;
221                     return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
222                 }
223             }
224             return null;
226         },
227         draw_shape_polygon : function (dropzoneno, coords, colour) {
228             var coordsparts = coords.split(';');
229             var xy = [];
230             for (var i in coordsparts) {
231                 var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
232                 if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
233                     xy[xy.length] = [parts[1], parts[2]];
234                 }
235             }
236             if (xy.length > 2) {
237                 var polygon = this.graphics.addShape({
238                     type: "path",
239                     stroke: {
240                         weight: 1,
241                         color: "black"
242                     },
243                     fill: {
244                         color: colour,
245                         opacity : "0.5"
246                     }
247                 });
248                 var maxxy = [0,0];
249                 var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
250                 for (i = 0; i < xy.length; i++) {
251                     //calculate min and max points to find center to show marker on
252                     minxy[0] = Math.min(xy[i][0], minxy[0]);
253                     minxy[1] = Math.min(xy[i][1], minxy[1]);
254                     maxxy[0] = Math.max(xy[i][0], maxxy[0]);
255                     maxxy[1] = Math.max(xy[i][1], maxxy[1]);
256                     if (i === 0) {
257                         polygon.moveTo(xy[i][0], xy[i][1]);
258                     } else {
259                         polygon.lineTo(xy[i][0], xy[i][1]);
260                     }
261                 }
262                 if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
263                     polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
264                 }
265                 polygon.end();
266                 polygon.setXY(this.doc.bg_img().getXY());
267                 this.shapes[dropzoneno] = polygon;
268                 return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
269             }
270             return null;
271         },
272         coords_in_img : function (coords) {
273             return (coords[0] <= this.doc.bg_img().get('width') &&
274                             coords[1] <= this.doc.bg_img().get('height'));
275         }
276     }, {
277         NAME : DDMARKERDDNAME,
278         ATTRS : {
279             drops : {value : null},
280             readonly : {value : false},
281             topnode : {value : null}
282         }
283     });
284     M.qtype_ddmarker = M.qtype_ddmarker || {};
285     M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
287     var DDMARKERQUESTIONNAME = 'ddmarker_question';
288     var DDMARKER_QUESTION = function() {
289         DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
290     };
291     /**
292      * This is the code for question rendering.
293      */
294     Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
295         initializer : function() {
296             this.doc = this.doc_structure(this);
297             this.poll_for_image_load(null, false, 0, this.after_image_load);
298             this.doc.bg_img().after('load', this.poll_for_image_load, this,
299                                                     false, 0, this.after_image_load);
300         },
301         after_image_load : function () {
302             this.redraw_drags_and_drops();
303             Y.later(2000, this, this.redraw_drags_and_drops, [], true);
304         },
305         clone_new_drag_item : function (draghome, itemno) {
306             var drag = draghome.cloneNode(true);
307             drag.removeClass('draghome');
308             drag.addClass('dragitem');
309             drag.addClass('item' + itemno);
310             drag.one('span.markertext').setStyle('opacity', 0.6);
311             draghome.insert(drag, 'after');
312             if (!this.get('readonly')) {
313                 this.draggable(drag);
314             }
315             return drag;
316         },
317         draggable : function (drag) {
318             var dd = new Y.DD.Drag({
319                 node: drag,
320                 dragMode: 'intersect'
321             }).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
322             dd.after('drag:start', function(e){
323                 var dragnode = e.target.get('node');
324                 dragnode.addClass('beingdragged');
325                 var choiceno = this.get_choiceno_for_node(dragnode);
326                 var itemno = this.get_itemno_for_node(dragnode);
327                 if (itemno !== null) {
328                     dragnode.removeClass('item' + dragnode);
329                 }
330                 this.save_all_xy_for_choice(choiceno, null);
331                 this.redraw_drags_and_drops();
332             }, this);
333             dd.after('drag:end', function(e) {
334                 var dragnode = e.target.get('node');
335                 dragnode.removeClass('beingdragged');
336                 var choiceno = this.get_choiceno_for_node(dragnode);
337                 this.save_all_xy_for_choice(choiceno, dragnode);
338                 this.redraw_drags_and_drops();
339             }, this);
340             //--- keyboard accessibility
341             drag.set('tabIndex', 0);
342             drag.on('dragchange', this.drop_zone_key_press, this);
343         },
344         save_all_xy_for_choice: function (choiceno, dropped) {
345             var coords = [];
346             var bgimgxy;
347             for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
348                 var dragitem = this.doc.drag_item_for_choice(choiceno, i);
349                 if (dragitem) {
350                     dragitem.removeClass('item' + i);
351                     if (!dragitem.hasClass('beingdragged')) {
352                         bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
353                         if (this.xy_in_bgimg(bgimgxy)) {
354                             dragitem.removeClass('item' + i);
355                             dragitem.addClass('item' + coords.length);
356                             coords[coords.length] = bgimgxy;
357                         }
358                     }
359                 }
360             }
361             if (dropped !== null){
362                 bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
363                 dropped.addClass('item' + coords.length);
364                 if (this.xy_in_bgimg(bgimgxy)) {
365                     coords[coords.length] = bgimgxy;
366                 }
367             }
368             this.set_form_value(choiceno, coords.join(';'));
369         },
370         reset_drag_xy : function (choiceno) {
371             this.set_form_value(choiceno, '');
372         },
373         set_form_value : function (choiceno, value) {
374             this.doc.input_for_choice(choiceno).set('value', value);
375         },
376         //make sure xy value is not out of bounds of bg image
377         xy_in_bgimg : function (bgimgxy) {
378             if ((bgimgxy[0] < 0) ||
379                     (bgimgxy[1] < 0) ||
380                     (bgimgxy[0] > this.doc.bg_img().get('width')) ||
381                     (bgimgxy[1] > this.doc.bg_img().get('height'))){
382                 return false;
383             } else {
384                 return true;
385             }
386         },
387         constrain_to_bgimg : function (windowxy) {
388             var bgimgxy = this.convert_to_bg_img_xy(windowxy);
389             bgimgxy[0] = Math.max(0, bgimgxy[0]);
390             bgimgxy[1] = Math.max(0, bgimgxy[1]);
391             bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
392             bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
393             return this.convert_to_window_xy(bgimgxy);
394         },
395         convert_to_bg_img_xy : function (windowxy) {
396             return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
397                     Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
398         },
399         redraw_drags_and_drops : function() {
400             this.doc.drag_items().each(function(item) {
401                 //if (!item.hasClass('beingdragged')){
402                     item.addClass('unneeded');
403                 //}
404             }, this);
405             this.doc.inputs_for_choices().each(function (input) {
406                 var choiceno = this.get_choiceno_for_node(input);
407                 var coords = this.get_coords(input);
408                 var dragitemhome = this.doc.drag_item_home(choiceno);
409                 for (var i = 0; i < coords.length; i++) {
410                     var dragitem = this.doc.drag_item_for_choice(choiceno, i);
411                     if (!dragitem || dragitem.hasClass('beingdragged')) {
412                         dragitem = this.clone_new_drag_item(dragitemhome, i);
413                     } else {
414                         dragitem.removeClass('unneeded');
415                     }
416                     dragitem.setXY(coords[i]);
417                 }
418             }, this);
419             this.doc.drag_items().each(function(item) {
420                 if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
421                     item.remove(true);
422                 }
423             }, this);
424             if (this.graphics !== null) {
425                 this.graphics.clear();
426             } else {
427                 this.graphics = new Y.Graphic(
428                     {render:this.doc.top_node().one("div.ddarea div.dropzones")}
429                 );
430             }
431             if (this.get('dropzones').length !== 0) {
432                 this.restart_colours();
433                 for (var dropzoneno in this.get('dropzones')) {
434                     var colourfordropzone = this.get_next_colour();
435                     var d = this.get('dropzones')[dropzoneno];
436                     this.draw_drop_zone(dropzoneno, d.markertext,
437                                         d.shape, d.coords, colourfordropzone, true);
438                 }
439             }
440         },
441         /**
442          * Determine what drag items need to be shown and
443          * return coords of all drag items except any that are currently being dragged
444          * based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
445          */
446         get_coords : function (input) {
447             var choiceno = this.get_choiceno_for_node(input);
448             var fv = input.get('value');
449             var infinite = input.hasClass('infinite');
450             var noofdrags = this.get_noofdrags_for_node(input);
451             var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
452             var coords = [];
453             if (fv !== '') {
454                 var coordsstrings = fv.split(';');
455                 for (var i = 0; i < coordsstrings.length; i++) {
456                     coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
457                 }
458             }
459             var displayeddrags = coords.length + (dragging ? 1 : 0);
460             if (infinite || (displayeddrags < noofdrags)) {
461                 coords[coords.length] = this.drag_home_xy(choiceno);
462             }
463             return coords;
464         },
465         drag_home_xy : function (choiceno) {
466             var dragitemhome = this.doc.drag_item_home(choiceno);
467             return [dragitemhome.getX(), dragitemhome.getY() - 12];
468         },
469         get_choiceno_for_node : function(node) {
470             return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
471         },
472         get_itemno_for_node : function(node) {
473             return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
474         },
475         get_noofdrags_for_node : function(node) {
476             return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
477         },
479         // Keyboard accessibility stuff below here.
480         drop_zone_key_press : function (e) {
481             var dragitem = e.target;
482             var xy = dragitem.getXY();
483             switch (e.direction) {
484                 case 'left' :
485                     xy[0] -= 1;
486                     break;
487                 case 'right' :
488                     xy[0] += 1;
489                     break;
490                 case 'down' :
491                     xy[1] += 1;
492                     break;
493                 case 'up' :
494                     xy[1] -= 1;
495                     break;
496                 case 'remove' :
497                     xy = null;
498                     break;
499             }
500             var choiceno = this.get_choiceno_for_node(dragitem);
501             if (xy !== null) {
502                 xy = this.constrain_to_bgimg(xy);
503             } else {
504                 xy = this.drag_home_xy(choiceno);
505             }
506             e.preventDefault();
507             dragitem.setXY(xy);
508             this.save_all_xy_for_choice(choiceno, null);
509         }
510     }, {NAME : DDMARKERQUESTIONNAME, ATTRS : {dropzones:{value:[]}}});
512     Y.Event.define('dragchange', {
513         // Webkit and IE repeat keydown when you hold down arrow keys.
514         // Opera links keypress to page scroll; others keydown.
515         // Firefox prevents page scroll via preventDefault() on either
516         // keydown or keypress.
517         _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
519         _keys: {
520             '32': 'remove',     // Space
521             '37': 'left', // Left arrow
522             '38': 'up', // Up arrow
523             '39': 'right',     // Right arrow
524             '40': 'down',     // Down arrow
525             '65': 'left', // a
526             '87': 'up', // w
527             '68': 'right',     // d
528             '83': 'down',     // s
529             '27': 'remove'    // Escape
530         },
532         _keyHandler: function (e, notifier) {
533             if (this._keys[e.keyCode]) {
534                 e.direction = this._keys[e.keyCode];
535                 notifier.fire(e);
536             }
537         },
539         on: function (node, sub, notifier) {
540             sub._detacher = node.on(this._event, this._keyHandler,
541                                     this, notifier);
542         }
543     });
544     M.qtype_ddmarker.init_question = function(config) {
545         return new DDMARKER_QUESTION(config);
546     };
547 }, '@VERSION@', {
548       requires:['node', 'event-resize', 'dd', 'dd-drop', 'dd-constrain', 'graphics']