Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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