MDL-55528 fileconverter_unoconv: Add new unoconv plugin
[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
DW
82
83 /**
84 * The yui node for the loading icon.
1f777e5c
AN
85 *
86 * @property loadingicon
87 * @type Node
5c386472
DW
88 * @protected
89 */
5bb4f444 90 loadingicon: null,
5c386472
DW
91
92 /**
93 * Image object of the current page image.
1f777e5c
AN
94 *
95 * @property pageimage
5c386472
DW
96 * @type Image
97 * @protected
98 */
5bb4f444 99 pageimage: null,
5c386472
DW
100
101 /**
102 * YUI Graphic class for drawing shapes.
1f777e5c
AN
103 *
104 * @property graphic
105 * @type Graphic
5c386472
DW
106 * @protected
107 */
5bb4f444 108 graphic: null,
5c386472
DW
109
110 /**
111 * Info about the current edit operation.
1f777e5c 112 *
5c386472
DW
113 * @property currentedit
114 * @type M.assignfeedback_editpdf.edit
115 * @protected
116 */
5bb4f444 117 currentedit: new M.assignfeedback_editpdf.edit(),
5c386472
DW
118
119 /**
120 * Current drawable.
1f777e5c 121 *
5c386472 122 * @property currentdrawable
1f777e5c 123 * @type M.assignfeedback_editpdf.drawable|false
5c386472
DW
124 * @protected
125 */
5bb4f444 126 currentdrawable: false,
5c386472
DW
127
128 /**
129 * Current drawables.
1f777e5c 130 *
5c386472
DW
131 * @property drawables
132 * @type array(M.assignfeedback_editpdf.drawable)
133 * @protected
134 */
5bb4f444 135 drawables: [],
5c386472
DW
136
137 /**
138 * Current comment when the comment menu is open.
139 * @property currentcomment
140 * @type M.assignfeedback_editpdf.comment
141 * @protected
142 */
5bb4f444 143 currentcomment: null,
5c386472
DW
144
145 /**
146 * Current annotation when the select tool is used.
147 * @property currentannotation
148 * @type M.assignfeedback_editpdf.annotation
149 * @protected
150 */
5bb4f444 151 currentannotation: null,
5c386472
DW
152
153 /**
154 * Last selected annotation tool
155 * @property lastannotationtool
156 * @type String
157 * @protected
158 */
7962b484 159 lastannotationtool: "pen",
5c386472
DW
160
161 /**
162 * The users comments quick list
163 * @property quicklist
164 * @type M.assignfeedback_editpdf.quickcommentlist
165 * @protected
166 */
5bb4f444 167 quicklist: null,
5c386472
DW
168
169 /**
170 * The search comments window.
171 * @property searchcommentswindow
172 * @type M.core.dialogue
173 * @protected
174 */
5bb4f444 175 searchcommentswindow: null,
5c386472
DW
176
177
178 /**
179 * The selected stamp picture.
180 * @property currentstamp
181 * @type String
182 * @protected
183 */
5bb4f444 184 currentstamp: null,
5c386472
DW
185
186 /**
187 * The stamps.
188 * @property stamps
189 * @type Array
190 * @protected
191 */
5bb4f444 192 stamps: [],
5c386472 193
60b87080
DS
194 /**
195 * Prevent new comments from appearing
196 * immediately after clicking off a current
197 * comment
198 * @property editingcomment
199 * @type Boolean
200 * @public
201 */
5bb4f444 202 editingcomment: false,
60b87080 203
5c386472
DW
204 /**
205 * Called during the initialisation process of the object.
206 * @method initializer
207 */
5bb4f444 208 initializer: function() {
5c386472
DW
209 var link;
210
211 link = Y.one('#' + this.get('linkid'));
212
213 if (link) {
214 link.on('click', this.link_handler, this);
215 link.on('key', this.link_handler, 'down:13', this);
216
bb690849
DW
217 // We call the amd module to see if we can take control of the review panel.
218 require(['mod_assign/grading_review_panel'], function(ReviewPanelManager) {
219 var panelManager = new ReviewPanelManager();
220
221 var panel = panelManager.getReviewPanel('assignfeedback_editpdf');
222 if (panel) {
223 panel = Y.one(panel);
224 panel.empty();
225 link.ancestor('.fitem').hide();
226 this.open_in_panel(panel);
227 }
228 this.currentedit.start = false;
229 this.currentedit.end = false;
230 if (!this.get('readonly')) {
231 this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this);
232 }
233 }.bind(this));
234
5c386472
DW
235 }
236 },
237
238 /**
239 * Called to show/hide buttons and set the current colours/stamps.
240 * @method refresh_button_state
241 */
5bb4f444 242 refresh_button_state: function() {
ab4d78ea 243 var button, currenttoolnode, imgurl, drawingregion;
5c386472
DW
244
245 // Initalise the colour buttons.
da91de78 246 button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
5c386472
DW
247
248 imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf');
249 button.one('img').setAttribute('src', imgurl);
250
251 if (this.currentedit.commentcolour === 'clear') {
252 button.one('img').setStyle('borderStyle', 'dashed');
253 } else {
254 button.one('img').setStyle('borderStyle', 'solid');
255 }
256
da91de78 257 button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
5c386472
DW
258 imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf');
259 button.one('img').setAttribute('src', imgurl);
260
da91de78 261 currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
5c386472
DW
262 currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton');
263 currenttoolnode.setAttribute('aria-pressed', 'true');
ab4d78ea
DW
264 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
265 drawingregion.setAttribute('data-currenttool', this.currentedit.tool);
5c386472 266
da91de78 267 button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
5c386472
DW
268 button.one('img').setAttrs({'src': this.get_stamp_image_url(this.currentedit.stamp),
269 'height': '16',
270 'width': '16'});
271 },
272
273 /**
274 * Called to get the bounds of the drawing region.
275 * @method get_canvas_bounds
276 */
5bb4f444 277 get_canvas_bounds: function() {
da91de78 278 var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
279 offsetcanvas = canvas.getXY(),
280 offsetleft = offsetcanvas[0],
281 offsettop = offsetcanvas[1],
282 width = parseInt(canvas.getStyle('width'), 10),
283 height = parseInt(canvas.getStyle('height'), 10);
284
285 return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height);
286 },
287
288 /**
289 * Called to translate from window coordinates to canvas coordinates.
290 * @method get_canvas_coordinates
291 * @param M.assignfeedback_editpdf.point point in window coordinats.
292 */
5bb4f444 293 get_canvas_coordinates: function(point) {
5c386472
DW
294 var bounds = this.get_canvas_bounds(),
295 newpoint = new M.assignfeedback_editpdf.point(point.x - bounds.x, point.y - bounds.y);
296
297 bounds.x = bounds.y = 0;
298
299 newpoint.clip(bounds);
300 return newpoint;
301 },
302
303 /**
304 * Called to translate from canvas coordinates to window coordinates.
305 * @method get_window_coordinates
306 * @param M.assignfeedback_editpdf.point point in window coordinats.
307 */
5bb4f444 308 get_window_coordinates: function(point) {
5c386472
DW
309 var bounds = this.get_canvas_bounds(),
310 newpoint = new M.assignfeedback_editpdf.point(point.x + bounds.x, point.y + bounds.y);
311
312 return newpoint;
313 },
314
bb690849
DW
315 /**
316 * Open the edit-pdf editor in the panel in the page instead of a popup.
317 * @method open_in_panel
318 */
5bb4f444 319 open_in_panel: function(panel) {
bb690849
DW
320 var drawingcanvas, drawingregion;
321
322 this.panel = panel;
323 panel.append(this.get('body'));
324 panel.addClass(CSS.DIALOGUE);
325
326 this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
327
328 drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
5bb4f444 329 this.graphic = new Y.Graphic({render: drawingcanvas});
bb690849
DW
330
331 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
332 drawingregion.on('scroll', this.move_canvas, this);
333
334 if (!this.get('readonly')) {
335 drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
336 drawingcanvas.on('gesturemove', this.edit_move, null, this);
337 drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
338
339 this.refresh_button_state();
340 }
341
342 this.load_all_pages();
343 },
344
5c386472
DW
345 /**
346 * Called to open the pdf editing dialogue.
347 * @method link_handler
348 */
5bb4f444 349 link_handler: function(e) {
bc8b6dc6
DP
350 var drawingcanvas, drawingregion;
351 var resize = true;
5c386472
DW
352 e.preventDefault();
353
354 if (!this.dialogue) {
355 this.dialogue = new M.core.dialogue({
356 headerContent: this.get('header'),
357 bodyContent: this.get('body'),
358 footerContent: this.get('footer'),
aebc768f 359 modal: true,
5c386472 360 width: '840px',
6d87dceb
DW
361 visible: false,
362 draggable: true
5c386472
DW
363 });
364
5c386472
DW
365 // Add custom class for styling.
366 this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE);
367
da91de78 368 this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);
5c386472 369
da91de78 370 drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
5bb4f444 371 this.graphic = new Y.Graphic({render: drawingcanvas});
5c386472 372
da91de78 373 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
50c12f01
DS
374 drawingregion.on('scroll', this.move_canvas, this);
375
5c386472
DW
376 if (!this.get('readonly')) {
377 drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
378 drawingcanvas.on('gesturemove', this.edit_move, null, this);
379 drawingcanvas.on('gesturemoveend', this.edit_end, null, this);
380
381 this.refresh_button_state();
382 }
d40ce26f
JM
383
384 this.load_all_pages();
50c12f01
DS
385 drawingcanvas.on('windowresize', this.resize, this);
386
387 resize = false;
5c386472 388 }
6d87dceb
DW
389 this.dialogue.centerDialogue();
390 this.dialogue.show();
50c12f01
DS
391
392 // Redraw when the dialogue is moved, to ensure the absolute elements are all positioned correctly.
393 this.dialogue.dd.on('drag:end', this.redraw, this);
394 if (resize) {
395 this.resize(); // When re-opening the dialog call redraw, to make sure the size + layout is correct.
396 }
5c386472
DW
397 },
398
399 /**
400 * Called to load the information and annotations for all pages.
401 * @method load_all_pages
402 */
5bb4f444 403 load_all_pages: function() {
5c386472 404 var ajaxurl = AJAXBASE,
aa3e4bde
JM
405 config,
406 checkconversionstatus,
407 ajax_error_total;
5c386472
DW
408
409 config = {
410 method: 'get',
411 context: this,
412 sync: false,
5bb4f444
DP
413 data: {
414 sesskey: M.cfg.sesskey,
415 action: 'loadallpages',
416 userid: this.get('userid'),
417 attemptnumber: this.get('attemptnumber'),
418 assignmentid: this.get('assignmentid'),
419 readonly: this.get('readonly') ? 1 : 0
5c386472
DW
420 },
421 on: {
422 success: function(tid, response) {
423 this.all_pages_loaded(response.responseText);
424 },
425 failure: function(tid, response) {
aa3e4bde 426 return new M.core.exception(response.responseText);
5c386472
DW
427 }
428 }
429 };
430
431 Y.io(ajaxurl, config);
aa3e4bde
JM
432
433 // If pages are not loaded, check PDF conversion status for the progress bar.
434 if (this.pagecount <= 0) {
435 checkconversionstatus = {
436 method: 'get',
437 context: this,
438 sync: false,
5bb4f444
DP
439 data: {
440 sesskey: M.cfg.sesskey,
441 action: 'conversionstatus',
442 userid: this.get('userid'),
443 attemptnumber: this.get('attemptnumber'),
444 assignmentid: this.get('assignmentid')
aa3e4bde
JM
445 },
446 on: {
447 success: function(tid, response) {
448 ajax_error_total = 0;
449 if (this.pagecount === 0) {
450 var pagetotal = this.get('pagetotal');
451
452 // Update the progress bar.
da91de78 453 var progressbarcontainer = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER);
aa3e4bde
JM
454 var progressbar = progressbarcontainer.one('.bar');
455 if (progressbar) {
456 // Calculate progress.
457 var progress = (response.response / pagetotal) * 100;
458 progressbar.setStyle('width', progress + '%');
459 progressbarcontainer.setAttribute('aria-valuenow', progress);
460 }
461
462 // New ajax request delayed of a second.
26ca972e 463 M.util.js_pending('checkconversionstatus');
5bb4f444 464 Y.later(1000, this, function() {
26ca972e 465 M.util.js_complete('checkconversionstatus');
aa3e4bde
JM
466 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
467 });
468 }
469 },
470 failure: function(tid, response) {
471 ajax_error_total = ajax_error_total + 1;
472 // We only continue on error if the all pages were not generated,
473 // and if the ajax call did not produce 5 errors in the row.
474 if (this.pagecount === 0 && ajax_error_total < 5) {
26ca972e 475 M.util.js_pending('checkconversionstatus');
5bb4f444 476 Y.later(1000, this, function() {
26ca972e 477 M.util.js_complete('checkconversionstatus');
aa3e4bde
JM
478 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
479 });
480 }
481 return new M.core.exception(response.responseText);
482 }
483 }
484 };
485 // We start the AJAX "generated page total number" call a second later to give a chance to
486 // the AJAX "combined pdf generation" call to clean the previous submission images.
26ca972e 487 M.util.js_pending('checkconversionstatus');
5bb4f444 488 Y.later(1000, this, function() {
aa3e4bde 489 ajax_error_total = 0;
26ca972e 490 M.util.js_complete('checkconversionstatus');
aa3e4bde
JM
491 Y.io(AJAXBASEPROGRESS, checkconversionstatus);
492 });
493 }
5c386472
DW
494 },
495
496 /**
497 * The info about all pages in the pdf has been returned.
498 * @param string The ajax response as text.
499 * @protected
500 * @method all_pages_loaded
501 */
5bb4f444 502 all_pages_loaded: function(responsetext) {
5c386472
DW
503 var data, i, j, comment, error;
504 try {
505 data = Y.JSON.parse(responsetext);
506 if (data.error || !data.pagecount) {
bb690849
DW
507 if (this.dialogue) {
508 this.dialogue.hide();
509 }
5c386472 510 // Display alert dialogue.
5bb4f444 511 error = new M.core.alert({message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')});
5c386472
DW
512 error.show();
513 return;
514 }
515 } catch (e) {
bb690849
DW
516 if (this.dialogue) {
517 this.dialogue.hide();
518 }
5c386472 519 // Display alert dialogue.
5bb4f444 520 error = new M.core.alert({title: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')});
5c386472
DW
521 error.show();
522 return;
523 }
524
525 this.pagecount = data.pagecount;
526 this.pages = data.pages;
527
528 for (i = 0; i < this.pages.length; i++) {
529 for (j = 0; j < this.pages[i].comments.length; j++) {
530 comment = this.pages[i].comments[j];
531 this.pages[i].comments[j] = new M.assignfeedback_editpdf.comment(this,
532 comment.gradeid,
533 comment.pageno,
534 comment.x,
535 comment.y,
536 comment.width,
537 comment.colour,
538 comment.rawtext);
539 }
540 for (j = 0; j < this.pages[i].annotations.length; j++) {
541 data = this.pages[i].annotations[j];
542 this.pages[i].annotations[j] = this.create_annotation(data.type, data);
543 }
544 }
545
546 // Update the ui.
8ae76e13
AN
547 if (this.quicklist) {
548 this.quicklist.load();
549 }
5c386472
DW
550 this.setup_navigation();
551 this.setup_toolbar();
552 this.change_page();
553 },
554
555 /**
556 * Get the full pluginfile url for an image file - just given the filename.
557 *
558 * @public
559 * @method get_stamp_image_url
560 * @param string filename
561 */
5bb4f444 562 get_stamp_image_url: function(filename) {
5c386472
DW
563 var urls = this.get('stampfiles'),
564 fullurl = '';
565
566 Y.Array.each(urls, function(url) {
567 if (url.indexOf(filename) > 0) {
568 fullurl = url;
569 }
570 }, this);
571
572 return fullurl;
573 },
574
575 /**
576 * Attach listeners and enable the color picker buttons.
577 * @protected
578 * @method setup_toolbar
579 */
5bb4f444 580 setup_toolbar: function() {
5c386472
DW
581 var toolnode,
582 commentcolourbutton,
583 annotationcolourbutton,
584 searchcommentsbutton,
585 currentstampbutton,
586 stampfiles,
587 picker,
588 filename;
589
da91de78 590 searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON);
5c386472
DW
591 searchcommentsbutton.on('click', this.open_search_comments, this);
592 searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this);
593
594 if (this.get('readonly')) {
595 return;
596 }
597 // Setup the tool buttons.
598 Y.each(TOOLSELECTOR, function(selector, tool) {
da91de78 599 toolnode = this.get_dialogue_element(selector);
5c386472
DW
600 toolnode.on('click', this.handle_tool_button, this, tool);
601 toolnode.on('key', this.handle_tool_button, 'down:13', this, tool);
602 toolnode.setAttribute('aria-pressed', 'false');
603 }, this);
604
605 // Set the default tool.
606
da91de78 607 commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
5c386472
DW
608 picker = new M.assignfeedback_editpdf.colourpicker({
609 buttonNode: commentcolourbutton,
610 colours: COMMENTCOLOUR,
611 iconprefix: 'background_colour_',
5bb4f444 612 callback: function(e) {
5c386472
DW
613 var colour = e.target.getAttribute('data-colour');
614 if (!colour) {
615 colour = e.target.ancestor().getAttribute('data-colour');
616 }
617 this.currentedit.commentcolour = colour;
618 this.handle_tool_button(e, "comment");
619 },
620 context: this
621 });
622
da91de78 623 annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
5c386472
DW
624 picker = new M.assignfeedback_editpdf.colourpicker({
625 buttonNode: annotationcolourbutton,
626 iconprefix: 'colour_',
627 colours: ANNOTATIONCOLOUR,
5bb4f444 628 callback: function(e) {
5c386472
DW
629 var colour = e.target.getAttribute('data-colour');
630 if (!colour) {
631 colour = e.target.ancestor().getAttribute('data-colour');
632 }
633 this.currentedit.annotationcolour = colour;
634 if (this.lastannotationtool) {
635 this.handle_tool_button(e, this.lastannotationtool);
636 } else {
637 this.handle_tool_button(e, "pen");
638 }
639 },
640 context: this
641 });
642
643 stampfiles = this.get('stampfiles');
644 if (stampfiles.length <= 0) {
da91de78 645 this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide();
5c386472
DW
646 } else {
647 filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1);
648 this.currentedit.stamp = filename;
da91de78 649 currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
5c386472
DW
650
651 picker = new M.assignfeedback_editpdf.stamppicker({
652 buttonNode: currentstampbutton,
653 stamps: stampfiles,
654 callback: function(e) {
655 var stamp = e.target.getAttribute('data-stamp'),
656 filename;
657
658 if (!stamp) {
659 stamp = e.target.ancestor().getAttribute('data-stamp');
660 }
661 filename = stamp.substr(stamp.lastIndexOf('/'));
662 this.currentedit.stamp = filename;
673c4ffe 663 this.handle_tool_button(e, "stamp");
5c386472
DW
664 },
665 context: this
666 });
667 this.refresh_button_state();
668 }
669 },
670
671 /**
672 * Change the current tool.
673 * @protected
674 * @method handle_tool_button
675 */
5bb4f444 676 handle_tool_button: function(e, tool) {
5c386472
DW
677 var currenttoolnode;
678
679 e.preventDefault();
680
681 // Change style of the pressed button.
da91de78 682 currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
5c386472
DW
683 currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton');
684 currenttoolnode.setAttribute('aria-pressed', 'false');
685 this.currentedit.tool = tool;
ab4d78ea 686
55907a73 687 if (tool !== "comment" && tool !== "select" && tool !== "drag" && tool !== "stamp") {
5c386472
DW
688 this.lastannotationtool = tool;
689 }
690 this.refresh_button_state();
691 },
692
693 /**
694 * JSON encode the current page data - stripping out drawable references which cannot be encoded.
695 * @protected
696 * @method stringify_current_page
697 * @return string
698 */
5bb4f444 699 stringify_current_page: function() {
5c386472
DW
700 var comments = [],
701 annotations = [],
702 page,
703 i = 0;
704
705 for (i = 0; i < this.pages[this.currentpage].comments.length; i++) {
706 comments[i] = this.pages[this.currentpage].comments[i].clean();
707 }
708 for (i = 0; i < this.pages[this.currentpage].annotations.length; i++) {
709 annotations[i] = this.pages[this.currentpage].annotations[i].clean();
710 }
711
5bb4f444 712 page = {comments: comments, annotations: annotations};
5c386472
DW
713
714 return Y.JSON.stringify(page);
715 },
716
717 /**
718 * Generate a drawable from the current in progress edit.
719 * @protected
720 * @method get_current_drawable
721 */
5bb4f444 722 get_current_drawable: function() {
5c386472
DW
723 var comment,
724 annotation,
725 drawable = false;
726
727 if (!this.currentedit.start || !this.currentedit.end) {
728 return false;
729 }
730
731 if (this.currentedit.tool === 'comment') {
732 comment = new M.assignfeedback_editpdf.comment(this);
733 drawable = comment.draw_current_edit(this.currentedit);
734 } else {
735 annotation = this.create_annotation(this.currentedit.tool, {});
736 if (annotation) {
737 drawable = annotation.draw_current_edit(this.currentedit);
738 }
739 }
740
741 return drawable;
742 },
743
da91de78
BL
744 /**
745 * Find an element within the dialogue.
746 * @protected
747 * @method get_dialogue_element
748 */
5bb4f444 749 get_dialogue_element: function(selector) {
bb690849
DW
750 if (this.panel) {
751 return this.panel.one(selector);
752 } else {
753 return this.dialogue.get('boundingBox').one(selector);
754 }
da91de78 755 },
5c386472
DW
756
757 /**
758 * Redraw the active edit.
759 * @protected
760 * @method redraw_active_edit
761 */
5bb4f444 762 redraw_current_edit: function() {
5c386472
DW
763 if (this.currentdrawable) {
764 this.currentdrawable.erase();
765 }
766 this.currentdrawable = this.get_current_drawable();
767 },
768
769 /**
770 * Event handler for mousedown or touchstart.
771 * @protected
772 * @param Event
773 * @method edit_start
774 */
5bb4f444 775 edit_start: function(e) {
ce091776 776 e.preventDefault();
da91de78 777 var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
778 offset = canvas.getXY(),
779 scrolltop = canvas.get('docScrollY'),
780 scrollleft = canvas.get('docScrollX'),
5bb4f444
DP
781 point = {x: e.clientX - offset[0] + scrollleft,
782 y: e.clientY - offset[1] + scrolltop},
5c386472
DW
783 selected = false,
784 lastannotation;
785
786 // Ignore right mouse click.
787 if (e.button === 3) {
788 return;
789 }
790
791 if (this.currentedit.starttime) {
792 return;
793 }
794
60b87080
DS
795 if (this.editingcomment) {
796 return;
797 }
798
5c386472
DW
799 this.currentedit.starttime = new Date().getTime();
800 this.currentedit.start = point;
5bb4f444 801 this.currentedit.end = {x: point.x, y: point.y};
5c386472
DW
802
803 if (this.currentedit.tool === 'select') {
557f44d9
AN
804 var x = this.currentedit.end.x,
805 y = this.currentedit.end.y,
806 annotations = this.pages[this.currentpage].annotations;
5c386472
DW
807 // Find the first annotation whose bounds encompass the click.
808 Y.each(annotations, function(annotation) {
809 if (((x - annotation.x) * (x - annotation.endx)) <= 0 &&
810 ((y - annotation.y) * (y - annotation.endy)) <= 0) {
811 selected = annotation;
812 }
813 });
814
815 if (selected) {
816 lastannotation = this.currentannotation;
817 this.currentannotation = selected;
818 if (lastannotation && lastannotation !== selected) {
819 // Redraw the last selected annotation to remove the highlight.
820 if (lastannotation.drawable) {
821 lastannotation.drawable.erase();
822 this.drawables.push(lastannotation.draw());
823 }
824 }
825 // Redraw the newly selected annotation to show the highlight.
826 if (this.currentannotation.drawable) {
827 this.currentannotation.drawable.erase();
828 }
829 this.drawables.push(this.currentannotation.draw());
830 }
831 }
832 if (this.currentannotation) {
833 // Used to calculate drag offset.
5bb4f444
DP
834 this.currentedit.annotationstart = {x: this.currentannotation.x,
835 y: this.currentannotation.y};
5c386472
DW
836 }
837 },
838
839 /**
840 * Event handler for mousemove.
841 * @protected
842 * @param Event
843 * @method edit_move
844 */
5bb4f444 845 edit_move: function(e) {
ce091776 846 e.preventDefault();
5c386472 847 var bounds = this.get_canvas_bounds(),
da91de78 848 canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
55907a73 849 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION),
5c386472
DW
850 clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'),
851 e.clientY + canvas.get('docScrollY')),
55907a73
DW
852 point = this.get_canvas_coordinates(clientpoint),
853 diffX,
854 diffY;
5c386472
DW
855
856 // Ignore events out of the canvas area.
857 if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) {
858 return;
859 }
860
861 if (this.currentedit.tool === 'pen') {
862 this.currentedit.path.push(point);
863 }
864
865 if (this.currentedit.tool === 'select') {
866 if (this.currentannotation && this.currentedit) {
5bb4f444 867 this.currentannotation.move(this.currentedit.annotationstart.x + point.x - this.currentedit.start.x,
5c386472
DW
868 this.currentedit.annotationstart.y + point.y - this.currentedit.start.y);
869 }
55907a73
DW
870 } else if (this.currentedit.tool === 'drag') {
871 diffX = point.x - this.currentedit.start.x;
872 diffY = point.y - this.currentedit.start.y;
873
874 drawingregion.getDOMNode().scrollLeft -= diffX;
875 drawingregion.getDOMNode().scrollTop -= diffY;
876
5c386472
DW
877 } else {
878 if (this.currentedit.start) {
879 this.currentedit.end = point;
880 this.redraw_current_edit();
881 }
882 }
883 },
884
885 /**
886 * Event handler for mouseup or touchend.
887 * @protected
888 * @param Event
889 * @method edit_end
890 */
5bb4f444 891 edit_end: function() {
5c386472
DW
892 var duration,
893 comment,
894 annotation;
895
896 duration = new Date().getTime() - this.currentedit.start;
897
898 if (duration < CLICKTIMEOUT || this.currentedit.start === false) {
899 return;
900 }
901
902 if (this.currentedit.tool === 'comment') {
903 if (this.currentdrawable) {
904 this.currentdrawable.erase();
905 }
906 this.currentdrawable = false;
907 comment = new M.assignfeedback_editpdf.comment(this);
baf881b8
RT
908 if (comment.init_from_edit(this.currentedit)) {
909 this.pages[this.currentpage].comments.push(comment);
910 this.drawables.push(comment.draw(true));
911 this.editingcomment = true;
912 }
5c386472
DW
913 } else {
914 annotation = this.create_annotation(this.currentedit.tool, {});
915 if (annotation) {
916 if (this.currentdrawable) {
917 this.currentdrawable.erase();
918 }
919 this.currentdrawable = false;
baf881b8
RT
920 if (annotation.init_from_edit(this.currentedit)) {
921 this.pages[this.currentpage].annotations.push(annotation);
922 this.drawables.push(annotation.draw());
923 }
5c386472
DW
924 }
925 }
926
5c386472
DW
927 // Save the changes.
928 this.save_current_page();
929
930 // Reset the current edit.
931 this.currentedit.starttime = 0;
932 this.currentedit.start = false;
933 this.currentedit.end = false;
934 this.currentedit.path = [];
935 },
936
452beca9
GF
937 /**
938 * Resize the dialogue window when the browser is resized.
939 * @public
940 * @method resize
941 */
5bb4f444 942 resize: function() {
50c12f01
DS
943 var drawingregion, drawregionheight;
944
bb690849
DW
945 if (this.dialogue) {
946 if (!this.dialogue.get('visible')) {
947 return;
948 }
949 this.dialogue.centerDialogue();
50c12f01 950 }
50c12f01
DS
951
952 // Make sure the dialogue box is not bigger than the max height of the viewport.
953 drawregionheight = Y.one('body').get('winHeight') - 120; // Space for toolbar + titlebar.
954 if (drawregionheight < 100) {
955 drawregionheight = 100;
956 }
da91de78 957 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
bb690849 958 if (this.dialogue) {
5bb4f444 959 drawingregion.setStyle('maxHeight', drawregionheight + 'px');
bb690849 960 }
50c12f01 961 this.redraw();
452beca9
GF
962 return true;
963 },
964
5c386472
DW
965 /**
966 * Factory method for creating annotations of the correct subclass.
967 * @public
968 * @method create_annotation
969 */
5bb4f444 970 create_annotation: function(type, data) {
5c386472
DW
971 data.type = type;
972 data.editor = this;
973 if (type === "line") {
974 return new M.assignfeedback_editpdf.annotationline(data);
975 } else if (type === "rectangle") {
976 return new M.assignfeedback_editpdf.annotationrectangle(data);
977 } else if (type === "oval") {
978 return new M.assignfeedback_editpdf.annotationoval(data);
979 } else if (type === "pen") {
980 return new M.assignfeedback_editpdf.annotationpen(data);
981 } else if (type === "highlight") {
982 return new M.assignfeedback_editpdf.annotationhighlight(data);
983 } else if (type === "stamp") {
984 return new M.assignfeedback_editpdf.annotationstamp(data);
985 }
986 return false;
987 },
988
989 /**
990 * Save all the annotations and comments for the current page.
991 * @protected
992 * @method save_current_page
993 */
5bb4f444 994 save_current_page: function() {
5c386472 995 var ajaxurl = AJAXBASE,
88d309d9 996 pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT),
5c386472
DW
997 config;
998
88d309d9
TB
999 this.currentpage = parseInt(pageselect.get('value'), 10);
1000
5c386472
DW
1001 config = {
1002 method: 'post',
1003 context: this,
1004 sync: false,
5bb4f444
DP
1005 data: {
1006 'sesskey': M.cfg.sesskey,
1007 'action': 'savepage',
1008 'index': this.currentpage,
1009 'userid': this.get('userid'),
1010 'attemptnumber': this.get('attemptnumber'),
1011 'assignmentid': this.get('assignmentid'),
1012 'page': this.stringify_current_page()
5c386472
DW
1013 },
1014 on: {
1015 success: function(tid, response) {
1016 var jsondata;
1017 try {
1018 jsondata = Y.JSON.parse(response.responseText);
1019 if (jsondata.error) {
1020 return new M.core.ajaxException(jsondata);
1021 }
bb690849
DW
1022 Y.one(SELECTOR.UNSAVEDCHANGESINPUT).set('value', 'true');
1023 Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('opacity', 1);
1024 Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('display', 'inline-block');
1025 Y.one(SELECTOR.UNSAVEDCHANGESDIV).transition({
1026 duration: 1,
1027 delay: 2,
1028 opacity: 0
1029 }, function() {
1030 Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('display', 'none');
1031 });
5c386472
DW
1032 } catch (e) {
1033 return new M.core.exception(e);
1034 }
1035 },
1036 failure: function(tid, response) {
aa3e4bde 1037 return new M.core.exception(response.responseText);
5c386472
DW
1038 }
1039 }
1040 };
1041
1042 Y.io(ajaxurl, config);
1043
1044 },
1045
1046 /**
1047 * Event handler to open the comment search interface.
1048 *
1049 * @param Event e
1050 * @protected
1051 * @method open_search_comments
1052 */
5bb4f444 1053 open_search_comments: function(e) {
5c386472
DW
1054 if (!this.searchcommentswindow) {
1055 this.searchcommentswindow = new M.assignfeedback_editpdf.commentsearch({
5bb4f444 1056 editor: this
5c386472
DW
1057 });
1058 }
1059
1060 this.searchcommentswindow.show();
1061 e.preventDefault();
1062 },
1063
1064 /**
1065 * Redraw all the comments and annotations.
1066 * @protected
1067 * @method redraw
1068 */
5bb4f444 1069 redraw: function() {
5c386472
DW
1070 var i,
1071 page;
1072
1073 page = this.pages[this.currentpage];
50c12f01
DS
1074 if (page === undefined) {
1075 return; // Can happen if a redraw is triggered by an event, before the page has been selected.
1076 }
5c386472
DW
1077 while (this.drawables.length > 0) {
1078 this.drawables.pop().erase();
1079 }
1080
1081 for (i = 0; i < page.annotations.length; i++) {
1082 this.drawables.push(page.annotations[i].draw());
1083 }
1084 for (i = 0; i < page.comments.length; i++) {
1085 this.drawables.push(page.comments[i].draw(false));
1086 }
1087 },
1088
1089 /**
1090 * Load the image for this pdf page and remove the loading icon (if there).
1091 * @protected
1092 * @method change_page
1093 */
5bb4f444 1094 change_page: function() {
da91de78 1095 var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
5c386472
DW
1096 page,
1097 previousbutton,
1098 nextbutton;
1099
da91de78
BL
1100 previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1101 nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
5c386472
DW
1102
1103 if (this.currentpage > 0) {
1104 previousbutton.removeAttribute('disabled');
1105 } else {
1106 previousbutton.setAttribute('disabled', 'true');
1107 }
1108 if (this.currentpage < (this.pagecount - 1)) {
1109 nextbutton.removeAttribute('disabled');
1110 } else {
1111 nextbutton.setAttribute('disabled', 'true');
1112 }
1113
1114 page = this.pages[this.currentpage];
1115 this.loadingicon.hide();
1116 drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
50c12f01
DS
1117 drawingcanvas.setStyle('width', page.width + 'px');
1118 drawingcanvas.setStyle('height', page.height + 'px');
5c386472 1119
ea6d8674 1120 // Update page select.
bb2281d5 1121 this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
ea6d8674 1122
50c12f01 1123 this.resize(); // Internally will call 'redraw', after checking the dialogue size.
5c386472
DW
1124 },
1125
1126 /**
1127 * Now we know how many pages there are,
1128 * we can enable the navigation controls.
1129 * @protected
1130 * @method setup_navigation
1131 */
5bb4f444 1132 setup_navigation: function() {
5c386472
DW
1133 var pageselect,
1134 i,
0d5a260e 1135 strinfo,
5c386472
DW
1136 option,
1137 previousbutton,
1138 nextbutton;
1139
da91de78 1140 pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
5c386472 1141
557f44d9 1142 var options = pageselect.all('option');
5c386472
DW
1143 if (options.size() <= 1) {
1144 for (i = 0; i < this.pages.length; i++) {
1145 option = Y.Node.create('<option/>');
1146 option.setAttribute('value', i);
5bb4f444 1147 strinfo = {page: i + 1, total: this.pages.length};
0d5a260e 1148 option.setHTML(M.util.get_string('pagexofy', 'assignfeedback_editpdf', strinfo));
5c386472
DW
1149 pageselect.append(option);
1150 }
1151 }
1152 pageselect.removeAttribute('disabled');
1153 pageselect.on('change', function() {
1154 this.currentpage = pageselect.get('value');
1155 this.change_page();
1156 }, this);
1157
da91de78
BL
1158 previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
1159 nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);
5c386472
DW
1160
1161 previousbutton.on('click', this.previous_page, this);
1162 previousbutton.on('key', this.previous_page, 'down:13', this);
1163 nextbutton.on('click', this.next_page, this);
1164 nextbutton.on('key', this.next_page, 'down:13', this);
1165 },
1166
1167 /**
1168 * Navigate to the previous page.
1169 * @protected
1170 * @method previous_page
1171 */
5bb4f444 1172 previous_page: function(e) {
114913e3 1173 e.preventDefault();
3cd718e8
TB
1174 var pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
1175
1176 this.currentpage = parseInt(pageselect.get('value'), 10) - 1;
5c386472
DW
1177 if (this.currentpage < 0) {
1178 this.currentpage = 0;
1179 }
1180 this.change_page();
1181 },
1182
1183 /**
1184 * Navigate to the next page.
1185 * @protected
1186 * @method next_page
1187 */
5bb4f444 1188 next_page: function(e) {
114913e3 1189 e.preventDefault();
3cd718e8
TB
1190 var pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
1191
1192 this.currentpage = parseInt(pageselect.get('value'), 10) + 1;
5c386472
DW
1193 if (this.currentpage >= this.pages.length) {
1194 this.currentpage = this.pages.length - 1;
1195 }
1196 this.change_page();
50c12f01
DS
1197 },
1198
1199 /**
1200 * Update any absolutely positioned nodes, within each drawable, when the drawing canvas is scrolled
1201 * @protected
1202 * @method move_canvas
1203 */
1204 move_canvas: function() {
1205 var drawingregion, x, y, i;
5c386472 1206
da91de78 1207 drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
50c12f01
DS
1208 x = parseInt(drawingregion.get('scrollLeft'), 10);
1209 y = parseInt(drawingregion.get('scrollTop'), 10);
5c386472 1210
50c12f01
DS
1211 for (i = 0; i < this.drawables.length; i++) {
1212 this.drawables[i].scroll_update(x, y);
1213 }
1214 }
5c386472
DW
1215
1216};
1217
1218Y.extend(EDITOR, Y.Base, EDITOR.prototype, {
5bb4f444
DP
1219 NAME: 'moodle-assignfeedback_editpdf-editor',
1220 ATTRS: {
1221 userid: {
1222 validator: Y.Lang.isInteger,
1223 value: 0
5c386472 1224 },
5bb4f444
DP
1225 assignmentid: {
1226 validator: Y.Lang.isInteger,
1227 value: 0
5c386472 1228 },
5bb4f444
DP
1229 attemptnumber: {
1230 validator: Y.Lang.isInteger,
1231 value: 0
5c386472 1232 },
5bb4f444
DP
1233 header: {
1234 validator: Y.Lang.isString,
1235 value: ''
5c386472 1236 },
5bb4f444
DP
1237 body: {
1238 validator: Y.Lang.isString,
1239 value: ''
5c386472 1240 },
5bb4f444
DP
1241 footer: {
1242 validator: Y.Lang.isString,
1243 value: ''
5c386472 1244 },
5bb4f444
DP
1245 linkid: {
1246 validator: Y.Lang.isString,
1247 value: ''
5c386472 1248 },
5bb4f444
DP
1249 deletelinkid: {
1250 validator: Y.Lang.isString,
1251 value: ''
5c386472 1252 },
5bb4f444
DP
1253 readonly: {
1254 validator: Y.Lang.isBoolean,
1255 value: true
5c386472 1256 },
5bb4f444
DP
1257 stampfiles: {
1258 validator: Y.Lang.isArray,
1259 value: ''
aa3e4bde 1260 },
5bb4f444
DP
1261 pagetotal: {
1262 validator: Y.Lang.isInteger,
1263 value: 0
5c386472
DW
1264 }
1265 }
1266});
1267
5c386472 1268M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
5c386472
DW
1269M.assignfeedback_editpdf.editor = M.assignfeedback_editpdf.editor || {};
1270
1271/**
1272 * Init function - will create a new instance every time.
1f777e5c 1273 * @method editor.init
5c386472
DW
1274 * @static
1275 * @param {Object} params
1276 */
1277M.assignfeedback_editpdf.editor.init = M.assignfeedback_editpdf.editor.init || function(params) {
5bb4f444 1278 M.assignfeedback_editpdf.instance = new EDITOR(params);
bb690849 1279 return M.assignfeedback_editpdf.instance;
5c386472 1280};