Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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
 
16
/**
17
 * Javascript library for enableing a drag and drop upload interface
18
 *
19
 * @package    moodlecore
20
 * @subpackage form
21
 * @copyright  2011 Davo Smith
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
M.form_dndupload = {}
26
 
27
M.form_dndupload.init = function(Y, options) {
28
    var dnduploadhelper = {
29
        // YUI object.
30
        Y: null,
31
        // URL for upload requests
32
        url: M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload',
33
        // options may include: itemid, acceptedtypes, maxfiles, maxbytes, clientid, repositoryid, author, contextid
34
        options: {},
35
        // itemid used for repository upload
36
        itemid: null,
37
        // accepted filetypes accepted by this form passed to repository
38
        acceptedtypes: [],
39
        // maximum size of files allowed in this form
40
        maxbytes: 0,
41
        // Maximum combined size of files allowed in this form. {@link FILE_AREA_MAX_BYTES_UNLIMITED}
42
        areamaxbytes: -1,
43
        // unqiue id of this form field used for html elements
44
        clientid: '',
45
        // upload repository id, used for upload
46
        repositoryid: 0,
47
        // container which holds the node which recieves drag events
48
        container: null,
49
        // filemanager element we are working with
50
        filemanager: null,
51
        // callback  to filepicker element to refesh when uploaded
52
        callback: null,
53
        // Nasty hack to distinguish between dragenter(first entry),
54
        // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
55
        entercount: 0,
56
        pageentercount: 0,
57
        // Holds the progress bar elements for each file.
58
        progressbars: {},
59
        // Number of request in queue and number of request uploading.
60
        totalOfRequest: 0,
61
        // Number of request upload.
62
        numberOfRequestUpload: 0,
63
 
64
        /**
65
         * Initalise the drag and drop upload interface
66
         * Note: one and only one of options.filemanager and options.formcallback must be defined
67
         *
68
         * @param Y the YUI object
69
         * @param object options {
70
         *            itemid: itemid used for repository upload in this form
71
         *            acceptdtypes: accepted filetypes by this form
72
         *            maxfiles: maximum number of files this form allows
73
         *            maxbytes: maximum size of files allowed in this form
74
         *            areamaxbytes: maximum combined size of files allowed in this form
75
         *            clientid: unqiue id of this form field used for html elements
76
         *            contextid: id of the current cotnext
77
         *            containerid: htmlid of container
78
         *            repositories: array of repository objects passed from filepicker
79
         *            filemanager: filemanager element we are working with
80
         *            formcallback: callback  to filepicker element to refesh when uploaded
81
         *          }
82
         */
83
        init: function(Y, options) {
84
            this.Y = Y;
85
 
86
            if (!this.browser_supported()) {
87
                Y.one('body').addClass('dndnotsupported');
88
                return; // Browser does not support the required functionality
89
            }
90
 
91
            // try and retrieve enabled upload repository
92
            this.repositoryid = this.get_upload_repositoryid(options.repositories);
93
 
94
            if (!this.repositoryid) {
95
                Y.one('body').addClass('dndnotsupported');
96
                return; // no upload repository is enabled to upload to
97
            }
98
 
99
            Y.one('body').addClass('dndsupported');
100
 
101
            this.options = options;
102
            this.acceptedtypes = options.acceptedtypes;
103
            this.clientid = options.clientid;
104
            this.maxbytes = options.maxbytes;
105
            this.areamaxbytes = options.areamaxbytes;
106
            this.itemid = options.itemid;
107
            this.author = options.author;
108
            this.container = this.Y.one('#'+options.containerid);
109
 
110
            if (options.filemanager) {
111
                // Needed to tell the filemanager to redraw when files uploaded
112
                // and to check how many files are already uploaded
113
                this.filemanager = options.filemanager;
114
            } else if (options.formcallback) {
115
 
116
                // Needed to tell the filepicker to update when a new
117
                // file is uploaded
118
                this.callback = options.formcallback;
119
            } else {
120
                if (M.cfg.developerdebug) {
121
                    alert('dndupload: Need to define either options.filemanager or options.formcallback');
122
                }
123
                return;
124
            }
125
 
126
            this.init_events();
127
            this.init_page_events();
128
        },
129
 
130
        /**
131
         * Check the browser has the required functionality
132
         * @return true if browser supports drag/drop upload
133
         */
134
        browser_supported: function() {
135
 
136
            if (typeof FileReader == 'undefined') {
137
                return false;
138
            }
139
            if (typeof FormData == 'undefined') {
140
                return false;
141
            }
142
            return true;
143
        },
144
 
145
        /**
146
         * Get upload repoistory from array of enabled repositories
147
         *
148
         * @param array repositories repository objects passed from filepicker
149
         * @param returns int id of upload repository or false if not found
150
         */
151
        get_upload_repositoryid: function(repositories) {
152
            for (var i in repositories) {
153
                if (repositories[i].type == "upload") {
154
                    return repositories[i].id;
155
                }
156
            }
157
 
158
            return false;
159
        },
