MDL-61537 assignfeedback_editpdf: Rotate PDF page
[moodle.git] / mod / assign / feedback / editpdf / yui / src / editor / js / editor.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15 /* eslint-disable no-unused-vars */
16 /* global SELECTOR, TOOLSELECTOR, AJAXBASE, COMMENTCOLOUR, ANNOTATIONCOLOUR, AJAXBASEPROGRESS, CLICKTIMEOUT */
18 /**
19  * Provides an in browser PDF editor.
20  *
21  * @module moodle-assignfeedback_editpdf-editor
22  */
24 /**
25  * EDITOR
26  * This is an in browser PDF editor.
27  *
28  * @namespace M.assignfeedback_editpdf
29  * @class editor
30  * @constructor
31  * @extends Y.Base
32  */
33 var EDITOR = function() {
34     EDITOR.superclass.constructor.apply(this, arguments);
35 };
36 EDITOR.prototype = {
38     /**
39      * Store old coordinates of the annotations before rotation happens.
40      */
41     oldannotationcoordinates: null,
43     /**
44      * The dialogue used for all action menu displays.
45      *
46      * @property type
47      * @type M.core.dialogue
48      * @protected
49      */
50     dialogue: null,
52     /**
53      * The panel used for all action menu displays.
54      *
55      * @property type
56      * @type Y.Node
57      * @protected
58      */
59     panel: null,
61     /**
62      * The number of pages in the pdf.
63      *
64      * @property pagecount
65      * @type Number
66      * @protected
67      */
68     pagecount: 0,
70     /**
71      * The active page in the editor.
72      *
73      * @property currentpage
74      * @type Number
75      * @protected
76      */
77     currentpage: 0,
79     /**
80      * A list of page objects. Each page has a list of comments and annotations.
81      *
82      * @property pages
83      * @type array
84      * @protected
85      */
86     pages: [],
88     /**
89      * The reported status of the document.
90      *
91      * @property documentstatus
92      * @type int
93      * @protected
94      */
95     documentstatus: 0,
97     /**
98      * The yui node for the loading icon.
99      *
100      * @property loadingicon
101      * @type Node
102      * @protected
103      */
104     loadingicon: null,
106     /**
107      * Image object of the current page image.
108      *
109      * @property pageimage
110      * @type Image
111      * @protected
112      */
113     pageimage: null,
115     /**
116      * YUI Graphic class for drawing shapes.
117      *
118      * @property graphic
119      * @type Graphic
120      * @protected
121      */
122     graphic: null,
124     /**
125      * Info about the current edit operation.
126      *
127      * @property currentedit
128      * @type M.assignfeedback_editpdf.edit
129      * @protected
130      */
131     currentedit: new M.assignfeedback_editpdf.edit(),
133     /**
134      * Current drawable.
135      *
136      * @property currentdrawable
137      * @type M.assignfeedback_editpdf.drawable|false
138      * @protected
139      */
140     currentdrawable: false,
142     /**
143      * Current drawables.
144      *
145      * @property drawables
146      * @type array(M.assignfeedback_editpdf.drawable)
147      * @protected
148      */
149     drawables: [],
151     /**
152      * Current comment when the comment menu is open.
153      * @property currentcomment
154      * @type M.assignfeedback_editpdf.comment
155      * @protected
156      */
157     currentcomment: null,
159     /**
160      * Current annotation when the select tool is used.
161      * @property currentannotation
162      * @type M.assignfeedback_editpdf.annotation
163      * @protected
164      */
165     currentannotation: null,
167     /**
168      * Track the previous annotation so we can remove selection highlights.
169      * @property lastannotation
170      * @type M.assignfeedback_editpdf.annotation
171      * @protected
172      */
173     lastannotation: null,
175     /**
176      * Last selected annotation tool
177      * @property lastannotationtool
178      * @type String
179      * @protected
180      */
181     lastannotationtool: "pen",
183     /**
184      * The users comments quick list
185      * @property quicklist
186      * @type M.assignfeedback_editpdf.quickcommentlist
187      * @protected
188      */
189     quicklist: null,
191     /**
192      * The search comments window.
193      * @property searchcommentswindow
194      * @type M.core.dialogue
195      * @protected
196      */
197     searchcommentswindow: null,
200     /**
201      * The selected stamp picture.
202      * @property currentstamp
203      * @type String
204      * @protected
205      */
206     currentstamp: null,
208     /**
209      * The stamps.
210      * @property stamps
211      * @type Array
212      * @protected
213      */
214     stamps: [],
216     /**
217      * Prevent new comments from appearing
218      * immediately after clicking off a current
219      * comment
220      * @property editingcomment
221      * @type Boolean
222      * @public
223      */
224     editingcomment: false,
226     /**
227      * Should inactive comments be collapsed?
228      *
229      * @property collapsecomments
230      * @type Boolean
231      * @public
232      */
233     collapsecomments: true,
235     /**
236      * Called during the initialisation process of the object.
237      * @method initializer
238      */
239     initializer: function() {
240         var link;
242         link = Y.one('#' + this.get('linkid'));
244         if (link) {
245             link.on('click', this.link_handler, this);
246             link.on('key', this.link_handler, 'down:13', this);
248             // We call the amd module to see if we can take control of the review panel.
249             require(['mod_assign/grading_review_panel'], function(ReviewPanelManager) {
250                 var panelManager = new ReviewPanelManager();
252                 var panel = panelManager.getReviewPanel('assignfeedback_editpdf');
253                 if (panel) {
254                     panel = Y.one(panel);
255                     panel.empty();
256                     link.ancestor('.fitem').hide();
257                     this.open_in_panel(panel);
258                 }
259                 this.currentedit.start = false;
260                 this.currentedit.end = false;
261                 if (!this.get('readonly')) {
262                     this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this);
263                 }
264             }.bind(this));
266         }
267     },
269     /**
270      * Called to show/hide buttons and set the current colours/stamps.
271      * @method refresh_button_state
272      */
273     refresh_button_state: function() {
274         var button, currenttoolnode, imgurl, drawingregion, stampimgurl, drawingcanvas;
276         // Initalise the colour buttons.
277         button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
279         imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf');
280         button.one('img').setAttribute('src', imgurl);
282         if (this.currentedit.commentcolour === 'clear') {
283             button.one('img').setStyle('borderStyle', 'dashed');
284         } else {
285             button.one('img').setStyle('borderStyle', 'solid');
286         }
288         button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
289         imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf');
290         button.one('img').setAttribute('src', imgurl);
292         currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
293         currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton');
294         currenttoolnode.setAttribute('aria-pressed', 'true');
295         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
296         drawingregion.setAttribute('data-currenttool', this.currentedit.tool);
298         button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
299         stampimgurl = this.get_stamp_image_url(this.currentedit.stamp);
300         button.one('img').setAttrs({'src': stampimgurl,
301                                     'height': '16',
302                                     'width': '16'});
304         drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
305         switch (this.currentedit.tool) {
306             case 'drag':
307                 drawingcanvas.setStyle('cursor', 'move');
308                 break;
309             case 'highlight':
310                 drawingcanvas.setStyle('cursor', 'text');
311                 break;
312             case 'select':
313                 drawingcanvas.setStyle('cursor', 'default');
314                 break;
315             case 'stamp':
316                 drawingcanvas.setStyle('cursor', 'url(' + stampimgurl + '), crosshair');
317                 break;
318             default:
319                 drawingcanvas.setStyle('cursor', 'crosshair');
320         }
321     },
323     /**
324      * Called to get the bounds of the drawing region.
325      * @method get_canvas_bounds
326      */
327     get_canvas_bounds: function() {
328         var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
329             offsetcanvas = canvas.getXY(),
330             offsetleft = offsetcanvas[0],
331             offsettop = offsetcanvas[1],
332             width = parseInt(canvas.getStyle('width'), 10),
333             height = parseInt(canvas.getStyle('height'), 10);
335         return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height);
336     },
338     /**
339      * Called to translate from window coordinates to canvas coordinates.
340      * @method get_canvas_coordinates
341      * @param M.assignfeedback_editpdf.point point in window coordinats.
342      */
343     get_canvas_coordinates: function(point) {
344         var bounds = this.get_canvas_bounds(),
345             newpoint = new M.assignfeedback_editpdf.point(point.x - bounds.x, point.y - bounds.y);
347         bounds.x = bounds.y = 0;
349         newpoint.clip(bounds);
350         return newpoint;
351     },
353     /**
354      * Called to translate from canvas coordinates to window coordinates.
355      * @method get_window_coordinates
356      * @param M.assignfeedback_editpdf.point point in window coordinats.
357      */
358     get_window_coordinates: function(point) {
359         var bounds = this.get_canvas_bounds(),
360             newpoint = new M.assignfeedback_editpdf.point(point.x + bounds.x, point.y + bounds.y);
362         return newpoint;
363     },
365     /**
366      * Open the edit-pdf editor in the panel in the page instead of a popup.
367      * @method open_in_panel
368      */
369     open_in_panel: function(panel) {
370         var drawingcanvas, drawingregion;
372         this.panel = panel;
373         panel.append(this.get('body'));
374         panel.addClass(CSS.DIALOGUE);
376         this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
378         drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
379         this.graphic = new Y.Graphic({render: drawingcanvas});
381         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
382         drawingregion.on('scroll', this.move_canvas, this);
384         if (!this.get('readonly')) {
385             drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
386             drawingcanvas.on('gesturemove', this.edit_move, null, this);
387             drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
389             this.refresh_button_state();
390         }
392         this.start_generation();
393     },
395     /**
396      * Called to open the pdf editing dialogue.
397      * @method link_handler
398      */
399     link_handler: function(e) {
400         var drawingcanvas, drawingregion;
401         var resize = true;
402         e.preventDefault();
404         if (!this.dialogue) {
405             this.dialogue = new M.core.dialogue({
406                 headerContent: this.get('header'),
407                 bodyContent: this.get('body'),
408                 footerContent: this.get('footer'),
409                 modal: true,
410                 width: '840px',
411                 visible: false,
412                 draggable: true
413             });
415             // Add custom class for styling.
416             this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE);
418             this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
420             drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
421             this.graphic = new Y.Graphic({render: drawingcanvas});
423             drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
424             drawingregion.on('scroll', this.move_canvas, this);
426             if (!this.get('readonly')) {
427                 drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
428                 drawingcanvas.on('gesturemove', this.edit_move, null, this);
429                 drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
431                 this.refresh_button_state();
432             }
434             this.start_generation();
435             drawingcanvas.on('windowresize', this.resize, this);
437             resize = false;
438         }
439         this.dialogue.centerDialogue();
440         this.dialogue.show();
442         // Redraw when the dialogue is moved, to ensure the absolute elements are all positioned correctly.
443         this.dialogue.dd.on('drag:end', this.redraw, this);
444         if (resize) {
445             this.resize(); // When re-opening the dialog call redraw, to make sure the size + layout is correct.
446         }
447     },
449     /**
450      * Called to load the information and annotations for all pages.
451      *
452      * @method start_generation
453      */
454     start_generation: function() {
455         this.poll_document_conversion_status();
456     },
458     /**
459      * Poll the current document conversion status and start the next step
460      * in the process.
461      *
462      * @method poll_document_conversion_status
463      */
464     poll_document_conversion_status: function() {
465         var requestUserId = this.get('userid');
467         Y.io(AJAXBASE, {
468             method: 'get',
469             context: this,
470             sync: false,
471             data: {
472                 sesskey: M.cfg.sesskey,
473                 action: 'pollconversions',
474                 userid: this.get('userid'),
475                 attemptnumber: this.get('attemptnumber'),
476                 assignmentid: this.get('assignmentid'),
477                 readonly: this.get('readonly') ? 1 : 0
478             },
479             on: {
480                 success: function(tid, response) {
481                     var currentUserRegion = Y.one(SELECTOR.USERINFOREGION);
482                     if (currentUserRegion) {
483                         var currentUserId = currentUserRegion.getAttribute('data-userid');
484                         if (currentUserId && (currentUserId != requestUserId)) {
485                             // Polling conversion status needs to abort because
486                             // the current user changed.
487                             return;
488                         }
489                     }
490                     var data = this.handle_response_data(response),
491                         poll = false;
492                     if (data) {
493                         this.documentstatus = data.status;
494                         if (data.status === 0) {
495                             // The combined document is still waiting for input to be ready.
496                             poll = true;
498                         } else if (data.status === 1 || data.status === 3) {
499                             // The combine document is ready for conversion into a single PDF.
500                             poll = true;
502                         } else if (data.status === 2 || data.status === -1) {
503                             // The combined PDF is ready.
504                             // We now know the page count and can convert it to a set of images.
505                             this.pagecount = data.pagecount;
507                             if (data.pageready == data.pagecount) {
508                                 this.prepare_pages_for_display(data);
509                             } else {
510                                 // Some pages are not ready yet.
511                                 // Note: We use a different polling process here which does not block.
512                                 this.update_page_load_progress();
514                                 // Fetch the images for the combined document.
515                                 this.start_document_to_image_conversion();
516                             }
517                         }
519                         if (poll) {
520                             // Check again in 1 second.
521                             Y.later(1000, this, this.poll_document_conversion_status);
522                         }
523                     }
524                 },
525                 failure: function(tid, response) {
526                     return new M.core.exception(response.responseText);
527                 }
528             }
529         });
530     },
532     /**
533      * Spwan the PDF to Image conversion on the server.
534      *
535      * @method get_images_for_documents
536      */
537     start_document_to_image_conversion: function() {
538         Y.io(AJAXBASE, {
539             method: 'get',
540             context: this,
541             sync: false,
542             data: {
543                 sesskey: M.cfg.sesskey,
544                 action: 'pollconversions',
545                 userid: this.get('userid'),
546                 attemptnumber: this.get('attemptnumber'),
547                 assignmentid: this.get('assignmentid'),
548                 readonly: this.get('readonly') ? 1 : 0
549             },
550             on: {
551                 success: function(tid, response) {
552                     var data = this.handle_response_data(response);
553                     if (data) {
554                         this.documentstatus = data.status;
555                         if (data.status === 2) {
556                             // The pages are ready. Add all of the annotations to them.
557                             this.prepare_pages_for_display(data);
558                         }
559                     }
560                 },
561                 failure: function(tid, response) {
562                     return new M.core.exception(response.responseText);
563                 }
564             }
565         });
566     },
568     /**
569      * Display an error in a small part of the page (don't block everything).
570      *
571      * @param string The error text.
572      * @param boolean dismissable Not critical messages can be removed after a short display.
573      * @protected
574      * @method warning
575      */
576     warning: function(message, dismissable) {
577         var icontemplate = this.get_dialogue_element(SELECTOR.ICONMESSAGECONTAINER);
578         var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER);
579         var delay = 15, duration = 1;
580         var messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-warning';
581         if (dismissable) {
582             delay = 4;
583             messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-info';
584         }
585         var warningelement = Y.Node.create('<div class="' + messageclasses + '" role="alert"></div>');
587         // Copy info icon template.
588         warningelement.append(icontemplate.one('*').cloneNode());
590         // Append the message.
591         warningelement.append(message);
593         // Add the entire warning to the container.
594         warningregion.prepend(warningelement);
596         // Remove the message after a short delay.
597         warningelement.transition(
598             {
599                 duration: duration,
600                 delay: delay,
601                 opacity: 0
602             },
603             function() {
604                 warningelement.remove();
605             }
606         );
607     },
609     /**
610      * The info about all pages in the pdf has been returned.
611      *
612      * @param string The ajax response as text.
613      * @protected
614      * @method prepare_pages_for_display
615      */
616     prepare_pages_for_display: function(data) {
617         var i, j, comment, error, annotation, readonly;
619         if (!data.pagecount) {
620             if (this.dialogue) {
621                 this.dialogue.hide();
622             }
623             // Display alert dialogue.
624             error = new M.core.alert({message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')});
625             error.show();
626             return;
627         }
629         this.pages = data.pages;
631         for (i = 0; i < this.pages.length; i++) {
632             for (j = 0; j < this.pages[i].comments.length; j++) {
633                 comment = this.pages[i].comments[j];
634                 this.pages[i].comments[j] = new M.assignfeedback_editpdf.comment(this,
635                                                                                  comment.gradeid,
636                                                                                  comment.pageno,
637                                                                                  comment.x,
638                                                                                  comment.y,
639                                                                                  comment.width,
640                                                                                  comment.colour,
641                                                                                  comment.rawtext);
642             }
643             for (j = 0; j < this.pages[i].annotations.length; j++) {
644                 annotation = this.pages[i].annotations[j];
645                 this.pages[i].annotations[j] = this.create_annotation(annotation.type, annotation);
646             }
647         }
649         readonly = this.get('readonly');
650         if (!readonly && data.partial) {
651             // Warn about non converted files, but only for teachers.
652             this.warning(M.util.get_string('partialwarning', 'assignfeedback_editpdf'), false);
653         }
655         // Update the ui.
656         if (this.quicklist) {
657             this.quicklist.load();
658         }
659         this.setup_navigation();
660         this.setup_toolbar();
661         this.change_page();
662     },
664     /**
665      * Fetch the page images.
666      *
667      * @method update_page_load_progress
668      */
669     update_page_load_progress: function() {
670         var checkconversionstatus,
671             ajax_error_total = 0,
672             progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar');
674         if (!progressbar) {
675             return;
676         }
678         // If pages are not loaded, check PDF conversion status for the progress bar.
679         checkconversionstatus = {
680             method: 'get',
681             context: this,
682             sync: false,
683             data: {
684                 sesskey: M.cfg.sesskey,
685                 action: 'conversionstatus',
686                 userid: this.get('userid'),
687                 attemptnumber: this.get('attemptnumber'),
688                 assignmentid: this.get('assignmentid')
689             },
690             on: {
691                 success: function(tid, response) {
692                     ajax_error_total = 0;
694                     var progress = 0;
695                     var progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar');
696                     if (progressbar) {
697                         // Calculate progress.
698                         progress = (response.response / this.pagecount) * 100;
699                         progressbar.setStyle('width', progress + '%');
700                         progressbar.ancestor(SELECTOR.PROGRESSBARCONTAINER).setAttribute('aria-valuenow', progress);
702                         if (progress < 100) {
703                             // Keep polling until all pages are generated.
704                             M.util.js_pending('checkconversionstatus');
705                             Y.later(1000, this, function() {
706                                 M.util.js_complete('checkconversionstatus');
707                                 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
708                             });
709                         }
710                     }
711                 },
712                 failure: function(tid, response) {
713                     ajax_error_total = ajax_error_total + 1;
714                     // We only continue on error if the all pages were not generated,
715                     // and if the ajax call did not produce 5 errors in the row.
716                     if (this.pagecount === 0 && ajax_error_total < 5) {
717                         M.util.js_pending('checkconversionstatus');
718                         Y.later(1000, this, function() {
719                             M.util.js_complete('checkconversionstatus');
720                             Y.io(AJAXBASEPROGRESS, checkconversionstatus);
721                         });
722                     }
723                     return new M.core.exception(response.responseText);
724                 }
725             }
726         };
727         // We start the AJAX "generated page total number" call a second later to give a chance to
728         // the AJAX "combined pdf generation" call to clean the previous submission images.
729         M.util.js_pending('checkconversionstatus');
730         Y.later(1000, this, function() {
731             ajax_error_total = 0;
732             M.util.js_complete('checkconversionstatus');
733             Y.io(AJAXBASEPROGRESS, checkconversionstatus);
734         });
735     },
737     /**
738      * Handle response data.
739      *
740      * @method  handle_response_data
741      * @param   {object} response
742      * @return  {object}
743      */
744     handle_response_data: function(response) {
745         var data;
746         try {
747             data = Y.JSON.parse(response.responseText);
748             if (data.error) {
749                 if (this.dialogue) {
750                     this.dialogue.hide();
751                 }
753                 new M.core.alert({
754                     message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'),
755                     visible: true
756                 });
757             } else {
758                 return data;
759             }
760         } catch (e) {
761             if (this.dialogue) {
762                 this.dialogue.hide();
763             }
765             new M.core.alert({
766                 title: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'),
767                 visible: true
768             });
769         }
771         return;
772     },
774     /**
775      * Get the full pluginfile url for an image file - just given the filename.
776      *
777      * @public
778      * @method get_stamp_image_url
779      * @param string filename
780      */
781     get_stamp_image_url: function(filename) {
782         var urls = this.get('stampfiles'),
783             fullurl = '';
785         Y.Array.each(urls, function(url) {
786             if (url.indexOf(filename) > 0) {
787                 fullurl = url;
788             }
789         }, this);
791         return fullurl;
792     },
794     /**
795      * Attach listeners and enable the color picker buttons.
796      * @protected
797      * @method setup_toolbar
798      */
799     setup_toolbar: function() {
800         var toolnode,
801             commentcolourbutton,
802             annotationcolourbutton,
803             searchcommentsbutton,
804             expcolcommentsbutton,
805             rotateleftbutton,
806             rotaterightbutton,
807             currentstampbutton,
808             stampfiles,
809             picker,
810             filename;
812         searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON);
813         searchcommentsbutton.on('click', this.open_search_comments, this);
814         searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this);
816         expcolcommentsbutton = this.get_dialogue_element(SELECTOR.EXPCOLCOMMENTSBUTTON);
817         expcolcommentsbutton.on('click', this.expandCollapseComments, this);
818         expcolcommentsbutton.on('key', this.expandCollapseComments, 'down:13', this);
820         if (this.get('readonly')) {
821             return;
822         }
824         // Rotate Left.
825         rotateleftbutton = this.get_dialogue_element(SELECTOR.ROTATELEFTBUTTON);
826         rotateleftbutton.on('click', this.rotatePDF, this, true);
827         rotateleftbutton.on('key', this.rotatePDF, 'down:13', this, true);
829         // Rotate Right.
830         rotaterightbutton = this.get_dialogue_element(SELECTOR.ROTATERIGHTBUTTON);
831         rotaterightbutton.on('click', this.rotatePDF, this, false);
832         rotaterightbutton.on('key', this.rotatePDF, 'down:13', this, false);
834         this.disable_touch_scroll();
836         // Setup the tool buttons.
837         Y.each(TOOLSELECTOR, function(selector, tool) {
838             toolnode = this.get_dialogue_element(selector);
839             toolnode.on('click', this.handle_tool_button, this, tool);
840             toolnode.on('key', this.handle_tool_button, 'down:13', this, tool);
841             toolnode.setAttribute('aria-pressed', 'false');
842         }, this);
844         // Set the default tool.
846         commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
847         picker = new M.assignfeedback_editpdf.colourpicker({
848             buttonNode: commentcolourbutton,
849             colours: COMMENTCOLOUR,
850             iconprefix: 'background_colour_',
851             callback: function(e) {
852                 var colour = e.target.getAttribute('data-colour');
853                 if (!colour) {
854                     colour = e.target.ancestor().getAttribute('data-colour');
855                 }
856                 this.currentedit.commentcolour = colour;
857                 this.handle_tool_button(e, "comment");
858             },
859             context: this
860         });
862         annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
863         picker = new M.assignfeedback_editpdf.colourpicker({
864             buttonNode: annotationcolourbutton,
865             iconprefix: 'colour_',
866             colours: ANNOTATIONCOLOUR,
867             callback: function(e) {
868                 var colour = e.target.getAttribute('data-colour');
869                 if (!colour) {
870                     colour = e.target.ancestor().getAttribute('data-colour');
871                 }
872                 this.currentedit.annotationcolour = colour;
873                 if (this.lastannotationtool) {
874                     this.handle_tool_button(e, this.lastannotationtool);
875                 } else {
876                     this.handle_tool_button(e, "pen");
877                 }
878             },
879             context: this
880         });
882         stampfiles = this.get('stampfiles');
883         if (stampfiles.length <= 0) {
884             this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide();
885         } else {
886             filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1);
887             this.currentedit.stamp = filename;
888             currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
890             picker = new M.assignfeedback_editpdf.stamppicker({
891                 buttonNode: currentstampbutton,
892                 stamps: stampfiles,
893                 callback: function(e) {
894                     var stamp = e.target.getAttribute('data-stamp'),
895                         filename;
897                     if (!stamp) {
898                         stamp = e.target.ancestor().getAttribute('data-stamp');
899                     }
900                     filename = stamp.substr(stamp.lastIndexOf('/'));
901                     this.currentedit.stamp = filename;
902                     this.handle_tool_button(e, "stamp");
903                 },
904                 context: this
905             });
906             this.refresh_button_state();
907         }
908     },
910     /**
911      * Change the current tool.
912      * @protected
913      * @method handle_tool_button
914      */
915     handle_tool_button: function(e, tool) {
916         var currenttoolnode;
918         e.preventDefault();
920         // Change style of the pressed button.
921         currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
922         currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton');
923         currenttoolnode.setAttribute('aria-pressed', 'false');
924         this.currentedit.tool = tool;
926         if (tool !== "comment" && tool !== "select" && tool !== "drag" && tool !== "stamp") {
927             this.lastannotationtool = tool;
928         }
930         this.refresh_button_state();
931     },
933     /**
934      * JSON encode the current page data - stripping out drawable references which cannot be encoded.
935      * @protected
936      * @method stringify_current_page
937      * @return string
938      */
939     stringify_current_page: function() {
940         var comments = [],
941             annotations = [],
942             page,
943             i = 0;
945         for (i = 0; i < this.pages[this.currentpage].comments.length; i++) {
946             comments[i] = this.pages[this.currentpage].comments[i].clean();
947         }
948         for (i = 0; i < this.pages[this.currentpage].annotations.length; i++) {
949             annotations[i] = this.pages[this.currentpage].annotations[i].clean();
950         }
952         page = {comments: comments, annotations: annotations};
954         return Y.JSON.stringify(page);
955     },
957     /**
958      * Generate a drawable from the current in progress edit.
959      * @protected
960      * @method get_current_drawable
961      */
962     get_current_drawable: function() {
963         var comment,
964             annotation,
965             drawable = false;
967         if (!this.currentedit.start || !this.currentedit.end) {
968             return false;
969         }
971         if (this.currentedit.tool === 'comment') {
972             comment = new M.assignfeedback_editpdf.comment(this);
973             drawable = comment.draw_current_edit(this.currentedit);
974         } else {
975             annotation = this.create_annotation(this.currentedit.tool, {});
976             if (annotation) {
977                 drawable = annotation.draw_current_edit(this.currentedit);
978             }
979         }
981         return drawable;
982     },
984     /**
985      * Find an element within the dialogue.
986      * @protected
987      * @method get_dialogue_element
988      */
989     get_dialogue_element: function(selector) {
990         if (this.panel) {
991             return this.panel.one(selector);
992         } else {
993             return this.dialogue.get('boundingBox').one(selector);
994         }
995     },
997     /**
998      * Redraw the active edit.
999      * @protected
1000      * @method redraw_active_edit
1001      */
1002     redraw_current_edit: function() {
1003         if (this.currentdrawable) {
1004             this.currentdrawable.erase();
1005         }
1006         this.currentdrawable = this.get_current_drawable();
1007     },
1009     /**
1010      * Event handler for mousedown or touchstart.
1011      * @protected
1012      * @param Event
1013      * @method edit_start
1014      */
1015     edit_start: function(e) {
1016         var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
1017             offset = canvas.getXY(),
1018             scrolltop = canvas.get('docScrollY'),
1019             scrollleft = canvas.get('docScrollX'),
1020             point = {x: e.clientX - offset[0] + scrollleft,
1021                      y: e.clientY - offset[1] + scrolltop},
1022             selected = false;
1024         // Ignore right mouse click.
1025         if (e.button === 3) {
1026             return;
1027         }
1029         if (this.currentedit.starttime) {
1030             return;
1031         }
1033         if (this.editingcomment) {
1034             return;
1035         }
1037         this.currentedit.starttime = new Date().getTime();
1038         this.currentedit.start = point;
1039         this.currentedit.end = {x: point.x, y: point.y};
1041         if (this.currentedit.tool === 'select') {
1042             var x = this.currentedit.end.x,
1043                 y = this.currentedit.end.y,
1044                 annotations = this.pages[this.currentpage].annotations;
1045             // Find the first annotation whose bounds encompass the click.
1046             Y.each(annotations, function(annotation) {
1047                 if (((x - annotation.x) * (x - annotation.endx)) <= 0 &&
1048                     ((y - annotation.y) * (y - annotation.endy)) <= 0) {
1049                     selected = annotation;
1050                 }
1051             });
1053             if (selected) {
1054                 this.lastannotation = this.currentannotation;
1055                 this.currentannotation = selected;
1056                 if (this.lastannotation && this.lastannotation !== selected) {
1057                     // Redraw the last selected annotation to remove the highlight.
1058                     if (this.lastannotation.drawable) {
1059                         this.lastannotation.drawable.erase();
1060                         this.drawables.push(this.lastannotation.draw());
1061                     }
1062                 }
1063                 // Redraw the newly selected annotation to show the highlight.
1064                 if (this.currentannotation.drawable) {
1065                     this.currentannotation.drawable.erase();
1066                 }
1067                 this.drawables.push(this.currentannotation.draw());
1068             } else {
1069                 this.lastannotation = this.currentannotation;
1070                 this.currentannotation = null;
1072                 // Redraw the last selected annotation to remove the highlight.
1073                 if (this.lastannotation && this.lastannotation.drawable) {
1074                     this.lastannotation.drawable.erase();
1075                     this.drawables.push(this.lastannotation.draw());
1076                 }
1077             }
1078         }
1079         if (this.currentannotation) {
1080             // Used to calculate drag offset.
1081             this.currentedit.annotationstart = {x: this.currentannotation.x,
1082                                                  y: this.currentannotation.y};
1083         }
1084     },
1086     /**
1087      * Event handler for mousemove.
1088      * @protected
1089      * @param Event
1090      * @method edit_move
1091      */
1092     edit_move: function(e) {
1093         e.preventDefault();
1094         var bounds = this.get_canvas_bounds(),
1095             canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
1096             drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION),
1097             clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'),
1098                                                              e.clientY + canvas.get('docScrollY')),
1099             point = this.get_canvas_coordinates(clientpoint),
1100             diffX,
1101             diffY;
1103         // Ignore events out of the canvas area.
1104         if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) {
1105             return;
1106         }
1108         if (this.currentedit.tool === 'pen') {
1109             this.currentedit.path.push(point);
1110         }
1112         if (this.currentedit.tool === 'select') {
1113             if (this.currentannotation && this.currentedit) {
1114                 this.currentannotation.move(this.currentedit.annotationstart.x + point.x - this.currentedit.start.x,
1115                                              this.currentedit.annotationstart.y + point.y - this.currentedit.start.y);
1116             }
1117         } else if (this.currentedit.tool === 'drag') {
1118             diffX = point.x - this.currentedit.start.x;
1119             diffY = point.y - this.currentedit.start.y;
1121             drawingregion.getDOMNode().scrollLeft -= diffX;
1122             drawingregion.getDOMNode().scrollTop -= diffY;
1124         } else {
1125             if (this.currentedit.start) {
1126                 this.currentedit.end = point;
1127                 this.redraw_current_edit();
1128             }
1129         }
1130     },
1132     /**
1133      * Event handler for mouseup or touchend.
1134      * @protected
1135      * @param Event
1136      * @method edit_end
1137      */
1138     edit_end: function() {
1139         var duration,
1140             comment,
1141             annotation;
1143         duration = new Date().getTime() - this.currentedit.start;
1145         if (duration < CLICKTIMEOUT || this.currentedit.start === false) {
1146             return;
1147         }
1149         if (this.currentedit.tool === 'comment') {
1150             if (this.currentdrawable) {
1151                 this.currentdrawable.erase();
1152             }
1153             this.currentdrawable = false;
1154             comment = new M.assignfeedback_editpdf.comment(this);
1155             if (comment.init_from_edit(this.currentedit)) {
1156                 this.pages[this.currentpage].comments.push(comment);
1157                 this.drawables.push(comment.draw(true));
1158                 this.editingcomment = true;
1159             }
1160         } else {
1161             annotation = this.create_annotation(this.currentedit.tool, {});
1162             if (annotation) {
1163                 if (this.currentdrawable) {
1164                     this.currentdrawable.erase();
1165                 }
1166                 this.currentdrawable = false;
1167                 if (annotation.init_from_edit(this.currentedit)) {
1168                     this.pages[this.currentpage].annotations.push(annotation);
1169                     this.drawables.push(annotation.draw());
1170                 }
1171             }
1172         }
1174         // Save the changes.
1175         this.save_current_page();
1177         // Reset the current edit.
1178         this.currentedit.starttime = 0;
1179         this.currentedit.start = false;
1180         this.currentedit.end = false;
1181         this.currentedit.path = [];
1182     },
1184     /**
1185      * Resize the dialogue window when the browser is resized.
1186      * @public
1187      * @method resize
1188      */
1189     resize: function() {
1190         var drawingregion, drawregionheight;
1192         if (this.dialogue) {
1193             if (!this.dialogue.get('visible')) {
1194                 return;
1195             }
1196             this.dialogue.centerDialogue();
1197         }
1199         // Make sure the dialogue box is not bigger than the max height of the viewport.
1200         drawregionheight = Y.one('body').get('winHeight') - 120; // Space for toolbar + titlebar.
1201         if (drawregionheight < 100) {
1202             drawregionheight = 100;
1203         }
1204         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
1205         if (this.dialogue) {
1206             drawingregion.setStyle('maxHeight', drawregionheight + 'px');
1207         }
1208         this.redraw();
1209         return true;
1210     },
1212     /**
1213      * Factory method for creating annotations of the correct subclass.
1214      * @public
1215      * @method create_annotation
1216      */
1217     create_annotation: function(type, data) {
1218         data.type = type;
1219         data.editor = this;
1220         if (type === "line") {
1221             return new M.assignfeedback_editpdf.annotationline(data);
1222         } else if (type === "rectangle") {
1223             return new M.assignfeedback_editpdf.annotationrectangle(data);
1224         } else if (type === "oval") {
1225             return new M.assignfeedback_editpdf.annotationoval(data);
1226         } else if (type === "pen") {
1227             return new M.assignfeedback_editpdf.annotationpen(data);
1228         } else if (type === "highlight") {
1229             return new M.assignfeedback_editpdf.annotationhighlight(data);
1230         } else if (type === "stamp") {
1231             return new M.assignfeedback_editpdf.annotationstamp(data);
1232         }
1233         return false;
1234     },
1236     /**
1237      * Save all the annotations and comments for the current page.
1238      * @protected
1239      * @method save_current_page
1240      */
1241     save_current_page: function() {
1242         this.clear_warnings(false);
1243         var ajaxurl = AJAXBASE,
1244             config;
1246         config = {
1247             method: 'post',
1248             context: this,
1249             sync: false,
1250             data: {
1251                 'sesskey': M.cfg.sesskey,
1252                 'action': 'savepage',
1253                 'index': this.currentpage,
1254                 'userid': this.get('userid'),
1255                 'attemptnumber': this.get('attemptnumber'),
1256                 'assignmentid': this.get('assignmentid'),
1257                 'page': this.stringify_current_page()
1258             },
1259             on: {
1260                 success: function(tid, response) {
1261                     var jsondata;
1262                     try {
1263                         jsondata = Y.JSON.parse(response.responseText);
1264                         if (jsondata.error) {
1265                             return new M.core.ajaxException(jsondata);
1266                         }
1267                         // Show warning that we have not saved the feedback.
1268                         Y.one(SELECTOR.UNSAVEDCHANGESINPUT).set('value', 'true');
1269                         this.warning(M.util.get_string('draftchangessaved', 'assignfeedback_editpdf'), true);
1270                     } catch (e) {
1271                         return new M.core.exception(e);
1272                     }
1273                 },
1274                 failure: function(tid, response) {
1275                     return new M.core.exception(response.responseText);
1276                 }
1277             }
1278         };
1280         Y.io(ajaxurl, config);
1281     },
1283     /**
1284      * Event handler to open the comment search interface.
1285      *
1286      * @param Event e
1287      * @protected
1288      * @method open_search_comments
1289      */
1290     open_search_comments: function(e) {
1291         if (!this.searchcommentswindow) {
1292             this.searchcommentswindow = new M.assignfeedback_editpdf.commentsearch({
1293                 editor: this
1294             });
1295         }
1297         this.searchcommentswindow.show();
1298         e.preventDefault();
1299     },
1301     /**
1302      * Toggle function to expand/collapse all comments on page.
1303      *
1304      * @protected
1305      * @method expandCollapseComments
1306      */
1307     expandCollapseComments: function() {
1308         var comments = Y.all('.commentdrawable');
1310         if (this.collapsecomments) {
1311             this.collapsecomments = false;
1312             comments.removeClass('commentcollapsed');
1313         } else {
1314             this.collapsecomments = true;
1315             comments.addClass('commentcollapsed');
1316         }
1317     },
1319     /**
1320      * Redraw all the comments and annotations.
1321      * @protected
1322      * @method redraw
1323      */
1324     redraw: function() {
1325         var i,
1326             page;
1328         page = this.pages[this.currentpage];
1329         if (page === undefined) {
1330             return; // Can happen if a redraw is triggered by an event, before the page has been selected.
1331         }
1332         while (this.drawables.length > 0) {
1333             this.drawables.pop().erase();
1334         }
1336         for (i = 0; i < page.annotations.length; i++) {
1337             this.drawables.push(page.annotations[i].draw());
1338         }
1339         for (i = 0; i < page.comments.length; i++) {
1340             this.drawables.push(page.comments[i].draw(false));
1341         }
1342     },
1344     /**
1345      * Clear all current warning messages from display.
1346      * @protected
1347      * @method clear_warnings
1348      * @param {Boolean} allwarnings If true, all previous warnings are removed.
1349      */
1350     clear_warnings: function(allwarnings) {
1351         // Remove all warning messages, they may not relate to the current document or page anymore.
1352         var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER);
1353         if (allwarnings) {
1354             warningregion.empty();
1355         } else {
1356             warningregion.all('.alert-info').remove(true);
1357         }
1358     },
1360     /**
1361      * Load the image for this pdf page and remove the loading icon (if there).
1362      * @protected
1363      * @method change_page
1364      */
1365     change_page: function() {
1366         var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
1367             page,
1368             previousbutton,
1369             nextbutton;
1371         previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1372         nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
1374         if (this.currentpage > 0) {
1375             previousbutton.removeAttribute('disabled');
1376         } else {
1377             previousbutton.setAttribute('disabled', 'true');
1378         }
1379         if (this.currentpage < (this.pagecount - 1)) {
1380             nextbutton.removeAttribute('disabled');
1381         } else {
1382             nextbutton.setAttribute('disabled', 'true');
1383         }
1385         page = this.pages[this.currentpage];
1386         if (this.loadingicon) {
1387             this.loadingicon.hide();
1388         }
1389         drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
1390         drawingcanvas.setStyle('width', page.width + 'px');
1391         drawingcanvas.setStyle('height', page.height + 'px');
1392         drawingcanvas.scrollIntoView();
1394         // Update page select.
1395         this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
1397         this.resize(); // Internally will call 'redraw', after checking the dialogue size.
1398     },
1400     /**
1401      * Now we know how many pages there are,
1402      * we can enable the navigation controls.
1403      * @protected
1404      * @method setup_navigation
1405      */
1406     setup_navigation: function() {
1407         var pageselect,
1408             i,
1409             strinfo,
1410             option,
1411             previousbutton,
1412             nextbutton;
1414         pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
1416         var options = pageselect.all('option');
1417         if (options.size() <= 1) {
1418             for (i = 0; i < this.pages.length; i++) {
1419                 option = Y.Node.create('<option/>');
1420                 option.setAttribute('value', i);
1421                 strinfo = {page: i + 1, total: this.pages.length};
1422                 option.setHTML(M.util.get_string('pagexofy', 'assignfeedback_editpdf', strinfo));
1423                 pageselect.append(option);
1424             }
1425         }
1426         pageselect.removeAttribute('disabled');
1427         pageselect.on('change', function() {
1428             this.currentpage = pageselect.get('value');
1429             this.clear_warnings(false);
1430             this.change_page();
1431         }, this);
1433         previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1434         nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
1436         previousbutton.on('click', this.previous_page, this);
1437         previousbutton.on('key', this.previous_page, 'down:13', this);
1438         nextbutton.on('click', this.next_page, this);
1439         nextbutton.on('key', this.next_page, 'down:13', this);
1440     },
1442     /**
1443      * Navigate to the previous page.
1444      * @protected
1445      * @method previous_page
1446      */
1447     previous_page: function(e) {
1448         e.preventDefault();
1449         this.currentpage--;
1450         if (this.currentpage < 0) {
1451             this.currentpage = 0;
1452         }
1453         this.clear_warnings(false);
1454         this.change_page();
1455     },
1457     /**
1458      * Navigate to the next page.
1459      * @protected
1460      * @method next_page
1461      */
1462     next_page: function(e) {
1463         e.preventDefault();
1464         this.currentpage++;
1465         if (this.currentpage >= this.pages.length) {
1466             this.currentpage = this.pages.length - 1;
1467         }
1468         this.clear_warnings(false);
1469         this.change_page();
1470     },
1472     /**
1473      * Update any absolutely positioned nodes, within each drawable, when the drawing canvas is scrolled
1474      * @protected
1475      * @method move_canvas
1476      */
1477     move_canvas: function() {
1478         var drawingregion, x, y, i;
1480         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
1481         x = parseInt(drawingregion.get('scrollLeft'), 10);
1482         y = parseInt(drawingregion.get('scrollTop'), 10);
1484         for (i = 0; i < this.drawables.length; i++) {
1485             this.drawables[i].scroll_update(x, y);
1486         }
1487     },
1489     /**
1490      * Calculate degree to rotate.
1491      * @protected
1492      * @param {Object} e javascript event
1493      * @param {boolean} left  true if rotating left, false if rotating right
1494      * @method rotatepdf
1495      */
1496     rotatePDF: function(e, left) {
1497         e.preventDefault();
1499         if (this.get('destroyed')) {
1500             return;
1501         }
1502         var self = this;
1503         // Save old coordinates.
1504         var i;
1505         this.oldannotationcoordinates = [];
1506         var annotations = this.pages[this.currentpage].annotations;
1507         for (i = 0; i < annotations.length; i++) {
1508             var oldannotation = annotations[i];
1509             this.oldannotationcoordinates.push([oldannotation.x, oldannotation.y]);
1510         }
1512         var ajaxurl = AJAXBASE;
1513         var config = {
1514             method: 'post',
1515             context: this,
1516             sync: false,
1517             data: {
1518                 'sesskey': M.cfg.sesskey,
1519                 'action': 'rotatepage',
1520                 'index': this.currentpage,
1521                 'userid': this.get('userid'),
1522                 'attemptnumber': this.get('attemptnumber'),
1523                 'assignmentid': this.get('assignmentid'),
1524                 'rotateleft': left
1525             },
1526             on: {
1527                 success: function(tid, response) {
1528                     var jsondata;
1529                     try {
1530                         jsondata = Y.JSON.parse(response.responseText);
1531                         var page = self.pages[self.currentpage];
1532                         page.url = jsondata.page.url;
1533                         page.width = jsondata.page.width;
1534                         page.height = jsondata.page.height;
1535                         self.loadingicon.hide();
1537                         // Change canvas size to fix the new page.
1538                         var drawingcanvas = self.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
1539                         drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
1540                         drawingcanvas.setStyle('width', page.width + 'px');
1541                         drawingcanvas.setStyle('height', page.height + 'px');
1543                         /**
1544                          * Move annotation to old position.
1545                          * Reason: When canvas size change
1546                          * > Shape annotations move with relation to canvas coordinates
1547                          * > Nodes of stamp annotations move with relation to canvas coordinates
1548                          * > Presentation (picture) of stamp annotations  stay to document coordinates (stick to its own position)
1549                          * > Without relocating the node and presentation of a stamp annotation to the same x,y position,
1550                          * the stamp annotation cannot be chosen when using "drag" tool.
1551                          * The following code brings all annotations to their old positions with relation to the canvas coordinates.
1552                          */
1553                         var i;
1554                         // Annotations.
1555                         var annotations = page.annotations;
1556                         for (i = 0; i < annotations.length; i++) {
1557                             if (self.oldannotationcoordinates && self.oldannotationcoordinates[i]) {
1558                                 var oldX = self.oldannotationcoordinates[i][0];
1559                                 var oldY = self.oldannotationcoordinates[i][1];
1560                                 var annotation = annotations[i];
1561                                 annotation.move(oldX, oldY);
1562                             }
1563                         }
1564                         /**
1565                          * Update Position of comments with relation to canvas coordinates.
1566                          * Without this code, the comments will stay at their positions in windows/document coordinates.
1567                          */
1568                         var oldcomments = page.comments;
1569                         for (i = 0; i < oldcomments.length; i++) {
1570                             oldcomments[i].updatePosition();
1571                         }
1572                         // Save Annotations.
1573                         return self.save_current_page();
1574                     } catch (e) {
1575                         return new M.core.exception(e);
1576                     }
1577                 },
1578                 failure: function(tid, response) {
1579                     return new M.core.exception(response.responseText);
1580                 }
1581             }
1582         };
1583         Y.io(ajaxurl, config);
1584     },
1586     /**
1587      * Test the browser support for options objects on event listeners.
1588      * @return Boolean
1589      */
1590     event_listener_options_supported: function() {
1591         var passivesupported = false,
1592             options,
1593             testeventname = "testpassiveeventoptions";
1595         // Options support testing example from:
1596         // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
1598         try {
1599             options = Object.defineProperty({}, "passive", {
1600                 get: function() {
1601                     passivesupported = true;
1602                 }
1603             });
1605             // We use an event name that is not likely to conflict with any real event.
1606             document.addEventListener(testeventname, options, options);
1607             // We remove the event listener as we have tested the options already.
1608             document.removeEventListener(testeventname, options, options);
1609         } catch(err) {
1610             // It's already false.
1611             passivesupported = false;
1612         }
1613         return passivesupported;
1614     },
1616     /**
1617      * Disable Touch Move scrolling
1618      */
1619     disable_touch_scroll: function() {
1620         if (this.event_listener_options_supported()) {
1621             document.addEventListener('touchmove', this.stop_touch_scroll.bind(this), {passive: false});
1622         }
1623     },
1625     /**
1626      * Stop Touch Scrolling
1627      * @param {Object} e
1628      */
1629     stop_touch_scroll: function(e) {
1630         var drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
1632         if (drawingregion.contains(e.target)) {
1633             e.stopPropagation();
1634             e.preventDefault();
1635         }
1636     }
1638 };
1640 Y.extend(EDITOR, Y.Base, EDITOR.prototype, {
1641     NAME: 'moodle-assignfeedback_editpdf-editor',
1642     ATTRS: {
1643         userid: {
1644             validator: Y.Lang.isInteger,
1645             value: 0
1646         },
1647         assignmentid: {
1648             validator: Y.Lang.isInteger,
1649             value: 0
1650         },
1651         attemptnumber: {
1652             validator: Y.Lang.isInteger,
1653             value: 0
1654         },
1655         header: {
1656             validator: Y.Lang.isString,
1657             value: ''
1658         },
1659         body: {
1660             validator: Y.Lang.isString,
1661             value: ''
1662         },
1663         footer: {
1664             validator: Y.Lang.isString,
1665             value: ''
1666         },
1667         linkid: {
1668             validator: Y.Lang.isString,
1669             value: ''
1670         },
1671         deletelinkid: {
1672             validator: Y.Lang.isString,
1673             value: ''
1674         },
1675         readonly: {
1676             validator: Y.Lang.isBoolean,
1677             value: true
1678         },
1679         stampfiles: {
1680             validator: Y.Lang.isArray,
1681             value: ''
1682         }
1683     }
1684 });
1686 M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
1687 M.assignfeedback_editpdf.editor = M.assignfeedback_editpdf.editor || {};
1689 /**
1690  * Init function - will create a new instance every time.
1691  * @method editor.init
1692  * @static
1693  * @param {Object} params
1694  */
1695 M.assignfeedback_editpdf.editor.init = M.assignfeedback_editpdf.editor.init || function(params) {
1696     M.assignfeedback_editpdf.instance = new EDITOR(params);
1697     return M.assignfeedback_editpdf.instance;
1698 };