// 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 . /** * Javascript library for enableing a drag and drop upload to courses * * @package core * @subpackage course * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ M.course_dndupload = { // YUI object. Y: null, // URL for upload requests url: M.cfg.wwwroot + '/course/dndupload.php', // maximum size of files allowed in this form maxbytes: 0, // ID of the course we are on courseid: null, // Data about the different file/data handlers that are available handlers: null, // Nasty hack to distinguish between dragenter(first entry), // dragenter+dragleave(moving between child elements) and dragleave (leaving element) entercount: 0, // Used to keep track of the section we are dragging across - to make // spotting movement between sections more reliable currentsection: null, // Used to store the pending uploads whilst the user is being asked for further input uploadqueue: null, // True if the there is currently a dialog being shown (asking for a name, or giving a // choice of file handlers) uploaddialog: false, // An array containing the last selected file handler for each file type lastselected: null, // The following are used to identify specific parts of the course page // The type of HTML element that is a course section sectiontypename: 'li', // The classes that an element must have to be identified as a course section sectionclasses: ['section', 'main'], // The ID of the main content area of the page (for adding the 'status' div) pagecontentid: 'page', // The selector identifying the list of modules within a section (note changing this may require // changes to the get_mods_element function) modslistselector: 'ul.section', /** * Initalise the drag and drop upload interface * Note: one and only one of options.filemanager and options.formcallback must be defined * * @param Y the YUI object * @param object options { * courseid: ID of the course we are on * maxbytes: maximum size of files allowed in this form * handlers: Data about the different file/data handlers that are available * } */ init: function(Y, options) { this.Y = Y; if (!this.browser_supported()) { return; // Browser does not support the required functionality } this.maxbytes = options.maxbytes; this.courseid = options.courseid; this.handlers = options.handlers; this.uploadqueue = new Array(); this.lastselected = new Array(); var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.'); var sections = this.Y.all(sectionselector); if (sections.isEmpty()) { return; // No sections - incompatible course format or front page. } sections.each( function(el) { this.add_preview_element(el); this.init_events(el); }, this); if (options.showstatus) { this.add_status_div(); } }, /** * Add a div element to tell the user that drag and drop upload * is available (or to explain why it is not available) */ add_status_div: function() { var coursecontents = document.getElementById(this.pagecontentid); if (!coursecontents) { return; } var div = document.createElement('div'); div.id = 'dndupload-status'; div.style.opacity = 0.0; coursecontents.insertBefore(div, coursecontents.firstChild); var Y = this.Y; div = Y.one(div); var handlefile = (this.handlers.filehandlers.length > 0); var handletext = false; var handlelink = false; var i; for (i=0; i 2) { this.entercount = 2; return false; } } this.show_preview_element(section, type); return false; }, /** * Handle a dragleave event: remove the 'add here' message (if present) * @param e event data * @return false to prevent the event from continuing to be processed */ drag_leave: function(e) { if (!this.check_drag(e)) { return false; } this.entercount--; if (this.entercount == 1) { return false; } this.entercount = 0; this.currentsection = null; this.hide_preview_element(); return false; }, /** * Handle a dragover event: just prevent the browser default (necessary * to allow drag and drop handling to work) * @param e event data * @return false to prevent the event from continuing to be processed */ drag_over: function(e) { this.check_drag(e); return false; }, /** * Handle a drop event: hide the 'add here' message, check the attached * data type and start the upload process * @param e event data * @return false to prevent the event from continuing to be processed */ drop: function(e) { if (!(type = this.check_drag(e))) { return false; } this.hide_preview_element(); // Work out the number of the section we are on (from its id) var section = this.get_section(e.currentTarget); var sectionnumber = this.get_section_number(section); // Process the file or the included data if (type.type == 'Files') { var files = e._event.dataTransfer.files; for (var i=0, f; f=files[i]; i++) { this.handle_file(f, section, sectionnumber); } } else { var contents = e._event.dataTransfer.getData(type.realtype); if (contents) { this.handle_item(type, contents, section, sectionnumber); } } return false; }, /** * Find or create the 'ul' element that contains all of the module * instances in this section * @param section the DOM element representing the section * @return false to prevent the event from continuing to be processed */ get_mods_element: function(section) { // Find the 'ul' containing the list of mods var modsel = section.one(this.modslistselector); if (!modsel) { // Create the above 'ul' if it doesn't exist var modsel = document.createElement('ul'); modsel.className = 'section img-text'; var contentel = section.get('children').pop(); var brel = contentel.get('children').pop(); contentel.insertBefore(modsel, brel); modsel = this.Y.one(modsel); } return modsel; }, /** * Add a new dummy item to the list of mods, to be replaced by a real * item & link once the AJAX upload call has completed * @param name the label to show in the element * @param section the DOM element reperesenting the course section * @return DOM element containing the new item */ add_resource_element: function(name, section) { var modsel = this.get_mods_element(section); var resel = { parent: modsel, li: document.createElement('li'), div: document.createElement('div'), a: document.createElement('a'), icon: document.createElement('img'), namespan: document.createElement('span'), progressouter: document.createElement('span'), progress: document.createElement('span') }; resel.li.className = 'activity resource modtype_resource'; resel.div.className = 'mod-indent'; resel.li.appendChild(resel.div); resel.a.href = '#'; resel.div.appendChild(resel.a); resel.icon.src = M.util.image_url('i/ajaxloader'); resel.icon.className = 'activityicon'; resel.a.appendChild(resel.icon); resel.a.appendChild(document.createTextNode(' ')); resel.namespan.className = 'instancename'; resel.namespan.innerHTML = name; resel.a.appendChild(resel.namespan); resel.div.appendChild(document.createTextNode(' ')); resel.progressouter.className = 'dndupload-progress-outer'; resel.progress.className = 'dndupload-progress-inner'; resel.progress.innerHTML = ' '; resel.progressouter.appendChild(resel.progress); resel.div.appendChild(resel.progressouter); modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom return resel; }, /** * Hide any visible dndupload-preview elements on the page */ hide_preview_element: function() { this.Y.all('li.dndupload-preview').addClass('dndupload-hidden'); }, /** * Unhide the preview element for the given section and set it to display * the correct message * @param section the YUI node representing the selected course section * @param type the details of the data type detected in the drag (including the message to display) */ show_preview_element: function(section, type) { this.hide_preview_element(); var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden'); preview.one('span').setContent(type.addmessage); }, /** * Add the preview element to a course section. Note: this needs to be done before 'addEventListener' * is called, otherwise Firefox will ignore events generated when the mouse is over the preview * element (instead of passing them up to the parent element) * @param section the YUI node representing the selected course section */ add_preview_element: function(section) { var modsel = this.get_mods_element(section); var preview = { li: document.createElement('li'), div: document.createElement('div'), icon: document.createElement('img'), namespan: document.createElement('span') }; preview.li.className = 'dndupload-preview dndupload-hidden'; preview.div.className = 'mod-indent'; preview.li.appendChild(preview.div); preview.icon.src = M.util.image_url('t/addfile'); preview.div.appendChild(preview.icon); preview.div.appendChild(document.createTextNode(' ')); preview.namespan.className = 'instancename'; preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle'); preview.div.appendChild(preview.namespan); modsel.appendChild(preview.li); }, /** * Find the registered handler for the given file type. If there is more than one, ask the * user which one to use. Then upload the file to the server * @param file the details of the file, taken from the FileList in the drop event * @param section the DOM element representing the selected course section * @param sectionnumber the number of the selected course section */ handle_file: function(file, section, sectionnumber) { var handlers = new Array(); var filehandlers = this.handlers.filehandlers; var extension = ''; var dotpos = file.name.lastIndexOf('.'); if (dotpos != -1) { extension = file.name.substr(dotpos+1, file.name.length); } for (var i=0; i'; content += '
'; for (var i=0; i'; content += '
'; } content += '
'; var Y = this.Y; var self = this; var panel = new Y.Panel({ bodyContent: content, width: 350, zIndex: 5, centered: true, modal: true, visible: true, render: true, buttons: [{ value: M.util.get_string('upload', 'moodle'), action: function(e) { e.preventDefault(); // Find out which module was selected var module = false; var div = Y.one('#dndupload_handlers'+uploadid); div.all('input').each(function(input) { if (input.get('checked')) { module = input.get('value'); } }); if (!module) { return; } panel.hide(); // Remember this selection for next time self.lastselected[extension] = module; // Do the upload self.upload_file(file, section, sectionnumber, module); }, section: Y.WidgetStdMod.FOOTER },{ value: M.util.get_string('cancel', 'moodle'), action: function(e) { e.preventDefault(); panel.hide(); }, section: Y.WidgetStdMod.FOOTER }] }); // When the panel is hidden - destroy it and then check for other pending uploads panel.after("visibleChange", function(e) { if (!panel.get('visible')) { panel.destroy(true); self.check_upload_queue(); } }); }, /** * Check to see if there are any other dialog boxes to show, now that the current one has * been dealt with */ check_upload_queue: function() { this.uploaddialog = false; if (this.uploadqueue.length == 0) { return; } var details = this.uploadqueue.shift(); if (details.isfile) { this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber); } else { this.handle_item(details.type, details.contents, details.section, details.sectionnumber); } }, /** * Do the file upload: show the dummy element, use an AJAX call to send the data * to the server, update the progress bar for the file, then replace the dummy * element with the real information once the AJAX call completes * @param file the details of the file, taken from the FileList in the drop event * @param section the DOM element representing the selected course section * @param sectionnumber the number of the selected course section */ upload_file: function(file, section, sectionnumber, module) { // This would be an ideal place to use the Y.io function // however, this does not support data encoded using the // FormData object, which is needed to transfer data from // the DataTransfer object into an XMLHTTPRequest // This can be converted when the YUI issue has been integrated: // http://yuilibrary.com/projects/yui3/ticket/2531274 var xhr = new XMLHttpRequest(); var self = this; if (file.size > this.maxbytes) { alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle')); return; } // Add the file to the display var resel = this.add_resource_element(file.name, section); // Update the progress bar as the file is uploaded xhr.upload.addEventListener('progress', function(e) { if (e.lengthComputable) { var percentage = Math.round((e.loaded * 100) / e.total); resel.progress.style.width = percentage + '%'; } }, false); // Wait for the AJAX call to complete, then update the // dummy element with the returned details xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { var result = JSON.parse(xhr.responseText); if (result) { if (result.error == 0) { // All OK - update the dummy element resel.icon.src = result.icon; resel.a.href = result.link; resel.namespan.innerHTML = result.name; resel.div.removeChild(resel.progressouter); resel.li.id = result.elementid; resel.div.innerHTML += result.commands; if (result.onclick) { resel.a.onclick = result.onclick; } if (self.Y.UA.gecko > 0) { // Fix a Firefox bug which makes sites with a '~' in their wwwroot // log the user out when clicking on the link (before refreshing the page). resel.div.innerHTML = unescape(resel.div.innerHTML); } self.add_editing(result.elementid); } else { // Error - remove the dummy element resel.parent.removeChild(resel.li); alert(result.error); } } } else { alert(M.util.get_string('servererror', 'moodle')); } } }; // Prepare the data to send var formData = new FormData(); formData.append('repo_upload_file', file); formData.append('sesskey', M.cfg.sesskey); formData.append('course', this.courseid); formData.append('section', sectionnumber); formData.append('module', module); formData.append('type', 'Files'); // Send the AJAX call xhr.open("POST", this.url, true); xhr.send(formData); }, /** * Show a dialog box to gather the name of the resource / activity to be created * from the uploaded content * @param type the details of the type of content * @param contents the contents to be uploaded * @section the DOM element for the section being uploaded to * @sectionnumber the number of the section being uploaded to */ handle_item: function(type, contents, section, sectionnumber) { if (type.handlers.length == 0) { // Nothing to handle this - should not have got here return; } if (this.uploaddialog) { var details = new Object(); details.isfile = false; details.type = type; details.contents = contents; details.section = section; details.setcionnumber = sectionnumber; this.uploadqueue.push(details); return; } this.uploaddialog = true; var timestamp = new Date().getTime(); var uploadid = Math.round(Math.random()*100000)+'-'+timestamp; var nameid = 'dndupload_handler_name'+uploadid; var content = ''; content += ''; content += ' '; if (type.handlers.length > 1) { content += '
'; var sel = type.handlers[0].module; for (var i=0; i'; content += '
'; } content += '
'; } var Y = this.Y; var self = this; var panel = new Y.Panel({ bodyContent: content, width: 350, zIndex: 5, centered: true, modal: true, visible: true, render: true, buttons: [{ value: M.util.get_string('upload', 'moodle'), action: function(e) { e.preventDefault(); var name = Y.one('#dndupload_handler_name'+uploadid).get('value'); name = name.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim if (name == '') { return; } var module = false; if (type.handlers.length > 1) { // Find out which module was selected var div = Y.one('#dndupload_handlers'+uploadid); div.all('input').each(function(input) { if (input.get('checked')) { module = input.get('value'); } }); if (!module) { return; } } else { module = type.handlers[0].module; } panel.hide(); // Do the upload self.upload_item(name, type.type, contents, section, sectionnumber, module); }, section: Y.WidgetStdMod.FOOTER },{ value: M.util.get_string('cancel', 'moodle'), action: function(e) { e.preventDefault(); panel.hide(); }, section: Y.WidgetStdMod.FOOTER }] }); // When the panel is hidden - destroy it and then check for other pending uploads panel.after("visibleChange", function(e) { if (!panel.get('visible')) { panel.destroy(true); self.check_upload_queue(); } }); // Focus on the 'name' box Y.one('#'+nameid).focus(); }, /** * Upload any data types that are not files: display a dummy resource element, send * the data to the server, update the progress bar for the file, then replace the * dummy element with the real information once the AJAX call completes * @param name the display name for the resource / activity to create * @param type the details of the data type found in the drop event * @param contents the actual data that was dropped * @param section the DOM element representing the selected course section * @param sectionnumber the number of the selected course section * @param module the module chosen to handle this upload */ upload_item: function(name, type, contents, section, sectionnumber, module) { // This would be an ideal place to use the Y.io function // however, this does not support data encoded using the // FormData object, which is needed to transfer data from // the DataTransfer object into an XMLHTTPRequest // This can be converted when the YUI issue has been integrated: // http://yuilibrary.com/projects/yui3/ticket/2531274 var xhr = new XMLHttpRequest(); var self = this; // Add the item to the display var resel = this.add_resource_element(name, section); // Wait for the AJAX call to complete, then update the // dummy element with the returned details xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { var result = JSON.parse(xhr.responseText); if (result) { if (result.error == 0) { // All OK - update the dummy element resel.icon.src = result.icon; resel.a.href = result.link; resel.namespan.innerHTML = result.name; resel.div.removeChild(resel.progressouter); resel.li.id = result.elementid; resel.div.innerHTML += result.commands; if (result.onclick) { resel.a.onclick = result.onclick; } if (self.Y.UA.gecko > 0) { // Fix a Firefox bug which makes sites with a '~' in their wwwroot // log the user out when clicking on the link (before refreshing the page). resel.div.innerHTML = unescape(resel.div.innerHTML); } self.add_editing(result.elementid, sectionnumber); } else { // Error - remove the dummy element resel.parent.removeChild(resel.li); alert(result.error); } } } else { alert(M.util.get_string('servererror', 'moodle')); } } }; // Prepare the data to send var formData = new FormData(); formData.append('contents', contents); formData.append('displayname', name); formData.append('sesskey', M.cfg.sesskey); formData.append('course', this.courseid); formData.append('section', sectionnumber); formData.append('type', type); formData.append('module', module); // Send the data xhr.open("POST", this.url, true); xhr.send(formData); }, /** * Call the AJAX course editing initialisation to add the editing tools * to the newly-created resource link * @param elementid the id of the DOM element containing the new resource link * @param sectionnumber the number of the selected course section */ add_editing: function(elementid) { YUI().use('moodle-course-coursebase', function(Y) { M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid); }); } };