160
 
161
        /**
162
         * Initialise drag events on node container, all events need
163
         * to be processed for drag and drop to work
164
         */
165
        init_events: function() {
166
            this.Y.on('dragenter', this.drag_enter, this.container, this);
167
            this.Y.on('dragleave', this.drag_leave, this.container, this);
168
            this.Y.on('dragover',  this.drag_over,  this.container, this);
169
            this.Y.on('drop',      this.drop,      this.container, this);
170
        },
171
 
172
        /**
173
         * Initialise whole-page events (to show / hide the 'drop files here'
174
         * message)
175
         */
176
        init_page_events: function() {
177
            this.Y.on('dragenter', this.drag_enter_page, 'body', this);
178
            this.Y.on('dragleave', this.drag_leave_page, 'body', this);
179
            this.Y.on('drop', function() {
180
                this.pageentercount = 0;
181
                this.hide_drop_target();
182
            }.bind(this));
183
        },
184
 
185
        /**
186
         * Check if the filemanager / filepicker is disabled
187
         * @return bool - true if disabled
188
         */
189
        is_disabled: function() {
190
            return (this.container.ancestor('.fitem.disabled') != null);
191
        },
192
 
193
        /**
194
         * Show the 'drop files here' message when file(s) are dragged
195
         * onto the page
196
         */
197
        drag_enter_page: function(e) {
198
            if (this.is_disabled()) {
199
                return false;
200
            }
201
            if (!this.has_files(e)) {
202
                return false;
203
            }
204
 
205
            this.pageentercount++;
206
            if (this.pageentercount >= 2) {
207
                this.pageentercount = 2;
208
                return false;
209
            }
210
 
211
            this.show_drop_target();
212
 
213
            return false;
214
        },
215
 
216
        /**
217
         * Hide the 'drop files here' message when file(s) are dragged off
218
         * the page again
219
         */
220
        drag_leave_page: function(e) {
221
            this.pageentercount--;
222
            if (this.pageentercount == 1) {
223
                return false;
224
            }
225
            this.pageentercount = 0;
226
 
227
            this.hide_drop_target();
228
 
229
            return false;
230
        },
231
 
232
        /**
233
         * Check if the drag contents are valid and then call
234
         * preventdefault / stoppropagation to let the browser know
235
         * we will handle this drag/drop
236
         *
237
         * @param e event object
238
         * @return boolean true if a valid file drag event
239
         */
240
        check_drag: function(e) {
241
            if (this.is_disabled()) {
242
                return false;
243
            }
244
            if (!this.has_files(e)) {
245
                return false;
246
            }
247
 
248
            e.preventDefault();
249
            e.stopPropagation();
250
 
251
            return true;
252
        },
253
 
254
        /**
255
         * Handle a dragenter event, highlight the destination node
256
         * when a suitable drag event occurs
257
         */
258
        drag_enter: function(e) {
259
            if (!this.check_drag(e)) {
260
                return true;
261
            }
262
 
263
            this.entercount++;
264
            if (this.entercount >= 2) {
265
                this.entercount = 2; // Just moved over a child element - nothing to do
266
                return false;
267
            }
268
 
269
            // These lines are needed if the user has dragged something directly
270
            // from application onto the 'fileupload' box, without crossing another
271
            // part of the page first
272
            this.pageentercount = 2;
273
            this.show_drop_target();
274
 
275
            this.show_upload_ready();
276
            return false;
277
        },
278
 
279
        /**
280
         * Handle a dragleave event, Remove the highlight if dragged from
281
         * node
282
         */
283
        drag_leave: function(e) {
284
            if (!this.check_drag(e)) {
285
                return true;
286
            }
287
 
288
            this.entercount--;
289
            if (this.entercount == 1) {
290
                return false; // Just moved over a child element - nothing to do
291
            }
292
 
293
            this.entercount = 0;
294
            this.hide_upload_ready();
295
            return false;
296
        },
297
 
298
        /**
299
         * Handle a dragover event. Required to intercept to prevent the browser from
300
         * handling the drag and drop event as normal
301
         */
302
        drag_over: function(e) {
303
            if (!this.check_drag(e)) {
304
                return true;
305
            }
306
 
307
            return false;
308
        },
309
 
310
        /**
311
         * Handle a drop event.  Remove the highlight and then upload each
312
         * of the files (until we reach the file limit, or run out of files)
313
         */
