MDL-59629 mod_block: Set the default region in add_region
[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      * The dialogue used for all action menu displays.
40      *
41      * @property type
42      * @type M.core.dialogue
43      * @protected
44      */
45     dialogue: null,
47     /**
48      * The panel used for all action menu displays.
49      *
50      * @property type
51      * @type Y.Node
52      * @protected
53      */
54     panel: null,
56     /**
57      * The number of pages in the pdf.
58      *
59      * @property pagecount
60      * @type Number
61      * @protected
62      */
63     pagecount: 0,
65     /**
66      * The active page in the editor.
67      *
68      * @property currentpage
69      * @type Number
70      * @protected
71      */
72     currentpage: 0,
74     /**
75      * A list of page objects. Each page has a list of comments and annotations.
76      *
77      * @property pages
78      * @type array
79      * @protected
80      */
81     pages: [],
83     /**
84      * The reported status of the document.
85      *
86      * @property documentstatus
87      * @type int
88      * @protected
89      */
90     documentstatus: 0,
92     /**
93      * The yui node for the loading icon.
94      *
95      * @property loadingicon
96      * @type Node
97      * @protected
98      */
99     loadingicon: null,
101     /**
102      * Image object of the current page image.
103      *
104      * @property pageimage
105      * @type Image
106      * @protected
107      */
108     pageimage: null,
110     /**
111      * YUI Graphic class for drawing shapes.
112      *
113      * @property graphic
114      * @type Graphic
115      * @protected
116      */
117     graphic: null,
119     /**
120      * Info about the current edit operation.
121      *
122      * @property currentedit
123      * @type M.assignfeedback_editpdf.edit
124      * @protected
125      */
126     currentedit: new M.assignfeedback_editpdf.edit(),
128     /**
129      * Current drawable.
130      *
131      * @property currentdrawable
132      * @type M.assignfeedback_editpdf.drawable|false
133      * @protected
134      */
135     currentdrawable: false,
137     /**
138      * Current drawables.
139      *
140      * @property drawables
141      * @type array(M.assignfeedback_editpdf.drawable)
142      * @protected
143      */
144     drawables: [],
146     /**
147      * Current comment when the comment menu is open.
148      * @property currentcomment
149      * @type M.assignfeedback_editpdf.comment
150      * @protected
151      */
152     currentcomment: null,
154     /**
155      * Current annotation when the select tool is used.
156      * @property currentannotation
157      * @type M.assignfeedback_editpdf.annotation
158      * @protected
159      */
160     currentannotation: null,
162     /**
163      * Track the previous annotation so we can remove selection highlights.
164      * @property lastannotation
165      * @type M.assignfeedback_editpdf.annotation
166      * @protected
167      */
168     lastannotation: null,
170     /**
171      * Last selected annotation tool
172      * @property lastannotationtool
173      * @type String
174      * @protected
175      */
176     lastannotationtool: "pen",
178     /**
179      * The users comments quick list
180      * @property quicklist
181      * @type M.assignfeedback_editpdf.quickcommentlist
182      * @protected
183      */
184     quicklist: null,
186     /**
187      * The search comments window.
188      * @property searchcommentswindow
189      * @type M.core.dialogue
190      * @protected
191      */
192     searchcommentswindow: null,
195     /**
196      * The selected stamp picture.
197      * @property currentstamp
198      * @type String
199      * @protected
200      */
201     currentstamp: null,
203     /**
204      * The stamps.
205      * @property stamps
206      * @type Array
207      * @protected
208      */
209     stamps: [],
211     /**
212      * Prevent new comments from appearing
213      * immediately after clicking off a current
214      * comment
215      * @property editingcomment
216      * @type Boolean
217      * @public
218      */
219     editingcomment: false,
221     /**
222      * Should inactive comments be collapsed?
223      *
224      * @property collapsecomments
225      * @type Boolean
226      * @public
227      */
228     collapsecomments: true,
230     /**
231      * Called during the initialisation process of the object.
232      * @method initializer
233      */
234     initializer: function() {
235         var link;
237         link = Y.one('#' + this.get('linkid'));
239         if (link) {
240             link.on('click', this.link_handler, this);
241             link.on('key', this.link_handler, 'down:13', this);
243             // We call the amd module to see if we can take control of the review panel.
244             require(['mod_assign/grading_review_panel'], function(ReviewPanelManager) {
245                 var panelManager = new ReviewPanelManager();
247                 var panel = panelManager.getReviewPanel('assignfeedback_editpdf');
248                 if (panel) {
249                     panel = Y.one(panel);
250                     panel.empty();
251                     link.ancestor('.fitem').hide();
252                     this.open_in_panel(panel);
253                 }
254                 this.currentedit.start = false;
255                 this.currentedit.end = false;
256                 if (!this.get('readonly')) {
257                     this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this);
258                 }
259             }.bind(this));
261         }
262     },
264     /**
265      * Called to show/hide buttons and set the current colours/stamps.
266      * @method refresh_button_state
267      */
268     refresh_button_state: function() {
269         var button, currenttoolnode, imgurl, drawingregion, stampimgurl, drawingcanvas;
271         // Initalise the colour buttons.
272         button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
274         imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf');
275         button.one('img').setAttribute('src', imgurl);
277         if (this.currentedit.commentcolour === 'clear') {
278             button.one('img').setStyle('borderStyle', 'dashed');
279         } else {
280             button.one('img').setStyle('borderStyle', 'solid');
281         }
283         button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
284         imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf');
285         button.one('img').setAttribute('src', imgurl);
287         currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
288         currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton');
289         currenttoolnode.setAttribute('aria-pressed', 'true');
290         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
291         drawingregion.setAttribute('data-currenttool', this.currentedit.tool);
293         button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
294         stampimgurl = this.get_stamp_image_url(this.currentedit.stamp);
295         button.one('img').setAttrs({'src': stampimgurl,
296                                     'height': '16',
297                                     'width': '16'});
299         drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
300         switch (this.currentedit.tool) {
301             case 'drag':
302                 drawingcanvas.setStyle('cursor', 'move');
303                 break;
304             case 'highlight':
305                 drawingcanvas.setStyle('cursor', 'text');
306                 break;
307             case 'select':
308                 drawingcanvas.setStyle('cursor', 'default');
309                 break;
310             case 'stamp':
311                 drawingcanvas.setStyle('cursor', 'url(' + stampimgurl + '), crosshair');
312                 break;
313             default:
314                 drawingcanvas.setStyle('cursor', 'crosshair');
315         }
316     },
318     /**
319      * Called to get the bounds of the drawing region.
320      * @method get_canvas_bounds
321      */
322     get_canvas_bounds: function() {
323         var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
324             offsetcanvas = canvas.getXY(),
325             offsetleft = offsetcanvas[0],
326             offsettop = offsetcanvas[1],
327             width = parseInt(canvas.getStyle('width'), 10),
328             height = parseInt(canvas.getStyle('height'), 10);
330         return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height);
331     },
333     /**
334      * Called to translate from window coordinates to canvas coordinates.
335      * @method get_canvas_coordinates
336      * @param M.assignfeedback_editpdf.point point in window coordinats.
337      */
338     get_canvas_coordinates: function(point) {
339         var bounds = this.get_canvas_bounds(),
340             newpoint = new M.assignfeedback_editpdf.point(point.x - bounds.x, point.y - bounds.y);
342         bounds.x = bounds.y = 0;
344         newpoint.clip(bounds);
345         return newpoint;
346     },
348     /**
349      * Called to translate from canvas coordinates to window coordinates.
350      * @method get_window_coordinates
351      * @param M.assignfeedback_editpdf.point point in window coordinats.
352      */
353     get_window_coordinates: function(point) {
354         var bounds = this.get_canvas_bounds(),
355             newpoint = new M.assignfeedback_editpdf.point(point.x + bounds.x, point.y + bounds.y);
357         return newpoint;
358     },
360     /**
361      * Open the edit-pdf editor in the panel in the page instead of a popup.
362      * @method open_in_panel
363      */
364     open_in_panel: function(panel) {
365         var drawingcanvas, drawingregion;
367         this.panel = panel;
368         panel.append(this.get('body'));
369         panel.addClass(CSS.DIALOGUE);
371         this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
373         drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
374         this.graphic = new Y.Graphic({render: drawingcanvas});
376         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
377         drawingregion.on('scroll', this.move_canvas, this);
379         if (!this.get('readonly')) {
380             drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
381             drawingcanvas.on('gesturemove', this.edit_move, null, this);
382             drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
384             this.refresh_button_state();
385         }
387         this.start_generation();
388     },
390     /**
391      * Called to open the pdf editing dialogue.
392      * @method link_handler
393      */
394     link_handler: function(e) {
395         var drawingcanvas, drawingregion;
396         var resize = true;
397         e.preventDefault();
399         if (!this.dialogue) {
400             this.dialogue = new M.core.dialogue({
401                 headerContent: this.get('header'),
402                 bodyContent: this.get('body'),
403                 footerContent: this.get('footer'),
404                 modal: true,
405                 width: '840px',
406                 visible: false,
407                 draggable: true
408             });
410             // Add custom class for styling.
411             this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE);
413             this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
415             drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
416             this.graphic = new Y.Graphic({render: drawingcanvas});
418             drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
419             drawingregion.on('scroll', this.move_canvas, this);
421             if (!this.get('readonly')) {
422                 drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
423                 drawingcanvas.on('gesturemove', this.edit_move, null, this);
424                 drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
426                 this.refresh_button_state();
427             }
429             this.start_generation();
430             drawingcanvas.on('windowresize', this.resize, this);
432             resize = false;
433         }
434         this.dialogue.centerDialogue();
435         this.dialogue.show();
437         // Redraw when the dialogue is moved, to ensure the absolute elements are all positioned correctly.
438         this.dialogue.dd.on('drag:end', this.redraw, this);
439         if (resize) {
440             this.resize(); // When re-opening the dialog call redraw, to make sure the size + layout is correct.
441         }
442     },
444     /**
445      * Called to load the information and annotations for all pages.
446      *
447      * @method start_generation
448      */
449     start_generation: function() {
450         this.poll_document_conversion_status();
451     },
453     /**
454      * Poll the current document conversion status and start the next step
455      * in the process.
456      *
457      * @method poll_document_conversion_status
458      */
459     poll_document_conversion_status: function() {
460         if (this.get('destroyed')) {
461             return;
462         }
464         Y.io(AJAXBASE, {
465             method: 'get',
466             context: this,
467             sync: false,
468             data: {
469                 sesskey: M.cfg.sesskey,
470                 action: 'pollconversions',
471                 userid: this.get('userid'),
472                 attemptnumber: this.get('attemptnumber'),
473                 assignmentid: this.get('assignmentid'),
474                 readonly: this.get('readonly') ? 1 : 0
475             },
476             on: {
477                 success: function(tid, response) {
478                     var data = this.handle_response_data(response),
479                         poll = false;
480                     if (data) {
481                         this.documentstatus = data.status;
482                         if (data.status === 0) {
483                             // The combined document is still waiting for input to be ready.
484                             poll = true;
486                         } else if (data.status === 1) {
487                             // The combine document is ready for conversion into a single PDF.
488                             poll = true;
490                         } else if (data.status === 2 || data.status === -1) {
491                             // The combined PDF is ready.
492                             // We now know the page count and can convert it to a set of images.
493                             this.pagecount = data.pagecount;
495                             if (data.pageready == data.pagecount) {
496                                 this.prepare_pages_for_display(data);
497                             } else {
498                                 // Some pages are not ready yet.
499                                 // Note: We use a different polling process here which does not block.
500                                 this.update_page_load_progress();
502                                 // Fetch the images for the combined document.
503                                 this.start_document_to_image_conversion();
504                             }
505                         }
507                         if (poll) {
508                             // Check again in 1 second.
509                             Y.later(1000, this, this.poll_document_conversion_status);
510                         }
511                     }
512                 },
513                 failure: function(tid, response) {
514                     return new M.core.exception(response.responseText);
515                 }
516             }
517         });
518     },
520     /**
521      * Spwan the PDF to Image conversion on the server.
522      *
523      * @method get_images_for_documents
524      */
525     start_document_to_image_conversion: function() {
526         if (this.get('destroyed')) {
527             return;
528         }
529         Y.io(AJAXBASE, {
530             method: 'get',
531             context: this,
532             sync: false,
533             data: {
534                 sesskey: M.cfg.sesskey,
535                 action: 'pollconversions',
536                 userid: this.get('userid'),
537                 attemptnumber: this.get('attemptnumber'),
538                 assignmentid: this.get('assignmentid'),
539                 readonly: this.get('readonly') ? 1 : 0
540             },
541             on: {
542                 success: function(tid, response) {
543                     var data = this.handle_response_data(response);
544                     if (data) {
545                         this.documentstatus = data.status;
546                         if (data.status === 2) {
547                             // The pages are ready. Add all of the annotations to them.
548                             this.prepare_pages_for_display(data);
549                         }
550                     }
551                 },
552                 failure: function(tid, response) {
553                     return new M.core.exception(response.responseText);
554                 }
555             }
556         });
557     },
559     /**
560      * The info about all pages in the pdf has been returned.
561      *
562      * @param string The ajax response as text.
563      * @protected
564      * @method prepare_pages_for_display
565      */
566     prepare_pages_for_display: function(data) {
567         var i, j, comment, error;
568         if (!data.pagecount) {
569             if (this.dialogue) {
570                 this.dialogue.hide();
571             }
572             // Display alert dialogue.
573             error = new M.core.alert({message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')});
574             error.show();
575             return;
576         }
578         this.pages = data.pages;
580         for (i = 0; i < this.pages.length; i++) {
581             for (j = 0; j < this.pages[i].comments.length; j++) {
582                 comment = this.pages[i].comments[j];
583                 this.pages[i].comments[j] = new M.assignfeedback_editpdf.comment(this,
584                                                                                  comment.gradeid,
585                                                                                  comment.pageno,
586                                                                                  comment.x,
587                                                                                  comment.y,
588                                                                                  comment.width,
589                                                                                  comment.colour,
590                                                                                  comment.rawtext);
591             }
592             for (j = 0; j < this.pages[i].annotations.length; j++) {
593                 data = this.pages[i].annotations[j];
594                 this.pages[i].annotations[j] = this.create_annotation(data.type, data);
595             }
596         }
598         // Update the ui.
599         if (this.quicklist) {
600             this.quicklist.load();
601         }
602         this.setup_navigation();
603         this.setup_toolbar();
604         this.change_page();
605     },
607     /**
608      * Fetch the page images.
609      *
610      * @method update_page_load_progress
611      */
612     update_page_load_progress: function() {
613         if (this.get('destroyed')) {
614             return;
615         }
616         var checkconversionstatus,
617             ajax_error_total = 0,
618             progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar');
620         if (!progressbar) {
621             return;
622         }
624         // If pages are not loaded, check PDF conversion status for the progress bar.
625         checkconversionstatus = {
626             method: 'get',
627             context: this,
628             sync: false,
629             data: {
630                 sesskey: M.cfg.sesskey,
631                 action: 'conversionstatus',
632                 userid: this.get('userid'),
633                 attemptnumber: this.get('attemptnumber'),
634                 assignmentid: this.get('assignmentid')
635             },
636             on: {
637                 success: function(tid, response) {
638                     if (this.get('destroyed')) {
639                         return;
640                     }
641                     ajax_error_total = 0;
643                     var progress = 0;
644                     var progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar');
645                     if (progressbar) {
646                         // Calculate progress.
647                         progress = (response.response / this.pagecount) * 100;
648                         progressbar.setStyle('width', progress + '%');
649                         progressbar.ancestor(SELECTOR.PROGRESSBARCONTAINER).setAttribute('aria-valuenow', progress);
651                         if (progress < 100) {
652                             // Keep polling until all pages are generated.
653                             M.util.js_pending('checkconversionstatus');
654                             Y.later(1000, this, function() {
655                                 M.util.js_complete('checkconversionstatus');
656                                 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
657                             });
658                         }
659                     }
660                 },
661                 failure: function(tid, response) {
662                     if (this.get('destroyed')) {
663                         return;
664                     }
665                     ajax_error_total = ajax_error_total + 1;
666                     // We only continue on error if the all pages were not generated,
667                     // and if the ajax call did not produce 5 errors in the row.
668                     if (this.pagecount === 0 && ajax_error_total < 5) {
669                         M.util.js_pending('checkconversionstatus');
670                         Y.later(1000, this, function() {
671                             M.util.js_complete('checkconversionstatus');
672                             Y.io(AJAXBASEPROGRESS, checkconversionstatus);
673                         });
674                     }
675                     return new M.core.exception(response.responseText);
676                 }
677             }
678         };
679         // We start the AJAX "generated page total number" call a second later to give a chance to
680         // the AJAX "combined pdf generation" call to clean the previous submission images.
681         M.util.js_pending('checkconversionstatus');
682         Y.later(1000, this, function() {
683             ajax_error_total = 0;
684             M.util.js_complete('checkconversionstatus');
685             Y.io(AJAXBASEPROGRESS, checkconversionstatus);
686         });
687     },
689     /**
690      * Handle response data.
691      *
692      * @method  handle_response_data
693      * @param   {object} response
694      * @return  {object}
695      */
696     handle_response_data: function(response) {
697         if (this.get('destroyed')) {
698             return;
699         }
700         var data;
701         try {
702             data = Y.JSON.parse(response.responseText);
703             if (data.error) {
704                 if (this.dialogue) {
705                     this.dialogue.hide();
706                 }
708                 new M.core.alert({
709                     message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'),
710                     visible: true
711                 });
712             } else {
713                 return data;
714             }
715         } catch (e) {
716             if (this.dialogue) {
717                 this.dialogue.hide();
718             }
720             new M.core.alert({
721                 title: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'),
722                 visible: true
723             });
724         }
726         return;
727     },
729     /**
730      * Get the full pluginfile url for an image file - just given the filename.
731      *
732      * @public
733      * @method get_stamp_image_url
734      * @param string filename
735      */
736     get_stamp_image_url: function(filename) {
737         var urls = this.get('stampfiles'),
738             fullurl = '';
740         Y.Array.each(urls, function(url) {
741             if (url.indexOf(filename) > 0) {
742                 fullurl = url;
743             }
744         }, this);
746         return fullurl;
747     },
749     /**
750      * Attach listeners and enable the color picker buttons.
751      * @protected
752      * @method setup_toolbar
753      */
754     setup_toolbar: function() {
755         var toolnode,
756             commentcolourbutton,
757             annotationcolourbutton,
758             searchcommentsbutton,
759             expcolcommentsbutton,
760             currentstampbutton,
761             stampfiles,
762             picker,
763             filename;
765         searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON);
766         searchcommentsbutton.on('click', this.open_search_comments, this);
767         searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this);
769         expcolcommentsbutton = this.get_dialogue_element(SELECTOR.EXPCOLCOMMENTSBUTTON);
770         expcolcommentsbutton.on('click', this.expandCollapseComments, this);
771         expcolcommentsbutton.on('key', this.expandCollapseComments, 'down:13', this);
773         if (this.get('readonly')) {
774             return;
775         }
776         this.disable_touch_scroll();
778         // Setup the tool buttons.
779         Y.each(TOOLSELECTOR, function(selector, tool) {
780             toolnode = this.get_dialogue_element(selector);
781             toolnode.on('click', this.handle_tool_button, this, tool);
782             toolnode.on('key', this.handle_tool_button, 'down:13', this, tool);
783             toolnode.setAttribute('aria-pressed', 'false');
784         }, this);
786         // Set the default tool.
788         commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
789         picker = new M.assignfeedback_editpdf.colourpicker({
790             buttonNode: commentcolourbutton,
791             colours: COMMENTCOLOUR,
792             iconprefix: 'background_colour_',
793             callback: function(e) {
794                 var colour = e.target.getAttribute('data-colour');
795                 if (!colour) {
796                     colour = e.target.ancestor().getAttribute('data-colour');
797                 }
798                 this.currentedit.commentcolour = colour;
799                 this.handle_tool_button(e, "comment");
800             },
801             context: this
802         });
804         annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
805         picker = new M.assignfeedback_editpdf.colourpicker({
806             buttonNode: annotationcolourbutton,
807             iconprefix: 'colour_',
808             colours: ANNOTATIONCOLOUR,
809             callback: function(e) {
810                 var colour = e.target.getAttribute('data-colour');
811                 if (!colour) {
812                     colour = e.target.ancestor().getAttribute('data-colour');
813                 }
814                 this.currentedit.annotationcolour = colour;
815                 if (this.lastannotationtool) {
816                     this.handle_tool_button(e, this.lastannotationtool);
817                 } else {
818                     this.handle_tool_button(e, "pen");
819                 }
820             },
821             context: this
822         });
824         stampfiles = this.get('stampfiles');
825         if (stampfiles.length <= 0) {
826             this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide();
827         } else {
828             filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1);
829             this.currentedit.stamp = filename;
830             currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
832             picker = new M.assignfeedback_editpdf.stamppicker({
833                 buttonNode: currentstampbutton,
834                 stamps: stampfiles,
835                 callback: function(e) {
836                     var stamp = e.target.getAttribute('data-stamp'),
837                         filename;
839                     if (!stamp) {
840                         stamp = e.target.ancestor().getAttribute('data-stamp');
841                     }
842                     filename = stamp.substr(stamp.lastIndexOf('/'));
843                     this.currentedit.stamp = filename;
844                     this.handle_tool_button(e, "stamp");
845                 },
846                 context: this
847             });
848             this.refresh_button_state();
849         }
850     },
852     /**
853      * Change the current tool.
854      * @protected
855      * @method handle_tool_button
856      */
857     handle_tool_button: function(e, tool) {
858         var currenttoolnode;
860         e.preventDefault();
862         // Change style of the pressed button.
863         currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
864         currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton');
865         currenttoolnode.setAttribute('aria-pressed', 'false');
866         this.currentedit.tool = tool;
868         if (tool !== "comment" && tool !== "select" && tool !== "drag" && tool !== "stamp") {
869             this.lastannotationtool = tool;
870         }
872         this.refresh_button_state();
873     },
875     /**
876      * JSON encode the current page data - stripping out drawable references which cannot be encoded.
877      * @protected
878      * @method stringify_current_page
879      * @return string
880      */
881     stringify_current_page: function() {
882         var comments = [],
883             annotations = [],
884             page,
885             i = 0;
887         for (i = 0; i < this.pages[this.currentpage].comments.length; i++) {
888             comments[i] = this.pages[this.currentpage].comments[i].clean();
889         }
890         for (i = 0; i < this.pages[this.currentpage].annotations.length; i++) {
891             annotations[i] = this.pages[this.currentpage].annotations[i].clean();
892         }
894         page = {comments: comments, annotations: annotations};
896         return Y.JSON.stringify(page);
897     },
899     /**
900      * Generate a drawable from the current in progress edit.
901      * @protected
902      * @method get_current_drawable
903      */
904     get_current_drawable: function() {
905         var comment,
906             annotation,
907             drawable = false;
909         if (!this.currentedit.start || !this.currentedit.end) {
910             return false;
911         }
913         if (this.currentedit.tool === 'comment') {
914             comment = new M.assignfeedback_editpdf.comment(this);
915             drawable = comment.draw_current_edit(this.currentedit);
916         } else {
917             annotation = this.create_annotation(this.currentedit.tool, {});
918             if (annotation) {
919                 drawable = annotation.draw_current_edit(this.currentedit);
920             }
921         }
923         return drawable;
924     },
926     /**
927      * Find an element within the dialogue.
928      * @protected
929      * @method get_dialogue_element
930      */
931     get_dialogue_element: function(selector) {
932         if (this.panel) {
933             return this.panel.one(selector);
934         } else {
935             return this.dialogue.get('boundingBox').one(selector);
936         }
937     },
939     /**
940      * Redraw the active edit.
941      * @protected
942      * @method redraw_active_edit
943      */
944     redraw_current_edit: function() {
945         if (this.currentdrawable) {
946             this.currentdrawable.erase();
947         }
948         this.currentdrawable = this.get_current_drawable();
949     },
951     /**
952      * Event handler for mousedown or touchstart.
953      * @protected
954      * @param Event
955      * @method edit_start
956      */
957     edit_start: function(e) {
958         var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
959             offset = canvas.getXY(),
960             scrolltop = canvas.get('docScrollY'),
961             scrollleft = canvas.get('docScrollX'),
962             point = {x: e.clientX - offset[0] + scrollleft,
963                      y: e.clientY - offset[1] + scrolltop},
964             selected = false;
966         // Ignore right mouse click.
967         if (e.button === 3) {
968             return;
969         }
971         if (this.currentedit.starttime) {
972             return;
973         }
975         if (this.editingcomment) {
976             return;
977         }
979         this.currentedit.starttime = new Date().getTime();
980         this.currentedit.start = point;
981         this.currentedit.end = {x: point.x, y: point.y};
983         if (this.currentedit.tool === 'select') {
984             var x = this.currentedit.end.x,
985                 y = this.currentedit.end.y,
986                 annotations = this.pages[this.currentpage].annotations;
987             // Find the first annotation whose bounds encompass the click.
988             Y.each(annotations, function(annotation) {
989                 if (((x - annotation.x) * (x - annotation.endx)) <= 0 &&
990                     ((y - annotation.y) * (y - annotation.endy)) <= 0) {
991                     selected = annotation;
992                 }
993             });
995             if (selected) {
996                 this.lastannotation = this.currentannotation;
997                 this.currentannotation = selected;
998                 if (this.lastannotation && this.lastannotation !== selected) {
999                     // Redraw the last selected annotation to remove the highlight.
1000                     if (this.lastannotation.drawable) {
1001                         this.lastannotation.drawable.erase();
1002                         this.drawables.push(this.lastannotation.draw());
1003                     }
1004                 }
1005                 // Redraw the newly selected annotation to show the highlight.
1006                 if (this.currentannotation.drawable) {
1007                     this.currentannotation.drawable.erase();
1008                 }
1009                 this.drawables.push(this.currentannotation.draw());
1010             } else {
1011                 this.lastannotation = this.currentannotation;
1012                 this.currentannotation = null;
1014                 // Redraw the last selected annotation to remove the highlight.
1015                 if (this.lastannotation && this.lastannotation.drawable) {
1016                     this.lastannotation.drawable.erase();
1017                     this.drawables.push(this.lastannotation.draw());
1018                 }
1019             }
1020         }
1021         if (this.currentannotation) {
1022             // Used to calculate drag offset.
1023             this.currentedit.annotationstart = {x: this.currentannotation.x,
1024                                                  y: this.currentannotation.y};
1025         }
1026     },
1028     /**
1029      * Event handler for mousemove.
1030      * @protected
1031      * @param Event
1032      * @method edit_move
1033      */
1034     edit_move: function(e) {
1035         e.preventDefault();
1036         var bounds = this.get_canvas_bounds(),
1037             canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
1038             drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION),
1039             clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'),
1040                                                              e.clientY + canvas.get('docScrollY')),
1041             point = this.get_canvas_coordinates(clientpoint),
1042             diffX,
1043             diffY;
1045         // Ignore events out of the canvas area.
1046         if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) {
1047             return;
1048         }
1050         if (this.currentedit.tool === 'pen') {
1051             this.currentedit.path.push(point);
1052         }
1054         if (this.currentedit.tool === 'select') {
1055             if (this.currentannotation && this.currentedit) {
1056                 this.currentannotation.move(this.currentedit.annotationstart.x + point.x - this.currentedit.start.x,
1057                                              this.currentedit.annotationstart.y + point.y - this.currentedit.start.y);
1058             }
1059         } else if (this.currentedit.tool === 'drag') {
1060             diffX = point.x - this.currentedit.start.x;
1061             diffY = point.y - this.currentedit.start.y;
1063             drawingregion.getDOMNode().scrollLeft -= diffX;
1064             drawingregion.getDOMNode().scrollTop -= diffY;
1066         } else {
1067             if (this.currentedit.start) {
1068                 this.currentedit.end = point;
1069                 this.redraw_current_edit();
1070             }
1071         }
1072     },
1074     /**
1075      * Event handler for mouseup or touchend.
1076      * @protected
1077      * @param Event
1078      * @method edit_end
1079      */
1080     edit_end: function() {
1081         var duration,
1082             comment,
1083             annotation;
1085         duration = new Date().getTime() - this.currentedit.start;
1087         if (duration < CLICKTIMEOUT || this.currentedit.start === false) {
1088             return;
1089         }
1091         if (this.currentedit.tool === 'comment') {
1092             if (this.currentdrawable) {
1093                 this.currentdrawable.erase();
1094             }
1095             this.currentdrawable = false;
1096             comment = new M.assignfeedback_editpdf.comment(this);
1097             if (comment.init_from_edit(this.currentedit)) {
1098                 this.pages[this.currentpage].comments.push(comment);
1099                 this.drawables.push(comment.draw(true));
1100                 this.editingcomment = true;
1101             }
1102         } else {
1103             annotation = this.create_annotation(this.currentedit.tool, {});
1104             if (annotation) {
1105                 if (this.currentdrawable) {
1106                     this.currentdrawable.erase();
1107                 }
1108                 this.currentdrawable = false;
1109                 if (annotation.init_from_edit(this.currentedit)) {
1110                     this.pages[this.currentpage].annotations.push(annotation);
1111                     this.drawables.push(annotation.draw());
1112                 }
1113             }
1114         }
1116         // Save the changes.
1117         this.save_current_page();
1119         // Reset the current edit.
1120         this.currentedit.starttime = 0;
1121         this.currentedit.start = false;
1122         this.currentedit.end = false;
1123         this.currentedit.path = [];
1124     },
1126     /**
1127      * Resize the dialogue window when the browser is resized.
1128      * @public
1129      * @method resize
1130      */
1131     resize: function() {
1132         var drawingregion, drawregionheight;
1134         if (this.dialogue) {
1135             if (!this.dialogue.get('visible')) {
1136                 return;
1137             }
1138             this.dialogue.centerDialogue();
1139         }
1141         // Make sure the dialogue box is not bigger than the max height of the viewport.
1142         drawregionheight = Y.one('body').get('winHeight') - 120; // Space for toolbar + titlebar.
1143         if (drawregionheight < 100) {
1144             drawregionheight = 100;
1145         }
1146         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
1147         if (this.dialogue) {
1148             drawingregion.setStyle('maxHeight', drawregionheight + 'px');
1149         }
1150         this.redraw();
1151         return true;
1152     },
1154     /**
1155      * Factory method for creating annotations of the correct subclass.
1156      * @public
1157      * @method create_annotation
1158      */
1159     create_annotation: function(type, data) {
1160         data.type = type;
1161         data.editor = this;
1162         if (type === "line") {
1163             return new M.assignfeedback_editpdf.annotationline(data);
1164         } else if (type === "rectangle") {
1165             return new M.assignfeedback_editpdf.annotationrectangle(data);
1166         } else if (type === "oval") {
1167             return new M.assignfeedback_editpdf.annotationoval(data);
1168         } else if (type === "pen") {
1169             return new M.assignfeedback_editpdf.annotationpen(data);
1170         } else if (type === "highlight") {
1171             return new M.assignfeedback_editpdf.annotationhighlight(data);
1172         } else if (type === "stamp") {
1173             return new M.assignfeedback_editpdf.annotationstamp(data);
1174         }
1175         return false;
1176     },
1178     /**
1179      * Save all the annotations and comments for the current page.
1180      * @protected
1181      * @method save_current_page
1182      */
1183     save_current_page: function() {
1184         if (this.get('destroyed')) {
1185             return;
1186         }
1187         var ajaxurl = AJAXBASE,
1188             config;
1190         config = {
1191             method: 'post',
1192             context: this,
1193             sync: false,
1194             data: {
1195                 'sesskey': M.cfg.sesskey,
1196                 'action': 'savepage',
1197                 'index': this.currentpage,
1198                 'userid': this.get('userid'),
1199                 'attemptnumber': this.get('attemptnumber'),
1200                 'assignmentid': this.get('assignmentid'),
1201                 'page': this.stringify_current_page()
1202             },
1203             on: {
1204                 success: function(tid, response) {
1205                     var jsondata;
1206                     try {
1207                         jsondata = Y.JSON.parse(response.responseText);
1208                         if (jsondata.error) {
1209                             return new M.core.ajaxException(jsondata);
1210                         }
1211                         Y.one(SELECTOR.UNSAVEDCHANGESINPUT).set('value', 'true');
1212                         Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('opacity', 1);
1213                         Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('display', 'inline-block');
1214                         Y.one(SELECTOR.UNSAVEDCHANGESDIV).transition({
1215                             duration: 1,
1216                             delay: 2,
1217                             opacity: 0
1218                         }, function() {
1219                             Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('display', 'none');
1220                         });
1221                     } catch (e) {
1222                         return new M.core.exception(e);
1223                     }
1224                 },
1225                 failure: function(tid, response) {
1226                     return new M.core.exception(response.responseText);
1227                 }
1228             }
1229         };
1231         Y.io(ajaxurl, config);
1232     },
1234     /**
1235      * Event handler to open the comment search interface.
1236      *
1237      * @param Event e
1238      * @protected
1239      * @method open_search_comments
1240      */
1241     open_search_comments: function(e) {
1242         if (!this.searchcommentswindow) {
1243             this.searchcommentswindow = new M.assignfeedback_editpdf.commentsearch({
1244                 editor: this
1245             });
1246         }
1248         this.searchcommentswindow.show();
1249         e.preventDefault();
1250     },
1252     /**
1253      * Toggle function to expand/collapse all comments on page.
1254      *
1255      * @protected
1256      * @method expandCollapseComments
1257      */
1258     expandCollapseComments: function() {
1259         var comments = Y.all('.commentdrawable');
1261         if (this.collapsecomments) {
1262             this.collapsecomments = false;
1263             comments.removeClass('commentcollapsed');
1264         } else {
1265             this.collapsecomments = true;
1266             comments.addClass('commentcollapsed');
1267         }
1268     },
1270     /**
1271      * Redraw all the comments and annotations.
1272      * @protected
1273      * @method redraw
1274      */
1275     redraw: function() {
1276         var i,
1277             page;
1279         page = this.pages[this.currentpage];
1280         if (page === undefined) {
1281             return; // Can happen if a redraw is triggered by an event, before the page has been selected.
1282         }
1283         while (this.drawables.length > 0) {
1284             this.drawables.pop().erase();
1285         }
1287         for (i = 0; i < page.annotations.length; i++) {
1288             this.drawables.push(page.annotations[i].draw());
1289         }
1290         for (i = 0; i < page.comments.length; i++) {
1291             this.drawables.push(page.comments[i].draw(false));
1292         }
1293     },
1295     /**
1296      * Load the image for this pdf page and remove the loading icon (if there).
1297      * @protected
1298      * @method change_page
1299      */
1300     change_page: function() {
1301         var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
1302             page,
1303             previousbutton,
1304             nextbutton;
1306         previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1307         nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
1309         if (this.currentpage > 0) {
1310             previousbutton.removeAttribute('disabled');
1311         } else {
1312             previousbutton.setAttribute('disabled', 'true');
1313         }
1314         if (this.currentpage < (this.pagecount - 1)) {
1315             nextbutton.removeAttribute('disabled');
1316         } else {
1317             nextbutton.setAttribute('disabled', 'true');
1318         }
1320         page = this.pages[this.currentpage];
1321         this.loadingicon.hide();
1322         drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
1323         drawingcanvas.setStyle('width', page.width + 'px');
1324         drawingcanvas.setStyle('height', page.height + 'px');
1326         // Update page select.
1327         this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
1329         this.resize(); // Internally will call 'redraw', after checking the dialogue size.
1330     },
1332     /**
1333      * Now we know how many pages there are,
1334      * we can enable the navigation controls.
1335      * @protected
1336      * @method setup_navigation
1337      */
1338     setup_navigation: function() {
1339         var pageselect,
1340             i,
1341             strinfo,
1342             option,
1343             previousbutton,
1344             nextbutton;
1346         pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
1348         var options = pageselect.all('option');
1349         if (options.size() <= 1) {
1350             for (i = 0; i < this.pages.length; i++) {
1351                 option = Y.Node.create('<option/>');
1352                 option.setAttribute('value', i);
1353                 strinfo = {page: i + 1, total: this.pages.length};
1354                 option.setHTML(M.util.get_string('pagexofy', 'assignfeedback_editpdf', strinfo));
1355                 pageselect.append(option);
1356             }
1357         }
1358         pageselect.removeAttribute('disabled');
1359         pageselect.on('change', function() {
1360             this.currentpage = pageselect.get('value');
1361             this.change_page();
1362         }, this);
1364         previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1365         nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
1367         previousbutton.on('click', this.previous_page, this);
1368         previousbutton.on('key', this.previous_page, 'down:13', this);
1369         nextbutton.on('click', this.next_page, this);
1370         nextbutton.on('key', this.next_page, 'down:13', this);
1371     },
1373     /**
1374      * Navigate to the previous page.
1375      * @protected
1376      * @method previous_page
1377      */
1378     previous_page: function(e) {
1379         e.preventDefault();
1380         this.currentpage--;
1381         if (this.currentpage < 0) {
1382             this.currentpage = 0;
1383         }
1384         this.change_page();
1385     },
1387     /**
1388      * Navigate to the next page.
1389      * @protected
1390      * @method next_page
1391      */
1392     next_page: function(e) {
1393         e.preventDefault();
1394         this.currentpage++;
1395         if (this.currentpage >= this.pages.length) {
1396             this.currentpage = this.pages.length - 1;
1397         }
1398         this.change_page();
1399     },
1401     /**
1402      * Update any absolutely positioned nodes, within each drawable, when the drawing canvas is scrolled
1403      * @protected
1404      * @method move_canvas
1405      */
1406     move_canvas: function() {
1407         var drawingregion, x, y, i;
1409         drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
1410         x = parseInt(drawingregion.get('scrollLeft'), 10);
1411         y = parseInt(drawingregion.get('scrollTop'), 10);
1413         for (i = 0; i < this.drawables.length; i++) {
1414             this.drawables[i].scroll_update(x, y);
1415         }
1416     },
1418     /**
1419      * Test the browser support for options objects on event listeners.
1420      * @return Boolean
1421      */
1422     event_listener_options_supported: function() {
1423         var passivesupported = false,
1424             options,
1425             testeventname = "testpassiveeventoptions";
1427         // Options support testing example from:
1428         // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
1430         try {
1431             options = Object.defineProperty({}, "passive", {
1432                 get: function() {
1433                     passivesupported = true;
1434                 }
1435             });
1437             // We use an event name that is not likely to conflict with any real event.
1438             document.addEventListener(testeventname, options, options);
1439             // We remove the event listener as we have tested the options already.
1440             document.removeEventListener(testeventname, options, options);
1441         } catch(err) {
1442             // It's already false.
1443             passivesupported = false;
1444         }
1445         return passivesupported;
1446     },
1448     /**
1449      * Disable Touch Move scrolling
1450      */
1451     disable_touch_scroll: function() {
1452         if (this.event_listener_options_supported()) {
1453             document.addEventListener('touchmove', this.stop_touch_scroll, {passive: false});
1454         }
1455     },
1457     /**
1458      * Stop Touch Scrolling
1459      * @param {Object} e
1460      */
1461     stop_touch_scroll: function(e) {
1462         e.stopPropagation();
1463         e.preventDefault();
1464     }
1466 };
1468 Y.extend(EDITOR, Y.Base, EDITOR.prototype, {
1469     NAME: 'moodle-assignfeedback_editpdf-editor',
1470     ATTRS: {
1471         userid: {
1472             validator: Y.Lang.isInteger,
1473             value: 0
1474         },
1475         assignmentid: {
1476             validator: Y.Lang.isInteger,
1477             value: 0
1478         },
1479         attemptnumber: {
1480             validator: Y.Lang.isInteger,
1481             value: 0
1482         },
1483         header: {
1484             validator: Y.Lang.isString,
1485             value: ''
1486         },
1487         body: {
1488             validator: Y.Lang.isString,
1489             value: ''
1490         },
1491         footer: {
1492             validator: Y.Lang.isString,
1493             value: ''
1494         },
1495         linkid: {
1496             validator: Y.Lang.isString,
1497             value: ''
1498         },
1499         deletelinkid: {
1500             validator: Y.Lang.isString,
1501             value: ''
1502         },
1503         readonly: {
1504             validator: Y.Lang.isBoolean,
1505             value: true
1506         },
1507         stampfiles: {
1508             validator: Y.Lang.isArray,
1509             value: ''
1510         }
1511     }
1512 });
1514 M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
1515 M.assignfeedback_editpdf.editor = M.assignfeedback_editpdf.editor || {};
1517 /**
1518  * Init function - will create a new instance every time.
1519  * @method editor.init
1520  * @static
1521  * @param {Object} params
1522  */
1523 M.assignfeedback_editpdf.editor.init = M.assignfeedback_editpdf.editor.init || function(params) {
1524     if (typeof M.assignfeedback_editpdf.instance !== 'undefined') {
1525         M.assignfeedback_editpdf.instance.destroy();
1526     }
1528     M.assignfeedback_editpdf.instance = new EDITOR(params);
1529     return M.assignfeedback_editpdf.instance;
1530 };