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