314
        drop: function(e) {
315
            if (!this.check_drag(e, true)) {
316
                return true;
317
            }
318
 
319
            this.entercount = 0;
320
            this.pageentercount = 0;
321
            this.hide_upload_ready();
322
            this.hide_drop_target();
323
 
324
            var files = e._event.dataTransfer.files;
325
            if (this.filemanager) {
326
                var options = {
327
                    files: files,
328
                    options: this.options,
329
                    repositoryid: this.repositoryid,
330
                    currentfilecount: this.filemanager.filecount, // All files uploaded.
331
                    currentfiles: this.filemanager.options.list, // Only the current folder.
332
                    callback: Y.bind('update_filemanager', this),
333
                    callbackprogress: Y.bind('update_progress', this),
334
                    callbackcancel: Y.bind('hide_progress', this),
335
                    callbackNumberOfRequestUpload: {
336
                        get: Y.bind('getNumberOfRequestUpload', this),
337
                        increase: Y.bind('increaseNumberOfRequestUpload', this),
338
                        decrease: Y.bind('decreaseNumberOfRequestUpload', this),
339
                        getTotal: Y.bind('getTotalRequestUpload', this),
340
                        increaseTotal: Y.bind('increaseTotalRequest', this),
341
                        reset: Y.bind('resetNumberOfRequestUpload', this)
342
                    },
343
                    callbackClearProgress: Y.bind('clear_progress', this),
344
                    callbackStartProgress: Y.bind('startProgress', this),
345
                };
346
                this.show_progress();
347
                var uploader = new dnduploader(options);
348
                uploader.start_upload();
349
            } else {
350
                if (files.length >= 1) {
351
                    options = {
352
                        files:[files[0]],
353
                        options: this.options,
354
                        repositoryid: this.repositoryid,
355
                        currentfilecount: 0,
356
                        currentfiles: [],
357
                        callback: Y.bind('update_filemanager', this),
358
                        callbackprogress: Y.bind('update_progress', this),
359
                        callbackcancel: Y.bind('hide_progress', this),
360
                        callbackNumberOfRequestUpload: {
361
                            get: Y.bind('getNumberOfRequestUpload', this),
362
                            increase: Y.bind('increaseNumberOfRequestUpload', this),
363
                            decrease: Y.bind('decreaseNumberOfRequestUpload', this),
364
                            getTotal: Y.bind('getTotalRequestUpload', this),
365
                            increaseTotal: Y.bind('increaseTotalRequest', this),
366
                            reset: Y.bind('resetNumberOfRequestUpload', this)
367
                        },
368
                        callbackClearProgress: Y.bind('clear_progress', this),
369
                        callbackStartProgress: Y.bind('startProgress', this),
370
                    };
371
                    this.show_progress();
372
                    uploader = new dnduploader(options);
373
                    uploader.start_upload();
374
                }
375
            }
376
 
377
            return false;
378
        },
379
 
380
        /**
381
         * Increase number of request upload.
382
         */
383
        increaseNumberOfRequestUpload: function() {
384
            this.numberOfRequestUpload++;
385
        },
386
 
387
        /**
388
         * Increase total request.
389
         *
390
         * @param {number} newFileCount Number of new files.
391
         */
392
        increaseTotalRequest: function(newFileCount) {
393
            this.totalOfRequest += newFileCount;
394
        },
395
 
396
        /**
397
         * Decrease number of request upload.
398
         */
399
        decreaseNumberOfRequestUpload: function() {
400
            this.numberOfRequestUpload--;
401
        },
402
 
403
        /**
404
         * Return number of request upload.
405
         *
406
         * @returns {number}
407
         */
408
        getNumberOfRequestUpload: function() {
409
            return this.numberOfRequestUpload;
410
        },
411
 
412
        /**
413
         * Return number of request upload.
414
         *
415
         * @returns {number}
416
         */
417
        getTotalRequestUpload: function() {
418
            return this.totalOfRequest;
419
        },
420
 
421
        /**
422
         * Return number of request upload.
423
         *
424
         * @returns {number}
425
         */
426
        resetNumberOfRequestUpload: function() {
427
            this.numberOfRequestUpload = 0;
428
            this.totalOfRequest = 0;
429
        },
430
 
431
        /**
432
         * Check to see if the drag event has any files in it
433
         *
434
         * @param e event object
435
         * @return boolean true if event has files
436
         */
437
        has_files: function(e) {
438
            // In some browsers, dataTransfer.types may be null for a
439
            // 'dragover' event, so ensure a valid Array is always
440
            // inspected.
441
            var types = e._event.dataTransfer.types || [];
442
            for (var i=0; i<types.length; i++) {
443
                if (types[i] == 'Files') {
444
                    return true;
445
                }
446
            }
447
            return false;
448
        },
449
 
450
        /**
451
         * Highlight the area where files could be dropped
452
         */
453
        show_drop_target: function() {
454
            this.container.addClass('dndupload-ready');
455
        },
456
 
457
        hide_drop_target: function() {
458
            this.container.removeClass('dndupload-ready');
459
        },
460
 
461
        /**
462
         * Highlight the destination node (ready to drop)
463
         */
464
        show_upload_ready: function() {
465
            this.container.addClass('dndupload-over');
466
        },
467
 
468
        /**
469
         * Remove highlight on destination node
470
         */
471
        hide_upload_ready: function() {
472
            this.container.removeClass('dndupload-over');
473
        },
474
 
475
        /**
476
         * Show the element showing the upload in progress
477
         */
478
        show_progress: function() {
479
            this.container.addClass('dndupload-inprogress');
480
        },
481
 
