__  __    __   __  _____      _            _          _____ _          _ _ 
 |  \/  |   \ \ / / |  __ \    (_)          | |        / ____| |        | | |
 | \  / |_ __\ V /  | |__) | __ ___   ____ _| |_ ___  | (___ | |__   ___| | |
 | |\/| | '__|> <   |  ___/ '__| \ \ / / _` | __/ _ \  \___ \| '_ \ / _ \ | |
 | |  | | |_ / . \  | |   | |  | |\ V / (_| | ||  __/  ____) | | | |  __/ | |
 |_|  |_|_(_)_/ \_\ |_|   |_|  |_| \_/ \__,_|\__\___| |_____/|_| |_|\___V 2.1
 if you need WebShell for Seo everyday contact me on Telegram
 Telegram Address : @jackleet
        
        
For_More_Tools: Telegram: @jackleet | Bulk Smtp support mail sender | Business Mail Collector | Mail Bouncer All Mail | Bulk Office Mail Validator | Html Letter private



Upload:

Command:

www-data@216.73.216.10: ~ $
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-unused-vars */

/**
 * Provides an in browser PDF editor.
 *
 * @module moodle-assignfeedback_editpdf-editor
 */

/**
 * EDITOR
 * This is an in browser PDF editor.
 *
 * @namespace M.assignfeedback_editpdf
 * @class editor
 * @constructor
 * @extends Y.Base
 */
var EDITOR = function() {
    EDITOR.superclass.constructor.apply(this, arguments);
};
EDITOR.prototype = {

    /**
     * Store old coordinates of the annotations before rotation happens.
     */
    oldannotationcoordinates: null,

    /**
     * The dialogue used for all action menu displays.
     *
     * @property type
     * @type M.core.dialogue
     * @protected
     */
    dialogue: null,

    /**
     * The panel used for all action menu displays.
     *
     * @property type
     * @type Y.Node
     * @protected
     */
    panel: null,

    /**
     * The number of pages in the pdf.
     *
     * @property pagecount
     * @type Number
     * @protected
     */
    pagecount: 0,

    /**
     * The active page in the editor.
     *
     * @property currentpage
     * @type Number
     * @protected
     */
    currentpage: 0,

    /**
     * A list of page objects. Each page has a list of comments and annotations.
     *
     * @property pages
     * @type array
     * @protected
     */
    pages: [],

    /**
     * The reported status of the document.
     *
     * @property documentstatus
     * @type int
     * @protected
     */
    documentstatus: 0,

    /**
     * The yui node for the loading icon.
     *
     * @property loadingicon
     * @type Node
     * @protected
     */
    loadingicon: null,

    /**
     * Image object of the current page image.
     *
     * @property pageimage
     * @type Image
     * @protected
     */
    pageimage: null,

    /**
     * YUI Graphic class for drawing shapes.
     *
     * @property graphic
     * @type Graphic
     * @protected
     */
    graphic: null,

    /**
     * Info about the current edit operation.
     *
     * @property currentedit
     * @type M.assignfeedback_editpdf.edit
     * @protected
     */
    currentedit: new M.assignfeedback_editpdf.edit(),

    /**
     * Current drawable.
     *
     * @property currentdrawable
     * @type M.assignfeedback_editpdf.drawable|false
     * @protected
     */
    currentdrawable: false,

    /**
     * Current drawables.
     *
     * @property drawables
     * @type array(M.assignfeedback_editpdf.drawable)
     * @protected
     */
    drawables: [],

    /**
     * Current comment when the comment menu is open.
     * @property currentcomment
     * @type M.assignfeedback_editpdf.comment
     * @protected
     */
    currentcomment: null,

    /**
     * Current annotation when the select tool is used.
     * @property currentannotation
     * @type M.assignfeedback_editpdf.annotation
     * @protected
     */
    currentannotation: null,

    /**
     * Track the previous annotation so we can remove selection highlights.
     * @property lastannotation
     * @type M.assignfeedback_editpdf.annotation
     * @protected
     */
    lastannotation: null,

    /**
     * Last selected annotation tool
     * @property lastannotationtool
     * @type String
     * @protected
     */
    lastannotationtool: "pen",

    /**
     * The users comments quick list
     * @property quicklist
     * @type M.assignfeedback_editpdf.quickcommentlist
     * @protected
     */
    quicklist: null,

    /**
     * The search comments window.
     * @property searchcommentswindow
     * @type M.core.dialogue
     * @protected
     */
    searchcommentswindow: null,


    /**
     * The selected stamp picture.
     * @property currentstamp
     * @type String
     * @protected
     */
    currentstamp: null,

    /**
     * The stamps.
     * @property stamps
     * @type Array
     * @protected
     */
    stamps: [],

    /**
     * Prevent new comments from appearing
     * immediately after clicking off a current
     * comment
     * @property editingcomment
     * @type Boolean
     * @public
     */
    editingcomment: false,

    /**
     * Should inactive comments be collapsed?
     *
     * @property collapsecomments
     * @type Boolean
     * @public
     */
    collapsecomments: true,

    /**
     * Called during the initialisation process of the object.
     * @method initializer
     */
    initializer: function() {
        var link;

        link = Y.one('#' + this.get('linkid'));

        if (link) {
            link.on('click', this.link_handler, this);
            link.on('key', this.link_handler, 'down:13', this);

            // We call the amd module to see if we can take control of the review panel.
            require(['mod_assign/grading_review_panel'], function(ReviewPanelManager) {
                var panelManager = new ReviewPanelManager();

                var panel = panelManager.getReviewPanel('assignfeedback_editpdf');
                if (panel) {
                    panel = Y.one(panel);
                    panel.empty();
                    link.ancestor('.fitem').hide();
                    this.open_in_panel(panel);
                }
                this.currentedit.start = false;
                this.currentedit.end = false;
                if (!this.get('readonly')) {
                    this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this);
                }
            }.bind(this));

        }
    },

    /**
     * Called to show/hide buttons and set the current colours/stamps.
     * @method refresh_button_state
     */
    refresh_button_state: function() {
        var button, currenttoolnode, imgurl, drawingregion, stampimgurl, drawingcanvas;

        // Initalise the colour buttons.
        button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);

        imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf');
        button.one('img').setAttribute('src', imgurl);

        if (this.currentedit.commentcolour === 'clear') {
            button.one('img').setStyle('borderStyle', 'dashed');
        } else {
            button.one('img').setStyle('borderStyle', 'solid');
        }

        button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
        imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf');
        button.one('img').setAttribute('src', imgurl);

        currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
        currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton');
        currenttoolnode.setAttribute('aria-pressed', 'true');
        drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
        drawingregion.setAttribute('data-currenttool', this.currentedit.tool);

        button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
        stampimgurl = this.get_stamp_image_url(this.currentedit.stamp);
        button.one('img').setAttrs({'src': stampimgurl,
                                    'height': '16',
                                    'width': '16'});

        drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
        switch (this.currentedit.tool) {
            case 'drag':
                drawingcanvas.setStyle('cursor', 'move');
                break;
            case 'highlight':
                drawingcanvas.setStyle('cursor', 'text');
                break;
            case 'select':
                drawingcanvas.setStyle('cursor', 'default');
                break;
            case 'stamp':
                drawingcanvas.setStyle('cursor', 'url(' + stampimgurl + '), crosshair');
                break;
            default:
                drawingcanvas.setStyle('cursor', 'crosshair');
        }
    },

    /**
     * Called to get the bounds of the drawing region.
     * @method get_canvas_bounds
     */
    get_canvas_bounds: function() {
        var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
            offsetcanvas = canvas.getXY(),
            offsetleft = offsetcanvas[0],
            offsettop = offsetcanvas[1],
            width = parseInt(canvas.getStyle('width'), 10),
            height = parseInt(canvas.getStyle('height'), 10);

        return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height);
    },

    /**
     * Called to translate from window coordinates to canvas coordinates.
     * @method get_canvas_coordinates
     * @param M.assignfeedback_editpdf.point point in window coordinats.
     */
    get_canvas_coordinates: function(point) {
        var bounds = this.get_canvas_bounds(),
            newpoint = new M.assignfeedback_editpdf.point(point.x - bounds.x, point.y - bounds.y);

        bounds.x = bounds.y = 0;

        newpoint.clip(bounds);
        return newpoint;
    },

    /**
     * Called to translate from canvas coordinates to window coordinates.
     * @method get_window_coordinates
     * @param M.assignfeedback_editpdf.point point in window coordinats.
     */
    get_window_coordinates: function(point) {
        var bounds = this.get_canvas_bounds(),
            newpoint = new M.assignfeedback_editpdf.point(point.x + bounds.x, point.y + bounds.y);

        return newpoint;
    },

    /**
     * Open the edit-pdf editor in the panel in the page instead of a popup.
     * @method open_in_panel
     */
    open_in_panel: function(panel) {
        var drawingcanvas;

        this.panel = panel;
        panel.append(this.get('body'));
        panel.addClass(CSS.DIALOGUE);

        this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);

        drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
        this.graphic = new Y.Graphic({render: drawingcanvas});

        if (!this.get('readonly')) {
            drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
            drawingcanvas.on('gesturemove', this.edit_move, null, this);
            drawingcanvas.on('gesturemoveend', this.edit_end, null, this);

            this.refresh_button_state();
        }

        this.start_generation();
    },

    /**
     * Called to open the pdf editing dialogue.
     * @method link_handler
     */
    link_handler: function(e) {
        var drawingcanvas;
        var resize = true;
        e.preventDefault();

        if (!this.dialogue) {
            this.dialogue = new M.core.dialogue({
                headerContent: this.get('header'),
                bodyContent: this.get('body'),
                footerContent: this.get('footer'),
                modal: true,
                width: '840px',
                visible: false,
                draggable: true
            });

            // Add custom class for styling.
            this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE);

            this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON);

            drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
            this.graphic = new Y.Graphic({render: drawingcanvas});

            if (!this.get('readonly')) {
                drawingcanvas.on('gesturemovestart', this.edit_start, null, this);
                drawingcanvas.on('gesturemove', this.edit_move, null, this);
                drawingcanvas.on('gesturemoveend', this.edit_end, null, this);

                this.refresh_button_state();
            }

            this.start_generation();
            drawingcanvas.on('windowresize', this.resize, this);

            resize = false;
        }
        this.dialogue.centerDialogue();
        this.dialogue.show();

        // Redraw when the dialogue is moved, to ensure the absolute elements are all positioned correctly.
        this.dialogue.dd.on('drag:end', this.redraw, this);
        if (resize) {
            this.resize(); // When re-opening the dialog call redraw, to make sure the size + layout is correct.
        }
    },

    /**
     * Called to load the information and annotations for all pages.
     *
     * @method start_generation
     */
    start_generation: function() {
        this.poll_document_conversion_status();
    },

    /**
     * Poll the current document conversion status and start the next step
     * in the process.
     *
     * @method poll_document_conversion_status
     */
    poll_document_conversion_status: function() {
        var requestUserId = this.get('userid');

        Y.io(AJAXBASE, {
            method: 'get',
            context: this,
            sync: false,
            data: {
                sesskey: M.cfg.sesskey,
                action: 'pollconversions',
                userid: this.get('userid'),
                attemptnumber: this.get('attemptnumber'),
                assignmentid: this.get('assignmentid'),
                readonly: this.get('readonly') ? 1 : 0
            },
            on: {
                success: function(tid, response) {
                    var currentUserRegion = Y.one(SELECTOR.USERINFOREGION);
                    if (currentUserRegion) {
                        var currentUserId = currentUserRegion.getAttribute('data-userid');
                        if (currentUserId && (currentUserId != requestUserId)) {
                            // Polling conversion status needs to abort because
                            // the current user changed.
                            return;
                        }
                    }
                    var data = this.handle_response_data(response),
                        poll = false;
                    if (data) {
                        // When we are requesting the readonly version of the pages, they should
                        // always be available (see document_services::get_page_images_for_attempt)
                        // so we can just serve them immediately without triggering any document
                        // conversion or polling.
                        //
                        // This is necessary to prevent situations where the student has updated
                        // their submission and the teacher has annotated a previous version of
                        // the submission in the assignment grader. In this situation if a student
                        // views the online version of the annotated PDF ("View annotated PDF" link)
                        // the readonly pages here and the updated pages (awaiting conversion) will
                        // never match, and the code endlessly polls.
                        //
                        // See also: MDL-45580, MDL-66626, MDL-75898.
                        if (this.get('readonly') === true) {
                            this.prepare_pages_for_display(data);
                            return;
                        }

                        this.documentstatus = data.status;
                        if (data.status === 0) {
                            // The combined document is still waiting for input to be ready.
                            poll = true;

                        } else if (data.status === 1 || data.status === 3) {
                            // The combine document is ready for conversion into a single PDF.
                            poll = true;

                        } else if (data.status === 2 || data.status === -1) {
                            // The combined PDF is ready.
                            // We now know the page count and can convert it to a set of images.
                            this.pagecount = data.pagecount;

                            if (data.pageready == data.pagecount) {
                                this.prepare_pages_for_display(data);
                            } else {
                                // Some pages are not ready yet.
                                // Note: We use a different polling process here which does not block.
                                this.update_page_load_progress();

                                // Fetch the images for the combined document.
                                this.start_document_to_image_conversion();
                            }
                        }

                        if (poll) {
                            // Check again in 1 second.
                            Y.later(1000, this, this.poll_document_conversion_status);
                        }
                    }
                },
                failure: function(tid, response) {
                    return new M.core.exception(response.responseText);
                }
            }
        });
    },

    /**
     * Spwan the PDF to Image conversion on the server.
     *
     * @method get_images_for_documents
     */
    start_document_to_image_conversion: function() {
        Y.io(AJAXBASE, {
            method: 'get',
            context: this,
            sync: false,
            data: {
                sesskey: M.cfg.sesskey,
                action: 'pollconversions',
                userid: this.get('userid'),
                attemptnumber: this.get('attemptnumber'),
                assignmentid: this.get('assignmentid'),
                readonly: this.get('readonly') ? 1 : 0
            },
            on: {
                success: function(tid, response) {
                    var data = this.handle_response_data(response);
                    if (data) {
                        this.documentstatus = data.status;
                        if (data.status === 2) {
                            // The pages are ready. Add all of the annotations to them.
                            this.prepare_pages_for_display(data);
                        }
                    }
                },
                failure: function(tid, response) {
                    return new M.core.exception(response.responseText);
                }
            }
        });
    },

    /**
     * Display an error in a small part of the page (don't block everything).
     *
     * @param string The error text.
     * @param boolean dismissable Not critical messages can be removed after a short display.
     * @protected
     * @method warning
     */
    warning: function(message, dismissable) {
        var icontemplate = this.get_dialogue_element(SELECTOR.ICONMESSAGECONTAINER);
        var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER);
        var delay = 15, duration = 1;
        var messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-warning';
        if (dismissable) {
            delay = 4;
            messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-info';
        }
        var warningelement = Y.Node.create('<div class="' + messageclasses + '" role="alert"></div>');

        // Copy info icon template.
        warningelement.append(icontemplate.one('*').cloneNode());

        // Append the message.
        warningelement.append(message);

        // Add the entire warning to the container.
        warningregion.prepend(warningelement);

        // Remove the message after a short delay.
        warningelement.transition(
            {
                duration: duration,
                delay: delay,
                opacity: 0
            },
            function() {
                warningelement.remove();
            }
        );
    },

    /**
     * The info about all pages in the pdf has been returned.
     *
     * @param string The ajax response as text.
     * @protected
     * @method prepare_pages_for_display
     */
    prepare_pages_for_display: function(data) {
        var i, j, comment, error, annotation, readonly;

        if (!data.pagecount) {
            if (this.dialogue) {
                this.dialogue.hide();
            }
            // Display alert dialogue.
            error = new M.core.alert({message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')});
            error.show();
            return;
        }

        this.pages = data.pages;

        for (i = 0; i < this.pages.length; i++) {
            for (j = 0; j < this.pages[i].comments.length; j++) {
                comment = this.pages[i].comments[j];
                this.pages[i].comments[j] = new M.assignfeedback_editpdf.comment(this,
                                                                                 comment.gradeid,
                                                                                 comment.pageno,
                                                                                 comment.x,
                                                                                 comment.y,
                                                                                 comment.width,
                                                                                 comment.colour,
                                                                                 comment.rawtext);
            }
            for (j = 0; j < this.pages[i].annotations.length; j++) {
                annotation = this.pages[i].annotations[j];
                this.pages[i].annotations[j] = this.create_annotation(annotation.type, annotation);
            }
        }

        readonly = this.get('readonly');
        if (!readonly && data.partial) {
            // Warn about non converted files, but only for teachers.
            this.warning(M.util.get_string('partialwarning', 'assignfeedback_editpdf'), false);
        }

        // Update the ui.
        if (this.quicklist) {
            this.quicklist.load();
        }
        this.setup_navigation();
        this.setup_toolbar();
        this.change_page();
    },

    /**
     * Fetch the page images.
     *
     * @method update_page_load_progress
     */
    update_page_load_progress: function() {
        var checkconversionstatus,
            ajax_error_total = 0,
            progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar');

        if (!progressbar) {
            return;
        }

        // If pages are not loaded, check PDF conversion status for the progress bar.
        checkconversionstatus = {
            method: 'get',
            context: this,
            sync: false,
            data: {
                sesskey: M.cfg.sesskey,
                action: 'conversionstatus',
                userid: this.get('userid'),
                attemptnumber: this.get('attemptnumber'),
                assignmentid: this.get('assignmentid')
            },
            on: {
                success: function(tid, response) {
                    ajax_error_total = 0;

                    var progress = 0;
                    var progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar');
                    if (progressbar) {
                        // Calculate progress.
                        progress = (response.response / this.pagecount) * 100;
                        progressbar.setStyle('width', progress + '%');
                        progressbar.ancestor(SELECTOR.PROGRESSBARCONTAINER).setAttribute('aria-valuenow', progress);

                        if (progress < 100) {
                            // Keep polling until all pages are generated.
                            M.util.js_pending('checkconversionstatus');
                            Y.later(1000, this, function() {
                                M.util.js_complete('checkconversionstatus');
                                Y.io(AJAXBASEPROGRESS, checkconversionstatus);
                            });
                        }
                    }
                },
                failure: function(tid, response) {
                    ajax_error_total = ajax_error_total + 1;
                    // We only continue on error if the all pages were not generated,
                    // and if the ajax call did not produce 5 errors in the row.
                    if (this.pagecount === 0 && ajax_error_total < 5) {
                        M.util.js_pending('checkconversionstatus');
                        Y.later(1000, this, function() {
                            M.util.js_complete('checkconversionstatus');
                            Y.io(AJAXBASEPROGRESS, checkconversionstatus);
                        });
                    }
                    return new M.core.exception(response.responseText);
                }
            }
        };
        // We start the AJAX "generated page total number" call a second later to give a chance to
        // the AJAX "combined pdf generation" call to clean the previous submission images.
        M.util.js_pending('checkconversionstatus');
        Y.later(1000, this, function() {
            ajax_error_total = 0;
            M.util.js_complete('checkconversionstatus');
            Y.io(AJAXBASEPROGRESS, checkconversionstatus);
        });
    },

    /**
     * Handle response data.
     *
     * @method  handle_response_data
     * @param   {object} response
     * @return  {object}
     */
    handle_response_data: function(response) {
        var data;
        try {
            data = Y.JSON.parse(response.responseText);
            if (data.error) {
                if (this.dialogue) {
                    this.dialogue.hide();
                }

                new M.core.alert({
                    message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'),
                    visible: true
                });
            } else {
                return data;
            }
        } catch (e) {
            if (this.dialogue) {
                this.dialogue.hide();
            }

            new M.core.alert({
                title: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'),
                visible: true
            });
        }

        return;
    },

    /**
     * Get the full pluginfile url for an image file - just given the filename.
     *
     * @public
     * @method get_stamp_image_url
     * @param string filename
     */
    get_stamp_image_url: function(filename) {
        var urls = this.get('stampfiles'),
            fullurl = '';

        Y.Array.each(urls, function(url) {
            if (url.indexOf(filename) > 0) {
                fullurl = url;
            }
        }, this);

        return fullurl;
    },

    /**
     * Attach listeners and enable the color picker buttons.
     * @protected
     * @method setup_toolbar
     */
    setup_toolbar: function() {
        var toolnode,
            commentcolourbutton,
            annotationcolourbutton,
            searchcommentsbutton,
            expcolcommentsbutton,
            rotateleftbutton,
            rotaterightbutton,
            currentstampbutton,
            stampfiles,
            picker,
            filename;

        searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON);
        searchcommentsbutton.on('click', this.open_search_comments, this);
        searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this);

        expcolcommentsbutton = this.get_dialogue_element(SELECTOR.EXPCOLCOMMENTSBUTTON);
        expcolcommentsbutton.on('click', this.expandCollapseComments, this);
        expcolcommentsbutton.on('key', this.expandCollapseComments, 'down:13', this);

        if (this.get('readonly')) {
            return;
        }

        // Rotate Left.
        rotateleftbutton = this.get_dialogue_element(SELECTOR.ROTATELEFTBUTTON);
        rotateleftbutton.on('click', this.rotatePDF, this, true);
        rotateleftbutton.on('key', this.rotatePDF, 'down:13', this, true);

        // Rotate Right.
        rotaterightbutton = this.get_dialogue_element(SELECTOR.ROTATERIGHTBUTTON);
        rotaterightbutton.on('click', this.rotatePDF, this, false);
        rotaterightbutton.on('key', this.rotatePDF, 'down:13', this, false);

        this.disable_touch_scroll();

        // Setup the tool buttons.
        Y.each(TOOLSELECTOR, function(selector, tool) {
            toolnode = this.get_dialogue_element(selector);
            toolnode.on('click', this.handle_tool_button, this, tool);
            toolnode.on('key', this.handle_tool_button, 'down:13', this, tool);
            toolnode.setAttribute('aria-pressed', 'false');
        }, this);

        // Set the default tool.

        commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
        picker = new M.assignfeedback_editpdf.colourpicker({
            buttonNode: commentcolourbutton,
            colours: COMMENTCOLOUR,
            iconprefix: 'background_colour_',
            callback: function(e) {
                var colour = e.target.getAttribute('data-colour');
                if (!colour) {
                    colour = e.target.ancestor().getAttribute('data-colour');
                }
                this.currentedit.commentcolour = colour;
                this.handle_tool_button(e, "comment");
            },
            context: this
        });

        annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON);
        picker = new M.assignfeedback_editpdf.colourpicker({
            buttonNode: annotationcolourbutton,
            iconprefix: 'colour_',
            colours: ANNOTATIONCOLOUR,
            callback: function(e) {
                var colour = e.target.getAttribute('data-colour');
                if (!colour) {
                    colour = e.target.ancestor().getAttribute('data-colour');
                }
                this.currentedit.annotationcolour = colour;
                if (this.lastannotationtool) {
                    this.handle_tool_button(e, this.lastannotationtool);
                } else {
                    this.handle_tool_button(e, "pen");
                }
            },
            context: this
        });

        stampfiles = this.get('stampfiles');
        if (stampfiles.length <= 0) {
            this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide();
        } else {
            filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1);
            this.currentedit.stamp = filename;
            currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);

            picker = new M.assignfeedback_editpdf.stamppicker({
                buttonNode: currentstampbutton,
                stamps: stampfiles,
                callback: function(e) {
                    var stamp = e.target.getAttribute('data-stamp'),
                        filename;

                    if (!stamp) {
                        stamp = e.target.ancestor().getAttribute('data-stamp');
                    }
                    filename = stamp.substr(stamp.lastIndexOf('/'));
                    this.currentedit.stamp = filename;
                    this.handle_tool_button(e, "stamp");
                },
                context: this
            });
            this.refresh_button_state();
        }
    },

    /**
     * Change the current tool.
     * @protected
     * @method handle_tool_button
     */
    handle_tool_button: function(e, tool) {
        var currenttoolnode;

        e.preventDefault();

        // Change style of the pressed button.
        currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]);
        currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton');
        currenttoolnode.setAttribute('aria-pressed', 'false');
        this.currentedit.tool = tool;

        if (tool !== "comment" && tool !== "select" && tool !== "drag" && tool !== "stamp") {
            this.lastannotationtool = tool;
        }

        this.refresh_button_state();
    },

    /**
     * JSON encode the current page data - stripping out drawable references which cannot be encoded.
     * @protected
     * @method stringify_current_page
     * @return string
     */
    stringify_current_page: function() {
        var comments = [],
            annotations = [],
            page,
            i = 0;

        for (i = 0; i < this.pages[this.currentpage].comments.length; i++) {
            comments[i] = this.pages[this.currentpage].comments[i].clean();
        }
        for (i = 0; i < this.pages[this.currentpage].annotations.length; i++) {
            annotations[i] = this.pages[this.currentpage].annotations[i].clean();
        }

        page = {comments: comments, annotations: annotations};

        return Y.JSON.stringify(page);
    },

    /**
     * Generate a drawable from the current in progress edit.
     * @protected
     * @method get_current_drawable
     */
    get_current_drawable: function() {
        var comment,
            annotation,
            drawable = false;

        if (!this.currentedit.start || !this.currentedit.end) {
            return false;
        }

        if (this.currentedit.tool === 'comment') {
            comment = new M.assignfeedback_editpdf.comment(this);
            drawable = comment.draw_current_edit(this.currentedit);
        } else {
            annotation = this.create_annotation(this.currentedit.tool, {});
            if (annotation) {
                drawable = annotation.draw_current_edit(this.currentedit);
            }
        }

        return drawable;
    },

    /**
     * Find an element within the dialogue.
     * @protected
     * @method get_dialogue_element
     */
    get_dialogue_element: function(selector) {
        if (this.panel) {
            return this.panel.one(selector);
        } else {
            return this.dialogue.get('boundingBox').one(selector);
        }
    },

    /**
     * Redraw the active edit.
     * @protected
     * @method redraw_active_edit
     */
    redraw_current_edit: function() {
        if (this.currentdrawable) {
            this.currentdrawable.erase();
        }
        this.currentdrawable = this.get_current_drawable();
    },

    /**
     * Event handler for mousedown or touchstart.
     * @protected
     * @param Event
     * @method edit_start
     */
    edit_start: function(e) {
        var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
            offset = canvas.getXY(),
            scrolltop = canvas.get('docScrollY'),
            scrollleft = canvas.get('docScrollX'),
            point = {x: e.clientX - offset[0] + scrollleft,
                     y: e.clientY - offset[1] + scrolltop},
            selected = false;

        // Ignore right mouse click.
        if (e.button === 3) {
            return;
        }

        if (this.currentedit.starttime) {
            return;
        }

        if (this.editingcomment) {
            return;
        }

        this.currentedit.starttime = new Date().getTime();
        this.currentedit.start = point;
        this.currentedit.end = {x: point.x, y: point.y};

        if (this.currentedit.tool === 'select') {
            var x = this.currentedit.end.x,
                y = this.currentedit.end.y,
                annotations = this.pages[this.currentpage].annotations;
            // Find the first annotation whose bounds encompass the click.
            Y.each(annotations, function(annotation) {
                if (((x - annotation.x) * (x - annotation.endx)) <= 0 &&
                    ((y - annotation.y) * (y - annotation.endy)) <= 0) {
                    selected = annotation;
                }
            });

            if (selected) {
                this.lastannotation = this.currentannotation;
                this.currentannotation = selected;
                if (this.lastannotation && this.lastannotation !== selected) {
                    // Redraw the last selected annotation to remove the highlight.
                    if (this.lastannotation.drawable) {
                        this.lastannotation.drawable.erase();
                        this.drawables.push(this.lastannotation.draw());
                    }
                }
                // Redraw the newly selected annotation to show the highlight.
                if (this.currentannotation.drawable) {
                    this.currentannotation.drawable.erase();
                }
                this.drawables.push(this.currentannotation.draw());
            } else {
                this.lastannotation = this.currentannotation;
                this.currentannotation = null;

                // Redraw the last selected annotation to remove the highlight.
                if (this.lastannotation && this.lastannotation.drawable) {
                    this.lastannotation.drawable.erase();
                    this.drawables.push(this.lastannotation.draw());
                }
            }
        }
        if (this.currentannotation) {
            // Used to calculate drag offset.
            this.currentedit.annotationstart = {x: this.currentannotation.x,
                                                 y: this.currentannotation.y};
        }
    },

    /**
     * Event handler for mousemove.
     * @protected
     * @param Event
     * @method edit_move
     */
    edit_move: function(e) {
        var bounds = this.get_canvas_bounds(),
            canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
            drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION),
            clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'),
                                                             e.clientY + canvas.get('docScrollY')),
            point = this.get_canvas_coordinates(clientpoint),
            activeelement = document.activeElement,
            diffX,
            diffY;

        if (activeelement.type === 'textarea') {
            return;
        }

        e.preventDefault();

        // Ignore events out of the canvas area.
        if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) {
            return;
        }

        if (this.currentedit.tool === 'pen') {
            this.currentedit.path.push(point);
        }

        if (this.currentedit.tool === 'select') {
            if (this.currentannotation && this.currentedit) {
                this.currentannotation.move(this.currentedit.annotationstart.x + point.x - this.currentedit.start.x,
                                             this.currentedit.annotationstart.y + point.y - this.currentedit.start.y);
            }
        } else if (this.currentedit.tool === 'drag') {
            diffX = point.x - this.currentedit.start.x;
            diffY = point.y - this.currentedit.start.y;

            drawingregion.getDOMNode().scrollLeft -= diffX;
            drawingregion.getDOMNode().scrollTop -= diffY;

        } else {
            if (this.currentedit.start) {
                this.currentedit.end = point;
                this.redraw_current_edit();
            }
        }
    },

    /**
     * Event handler for mouseup or touchend.
     * @protected
     * @param Event
     * @method edit_end
     */
    edit_end: function() {
        var duration,
            comment,
            annotation;

        duration = new Date().getTime() - this.currentedit.start;

        if (duration < CLICKTIMEOUT || this.currentedit.start === false) {
            return;
        }

        if (this.currentedit.tool === 'comment') {
            if (this.currentdrawable) {
                this.currentdrawable.erase();
            }
            this.currentdrawable = false;
            comment = new M.assignfeedback_editpdf.comment(this);
            if (comment.init_from_edit(this.currentedit)) {
                this.pages[this.currentpage].comments.push(comment);
                this.drawables.push(comment.draw(true));
                this.editingcomment = true;
            }
        } else {
            annotation = this.create_annotation(this.currentedit.tool, {});
            if (annotation) {
                if (this.currentdrawable) {
                    this.currentdrawable.erase();
                }
                this.currentdrawable = false;
                if (annotation.init_from_edit(this.currentedit)) {
                    this.pages[this.currentpage].annotations.push(annotation);
                    this.drawables.push(annotation.draw());
                }
            }
        }

        // Save the changes.
        this.save_current_page();

        // Reset the current edit.
        this.currentedit.starttime = 0;
        this.currentedit.start = false;
        this.currentedit.end = false;
        this.currentedit.path = [];
    },

    /**
     * Resize the dialogue window when the browser is resized.
     * @public
     * @method resize
     */
    resize: function() {
        var drawingregion, drawregionheight;

        if (this.dialogue) {
            if (!this.dialogue.get('visible')) {
                return;
            }
            this.dialogue.centerDialogue();
        }

        // Make sure the dialogue box is not bigger than the max height of the viewport.
        drawregionheight = Y.one('body').get('winHeight') - 120; // Space for toolbar + titlebar.
        if (drawregionheight < 100) {
            drawregionheight = 100;
        }
        drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
        if (this.dialogue) {
            drawingregion.setStyle('maxHeight', drawregionheight + 'px');
        }
        this.redraw();
        return true;
    },

    /**
     * Factory method for creating annotations of the correct subclass.
     * @public
     * @method create_annotation
     */
    create_annotation: function(type, data) {
        data.type = type;
        data.editor = this;
        if (type === "line") {
            return new M.assignfeedback_editpdf.annotationline(data);
        } else if (type === "rectangle") {
            return new M.assignfeedback_editpdf.annotationrectangle(data);
        } else if (type === "oval") {
            return new M.assignfeedback_editpdf.annotationoval(data);
        } else if (type === "pen") {
            return new M.assignfeedback_editpdf.annotationpen(data);
        } else if (type === "highlight") {
            return new M.assignfeedback_editpdf.annotationhighlight(data);
        } else if (type === "stamp") {
            return new M.assignfeedback_editpdf.annotationstamp(data);
        }
        return false;
    },

    /**
     * Save all the annotations and comments for the current page.
     * @protected
     * @method save_current_page
     */
    save_current_page: function() {
        this.clear_warnings(false);
        var ajaxurl = AJAXBASE,
            config;

        config = {
            method: 'post',
            context: this,
            sync: false,
            data: {
                'sesskey': M.cfg.sesskey,
                'action': 'savepage',
                'index': this.currentpage,
                'userid': this.get('userid'),
                'attemptnumber': this.get('attemptnumber'),
                'assignmentid': this.get('assignmentid'),
                'page': this.stringify_current_page()
            },
            on: {
                success: function(tid, response) {
                    var jsondata;
                    try {
                        jsondata = Y.JSON.parse(response.responseText);
                        if (jsondata.error) {
                            return new M.core.ajaxException(jsondata);
                        }
                        // Show warning that we have not saved the feedback.
                        Y.one(SELECTOR.UNSAVEDCHANGESINPUT).set('value', 'true');
                        this.warning(M.util.get_string('draftchangessaved', 'assignfeedback_editpdf'), true);
                    } catch (e) {
                        return new M.core.exception(e);
                    }
                },
                failure: function(tid, response) {
                    return new M.core.exception(response.responseText);
                }
            }
        };

        Y.io(ajaxurl, config);
    },

    /**
     * Event handler to open the comment search interface.
     *
     * @param Event e
     * @protected
     * @method open_search_comments
     */
    open_search_comments: function(e) {
        if (!this.searchcommentswindow) {
            this.searchcommentswindow = new M.assignfeedback_editpdf.commentsearch({
                editor: this
            });
        }

        this.searchcommentswindow.show();
        e.preventDefault();
    },

    /**
     * Toggle function to expand/collapse all comments on page.
     *
     * @protected
     * @method expandCollapseComments
     */
    expandCollapseComments: function() {
        var comments = Y.all('.commentdrawable');

        if (this.collapsecomments) {
            this.collapsecomments = false;
            comments.removeClass('commentcollapsed');
        } else {
            this.collapsecomments = true;
            comments.addClass('commentcollapsed');
        }
    },

    /**
     * Redraw all the comments and annotations.
     * @protected
     * @method redraw
     */
    redraw: function() {
        var i,
            page;

        page = this.pages[this.currentpage];
        if (page === undefined) {
            return; // Can happen if a redraw is triggered by an event, before the page has been selected.
        }
        while (this.drawables.length > 0) {
            this.drawables.pop().erase();
        }

        for (i = 0; i < page.annotations.length; i++) {
            this.drawables.push(page.annotations[i].draw());
        }
        for (i = 0; i < page.comments.length; i++) {
            this.drawables.push(page.comments[i].draw(false));
        }
    },

    /**
     * Clear all current warning messages from display.
     * @protected
     * @method clear_warnings
     * @param {Boolean} allwarnings If true, all previous warnings are removed.
     */
    clear_warnings: function(allwarnings) {
        // Remove all warning messages, they may not relate to the current document or page anymore.
        var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER);
        if (allwarnings) {
            warningregion.empty();
        } else {
            warningregion.all('.alert-info').remove(true);
        }
    },

    /**
     * Load the image for this pdf page and remove the loading icon (if there).
     * @protected
     * @method change_page
     */
    change_page: function() {
        var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
            page,
            previousbutton,
            nextbutton;

        previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
        nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);

        if (this.currentpage > 0) {
            previousbutton.removeAttribute('disabled');
        } else {
            previousbutton.setAttribute('disabled', 'true');
        }
        if (this.currentpage < (this.pagecount - 1)) {
            nextbutton.removeAttribute('disabled');
        } else {
            nextbutton.setAttribute('disabled', 'true');
        }

        page = this.pages[this.currentpage];
        if (this.loadingicon) {
            this.loadingicon.hide();
        }
        drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
        drawingcanvas.setStyle('width', page.width + 'px');
        drawingcanvas.setStyle('height', page.height + 'px');
        drawingcanvas.scrollIntoView();

        // Update page select.
        this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);

        this.resize(); // Internally will call 'redraw', after checking the dialogue size.
    },

    /**
     * Now we know how many pages there are,
     * we can enable the navigation controls.
     * @protected
     * @method setup_navigation
     */
    setup_navigation: function() {
        var pageselect,
            i,
            strinfo,
            option,
            previousbutton,
            nextbutton;

        pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);

        var options = pageselect.all('option');
        if (options.size() <= 1) {
            for (i = 0; i < this.pages.length; i++) {
                option = Y.Node.create('<option/>');
                option.setAttribute('value', i);
                strinfo = {page: i + 1, total: this.pages.length};
                option.setHTML(M.util.get_string('pagexofy', 'assignfeedback_editpdf', strinfo));
                pageselect.append(option);
            }
        }
        pageselect.removeAttribute('disabled');
        pageselect.on('change', function() {
            this.currentpage = pageselect.get('value');
            this.clear_warnings(false);
            this.change_page();
        }, this);

        previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON);
        nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON);

        previousbutton.on('click', this.previous_page, this);
        previousbutton.on('key', this.previous_page, 'down:13', this);
        nextbutton.on('click', this.next_page, this);
        nextbutton.on('key', this.next_page, 'down:13', this);
    },

    /**
     * Navigate to the previous page.
     * @protected
     * @method previous_page
     */
    previous_page: function(e) {
        e.preventDefault();
        this.currentpage--;
        if (this.currentpage < 0) {
            this.currentpage = 0;
        }
        this.clear_warnings(false);
        this.change_page();
    },

    /**
     * Navigate to the next page.
     * @protected
     * @method next_page
     */
    next_page: function(e) {
        e.preventDefault();
        this.currentpage++;
        if (this.currentpage >= this.pages.length) {
            this.currentpage = this.pages.length - 1;
        }
        this.clear_warnings(false);
        this.change_page();
    },

    /**
     * Update any absolutely positioned nodes, within each drawable, when the drawing canvas is scrolled
     * @protected
     * @method move_canvas
     */
    move_canvas: function() {
        var drawingregion, x, y, i;

        drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);
        x = parseInt(drawingregion.get('scrollLeft'), 10);
        y = parseInt(drawingregion.get('scrollTop'), 10);

        for (i = 0; i < this.drawables.length; i++) {
            this.drawables[i].scroll_update(x, y);
        }
    },

    /**
     * Calculate degree to rotate.
     * @protected
     * @param {Object} e javascript event
     * @param {boolean} left  true if rotating left, false if rotating right
     * @method rotatepdf
     */
    rotatePDF: function(e, left) {
        e.preventDefault();

        if (this.get('destroyed')) {
            return;
        }
        var self = this;
        // Save old coordinates.
        var i;
        this.oldannotationcoordinates = [];
        var annotations = this.pages[this.currentpage].annotations;
        for (i = 0; i < annotations.length; i++) {
            var oldannotation = annotations[i];
            this.oldannotationcoordinates.push([oldannotation.x, oldannotation.y]);
        }

        var ajaxurl = AJAXBASE;
        var config = {
            method: 'post',
            context: this,
            sync: false,
            data: {
                'sesskey': M.cfg.sesskey,
                'action': 'rotatepage',
                'index': this.currentpage,
                'userid': this.get('userid'),
                'attemptnumber': this.get('attemptnumber'),
                'assignmentid': this.get('assignmentid'),
                'rotateleft': left
            },
            on: {
                success: function(tid, response) {
                    var jsondata;
                    try {
                        jsondata = Y.JSON.parse(response.responseText);
                        var page = self.pages[self.currentpage];
                        page.url = jsondata.page.url;
                        page.width = jsondata.page.width;
                        page.height = jsondata.page.height;
                        self.loadingicon.hide();

                        // Change canvas size to fix the new page.
                        var drawingcanvas = self.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
                        drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
                        drawingcanvas.setStyle('width', page.width + 'px');
                        drawingcanvas.setStyle('height', page.height + 'px');

                        /**
                         * Move annotation to old position.
                         * Reason: When canvas size change
                         * > Shape annotations move with relation to canvas coordinates
                         * > Nodes of stamp annotations move with relation to canvas coordinates
                         * > Presentation (picture) of stamp annotations  stay to document coordinates (stick to its own position)
                         * > Without relocating the node and presentation of a stamp annotation to the same x,y position,
                         * the stamp annotation cannot be chosen when using "drag" tool.
                         * The following code brings all annotations to their old positions with relation to the canvas coordinates.
                         */
                        var i;
                        // Annotations.
                        var annotations = page.annotations;
                        for (i = 0; i < annotations.length; i++) {
                            if (self.oldannotationcoordinates && self.oldannotationcoordinates[i]) {
                                var oldX = self.oldannotationcoordinates[i][0];
                                var oldY = self.oldannotationcoordinates[i][1];
                                var annotation = annotations[i];
                                annotation.move(oldX, oldY);
                            }
                        }
                        /**
                         * Update Position of comments with relation to canvas coordinates.
                         * Without this code, the comments will stay at their positions in windows/document coordinates.
                         */
                        var oldcomments = page.comments;
                        for (i = 0; i < oldcomments.length; i++) {
                            oldcomments[i].updatePosition();
                        }
                        // Save Annotations.
                        return self.save_current_page();
                    } catch (e) {
                        return new M.core.exception(e);
                    }
                },
                failure: function(tid, response) {
                    return new M.core.exception(response.responseText);
                }
            }
        };
        Y.io(ajaxurl, config);
    },

    /**
     * Test the browser support for options objects on event listeners.
     * @return Boolean
     */
    event_listener_options_supported: function() {
        var passivesupported = false,
            options,
            testeventname = "testpassiveeventoptions";

        // Options support testing example from:
        // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener

        try {
            options = Object.defineProperty({}, "passive", {
                // eslint-disable-next-line getter-return
                get: function() {
                    passivesupported = true;
                }
            });

            // We use an event name that is not likely to conflict with any real event.
            document.addEventListener(testeventname, options, options);
            // We remove the event listener as we have tested the options already.
            document.removeEventListener(testeventname, options, options);
        } catch(err) {
            // It's already false.
            passivesupported = false;
        }
        return passivesupported;
    },

    /**
     * Disable Touch Move scrolling
     */
    disable_touch_scroll: function() {
        if (this.event_listener_options_supported()) {
            document.addEventListener('touchmove', this.stop_touch_scroll.bind(this), {passive: false});
        }
    },

    /**
     * Stop Touch Scrolling
     * @param {Object} e
     */
    stop_touch_scroll: function(e) {
        var drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION);

        if (drawingregion.contains(e.target)) {
            e.stopPropagation();
            e.preventDefault();
        }
    }

};

Y.extend(EDITOR, Y.Base, EDITOR.prototype, {
    NAME: 'moodle-assignfeedback_editpdf-editor',
    ATTRS: {
        userid: {
            validator: Y.Lang.isInteger,
            value: 0
        },
        assignmentid: {
            validator: Y.Lang.isInteger,
            value: 0
        },
        attemptnumber: {
            validator: Y.Lang.isInteger,
            value: 0
        },
        header: {
            validator: Y.Lang.isString,
            value: ''
        },
        body: {
            validator: Y.Lang.isString,
            value: ''
        },
        footer: {
            validator: Y.Lang.isString,
            value: ''
        },
        linkid: {
            validator: Y.Lang.isString,
            value: ''
        },
        deletelinkid: {
            validator: Y.Lang.isString,
            value: ''
        },
        readonly: {
            validator: Y.Lang.isBoolean,
            value: true
        },
        stampfiles: {
            validator: Y.Lang.isArray,
            value: ''
        }
    }
});

M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
M.assignfeedback_editpdf.editor = M.assignfeedback_editpdf.editor || {};

/**
 * Init function - will create a new instance every time.
 * @method editor.init
 * @static
 * @param {Object} params
 */
M.assignfeedback_editpdf.editor.init = M.assignfeedback_editpdf.editor.init || function(params) {
    M.assignfeedback_editpdf.instance = new EDITOR(params);
    return M.assignfeedback_editpdf.instance;
};

Filemanager

Name Type Size Permission Actions
annotation.js File 8.78 KB 0777
annotationhighlight.js File 4.65 KB 0777
annotationline.js File 3.36 KB 0777
annotationoval.js File 3.35 KB 0777
annotationpen.js File 4.39 KB 0777
annotationrectangle.js File 3.41 KB 0777
annotationstamp.js File 5.48 KB 0777
colourpicker.js File 3.62 KB 0777
comment.js File 20.52 KB 0777
commentmenu.js File 4.25 KB 0777
commentsearch.js File 4.81 KB 0777
drawable.js File 3.19 KB 0777
dropdown.js File 3.23 KB 0777
edit.js File 2.34 KB 0777
editor.js File 57.43 KB 0777
globals.js File 3.32 KB 0777
point.js File 1.83 KB 0777
quickcomment.js File 1.57 KB 0777
quickcommentlist.js File 6.81 KB 0777
rect.js File 3.25 KB 0777
stamppicker.js File 3.04 KB 0777
Filemanager