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