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