MDL-52954 marking guide: Wrap radio fields in a label for styling
[moodle.git] / mod / assign / feedback / editpdf / yui / src / editor / js / editor.js
CommitLineData
5c386472
DW
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/>.
15
16/**
17 * Provides an in browser PDF editor.
18 *
19 * @module moodle-assignfeedback_editpdf-editor
20 */
21
22/**
23 * EDITOR
24 * This is an in browser PDF editor.
25 *
1f777e5c
AN
26 * @namespace M.assignfeedback_editpdf
27 * @class editor
5c386472
DW
28 * @constructor
29 * @extends Y.Base
30 */
557f44d9 31var EDITOR = function() {
5c386472
DW
32 EDITOR.superclass.constructor.apply(this, arguments);
33};
34EDITOR.prototype = {
35
5c386472
DW
36 /**
37 * The dialogue used for all action menu displays.
1f777e5c 38 *
5c386472
DW
39 * @property type
40 * @type M.core.dialogue
41 * @protected
42 */
43 dialogue : null,
44
45 /**
46 * The number of pages in the pdf.
1f777e5c
AN
47 *
48 * @property pagecount
49 * @type Number
5c386472
DW
50 * @protected
51 */
52 pagecount : 0,
53
54 /**
55 * The active page in the editor.
1f777e5c
AN
56 *
57 * @property currentpage
58 * @type Number
5c386472
DW
59 * @protected
60 */
61 currentpage : 0,
62
63 /**
64 * A list of page objects. Each page has a list of comments and annotations.
1f777e5c
AN
65 *
66 * @property pages
5c386472
DW
67 * @type array
68 * @protected
69 */
70 pages : [],
71
72 /**
73 * The yui node for the loading icon.
1f777e5c
AN
74 *
75 * @property loadingicon
76 * @type Node
5c386472
DW
77 * @protected
78 */
79 loadingicon : null,
80
81 /**
82 * Image object of the current page image.
1f777e5c
AN
83 *
84 * @property pageimage
5c386472
DW
85 * @type Image
86 * @protected
87 */
88 pageimage : null,
89
90 /**
91 * YUI Graphic class for drawing shapes.
1f777e5c
AN
92 *
93 * @property graphic
94 * @type Graphic
5c386472
DW
95 * @protected
96 */
97 graphic : null,
98
99 /**
100 * Info about the current edit operation.
1f777e5c 101 *
5c386472
DW
102 * @property currentedit
103 * @type M.assignfeedback_editpdf.edit
104 * @protected
105 */
106 currentedit : new M.assignfeedback_editpdf.edit(),
107
108 /**
109 * Current drawable.
1f777e5c 110 *
5c386472 111 * @property currentdrawable
1f777e5c 112 * @type M.assignfeedback_editpdf.drawable|false
5c386472
DW
113 * @protected
114 */
115 currentdrawable : false,
116
117 /**
118 * Current drawables.
1f777e5c 119 *
5c386472
DW
120 * @property drawables
121 * @type array(M.assignfeedback_editpdf.drawable)
122 * @protected
123 */
124 drawables : [],
125
126 /**
127 * Current comment when the comment menu is open.
128 * @property currentcomment
129 * @type M.assignfeedback_editpdf.comment
130 * @protected
131 */
132 currentcomment : null,
133
134 /**
135 * Current annotation when the select tool is used.
136 * @property currentannotation
137 * @type M.assignfeedback_editpdf.annotation
138 * @protected
139 */
140 currentannotation : null,
141
142 /**
143 * Last selected annotation tool
144 * @property lastannotationtool
145 * @type String
146 * @protected
147 */
148 lastanntationtool : "pen",
149
150 /**
151 * The users comments quick list
152 * @property quicklist
153 * @type M.assignfeedback_editpdf.quickcommentlist
154 * @protected
155 */
156 quicklist : null,
157
158 /**
159 * The search comments window.
160 * @property searchcommentswindow
161 * @type M.core.dialogue
162 * @protected
163 */
164 searchcommentswindow : null,
165
166
167 /**
168 * The selected stamp picture.
169 * @property currentstamp
170 * @type String
171 * @protected
172 */
173 currentstamp : null,
174
175 /**
176 * The stamps.
177 * @property stamps
178 * @type Array
179 * @protected
180 */
181 stamps : [],
182
60b87080
DS
183 /**
184 * Prevent new comments from appearing
185 * immediately after clicking off a current
186 * comment
187 * @property editingcomment
188 * @type Boolean
189 * @public
190 */
191 editingcomment : false,
192
5c386472
DW
193 /**
194 * Called during the initialisation process of the object.
195 * @method initializer
196 */
197 initializer : function() {
198 var link;
199
200 link = Y.one('#' + this.get('linkid'));
201
202 if (link) {
203 link.on('click', this.link_handler, this);
204 link.on('key', this.link_handler, 'down:13', this);
205
206 this.currentedit.start = false;
207 this.currentedit.end = false;
8ae76e13
AN
208 if (!this.get('readonly')) {
209 this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this);
210 }
5c386472
DW
211 }
212 },
213
214 /**
215 * Called to show/hide buttons and set the current colours/stamps.
216 * @method refresh_button_state
217 */
218 refresh_button_state : function() {
219 var button, currenttoolnode, imgurl;
220
221 // Initalise the colour buttons.
da91de78 222 button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
5c386472
DW
223
224 imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf');
225 button.one('img').setAttribute('src', imgurl);
226
227 if (this.currentedit.commentcolour === 'clear') {
228 button.one('img').setStyle('borderStyle', 'dashed');
229 } else {
230 button.one('img').setStyle('borderStyle', 'solid');
231 }
232
da91de78 233 button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
5c386472
DW
234 imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf');
235 button.one('img').setAttribute('src', imgurl);
236
da91de78 237 currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
5c386472
DW
238 currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton');
239 currenttoolnode.setAttribute('aria-pressed', 'true');
240
da91de78 241 button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
5c386472
DW
242 button.one('img').setAttrs({'src': this.get_stamp_image_url(this.currentedit.stamp),
243 'height': '16',
244 'width': '16'});
245 },
246
247 /**
248 * Called to get the bounds of the drawing region.
249 * @method get_canvas_bounds
250 */
251 get_canvas_bounds : function() {
da91de78 252 var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
253 offsetcanvas = canvas.getXY(),
254 offsetleft = offsetcanvas[0],
255 offsettop = offsetcanvas[1],
256 width = parseInt(canvas.getStyle('width'), 10),
257 height = parseInt(canvas.getStyle('height'), 10);
258
259 return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height);
260 },
261
262 /**
263 * Called to translate from window coordinates to canvas coordinates.
264 * @method get_canvas_coordinates
265 * @param M.assignfeedback_editpdf.point point in window coordinats.
266 */
267 get_canvas_coordinates : function(point) {
268 var bounds = this.get_canvas_bounds(),
269 newpoint = new M.assignfeedback_editpdf.point(point.x - bounds.x, point.y - bounds.y);
270
271 bounds.x = bounds.y = 0;
272
273 newpoint.clip(bounds);
274 return newpoint;
275 },
276
277 /**
278 * Called to translate from canvas coordinates to window coordinates.
279 * @method get_window_coordinates
280 * @param M.assignfeedback_editpdf.point point in window coordinats.
281 */
282 get_window_coordinates : function(point) {
283 var bounds = this.get_canvas_bounds(),
284 newpoint = new M.assignfeedback_editpdf.point(point.x + bounds.x, point.y + bounds.y);
285
286 return newpoint;
287 },
288
289 /**
290 * Called to open the pdf editing dialogue.
291 * @method link_handler
292 */
293 link_handler : function(e) {
50c12f01 294 var drawingcanvas, drawingregion, resize = true;
5c386472
DW
295 e.preventDefault();
296
297 if (!this.dialogue) {
298 this.dialogue = new M.core.dialogue({
299 headerContent: this.get('header'),
300 bodyContent: this.get('body'),
301 footerContent: this.get('footer'),
aebc768f 302 modal: true,
5c386472 303 width: '840px',
6d87dceb
DW
304 visible: false,
305 draggable: true
5c386472
DW
306 });
307
5c386472
DW
308 // Add custom class for styling.
309 this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE);
310
da91de78 311 this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
5c386472 312
da91de78 313 drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
402f4a1f 314 this.graphic = new Y.Graphic({render : drawingcanvas});
5c386472 315
da91de78 316 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
50c12f01
DS
317 drawingregion.on('scroll', this.move_canvas, this);
318
5c386472
DW
319 if (!this.get('readonly')) {
320 drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
321 drawingcanvas.on('gesturemove', this.edit_move, null, this);
322 drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
323
324 this.refresh_button_state();
325 }
d40ce26f
JM
326
327 this.load_all_pages();
50c12f01
DS
328 drawingcanvas.on('windowresize', this.resize, this);
329
330 resize = false;
5c386472 331 }
6d87dceb
DW
332 this.dialogue.centerDialogue();
333 this.dialogue.show();
50c12f01
DS
334
335 // Redraw when the dialogue is moved, to ensure the absolute elements are all positioned correctly.
336 this.dialogue.dd.on('drag:end', this.redraw, this);
337 if (resize) {
338 this.resize(); // When re-opening the dialog call redraw, to make sure the size + layout is correct.
339 }
5c386472
DW
340 },
341
342 /**
343 * Called to load the information and annotations for all pages.
344 * @method load_all_pages
345 */
346 load_all_pages : function() {
347 var ajaxurl = AJAXBASE,
aa3e4bde
JM
348 config,
349 checkconversionstatus,
350 ajax_error_total;
5c386472
DW
351
352 config = {
353 method: 'get',
354 context: this,
355 sync: false,
356 data : {
aa3e4bde
JM
357 sesskey : M.cfg.sesskey,
358 action : 'loadallpages',
359 userid : this.get('userid'),
360 attemptnumber : this.get('attemptnumber'),
098f7dd4
FM
361 assignmentid : this.get('assignmentid'),
362 readonly : this.get('readonly') ? 1 : 0
5c386472
DW
363 },
364 on: {
365 success: function(tid, response) {
366 this.all_pages_loaded(response.responseText);
367 },
368 failure: function(tid, response) {
aa3e4bde 369 return new M.core.exception(response.responseText);
5c386472
DW
370 }
371 }
372 };
373
374 Y.io(ajaxurl, config);
aa3e4bde
JM
375
376 // If pages are not loaded, check PDF conversion status for the progress bar.
377 if (this.pagecount <= 0) {
378 checkconversionstatus = {
379 method: 'get',
380 context: this,
381 sync: false,
382 data : {
383 sesskey : M.cfg.sesskey,
384 action : 'conversionstatus',
385 userid : this.get('userid'),
386 attemptnumber : this.get('attemptnumber'),
387 assignmentid : this.get('assignmentid')
388 },
389 on: {
390 success: function(tid, response) {
391 ajax_error_total = 0;
392 if (this.pagecount === 0) {
393 var pagetotal = this.get('pagetotal');
394
395 // Update the progress bar.
da91de78 396 var progressbarcontainer = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER);
aa3e4bde
JM
397 var progressbar = progressbarcontainer.one('.bar');
398 if (progressbar) {
399 // Calculate progress.
400 var progress = (response.response / pagetotal) * 100;
401 progressbar.setStyle('width', progress + '%');
402 progressbarcontainer.setAttribute('aria-valuenow', progress);
403 }
404
405 // New ajax request delayed of a second.
406 Y.later(1000, this, function () {
407 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
408 });
409 }
410 },
411 failure: function(tid, response) {
412 ajax_error_total = ajax_error_total + 1;
413 // We only continue on error if the all pages were not generated,
414 // and if the ajax call did not produce 5 errors in the row.
415 if (this.pagecount === 0 && ajax_error_total < 5) {
416 Y.later(1000, this, function () {
417 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
418 });
419 }
420 return new M.core.exception(response.responseText);
421 }
422 }
423 };
424 // We start the AJAX "generated page total number" call a second later to give a chance to
425 // the AJAX "combined pdf generation" call to clean the previous submission images.
426 Y.later(1000, this, function () {
427 ajax_error_total = 0;
428 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
429 });
430 }
5c386472
DW
431 },
432
433 /**
434 * The info about all pages in the pdf has been returned.
435 * @param string The ajax response as text.
436 * @protected
437 * @method all_pages_loaded
438 */
439 all_pages_loaded : function(responsetext) {
440 var data, i, j, comment, error;
441 try {
442 data = Y.JSON.parse(responsetext);
443 if (data.error || !data.pagecount) {
444 this.dialogue.hide();
445 // Display alert dialogue.
446 error = new M.core.alert({ message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf') });
447 error.show();
448 return;
449 }
450 } catch (e) {
451 this.dialogue.hide();
452 // Display alert dialogue.
453 error = new M.core.alert({ title: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')});
454 error.show();
455 return;
456 }
457
458 this.pagecount = data.pagecount;
459 this.pages = data.pages;
460
461 for (i = 0; i < this.pages.length; i++) {
462 for (j = 0; j < this.pages[i].comments.length; j++) {
463 comment = this.pages[i].comments[j];
464 this.pages[i].comments[j] = new M.assignfeedback_editpdf.comment(this,
465 comment.gradeid,
466 comment.pageno,
467 comment.x,
468 comment.y,
469 comment.width,
470 comment.colour,
471 comment.rawtext);
472 }
473 for (j = 0; j < this.pages[i].annotations.length; j++) {
474 data = this.pages[i].annotations[j];
475 this.pages[i].annotations[j] = this.create_annotation(data.type, data);
476 }
477 }
478
479 // Update the ui.
8ae76e13
AN
480 if (this.quicklist) {
481 this.quicklist.load();
482 }
5c386472
DW
483 this.setup_navigation();
484 this.setup_toolbar();
485 this.change_page();
486 },
487
488 /**
489 * Get the full pluginfile url for an image file - just given the filename.
490 *
491 * @public
492 * @method get_stamp_image_url
493 * @param string filename
494 */
495 get_stamp_image_url : function(filename) {
496 var urls = this.get('stampfiles'),
497 fullurl = '';
498
499 Y.Array.each(urls, function(url) {
500 if (url.indexOf(filename) > 0) {
501 fullurl = url;
502 }
503 }, this);
504
505 return fullurl;
506 },
507
508 /**
509 * Attach listeners and enable the color picker buttons.
510 * @protected
511 * @method setup_toolbar
512 */
513 setup_toolbar : function() {
514 var toolnode,
515 commentcolourbutton,
516 annotationcolourbutton,
517 searchcommentsbutton,
518 currentstampbutton,
519 stampfiles,
520 picker,
521 filename;
522
da91de78 523 searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON);
5c386472
DW
524 searchcommentsbutton.on('click', this.open_search_comments, this);
525 searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this);
526
527 if (this.get('readonly')) {
528 return;
529 }
530 // Setup the tool buttons.
531 Y.each(TOOLSELECTOR, function(selector, tool) {
da91de78 532 toolnode = this.get_dialogue_element(selector);
5c386472
DW
533 toolnode.on('click', this.handle_tool_button, this, tool);
534 toolnode.on('key', this.handle_tool_button, 'down:13', this, tool);
535 toolnode.setAttribute('aria-pressed', 'false');
536 }, this);
537
538 // Set the default tool.
539
da91de78 540 commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
5c386472
DW
541 picker = new M.assignfeedback_editpdf.colourpicker({
542 buttonNode: commentcolourbutton,
543 colours: COMMENTCOLOUR,
544 iconprefix: 'background_colour_',
545 callback: function (e) {
546 var colour = e.target.getAttribute('data-colour');
547 if (!colour) {
548 colour = e.target.ancestor().getAttribute('data-colour');
549 }
550 this.currentedit.commentcolour = colour;
551 this.handle_tool_button(e, "comment");
552 },
553 context: this
554 });
555
da91de78 556 annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
5c386472
DW
557 picker = new M.assignfeedback_editpdf.colourpicker({
558 buttonNode: annotationcolourbutton,
559 iconprefix: 'colour_',
560 colours: ANNOTATIONCOLOUR,
561 callback: function (e) {
562 var colour = e.target.getAttribute('data-colour');
563 if (!colour) {
564 colour = e.target.ancestor().getAttribute('data-colour');
565 }
566 this.currentedit.annotationcolour = colour;
567 if (this.lastannotationtool) {
568 this.handle_tool_button(e, this.lastannotationtool);
569 } else {
570 this.handle_tool_button(e, "pen");
571 }
572 },
573 context: this
574 });
575
576 stampfiles = this.get('stampfiles');
577 if (stampfiles.length <= 0) {
da91de78 578 this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide();
5c386472
DW
579 } else {
580 filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1);
581 this.currentedit.stamp = filename;
da91de78 582 currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
5c386472
DW
583
584 picker = new M.assignfeedback_editpdf.stamppicker({
585 buttonNode: currentstampbutton,
586 stamps: stampfiles,
587 callback: function(e) {
588 var stamp = e.target.getAttribute('data-stamp'),
589 filename;
590
591 if (!stamp) {
592 stamp = e.target.ancestor().getAttribute('data-stamp');
593 }
594 filename = stamp.substr(stamp.lastIndexOf('/'));
595 this.currentedit.stamp = filename;
673c4ffe 596 this.handle_tool_button(e, "stamp");
5c386472
DW
597 },
598 context: this
599 });
600 this.refresh_button_state();
601 }
602 },
603
604 /**
605 * Change the current tool.
606 * @protected
607 * @method handle_tool_button
608 */
609 handle_tool_button : function(e, tool) {
610 var currenttoolnode;
611
612 e.preventDefault();
613
614 // Change style of the pressed button.
da91de78 615 currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
5c386472
DW
616 currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton');
617 currenttoolnode.setAttribute('aria-pressed', 'false');
618 this.currentedit.tool = tool;
673c4ffe 619 if (tool !== "comment" && tool !== "select" && tool !== "stamp") {
5c386472
DW
620 this.lastannotationtool = tool;
621 }
622 this.refresh_button_state();
623 },
624
625 /**
626 * JSON encode the current page data - stripping out drawable references which cannot be encoded.
627 * @protected
628 * @method stringify_current_page
629 * @return string
630 */
631 stringify_current_page : function() {
632 var comments = [],
633 annotations = [],
634 page,
635 i = 0;
636
637 for (i = 0; i < this.pages[this.currentpage].comments.length; i++) {
638 comments[i] = this.pages[this.currentpage].comments[i].clean();
639 }
640 for (i = 0; i < this.pages[this.currentpage].annotations.length; i++) {
641 annotations[i] = this.pages[this.currentpage].annotations[i].clean();
642 }
643
644 page = { comments : comments, annotations : annotations };
645
646 return Y.JSON.stringify(page);
647 },
648
649 /**
650 * Generate a drawable from the current in progress edit.
651 * @protected
652 * @method get_current_drawable
653 */
654 get_current_drawable : function() {
655 var comment,
656 annotation,
657 drawable = false;
658
659 if (!this.currentedit.start || !this.currentedit.end) {
660 return false;
661 }
662
663 if (this.currentedit.tool === 'comment') {
664 comment = new M.assignfeedback_editpdf.comment(this);
665 drawable = comment.draw_current_edit(this.currentedit);
666 } else {
667 annotation = this.create_annotation(this.currentedit.tool, {});
668 if (annotation) {
669 drawable = annotation.draw_current_edit(this.currentedit);
670 }
671 }
672
673 return drawable;
674 },
675
da91de78
BL
676 /**
677 * Find an element within the dialogue.
678 * @protected
679 * @method get_dialogue_element
680 */
681 get_dialogue_element : function(selector) {
682 return this.dialogue.get('boundingBox').one(selector);
683 },
5c386472
DW
684
685 /**
686 * Redraw the active edit.
687 * @protected
688 * @method redraw_active_edit
689 */
690 redraw_current_edit : function() {
691 if (this.currentdrawable) {
692 this.currentdrawable.erase();
693 }
694 this.currentdrawable = this.get_current_drawable();
695 },
696
697 /**
698 * Event handler for mousedown or touchstart.
699 * @protected
700 * @param Event
701 * @method edit_start
702 */
703 edit_start : function(e) {
ce091776 704 e.preventDefault();
da91de78 705 var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
706 offset = canvas.getXY(),
707 scrolltop = canvas.get('docScrollY'),
708 scrollleft = canvas.get('docScrollX'),
709 point = {x : e.clientX - offset[0] + scrollleft,
710 y : e.clientY - offset[1] + scrolltop},
711 selected = false,
712 lastannotation;
713
714 // Ignore right mouse click.
715 if (e.button === 3) {
716 return;
717 }
718
719 if (this.currentedit.starttime) {
720 return;
721 }
722
60b87080
DS
723 if (this.editingcomment) {
724 return;
725 }
726
5c386472
DW
727 this.currentedit.starttime = new Date().getTime();
728 this.currentedit.start = point;
729 this.currentedit.end = {x : point.x, y : point.y};
730
731 if (this.currentedit.tool === 'select') {
557f44d9
AN
732 var x = this.currentedit.end.x,
733 y = this.currentedit.end.y,
734 annotations = this.pages[this.currentpage].annotations;
5c386472
DW
735 // Find the first annotation whose bounds encompass the click.
736 Y.each(annotations, function(annotation) {
737 if (((x - annotation.x) * (x - annotation.endx)) <= 0 &&
738 ((y - annotation.y) * (y - annotation.endy)) <= 0) {
739 selected = annotation;
740 }
741 });
742
743 if (selected) {
744 lastannotation = this.currentannotation;
745 this.currentannotation = selected;
746 if (lastannotation && lastannotation !== selected) {
747 // Redraw the last selected annotation to remove the highlight.
748 if (lastannotation.drawable) {
749 lastannotation.drawable.erase();
750 this.drawables.push(lastannotation.draw());
751 }
752 }
753 // Redraw the newly selected annotation to show the highlight.
754 if (this.currentannotation.drawable) {
755 this.currentannotation.drawable.erase();
756 }
757 this.drawables.push(this.currentannotation.draw());
758 }
759 }
760 if (this.currentannotation) {
761 // Used to calculate drag offset.
762 this.currentedit.annotationstart = { x : this.currentannotation.x,
763 y : this.currentannotation.y };
764 }
765 },
766
767 /**
768 * Event handler for mousemove.
769 * @protected
770 * @param Event
771 * @method edit_move
772 */
773 edit_move : function(e) {
ce091776 774 e.preventDefault();
5c386472 775 var bounds = this.get_canvas_bounds(),
da91de78 776 canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
777 clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'),
778 e.clientY + canvas.get('docScrollY')),
779 point = this.get_canvas_coordinates(clientpoint);
780
781 // Ignore events out of the canvas area.
782 if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) {
783 return;
784 }
785
786 if (this.currentedit.tool === 'pen') {
787 this.currentedit.path.push(point);
788 }
789
790 if (this.currentedit.tool === 'select') {
791 if (this.currentannotation && this.currentedit) {
792 this.currentannotation.move( this.currentedit.annotationstart.x + point.x - this.currentedit.start.x,
793 this.currentedit.annotationstart.y + point.y - this.currentedit.start.y);
794 }
795 } else {
796 if (this.currentedit.start) {
797 this.currentedit.end = point;
798 this.redraw_current_edit();
799 }
800 }
801 },
802
803 /**
804 * Event handler for mouseup or touchend.
805 * @protected
806 * @param Event
807 * @method edit_end
808 */
809 edit_end : function() {
810 var duration,
811 comment,
812 annotation;
813
814 duration = new Date().getTime() - this.currentedit.start;
815
816 if (duration < CLICKTIMEOUT || this.currentedit.start === false) {
817 return;
818 }
819
820 if (this.currentedit.tool === 'comment') {
821 if (this.currentdrawable) {
822 this.currentdrawable.erase();
823 }
824 this.currentdrawable = false;
825 comment = new M.assignfeedback_editpdf.comment(this);
baf881b8
RT
826 if (comment.init_from_edit(this.currentedit)) {
827 this.pages[this.currentpage].comments.push(comment);
828 this.drawables.push(comment.draw(true));
829 this.editingcomment = true;
830 }
5c386472
DW
831 } else {
832 annotation = this.create_annotation(this.currentedit.tool, {});
833 if (annotation) {
834 if (this.currentdrawable) {
835 this.currentdrawable.erase();
836 }
837 this.currentdrawable = false;
baf881b8
RT
838 if (annotation.init_from_edit(this.currentedit)) {
839 this.pages[this.currentpage].annotations.push(annotation);
840 this.drawables.push(annotation.draw());
841 }
5c386472
DW
842 }
843 }
844
5c386472
DW
845 // Save the changes.
846 this.save_current_page();
847
848 // Reset the current edit.
849 this.currentedit.starttime = 0;
850 this.currentedit.start = false;
851 this.currentedit.end = false;
852 this.currentedit.path = [];
853 },
854
452beca9
GF
855 /**
856 * Resize the dialogue window when the browser is resized.
857 * @public
858 * @method resize
859 */
860 resize : function() {
50c12f01
DS
861 var drawingregion, drawregionheight;
862
863 if (!this.dialogue.get('visible')) {
864 return;
865 }
452beca9 866 this.dialogue.centerDialogue();
50c12f01
DS
867
868 // Make sure the dialogue box is not bigger than the max height of the viewport.
869 drawregionheight = Y.one('body').get('winHeight') - 120; // Space for toolbar + titlebar.
870 if (drawregionheight < 100) {
871 drawregionheight = 100;
872 }
da91de78 873 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
50c12f01
DS
874 drawingregion.setStyle('maxHeight', drawregionheight +'px');
875 this.redraw();
452beca9
GF
876 return true;
877 },
878
5c386472
DW
879 /**
880 * Factory method for creating annotations of the correct subclass.
881 * @public
882 * @method create_annotation
883 */
884 create_annotation : function(type, data) {
885 data.type = type;
886 data.editor = this;
887 if (type === "line") {
888 return new M.assignfeedback_editpdf.annotationline(data);
889 } else if (type === "rectangle") {
890 return new M.assignfeedback_editpdf.annotationrectangle(data);
891 } else if (type === "oval") {
892 return new M.assignfeedback_editpdf.annotationoval(data);
893 } else if (type === "pen") {
894 return new M.assignfeedback_editpdf.annotationpen(data);
895 } else if (type === "highlight") {
896 return new M.assignfeedback_editpdf.annotationhighlight(data);
897 } else if (type === "stamp") {
898 return new M.assignfeedback_editpdf.annotationstamp(data);
899 }
900 return false;
901 },
902
903 /**
904 * Save all the annotations and comments for the current page.
905 * @protected
906 * @method save_current_page
907 */
908 save_current_page : function() {
909 var ajaxurl = AJAXBASE,
910 config;
911
912 config = {
913 method: 'post',
914 context: this,
915 sync: false,
916 data : {
917 'sesskey' : M.cfg.sesskey,
918 'action' : 'savepage',
919 'index' : this.currentpage,
920 'userid' : this.get('userid'),
921 'attemptnumber' : this.get('attemptnumber'),
922 'assignmentid' : this.get('assignmentid'),
923 'page' : this.stringify_current_page()
924 },
925 on: {
926 success: function(tid, response) {
927 var jsondata;
928 try {
929 jsondata = Y.JSON.parse(response.responseText);
930 if (jsondata.error) {
931 return new M.core.ajaxException(jsondata);
932 }
da91de78
BL
933 Y.one('#' + this.get('linkid')).siblings(SELECTOR.UNSAVEDCHANGESDIV)
934 .item(0).addClass('haschanges');
5c386472
DW
935 } catch (e) {
936 return new M.core.exception(e);
937 }
938 },
939 failure: function(tid, response) {
aa3e4bde 940 return new M.core.exception(response.responseText);
5c386472
DW
941 }
942 }
943 };
944
945 Y.io(ajaxurl, config);
946
947 },
948
949 /**
950 * Event handler to open the comment search interface.
951 *
952 * @param Event e
953 * @protected
954 * @method open_search_comments
955 */
956 open_search_comments : function(e) {
957 if (!this.searchcommentswindow) {
958 this.searchcommentswindow = new M.assignfeedback_editpdf.commentsearch({
959 editor : this
960 });
961 }
962
963 this.searchcommentswindow.show();
964 e.preventDefault();
965 },
966
967 /**
968 * Redraw all the comments and annotations.
969 * @protected
970 * @method redraw
971 */
972 redraw : function() {
973 var i,
974 page;
975
976 page = this.pages[this.currentpage];
50c12f01
DS
977 if (page === undefined) {
978 return; // Can happen if a redraw is triggered by an event, before the page has been selected.
979 }
5c386472
DW
980 while (this.drawables.length > 0) {
981 this.drawables.pop().erase();
982 }
983
984 for (i = 0; i < page.annotations.length; i++) {
985 this.drawables.push(page.annotations[i].draw());
986 }
987 for (i = 0; i < page.comments.length; i++) {
988 this.drawables.push(page.comments[i].draw(false));
989 }
990 },
991
992 /**
993 * Load the image for this pdf page and remove the loading icon (if there).
994 * @protected
995 * @method change_page
996 */
997 change_page : function() {
da91de78 998 var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
999 page,
1000 previousbutton,
1001 nextbutton;
1002
da91de78
BL
1003 previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1004 nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
5c386472
DW
1005
1006 if (this.currentpage > 0) {
1007 previousbutton.removeAttribute('disabled');
1008 } else {
1009 previousbutton.setAttribute('disabled', 'true');
1010 }
1011 if (this.currentpage < (this.pagecount - 1)) {
1012 nextbutton.removeAttribute('disabled');
1013 } else {
1014 nextbutton.setAttribute('disabled', 'true');
1015 }
1016
1017 page = this.pages[this.currentpage];
1018 this.loadingicon.hide();
1019 drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
50c12f01
DS
1020 drawingcanvas.setStyle('width', page.width + 'px');
1021 drawingcanvas.setStyle('height', page.height + 'px');
5c386472 1022
ea6d8674 1023 // Update page select.
da91de78 1024 this.get_dialogue_element(SELECTOR.PAGESELECT).set('value', this.currentpage);
ea6d8674 1025
50c12f01 1026 this.resize(); // Internally will call 'redraw', after checking the dialogue size.
5c386472
DW
1027 },
1028
1029 /**
1030 * Now we know how many pages there are,
1031 * we can enable the navigation controls.
1032 * @protected
1033 * @method setup_navigation
1034 */
1035 setup_navigation : function() {
1036 var pageselect,
1037 i,
0d5a260e 1038 strinfo,
5c386472
DW
1039 option,
1040 previousbutton,
1041 nextbutton;
1042
da91de78 1043 pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
5c386472 1044
557f44d9 1045 var options = pageselect.all('option');
5c386472
DW
1046 if (options.size() <= 1) {
1047 for (i = 0; i < this.pages.length; i++) {
1048 option = Y.Node.create('<option/>');
1049 option.setAttribute('value', i);
0d5a260e
DS
1050 strinfo = {page: i+1, total: this.pages.length};
1051 option.setHTML(M.util.get_string('pagexofy', 'assignfeedback_editpdf', strinfo));
5c386472
DW
1052 pageselect.append(option);
1053 }
1054 }
1055 pageselect.removeAttribute('disabled');
1056 pageselect.on('change', function() {
1057 this.currentpage = pageselect.get('value');
1058 this.change_page();
1059 }, this);
1060
da91de78
BL
1061 previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1062 nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
5c386472
DW
1063
1064 previousbutton.on('click', this.previous_page, this);
1065 previousbutton.on('key', this.previous_page, 'down:13', this);
1066 nextbutton.on('click', this.next_page, this);
1067 nextbutton.on('key', this.next_page, 'down:13', this);
1068 },
1069
1070 /**
1071 * Navigate to the previous page.
1072 * @protected
1073 * @method previous_page
1074 */
114913e3
DS
1075 previous_page : function(e) {
1076 e.preventDefault();
5c386472
DW
1077 this.currentpage--;
1078 if (this.currentpage < 0) {
1079 this.currentpage = 0;
1080 }
1081 this.change_page();
1082 },
1083
1084 /**
1085 * Navigate to the next page.
1086 * @protected
1087 * @method next_page
1088 */
114913e3
DS
1089 next_page : function(e) {
1090 e.preventDefault();
5c386472
DW
1091 this.currentpage++;
1092 if (this.currentpage >= this.pages.length) {
1093 this.currentpage = this.pages.length - 1;
1094 }
1095 this.change_page();
50c12f01
DS
1096 },
1097
1098 /**
1099 * Update any absolutely positioned nodes, within each drawable, when the drawing canvas is scrolled
1100 * @protected
1101 * @method move_canvas
1102 */
1103 move_canvas: function() {
1104 var drawingregion, x, y, i;
5c386472 1105
da91de78 1106 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
50c12f01
DS
1107 x = parseInt(drawingregion.get('scrollLeft'), 10);
1108 y = parseInt(drawingregion.get('scrollTop'), 10);
5c386472 1109
50c12f01
DS
1110 for (i = 0; i < this.drawables.length; i++) {
1111 this.drawables[i].scroll_update(x, y);
1112 }
1113 }
5c386472
DW
1114
1115};
1116
1117Y.extend(EDITOR, Y.Base, EDITOR.prototype, {
1118 NAME : 'moodle-assignfeedback_editpdf-editor',
1119 ATTRS : {
1120 userid : {
1121 validator : Y.Lang.isInteger,
1122 value : 0
1123 },
1124 assignmentid : {
1125 validator : Y.Lang.isInteger,
1126 value : 0
1127 },
1128 attemptnumber : {
1129 validator : Y.Lang.isInteger,
1130 value : 0
1131 },
1132 header : {
1133 validator : Y.Lang.isString,
1134 value : ''
1135 },
1136 body : {
1137 validator : Y.Lang.isString,
1138 value : ''
1139 },
1140 footer : {
1141 validator : Y.Lang.isString,
1142 value : ''
1143 },
1144 linkid : {
1145 validator : Y.Lang.isString,
1146 value : ''
1147 },
1148 deletelinkid : {
1149 validator : Y.Lang.isString,
1150 value : ''
1151 },
1152 readonly : {
1153 validator : Y.Lang.isBoolean,
1154 value : true
1155 },
1156 stampfiles : {
1157 validator : Y.Lang.isArray,
1158 value : ''
aa3e4bde
JM
1159 },
1160 pagetotal : {
1161 validator : Y.Lang.isInteger,
1162 value : 0
5c386472
DW
1163 }
1164 }
1165});
1166
5c386472 1167M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
5c386472
DW
1168M.assignfeedback_editpdf.editor = M.assignfeedback_editpdf.editor || {};
1169
1170/**
1171 * Init function - will create a new instance every time.
1f777e5c 1172 * @method editor.init
5c386472
DW
1173 * @static
1174 * @param {Object} params
1175 */
1176M.assignfeedback_editpdf.editor.init = M.assignfeedback_editpdf.editor.init || function(params) {
1177 return new EDITOR(params);
1178};