MDL-42571 editpdf: Fix comments search and multiple views drawing
[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/>.
16 /**
17  * Provides an in browser PDF editor.
18  *
19  * @module moodle-assignfeedback_editpdf-editor
20  */
22 /**
23  * EDITOR
24  * This is an in browser PDF editor.
25  *
26  * @namespace M.assignfeedback_editpdf
27  * @class editor
28  * @constructor
29  * @extends Y.Base
30  */
31 var EDITOR = function() {
32     EDITOR.superclass.constructor.apply(this, arguments);
33 };
34 EDITOR.prototype = {
36     /**
37      * The dialogue used for all action menu displays.
38      *
39      * @property type
40      * @type M.core.dialogue
41      * @protected
42      */
43     dialogue : null,
45     /**
46      * The number of pages in the pdf.
47      *
48      * @property pagecount
49      * @type Number
50      * @protected
51      */
52     pagecount : 0,
54     /**
55      * The active page in the editor.
56      *
57      * @property currentpage
58      * @type Number
59      * @protected
60      */
61     currentpage : 0,
63     /**
64      * A list of page objects. Each page has a list of comments and annotations.
65      *
66      * @property pages
67      * @type array
68      * @protected
69      */
70     pages : [],
72     /**
73      * The yui node for the loading icon.
74      *
75      * @property loadingicon
76      * @type Node
77      * @protected
78      */
79     loadingicon : null,
81     /**
82      * Image object of the current page image.
83      *
84      * @property pageimage
85      * @type Image
86      * @protected
87      */
88     pageimage : null,
90     /**
91      * YUI Graphic class for drawing shapes.
92      *
93      * @property graphic
94      * @type Graphic
95      * @protected
96      */
97     graphic : null,
99     /**
100      * Info about the current edit operation.
101      *
102      * @property currentedit
103      * @type M.assignfeedback_editpdf.edit
104      * @protected
105      */
106     currentedit : new M.assignfeedback_editpdf.edit(),
108     /**
109      * Current drawable.
110      *
111      * @property currentdrawable
112      * @type M.assignfeedback_editpdf.drawable|false
113      * @protected
114      */
115     currentdrawable : false,
117     /**
118      * Current drawables.
119      *
120      * @property drawables
121      * @type array(M.assignfeedback_editpdf.drawable)
122      * @protected
123      */
124     drawables : [],
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,
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,
142     /**
143      * Last selected annotation tool
144      * @property lastannotationtool
145      * @type String
146      * @protected
147      */
148     lastanntationtool : "pen",
150     /**
151      * The users comments quick list
152      * @property quicklist
153      * @type M.assignfeedback_editpdf.quickcommentlist
154      * @protected
155      */
156     quicklist : null,
158     /**
159      * The search comments window.
160      * @property searchcommentswindow
161      * @type M.core.dialogue
162      * @protected
163      */
164     searchcommentswindow : null,
167     /**
168      * The selected stamp picture.
169      * @property currentstamp
170      * @type String
171      * @protected
172      */
173     currentstamp : null,
175     /**
176      * The stamps.
177      * @property stamps
178      * @type Array
179      * @protected
180      */
181     stamps : [],
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,
193     /**
194      * Called during the initialisation process of the object.
195      * @method initializer
196      */
197     initializer : function() {
198         var link;
200         link = Y.one('#' + this.get('linkid'));
202         if (link) {
203             link.on('click', this.link_handler, this);
204             link.on('key', this.link_handler, 'down:13', this);
206             this.currentedit.start = false;
207             this.currentedit.end = false;
208             if (!this.get('readonly')) {
209                 this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this);
210             }
211         }
212     },
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;
221         // Initalise the colour buttons.
222         button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
224         imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf');
225         button.one('img').setAttribute('src', imgurl);
227         if (this.currentedit.commentcolour === 'clear') {
228             button.one('img').setStyle('borderStyle', 'dashed');
229         } else {
230             button.one('img').setStyle('borderStyle', 'solid');
231         }
233         button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
234         imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf');
235         button.one('img').setAttribute('src', imgurl);
237         currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
238         currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton');
239         currenttoolnode.setAttribute('aria-pressed', 'true');
241         button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
242         button.one('img').setAttrs({'src': this.get_stamp_image_url(this.currentedit.stamp),
243                                     'height': '16',
244                                     'width': '16'});
245     },
247     /**
248      * Called to get the bounds of the drawing region.
249      * @method get_canvas_bounds
250      */
251     get_canvas_bounds : function() {
252         var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
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);
259         return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height);
260     },
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);
271         bounds.x = bounds.y = 0;
273         newpoint.clip(bounds);
274         return newpoint;
275     },
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);
286         return newpoint;
287     },
289     /**
290      * Called to open the pdf editing dialogue.
291      * @method link_handler
292      */
293     link_handler : function(e) {
294         var drawingcanvas, drawingregion, resize = true;
295         e.preventDefault();
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'),
302                 modal: true,
303                 width: '840px',
304                 visible: false,
305                 draggable: true
306             });
308             // Add custom class for styling.
309             this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE);
311             this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
313             drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
314             this.graphic = new Y.Graphic({render : drawingcanvas});
316             drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
317             drawingregion.on('scroll', this.move_canvas, this);
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);
324                 this.refresh_button_state();
325             }
327             this.load_all_pages();
328             drawingcanvas.on('windowresize', this.resize, this);
330             resize = false;
331         }
332         this.dialogue.centerDialogue();
333         this.dialogue.show();
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         }
340     },
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,
348             config,
349             checkconversionstatus,
350             ajax_error_total;
352         config = {
353             method: 'get',
354             context: this,
355             sync: false,
356             data : {
357                 sesskey : M.cfg.sesskey,
358                 action : 'loadallpages',
359                 userid : this.get('userid'),
360                 attemptnumber : this.get('attemptnumber'),
361                 assignmentid : this.get('assignmentid'),
362                 readonly : this.get('readonly') ? 1 : 0
363             },
364             on: {
365                 success: function(tid, response) {
366                     this.all_pages_loaded(response.responseText);
367                 },
368                 failure: function(tid, response) {
369                     return new M.core.exception(response.responseText);
370                 }
371             }
372         };
374         Y.io(ajaxurl, config);
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');
395                             // Update the progress bar.
396                             var progressbarcontainer = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER);
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                             }
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         }
431     },
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         }
458         this.pagecount = data.pagecount;
459         this.pages = data.pages;
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         }
479         // Update the ui.
480         if (this.quicklist) {
481             this.quicklist.load();
482         }
483         this.setup_navigation();
484         this.setup_toolbar();
485         this.change_page();
486     },
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 = '';
499         Y.Array.each(urls, function(url) {
500             if (url.indexOf(filename) > 0) {
501                 fullurl = url;
502             }
503         }, this);
505         return fullurl;
506     },
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;
523         searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON);
524         searchcommentsbutton.on('click', this.open_search_comments, this);
525         searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this);
527         if (this.get('readonly')) {
528             return;
529         }
530         // Setup the tool buttons.
531         Y.each(TOOLSELECTOR, function(selector, tool) {
532             toolnode = this.get_dialogue_element(selector);
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);
538         // Set the default tool.
540         commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
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         });
556         annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
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         });
576         stampfiles = this.get('stampfiles');
577         if (stampfiles.length <= 0) {
578             this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide();
579         } else {
580             filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1);
581             this.currentedit.stamp = filename;
582             currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
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;
591                     if (!stamp) {
592                         stamp = e.target.ancestor().getAttribute('data-stamp');
593                     }
594                     filename = stamp.substr(stamp.lastIndexOf('/'));
595                     this.currentedit.stamp = filename;
596                     this.handle_tool_button(e, "stamp");
597                 },
598                 context: this
599             });
600             this.refresh_button_state();
601         }
602     },
604     /**
605      * Change the current tool.
606      * @protected
607      * @method handle_tool_button
608      */
609     handle_tool_button : function(e, tool) {
610         var currenttoolnode;
612         e.preventDefault();
614         // Change style of the pressed button.
615         currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
616         currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton');
617         currenttoolnode.setAttribute('aria-pressed', 'false');
618         this.currentedit.tool = tool;
619         if (tool !== "comment" && tool !== "select" && tool !== "stamp") {
620             this.lastannotationtool = tool;
621         }
622         this.refresh_button_state();
623     },
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;
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         }
644         page = { comments : comments, annotations : annotations };
646         return Y.JSON.stringify(page);
647     },
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;
659         if (!this.currentedit.start || !this.currentedit.end) {
660             return false;
661         }
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         }
673         return drawable;
674     },
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     },
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     },
697     /**
698      * Event handler for mousedown or touchstart.
699      * @protected
700      * @param Event
701      * @method edit_start
702      */
703     edit_start : function(e) {
704         e.preventDefault();
705         var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
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;
714         // Ignore right mouse click.
715         if (e.button === 3) {
716             return;
717         }
719         if (this.currentedit.starttime) {
720             return;
721         }
723         if (this.editingcomment) {
724             return;
725         }
727         this.currentedit.starttime = new Date().getTime();
728         this.currentedit.start = point;
729         this.currentedit.end = {x : point.x, y : point.y};
731         if (this.currentedit.tool === 'select') {
732             var x = this.currentedit.end.x,
733                 y = this.currentedit.end.y,
734                 annotations = this.pages[this.currentpage].annotations;
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             });
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     },
767     /**
768      * Event handler for mousemove.
769      * @protected
770      * @param Event
771      * @method edit_move
772      */
773     edit_move : function(e) {
774         e.preventDefault();
775         var bounds = this.get_canvas_bounds(),
776             canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
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);
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         }
786         if (this.currentedit.tool === 'pen') {
787             this.currentedit.path.push(point);
788         }
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     },
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;
814         duration = new Date().getTime() - this.currentedit.start;
816         if (duration < CLICKTIMEOUT || this.currentedit.start === false) {
817             return;
818         }
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);
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             }
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;
838                 if (annotation.init_from_edit(this.currentedit)) {
839                     this.pages[this.currentpage].annotations.push(annotation);
840                     this.drawables.push(annotation.draw());
841                 }
842             }
843         }
845         // Save the changes.
846         this.save_current_page();
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     },
855     /**
856      * Resize the dialogue window when the browser is resized.
857      * @public
858      * @method resize
859      */
860     resize : function() {
861         var drawingregion, drawregionheight;
863         if (!this.dialogue.get('visible')) {
864             return;
865         }
866         this.dialogue.centerDialogue();
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         }
873         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
874         drawingregion.setStyle('maxHeight', drawregionheight +'px');
875         this.redraw();
876         return true;
877     },
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     },
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;
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                         }
933                         Y.one('#' + this.get('linkid')).siblings(SELECTOR.UNSAVEDCHANGESDIV)
934                             .item(0).addClass('haschanges');
935                     } catch (e) {
936                         return new M.core.exception(e);
937                     }
938                 },
939                 failure: function(tid, response) {
940                     return new M.core.exception(response.responseText);
941                 }
942             }
943         };
945         Y.io(ajaxurl, config);
947     },
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         }
963         this.searchcommentswindow.show();
964         e.preventDefault();
965     },
967     /**
968      * Redraw all the comments and annotations.
969      * @protected
970      * @method redraw
971      */
972     redraw : function() {
973         var i,
974             page;
976         page = this.pages[this.currentpage];
977         if (page === undefined) {
978             return; // Can happen if a redraw is triggered by an event, before the page has been selected.
979         }
980         while (this.drawables.length > 0) {
981             this.drawables.pop().erase();
982         }
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     },
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() {
998         var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
999             page,
1000             previousbutton,
1001             nextbutton;
1003         previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1004         nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
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         }
1017         page = this.pages[this.currentpage];
1018         this.loadingicon.hide();
1019         drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
1020         drawingcanvas.setStyle('width', page.width + 'px');
1021         drawingcanvas.setStyle('height', page.height + 'px');
1023         // Update page select.
1024         this.get_dialogue_element(SELECTOR.PAGESELECT).set('value', this.currentpage);
1026         this.resize(); // Internally will call 'redraw', after checking the dialogue size.
1027     },
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,
1038             option,
1039             previousbutton,
1040             nextbutton;
1042         pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
1044         var options = pageselect.all('option');
1045         if (options.size() <= 1) {
1046             for (i = 0; i < this.pages.length; i++) {
1047                 option = Y.Node.create('<option/>');
1048                 option.setAttribute('value', i);
1049                 option.setHTML(M.util.get_string('pagenumber', 'assignfeedback_editpdf', i+1));
1050                 pageselect.append(option);
1051             }
1052         }
1053         pageselect.removeAttribute('disabled');
1054         pageselect.on('change', function() {
1055             this.currentpage = pageselect.get('value');
1056             this.change_page();
1057         }, this);
1059         previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1060         nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
1062         previousbutton.on('click', this.previous_page, this);
1063         previousbutton.on('key', this.previous_page, 'down:13', this);
1064         nextbutton.on('click', this.next_page, this);
1065         nextbutton.on('key', this.next_page, 'down:13', this);
1066     },
1068     /**
1069      * Navigate to the previous page.
1070      * @protected
1071      * @method previous_page
1072      */
1073     previous_page : function(e) {
1074         e.preventDefault();
1075         this.currentpage--;
1076         if (this.currentpage < 0) {
1077             this.currentpage = 0;
1078         }
1079         this.change_page();
1080     },
1082     /**
1083      * Navigate to the next page.
1084      * @protected
1085      * @method next_page
1086      */
1087     next_page : function(e) {
1088         e.preventDefault();
1089         this.currentpage++;
1090         if (this.currentpage >= this.pages.length) {
1091             this.currentpage = this.pages.length - 1;
1092         }
1093         this.change_page();
1094     },
1096     /**
1097      * Update any absolutely positioned nodes, within each drawable, when the drawing canvas is scrolled
1098      * @protected
1099      * @method move_canvas
1100      */
1101     move_canvas: function() {
1102         var drawingregion, x, y, i;
1104         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
1105         x = parseInt(drawingregion.get('scrollLeft'), 10);
1106         y = parseInt(drawingregion.get('scrollTop'), 10);
1108         for (i = 0; i < this.drawables.length; i++) {
1109             this.drawables[i].scroll_update(x, y);
1110         }
1111     }
1113 };
1115 Y.extend(EDITOR, Y.Base, EDITOR.prototype, {
1116     NAME : 'moodle-assignfeedback_editpdf-editor',
1117     ATTRS : {
1118         userid : {
1119             validator : Y.Lang.isInteger,
1120             value : 0
1121         },
1122         assignmentid : {
1123             validator : Y.Lang.isInteger,
1124             value : 0
1125         },
1126         attemptnumber : {
1127             validator : Y.Lang.isInteger,
1128             value : 0
1129         },
1130         header : {
1131             validator : Y.Lang.isString,
1132             value : ''
1133         },
1134         body : {
1135             validator : Y.Lang.isString,
1136             value : ''
1137         },
1138         footer : {
1139             validator : Y.Lang.isString,
1140             value : ''
1141         },
1142         linkid : {
1143             validator : Y.Lang.isString,
1144             value : ''
1145         },
1146         deletelinkid : {
1147             validator : Y.Lang.isString,
1148             value : ''
1149         },
1150         readonly : {
1151             validator : Y.Lang.isBoolean,
1152             value : true
1153         },
1154         stampfiles : {
1155             validator : Y.Lang.isArray,
1156             value : ''
1157         },
1158         pagetotal : {
1159             validator : Y.Lang.isInteger,
1160             value : 0
1161         }
1162     }
1163 });
1165 M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
1166 M.assignfeedback_editpdf.editor = M.assignfeedback_editpdf.editor || {};
1168 /**
1169  * Init function - will create a new instance every time.
1170  * @method editor.init
1171  * @static
1172  * @param {Object} params
1173  */
1174 M.assignfeedback_editpdf.editor.init = M.assignfeedback_editpdf.editor.init || function(params) {
1175     return new EDITOR(params);
1176 };