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);
7 * This is the base class for the question rendering and question editing form code.
9 Y.extend(DDMARKER_DD, Y.Base, {
12 afterimageloaddone : false,
14 poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
15 if (this.afterimageloaddone) {
18 var bgdone = this.doc.bg_img().get('complete');
19 if (waitforimageconstrain) {
20 bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
23 if (this.polltimer !== null) {
24 this.polltimer.cancel();
25 this.polltimer = null;
27 this.doc.bg_img().detach('load', this.poll_for_image_load);
29 Y.later(pause, this, doafterwords);
31 doafterwords.call(this);
33 this.afterimageloaddone = true;
34 } else if (this.polltimer === null) {
35 var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
37 Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
42 * Object to encapsulate operations on dd area.
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');
49 top_node : function() {
53 return topnode.one('.dropbackground');
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');
59 drag_items : function() {
60 return dragitemsarea.all('.dragitem');
62 drag_items_for_choice : function(choiceno) {
63 return dragitemsarea.all('span.dragitem.choice' + choiceno);
65 drag_item_for_choice : function(choiceno, itemno) {
66 return dragitemsarea.one('span.dragitem.choice' + choiceno +
69 drag_item_being_dragged : function(choiceno) {
70 return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
72 drag_item_home : function (choiceno) {
73 return dragitemsarea.one('span.draghome.choice' + choiceno);
75 drag_item_homes : function() {
76 return dragitemsarea.all('span.draghome');
78 get_classname_numeric_suffix : function(node, prefix) {
79 var classes = node.getAttribute('class');
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]);
93 inputs_for_choices : function () {
94 return topnode.all('input.choices');
96 input_for_choice : function (choiceno) {
97 return topnode.one('input.choice' + choiceno);
99 marker_texts : function () {
100 return topnode.one('div.markertexts');
105 colours : ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
106 '#87CEFA','#DAA520', '#FFD700', '#F0E68C'],
108 restart_colours : function () {
109 this.nextcolourindex = 0;
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;
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];
124 draw_drop_zone : function (dropzoneno, markertext, shape, coords, colour, link) {
125 var existingmarkertext;
127 existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
129 existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
132 if (existingmarkertext) {
133 if (markertext !== '') {
134 existingmarkertext.setContent(markertext);
136 existingmarkertext.remove(true);
138 } else if (markertext !== '') {
139 var classnames = 'markertext markertext' + dropzoneno;
141 this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
142 markertext + '</a></span>');
144 this.doc.marker_texts().append('<span class="' + classnames + '">' +
145 markertext + '</span>');
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');
163 this.shapes[dropzoneno].set('fill', fill);
168 markerspananchor.set('tabIndex', 0);
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({
182 width: widthheight[0],
183 height: widthheight[1],
193 shape.setXY(this.convert_to_window_xy(xy));
194 this.shapes[dropzoneno] = shape;
195 return [Number(coordsparts[1]), Number(coordsparts[2])];
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({
208 width: widthheight[0],
209 height: widthheight[1],
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];
227 draw_shape_polygon : function (dropzoneno, coords, colour) {
228 var coordsparts = coords.split(';');
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]];
237 var polygon = this.graphics.addShape({
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]);
257 polygon.moveTo(xy[i][0], xy[i][1]);
259 polygon.lineTo(xy[i][0], xy[i][1]);
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.
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];
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'));
277 NAME : DDMARKERDDNAME,
279 drops : {value : null},
280 readonly : {value : false},
281 topnode : {value : null}
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);
292 * This is the code for question rendering.
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);
301 after_image_load : function () {
302 this.redraw_drags_and_drops();
303 Y.later(2000, this, this.redraw_drags_and_drops, [], true);
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);
317 draggable : function (drag) {
318 var dd = new Y.DD.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);
330 this.save_all_xy_for_choice(choiceno, null);
331 this.redraw_drags_and_drops();
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();
340 //--- keyboard accessibility
341 drag.set('tabIndex', 0);
342 drag.on('dragchange', this.drop_zone_key_press, this);
344 save_all_xy_for_choice: function (choiceno, dropped) {
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);
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;
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;
368 this.set_form_value(choiceno, coords.join(';'));
370 reset_drag_xy : function (choiceno) {
371 this.set_form_value(choiceno, '');
373 set_form_value : function (choiceno, value) {
374 this.doc.input_for_choice(choiceno).set('value', value);
376 //make sure xy value is not out of bounds of bg image
377 xy_in_bgimg : function (bgimgxy) {
378 if ((bgimgxy[0] < 0) ||
380 (bgimgxy[0] > this.doc.bg_img().get('width')) ||
381 (bgimgxy[1] > this.doc.bg_img().get('height'))){
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);
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];
399 redraw_drags_and_drops : function() {
400 this.doc.drag_items().each(function(item) {
401 //if (!item.hasClass('beingdragged')){
402 item.addClass('unneeded');
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);
414 dragitem.removeClass('unneeded');
416 dragitem.setXY(coords[i]);
419 this.doc.drag_items().each(function(item) {
420 if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
424 if (this.graphics !== null) {
425 this.graphics.clear();
427 this.graphics = new Y.Graphic(
428 {render:this.doc.top_node().one("div.ddarea div.dropzones")}
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);
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.
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));
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(','));
459 var displayeddrags = coords.length + (dragging ? 1 : 0);
460 if (infinite || (displayeddrags < noofdrags)) {
461 coords[coords.length] = this.drag_home_xy(choiceno);
465 drag_home_xy : function (choiceno) {
466 var dragitemhome = this.doc.drag_item_home(choiceno);
467 return [dragitemhome.getX(), dragitemhome.getY() - 12];
469 get_choiceno_for_node : function(node) {
470 return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
472 get_itemno_for_node : function(node) {
473 return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
475 get_noofdrags_for_node : function(node) {
476 return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
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) {
500 var choiceno = this.get_choiceno_for_node(dragitem);
502 xy = this.constrain_to_bgimg(xy);
504 xy = this.drag_home_xy(choiceno);
508 this.save_all_xy_for_choice(choiceno, null);
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',
520 '32': 'remove', // Space
521 '37': 'left', // Left arrow
522 '38': 'up', // Up arrow
523 '39': 'right', // Right arrow
524 '40': 'down', // Down arrow
529 '27': 'remove' // Escape
532 _keyHandler: function (e, notifier) {
533 if (this._keys[e.keyCode]) {
534 e.direction = this._keys[e.keyCode];
539 on: function (node, sub, notifier) {
540 sub._detacher = node.on(this._event, this._keyHandler,
544 M.qtype_ddmarker.init_question = function(config) {
545 return new DDMARKER_QUESTION(config);
548 requires:['node', 'event-resize', 'dd', 'dd-drop', 'dd-constrain', 'graphics']