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