482
        /**
483
         * Hide the element showing upload in progress
484
         */
485
        hide_progress: function() {
486
            if (!Object.keys(this.progressbars).length) {
487
                this.container.removeClass('dndupload-inprogress');
488
            }
489
        },
490
 
491
        /**
492
         * Tell the attached filemanager element (if any) to refresh on file
493
         * upload
494
         */
495
        update_filemanager: function(params) {
496
            this.clear_progress();
497
            this.hide_progress();
498
            if (this.filemanager) {
499
                // update the filemanager that we've uploaded the files
500
                this.filemanager.filepicker_callback();
501
            } else if (this.callback) {
502
                this.callback(params);
503
            }
504
        },
505
 
506
        /**
507
         * Clear the all progress bars.
508
         */
509
        clear_progress: function() {
510
            var filename;
511
            for (filename in this.progressbars) {
512
                if (this.progressbars.hasOwnProperty(filename)) {
513
                    this.progressbars[filename].progressouter.remove(true);
514
                    delete this.progressbars[filename];
515
                }
516
            }
517
        },
518
 
519
        /**
520
         * Show the current progress of the uploaded file.
521
         */
522
        update_progress: function(filename, percent) {
523
            this.startProgress(filename);
524
            this.progressbars[filename].progressinner.setStyle('width', percent + '%');
525
            this.progressbars[filename].progressinner.setAttribute('aria-valuenow', percent);
526
            this.progressbars[filename].progressinnertext.setContent(percent + '% ' + M.util.get_string('complete', 'moodle'));
527
        },
528
 
529
        /**
530
         * Start to show the progress of the uploaded file.
531
         *
532
         * @param {String} filename Name of file upload.
533
         */
534
        startProgress: function(filename) {
535
            if (this.progressbars[filename] === undefined) {
536
                var dispfilename = filename;
537
                if (dispfilename.length > 50) {
538
                    dispfilename = dispfilename.substr(0, 49) + '&hellip;';
539
                }
540
                var progressouter = this.container.create('<div>' + dispfilename +
541
                    '<div class="progress">' +
542
                    '   <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">' +
543
                    '       <span class="sr-only"></span>' +
544
                    '   </div>' +
545
                    '</div></div>');
546
                var progressinner = progressouter.one('.progress-bar');
547
                var progressinnertext = progressinner.one('.sr-only');
548
                var progresscontainer = this.container.one('.dndupload-progressbars');
549
                progresscontainer.appendChild(progressouter);
550
 
551
                this.progressbars[filename] = {
552
                    progressouter: progressouter,
553
                    progressinner: progressinner,
554
                    progressinnertext: progressinnertext
555
                };
556
            }
557
        }
558
    };
559
 
560
    var dnduploader = function(options) {
561
        dnduploader.superclass.constructor.apply(this, arguments);
562
    };
563
 
