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