564
    Y.extend(dnduploader, Y.Base, {
565
        // The URL to send the upload data to.
566
        api: M.cfg.wwwroot+'/repository/repository_ajax.php',
567
        // Options passed into the filemanager/filepicker element.
568
        options: {},
569
        // The function to call when all uploads complete.
570
        callback: null,
571
        // The function to call as the upload progresses
572
        callbackprogress: null,
573
        // The function to call if the upload is cancelled
574
        callbackcancel: null,
575
        // The list of files dropped onto the element.
576
        files: null,
577
        // The ID of the 'upload' repository.
578
        repositoryid: 0,
579
        // Array of files already in the current folder (to check for name clashes).
580
        currentfiles: null,
581
        // Total number of files already uploaded (to check for exceeding limits).
582
        currentfilecount: 0,
583
        // Number of new files will be upload in this dndupload (to check for exceeding limits).
584
        newFileCount: 0,
585
        // Total size of the files present in the area.
586
        currentareasize: 0,
587
        // The list of files to upload.
588
        uploadqueue: [],
589
        // This list of files with name clashes.
590
        renamequeue: [],
591
        // Size of the current queue.
592
        queuesize: 0,
593
        // Set to true if the user has clicked on 'overwrite all'.
594
        overwriteall: false,
595
        // Set to true if the user has clicked on 'rename all'.
596
        renameall: false,
597
        // The file manager helper.
598
        filemanagerhelper: null,
599
        // The function to call as the number of request upload.
600
        callbackNumberOfRequestUpload: null,
601
        // The function to call as the clear progresses.
602
        callbackClearProgress: null,
603
        // The function to call as the start progress.
604
        callbackStartProgress: null,
605
 
606
        /**
607
         * Initialise the settings for the dnduploader
608
         * @param object params - includes:
609
         *                     options (copied from the filepicker / filemanager)
610
         *                     repositoryid - ID of the upload repository
611
         *                     callback - the function to call when uploads are complete
612
         *                     currentfiles - the list of files already in the current folder in the filemanager
613
         *                     currentfilecount - the total files already in the filemanager
614
         *                     files - the list of files to upload
615
         * @return void
616
         */
617
        initializer: function(params) {
618
            this.options = params.options;
619
            this.repositoryid = params.repositoryid;
620
            this.callback = params.callback;
621
            this.callbackprogress = params.callbackprogress;
622
            this.callbackcancel = params.callbackcancel;
623
            this.currentfiles = params.currentfiles;
624
            this.currentfilecount = params.currentfilecount;
625
            this.currentareasize = 0;
626
            this.filemanagerhelper = this.options.filemanager;
627
            this.callbackNumberOfRequestUpload = params.callbackNumberOfRequestUpload;
628
            this.callbackClearProgress = params.callbackClearProgress;
629
            this.callbackStartProgress = params.callbackStartProgress;
630
 
631
            // Retrieve the current size of the area.
632
            for (var i = 0; i < this.currentfiles.length; i++) {
633
                this.currentareasize += this.currentfiles[i].size;
634
            };
635
 
636
            if (!this.initialise_queue(params.files)) {
637
                if (this.callbackcancel) {
638
                    this.callbackcancel();
639
                }
640
            }
641
        },
642
 
643
        /**
644
         * Entry point for starting the upload process (starts by processing any
645
         * renames needed)
646
         */
647
        start_upload: function() {
648
            this.process_renames(); // Automatically calls 'do_upload' once renames complete.
649
        },
650
 
651
        /**
652
         * Display a message in a popup
653
         * @param string msg - the message to display
654
         * @param string type - 'error' or 'info'
655
         */
656
        print_msg: function(msg, type) {
657
            var header = M.util.get_string('error', 'moodle');
658
            if (type != 'error') {
659
                type = 'info'; // one of only two types excepted
660
                header = M.util.get_string('info', 'moodle');
661
            }
662
            if (!this.msg_dlg) {
663
                this.msg_dlg_node = Y.Node.create(M.core_filepicker.templates.message);
664
                this.msg_dlg_node.generateID();
665
 
666
                this.msg_dlg = new M.core.dialogue({
667
                    bodyContent: this.msg_dlg_node,
668
                    centered: true,
669
                    modal: true,
670
                    visible: false
671
                });
672
                this.msg_dlg.plug(Y.Plugin.Drag,{handles:['#'+this.msg_dlg_node.get('id')+' .yui3-widget-hd']});
673
                this.msg_dlg_node.one('.fp-msg-butok').on('click', function(e) {
674
                    e.preventDefault();
675
                    this.msg_dlg.hide();
676
                }, this);
677
            }
678
 
679
            this.msg_dlg.set('headerContent', header);
680
            this.msg_dlg_node.removeClass('fp-msg-info').removeClass('fp-msg-error').addClass('fp-msg-'+type)
681
            this.msg_dlg_node.one('.fp-msg-text').setContent(msg);
682
            this.msg_dlg.show();
683
        },
684
 
685
        /**
686
         * Check the size of each file and add to either the uploadqueue or, if there
687
         * is a name clash, the renamequeue
688
         * @param FileList files - the files to upload
689
         * @return void
690
         */
691
        initialise_queue: function(files) {
692
            this.uploadqueue = [];
693
            this.renamequeue = [];
694
            this.queuesize = 0;
695
 
696
            // Loop through the files and find any name clashes with existing files.
697
            var i;
698
            for (i=0; i<files.length; i++) {
699
                if (this.options.maxbytes > 0 && files[i].size > this.options.maxbytes) {
700
                    // Check filesize before attempting to upload.
701
                    var maxbytesdisplay = this.display_size(this.options.maxbytes);
702
                    this.print_msg(M.util.get_string('maxbytesfile', 'error', {
703
                            file: files[i].name,
704
                            size: maxbytesdisplay
705
                        }), 'error');
706
                    this.uploadqueue = []; // No uploads if one file is too big.
707
                    return;
708
                }
709
 
710
                if (this.has_name_clash(files[i].name)) {
711
                    this.renamequeue.push(files[i]);
712
                } else {
713
                    if (!this.add_to_upload_queue(files[i], files[i].name, false)) {
714
                        return false;
715
                    }
716
                }
717
                this.queuesize += files[i].size;
718
            }
719
            return true;
720
        },
721
 
722
        /**
723
         * Generate the display for file size
724
         * @param int size The size to convert to human readable form
725
         * @return string
726
         */
727
        display_size: function(size) {
728
            // This is snippet of code (with some changes) is from the display_size function in moodlelib.
729
            var gb = M.util.get_string('sizegb', 'moodle'),
730
                mb = M.util.get_string('sizemb', 'moodle'),
731
                kb = M.util.get_string('sizekb', 'moodle'),
732
                b  = M.util.get_string('sizeb', 'moodle');
733
 
734
            if (size >= 1073741824) {
735
                size = Math.round(size / 1073741824 * 10) / 10 + gb;
736
            } else if (size >= 1048576) {
737
                size = Math.round(size / 1048576 * 10) / 10 + mb;
738
            } else if (size >= 1024) {
739
                size = Math.round(size / 1024 * 10) / 10 + kb;
740
            } else {
741
                size = parseInt(size, 10) + ' ' + b;
742
            }
743
 
744
            return size;
745
        },
746
 
747
        /**
748
         * Add a single file to the uploadqueue, whilst checking the maxfiles limit
749
         * @param File file - the file to add
750
         * @param string filename - the name to give the file on upload
751
         * @param bool overwrite - true to overwrite the existing file
752
         * @return bool true if added successfully
753
         */
754
        add_to_upload_queue: function(file, filename, overwrite) {
755
            if (!overwrite) {
756
                this.newFileCount++;
757
            }
758
 
759
            // The value for "unlimited files" is -1, so 0 should mean 0.
760
            if (this.options.maxfiles >= 0 && this.getTotalNumberOfFiles() > this.options.maxfiles) {
761
                // Too many files - abort entire upload.
762
                this.uploadqueue = [];
763
                this.renamequeue = [];
764
                this.print_msg(M.util.get_string('maxfilesreached', 'moodle', this.options.maxfiles), 'error');
765
                return false;
766
            }
767
            // The new file will cause the area to reach its limit, we cancel the upload of all files.
768
            // -1 is the value defined by FILE_AREA_MAX_BYTES_UNLIMITED.
769
            if (this.options.areamaxbytes > -1) {
770
                var sizereached = this.currentareasize + this.queuesize + file.size;
771
                if (sizereached > this.options.areamaxbytes) {
772
                    this.uploadqueue = [];
773
                    this.renamequeue = [];
774
                    this.print_msg(M.util.get_string('maxareabytesreached', 'moodle'), 'error');
775
                    return false;
776
                }
777
            }
778
            this.uploadqueue.push({file:file, filename:filename, overwrite:overwrite});
779
            return true;
780
        },
781
 
782
        /**
783
         * Get total number of files: Number of uploaded files, number of files unloading in other dndupload,
784
         * number of files need to be upload in this dndupload.
785
         * @return number Total number of files.
786
         */
787
        getTotalNumberOfFiles: function() {
788
            // Get number of files we added into other dndupload.
789
            let totalOfFiles = 0;
790
            if(this.callbackNumberOfRequestUpload) {
791
                totalOfFiles = this.callbackNumberOfRequestUpload.getTotal();
792
            }
793
 
794
            return this.currentfilecount + this.newFileCount + totalOfFiles;
795
        },
796
 
797
        /**
798
         * Take the next file from the renamequeue and ask the user what to do with
799
         * it. Called recursively until the queue is empty, then calls do_upload.
800
         * @return void
801
         */
802
        process_renames: function() {
803
            if (this.renamequeue.length == 0) {
804
                // All rename processing complete - start the actual upload.
805
                if(this.callbackNumberOfRequestUpload && this.uploadqueue.length > 0) {
806
                    this.callbackNumberOfRequestUpload.increaseTotal(this.newFileCount);
807
                }
808
                this.do_upload();
809
                return;
810
            }
811
            var multiplefiles = (this.renamequeue.length > 1);
812
 
813
            // Get the next file from the rename queue.
814
            var file = this.renamequeue.shift();
815
            // Generate a non-conflicting name for it.
816
            var newname = this.generate_unique_name(file.name);
817
 
818
            // If the user has clicked on overwrite/rename ALL then process
819
            // this file, as appropriate, then process the rest of the queue.
820
            if (this.overwriteall) {
821
                if (this.add_to_upload_queue(file, file.name, true)) {
822
                    this.process_renames();
823
                }
824
                return;
825
            }
826
            if (this.renameall) {
827
                if (this.add_to_upload_queue(file, newname, false)) {
828
                    this.process_renames();
829
                }
830
                return;
831
            }
832
 
833
            // Ask the user what to do with this file.
834
            var self = this;
835
 
836
            var process_dlg_node;
837
            if (multiplefiles) {
838
                process_dlg_node = Y.Node.create(M.core_filepicker.templates.processexistingfilemultiple);
839
            } else {
840
                process_dlg_node = Y.Node.create(M.core_filepicker.templates.processexistingfile);
841
            }
842
            var node = process_dlg_node;
843
            node.generateID();
844
            var process_dlg = new M.core.dialogue({
845
                bodyContent: node,
846
                headerContent: M.util.get_string('fileexistsdialogheader', 'repository'),
847
                centered: true,
848
                modal: true,
849
                visible: false
850
            });
851
            process_dlg.plug(Y.Plugin.Drag,{handles:['#'+node.get('id')+' .yui3-widget-hd']});
852
 
853
            // Overwrite original.
854
            node.one('.fp-dlg-butoverwrite').on('click', function(e) {
855
                e.preventDefault();
856
                process_dlg.hide();
857
                if (self.add_to_upload_queue(file, file.name, true)) {
858
                    self.process_renames();
859
                }
860
            }, this);
861
 
862
            // Rename uploaded file.
863
            node.one('.fp-dlg-butrename').on('click', function(e) {
864
                e.preventDefault();
865
                process_dlg.hide();
866
                if (self.add_to_upload_queue(file, newname, false)) {
867
                    self.process_renames();
868
                }
869
            }, this);
870
 
871
            // Cancel all uploads.
872
            node.one('.fp-dlg-butcancel').on('click', function(e) {
873
                e.preventDefault();
874
                process_dlg.hide();
875
                if (self.callbackcancel) {
876
                    this.notifyUploadCompleted();
877
                    self.callbackClearProgress();
878
                    self.callbackcancel();
879
                }
880
            }, this);
881
 
882
            // When we are at the file limit, only allow 'overwrite', not rename.
883
            if (this.getTotalNumberOfFiles() == this.options.maxfiles) {
884
                node.one('.fp-dlg-butrename').setStyle('display', 'none');
885
                if (multiplefiles) {
886
                    node.one('.fp-dlg-butrenameall').setStyle('display', 'none');
887
                }
888
            }
889
 
890
            // If there are more files still to go, offer the 'overwrite/rename all' options.
891
            if (multiplefiles) {
892
                // Overwrite all original files.
893
                node.one('.fp-dlg-butoverwriteall').on('click', function(e) {
894
                    e.preventDefault();
895
                    process_dlg.hide();
896
                    this.overwriteall = true;
897
                    if (self.add_to_upload_queue(file, file.name, true)) {
898
                        self.process_renames();
899
                    }
900
                }, this);
901
 
902
                // Rename all new files.
903
                node.one('.fp-dlg-butrenameall').on('click', function(e) {
904
                    e.preventDefault();
905
                    process_dlg.hide();
906
                    this.renameall = true;
907
                    if (self.add_to_upload_queue(file, newname, false)) {
908
                        self.process_renames();
909
                    }
910
                }, this);
911
            }
912
            node.one('.fp-dlg-text').setContent(M.util.get_string('fileexists', 'moodle', file.name));
913
            process_dlg_node.one('.fp-dlg-butrename').setContent(M.util.get_string('renameto', 'repository', newname));
914
 
915
            // Destroy the dialog once it has been hidden.
916
            process_dlg.after('visibleChange', function(e) {
917
                if (!process_dlg.get('visible')) {
918
                    if (self.callbackcancel) {
919
                        self.callbackcancel();
920
                    }
921
                    process_dlg.destroy(true);
922
                }
923
            }, this);
924
 
925
            process_dlg.show();
926
        },
927
 
928
        /**
929
         * Trigger upload completed event.
930
         */
931
        notifyUploadCompleted: function() {
932
            require(['core_form/events'], function(FormEvent) {
933
                const elementId = this.filemanagerhelper ? this.filemanagerhelper.filemanager.get('id') : this.options.containerid;
934
                FormEvent.triggerUploadCompleted(elementId);
935
            }.bind(this));
936
         },
937
 
938
        /**
939
         * Trigger form upload start events.
940
         */
941
        notifyUploadStarted: function() {
942
            require(['core_form/events'], function(FormEvent) {
943
                const elementId = this.filemanagerhelper ? this.filemanagerhelper.filemanager.get('id') : this.options.containerid;
944
                FormEvent.triggerUploadStarted(elementId);
945
            }.bind(this));
946
        },
947
 
948
        /**
949
         * Checks if there is already a file with the given name in the current folder
950
         * or in the list of already uploading files
951
         * @param string filename - the name to test
952
         * @return bool true if the name already exists
953
         */
954
        has_name_clash: function(filename) {
955
            // Check against the already uploaded files
956
            var i;
957
            for (i=0; i<this.currentfiles.length; i++) {
958
                if (filename == this.currentfiles[i].filename) {
959
                    return true;
960
                }
961
            }
962
            // Check against the uploading files that have already been processed
963
            for (i=0; i<this.uploadqueue.length; i++) {
964
                if (filename == this.uploadqueue[i].filename) {
965
                    return true;
966
                }
967
            }
968
            return false;
969
        },
970
 
971
        /**
972
         * Gets a unique file name
973
         *
974
         * @param string filename
975
         * @return string the unique filename generated
976
         */
977
        generate_unique_name: function(filename) {
978
            // Loop through increating numbers until a unique name is found.
979
            while (this.has_name_clash(filename)) {
980
                filename = increment_filename(filename);
981
            }
982
            return filename;
983
        },
984
 
985
        /**
986
         * Upload the next file from the uploadqueue - called recursively after each
987
         * upload is complete, then handles the callback to the filemanager/filepicker
988
         * @param lastresult - the last result from the server
989
         */
990
        do_upload: function(lastresult) {
991
            if (this.uploadqueue.length > 0) {
992
                var filedetails = this.uploadqueue.shift();
993
                this.upload_file(filedetails.file, filedetails.filename, filedetails.overwrite);
994
            } else {
995
                if (this.callbackNumberOfRequestUpload && !this.callbackNumberOfRequestUpload.get()) {
996
                    this.uploadfinished(lastresult);
997
                }
998
            }
999
        },
1000
 
1001
        /**
1002
         * Run the callback to the filemanager/filepicker
1003
         */
1004
        uploadfinished: function(lastresult) {
1005
            this.callbackNumberOfRequestUpload.reset();
1006
            this.callback(lastresult);
1007
        },
1008
 
1009
        /**
1010
         * Upload a single file via an AJAX call to the 'upload' repository. Automatically
1011
         * calls do_upload as each upload completes.
1012
         * @param File file - the file to upload
1013
         * @param string filename - the name to give the file
1014
         * @param bool overwrite - true if the existing file should be overwritten
1015
         */
1016
        upload_file: function(file, filename, overwrite) {
1017
 
1018
            // This would be an ideal place to use the Y.io function
1019
            // however, this does not support data encoded using the
1020
            // FormData object, which is needed to transfer data from
1021
            // the DataTransfer object into an XMLHTTPRequest
1022
            // This can be converted when the YUI issue has been integrated:
1023
            // http://yuilibrary.com/projects/yui3/ticket/2531274
1024
            var xhr = new XMLHttpRequest();
1025
            var self = this;
1026
            if (self.callbackNumberOfRequestUpload) {
1027
                self.callbackNumberOfRequestUpload.increase();
1028
            }
1029
 
1030
            // Start progress bar.
1031
            xhr.onloadstart = function() {
1032
                self.callbackStartProgress(filename);
1033
                self.notifyUploadStarted();
1034
            };
1035
 
1036
            // Update the progress bar
1037
            xhr.upload.addEventListener('progress', function(e) {
1038
                if (e.lengthComputable && self.callbackprogress) {
1039
                    var percentage = Math.round((e.loaded * 100) / e.total);
1040
                    self.callbackprogress(filename, percentage);
1041
                }
1042
            }, false);
1043
 
1044
            xhr.onreadystatechange = function() { // Process the server response
1045
                if (xhr.readyState == 4) {
1046
                    self.notifyUploadCompleted();
1047
                    if (xhr.status == 200) {
1048
                        var result = JSON.parse(xhr.responseText);
1049
                        if (result) {
1050
                            if (result.error) {
1051
                                self.print_msg(result.error, 'error'); // TODO add filename?
1052
                                self.uploadfinished();
1053
                            } else {
1054
                                // Only update the filepicker if there were no errors
1055
                                if (result.event == 'fileexists') {
1056
                                    // Do not worry about this, as we only care about the last
1057
                                    // file uploaded, with the filepicker
1058
                                    result.file = result.newfile.filename;
1059
                                    result.url = result.newfile.url;
1060
                                }
1061
                                result.client_id = self.options.clientid;
1062
                                if (self.callbackprogress) {
1063
                                    self.callbackprogress(filename, 100);
1064
                                }
1065
                            }
1066
                        }
1067
                        if (self.callbackNumberOfRequestUpload) {
1068
                            self.callbackNumberOfRequestUpload.decrease();
1069
                        }
1070
                        self.do_upload(result); // continue uploading
1071
                    } else {
1072
                        self.print_msg(M.util.get_string('serverconnection', 'error'), 'error');
1073
                        self.uploadfinished();
1074
                    }
1075
                }
1076
            };
1077
 
1078
            // Prepare the data to send
1079
            var formdata = new FormData();
1080
            formdata.append('repo_upload_file', file); // The FormData class allows us to attach a file
1081
            formdata.append('sesskey', M.cfg.sesskey);
1082
            formdata.append('repo_id', this.repositoryid);
1083
            formdata.append('itemid', this.options.itemid);
1084
            if (this.options.author) {
1085
                formdata.append('author', this.options.author);
1086
            }
1087
            if (this.options.filemanager) { // Filepickers do not have folders
1088
                formdata.append('savepath', this.options.filemanager.currentpath);
1089
            }
1090
            formdata.append('title', filename);
1091
            if (overwrite) {
1092
                formdata.append('overwrite', 1);
1093
            }
1094
            if (this.options.contextid) {
1095
                formdata.append('ctx_id', this.options.contextid);
1096
            }
1097
 
1098
            // Accepted types can be either a string or an array, but an array is
1099
            // expected in the processing script, so make sure we are sending an array
1100
            if (this.options.acceptedtypes.constructor == Array) {
1101
                for (var i=0; i<this.options.acceptedtypes.length; i++) {
1102
                    formdata.append('accepted_types[]', this.options.acceptedtypes[i]);
1103
                }
1104
            } else {
1105
                formdata.append('accepted_types[]', this.options.acceptedtypes);
1106
            }
1107
 
1108
            // Send the file & required details.
1109
            var uploadUrl = this.api;
1110
            if (uploadUrl.indexOf('?') !== -1) {
1111
                uploadUrl += '&action=upload';
1112
            } else {
1113
                uploadUrl += '?action=upload';
1114
            }
1115
            xhr.open("POST", uploadUrl, true);
1116
            xhr.send(formdata);
1117
            return true;
1118
        }
1119
    });
1120
 
1121
    dnduploadhelper.init(Y, options);
1122
};