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 to courses
|
|
|
18 |
*
|
|
|
19 |
* @package core
|
|
|
20 |
* @subpackage course
|
|
|
21 |
* @copyright 2012 Davo Smith
|
|
|
22 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
23 |
*/
|
|
|
24 |
M.course_dndupload = {
|
|
|
25 |
// YUI object.
|
|
|
26 |
Y: null,
|
|
|
27 |
// URL for upload requests
|
|
|
28 |
url: M.cfg.wwwroot + '/course/dndupload.php',
|
|
|
29 |
// maximum size of files allowed in this form
|
|
|
30 |
maxbytes: 0,
|
|
|
31 |
// ID of the course we are on
|
|
|
32 |
courseid: null,
|
|
|
33 |
// Data about the different file/data handlers that are available
|
|
|
34 |
handlers: null,
|
|
|
35 |
// Nasty hack to distinguish between dragenter(first entry),
|
|
|
36 |
// dragenter+dragleave(moving between child elements) and dragleave (leaving element)
|
|
|
37 |
entercount: 0,
|
|
|
38 |
// Used to keep track of the section we are dragging across - to make
|
|
|
39 |
// spotting movement between sections more reliable
|
|
|
40 |
currentsection: null,
|
|
|
41 |
// Used to store the pending uploads whilst the user is being asked for further input
|
|
|
42 |
uploadqueue: null,
|
|
|
43 |
// True if the there is currently a dialog being shown (asking for a name, or giving a
|
|
|
44 |
// choice of file handlers)
|
|
|
45 |
uploaddialog: false,
|
|
|
46 |
// An array containing the last selected file handler for each file type
|
|
|
47 |
lastselected: null,
|
|
|
48 |
|
|
|
49 |
// The following are used to identify specific parts of the course page
|
|
|
50 |
|
|
|
51 |
// The type of HTML element that is a course section
|
|
|
52 |
sectiontypename: 'li',
|
|
|
53 |
// The classes that an element must have to be identified as a course section
|
|
|
54 |
sectionclasses: ['section', 'main'],
|
|
|
55 |
// The ID of the main content area of the page (for adding the 'status' div)
|
|
|
56 |
pagecontentid: 'page',
|
|
|
57 |
// The selector identifying the list of modules within a section (note changing this may require
|
|
|
58 |
// changes to the get_mods_element function)
|
|
|
59 |
modslistselector: 'ul.section',
|
|
|
60 |
|
|
|
61 |
/**
|
|
|
62 |
* Initalise the drag and drop upload interface
|
|
|
63 |
* Note: one and only one of options.filemanager and options.formcallback must be defined
|
|
|
64 |
*
|
|
|
65 |
* @param Y the YUI object
|
|
|
66 |
* @param object options {
|
|
|
67 |
* courseid: ID of the course we are on
|
|
|
68 |
* maxbytes: maximum size of files allowed in this form
|
|
|
69 |
* handlers: Data about the different file/data handlers that are available
|
|
|
70 |
* }
|
|
|
71 |
*/
|
|
|
72 |
init: function(Y, options) {
|
|
|
73 |
this.Y = Y;
|
|
|
74 |
|
|
|
75 |
if (!this.browser_supported()) {
|
|
|
76 |
return; // Browser does not support the required functionality
|
|
|
77 |
}
|
|
|
78 |
|
|
|
79 |
this.maxbytes = options.maxbytes;
|
|
|
80 |
this.courseid = options.courseid;
|
|
|
81 |
this.handlers = options.handlers;
|
|
|
82 |
this.uploadqueue = new Array();
|
|
|
83 |
this.lastselected = new Array();
|
|
|
84 |
|
|
|
85 |
var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.');
|
|
|
86 |
var sections = this.Y.all(sectionselector);
|
|
|
87 |
if (sections.isEmpty()) {
|
|
|
88 |
return; // No sections - incompatible course format or front page.
|
|
|
89 |
}
|
|
|
90 |
sections.each( function(el) {
|
|
|
91 |
this.add_preview_element(el);
|
|
|
92 |
this.init_events(el);
|
|
|
93 |
}, this);
|
|
|
94 |
|
|
|
95 |
if (options.showstatus) {
|
|
|
96 |
this.add_status_div();
|
|
|
97 |
}
|
|
|
98 |
|
|
|
99 |
var self = this;
|
|
|
100 |
require([
|
|
|
101 |
'core_courseformat/courseeditor',
|
|
|
102 |
'core_course/events'
|
|
|
103 |
], function(
|
|
|
104 |
Editor,
|
|
|
105 |
CourseEvents
|
|
|
106 |
) {
|
|
|
107 |
// Any change to the course must be applied also to the course state via the courseeditor module.
|
|
|
108 |
self.courseeditor = Editor.getCurrentCourseEditor();
|
|
|
109 |
|
|
|
110 |
// Some formats can add sections without reloading the page.
|
|
|
111 |
document.querySelector('#' + self.pagecontentid).addEventListener(
|
|
|
112 |
CourseEvents.sectionRefreshed,
|
|
|
113 |
self.sectionRefreshed.bind(self)
|
|
|
114 |
);
|
|
|
115 |
});
|
|
|
116 |
document.addEventListener('scroll', function() {
|
|
|
117 |
sections.each(function(el) {
|
|
|
118 |
if (el.hasClass('dndupload-dropzone')) {
|
|
|
119 |
self.rePositionPreviewInfoElement(el);
|
|
|
120 |
}
|
|
|
121 |
}, this);
|
|
|
122 |
}, true);
|
|
|
123 |
},
|
|
|
124 |
|
|
|
125 |
/**
|
|
|
126 |
* Setup Drag and Drop in a section.
|
|
|
127 |
* @param {CustomEvent} event The custom event
|
|
|
128 |
*/
|
|
|
129 |
sectionRefreshed: function(event) {
|
|
|
130 |
if (event.detail.newSectionElement === undefined) {
|
|
|
131 |
return;
|
|
|
132 |
}
|
|
|
133 |
var element = this.Y.one(event.detail.newSectionElement);
|
|
|
134 |
this.add_preview_element(element);
|
|
|
135 |
this.init_events(element);
|
|
|
136 |
},
|
|
|
137 |
|
|
|
138 |
/**
|
|
|
139 |
* Add a div element to tell the user that drag and drop upload
|
|
|
140 |
* is available (or to explain why it is not available)
|
|
|
141 |
*/
|
|
|
142 |
add_status_div: function() {
|
|
|
143 |
var Y = this.Y,
|
|
|
144 |
coursecontents = Y.one('#' + this.pagecontentid),
|
|
|
145 |
div,
|
|
|
146 |
handlefile = (this.handlers.filehandlers.length > 0),
|
|
|
147 |
handletext = false,
|
|
|
148 |
handlelink = false,
|
|
|
149 |
i = 0,
|
|
|
150 |
styletop,
|
|
|
151 |
styletopunit;
|
|
|
152 |
|
|
|
153 |
if (!coursecontents) {
|
|
|
154 |
return;
|
|
|
155 |
}
|
|
|
156 |
|
|
|
157 |
div = Y.Node.create('<div id="dndupload-status"></div>').setStyle('opacity', '0.0');
|
|
|
158 |
coursecontents.insert(div, 0);
|
|
|
159 |
|
|
|
160 |
for (i = 0; i < this.handlers.types.length; i++) {
|
|
|
161 |
switch (this.handlers.types[i].identifier) {
|
|
|
162 |
case 'text':
|
|
|
163 |
case 'text/html':
|
|
|
164 |
handletext = true;
|
|
|
165 |
break;
|
|
|
166 |
case 'url':
|
|
|
167 |
handlelink = true;
|
|
|
168 |
break;
|
|
|
169 |
}
|
|
|
170 |
}
|
|
|
171 |
$msgident = 'dndworking';
|
|
|
172 |
if (handlefile) {
|
|
|
173 |
$msgident += 'file';
|
|
|
174 |
}
|
|
|
175 |
if (handletext) {
|
|
|
176 |
$msgident += 'text';
|
|
|
177 |
}
|
|
|
178 |
if (handlelink) {
|
|
|
179 |
$msgident += 'link';
|
|
|
180 |
}
|
|
|
181 |
div.setContent(M.util.get_string($msgident, 'moodle'));
|
|
|
182 |
|
|
|
183 |
styletop = div.getStyle('top') || '0px';
|
|
|
184 |
styletopunit = styletop.replace(/^\d+/, '');
|
|
|
185 |
styletop = parseInt(styletop.replace(/\D*$/, ''), 10);
|
|
|
186 |
|
|
|
187 |
var fadein = new Y.Anim({
|
|
|
188 |
node: '#dndupload-status',
|
|
|
189 |
from: {
|
|
|
190 |
opacity: 0.0,
|
|
|
191 |
top: (styletop - 30).toString() + styletopunit
|
|
|
192 |
},
|
|
|
193 |
|
|
|
194 |
to: {
|
|
|
195 |
opacity: 1.0,
|
|
|
196 |
top: styletop.toString() + styletopunit
|
|
|
197 |
},
|
|
|
198 |
duration: 0.5
|
|
|
199 |
});
|
|
|
200 |
|
|
|
201 |
var fadeout = new Y.Anim({
|
|
|
202 |
node: '#dndupload-status',
|
|
|
203 |
from: {
|
|
|
204 |
opacity: 1.0,
|
|
|
205 |
top: styletop.toString() + styletopunit
|
|
|
206 |
},
|
|
|
207 |
|
|
|
208 |
to: {
|
|
|
209 |
opacity: 0.0,
|
|
|
210 |
top: (styletop - 30).toString() + styletopunit
|
|
|
211 |
},
|
|
|
212 |
duration: 0.5
|
|
|
213 |
});
|
|
|
214 |
|
|
|
215 |
fadein.run();
|
|
|
216 |
fadein.on('end', function(e) {
|
|
|
217 |
Y.later(3000, this, function() {
|
|
|
218 |
fadeout.run();
|
|
|
219 |
});
|
|
|
220 |
});
|
|
|
221 |
|
|
|
222 |
fadeout.on('end', function(e) {
|
|
|
223 |
Y.one('#dndupload-status').remove(true);
|
|
|
224 |
});
|
|
|
225 |
},
|
|
|
226 |
|
|
|
227 |
/**
|
|
|
228 |
* Check the browser has the required functionality
|
|
|
229 |
* @return true if browser supports drag/drop upload
|
|
|
230 |
*/
|
|
|
231 |
browser_supported: function() {
|
|
|
232 |
if (typeof FileReader == 'undefined') {
|
|
|
233 |
return false;
|
|
|
234 |
}
|
|
|
235 |
if (typeof FormData == 'undefined') {
|
|
|
236 |
return false;
|
|
|
237 |
}
|
|
|
238 |
return true;
|
|
|
239 |
},
|
|
|
240 |
|
|
|
241 |
/**
|
|
|
242 |
* Initialise drag events on node container, all events need
|
|
|
243 |
* to be processed for drag and drop to work
|
|
|
244 |
* @param el the element to add events to
|
|
|
245 |
*/
|
|
|
246 |
init_events: function(el) {
|
|
|
247 |
this.Y.on('dragenter', this.drag_enter, el, this);
|
|
|
248 |
this.Y.on('dragleave', this.drag_leave, el, this);
|
|
|
249 |
this.Y.on('dragover', this.drag_over, el, this);
|
|
|
250 |
this.Y.on('drop', this.drop, el, this);
|
|
|
251 |
},
|
|
|
252 |
|
|
|
253 |
/**
|
|
|
254 |
* Work out which course section a given element is in
|
|
|
255 |
* @param el the child DOM element within the section
|
|
|
256 |
* @return the DOM element representing the section
|
|
|
257 |
*/
|
|
|
258 |
get_section: function(el) {
|
|
|
259 |
var sectionclasses = this.sectionclasses;
|
|
|
260 |
return el.ancestor( function(test) {
|
|
|
261 |
var i;
|
|
|
262 |
for (i=0; i<sectionclasses.length; i++) {
|
|
|
263 |
if (!test.hasClass(sectionclasses[i])) {
|
|
|
264 |
return false;
|
|
|
265 |
}
|
|
|
266 |
return true;
|
|
|
267 |
}
|
|
|
268 |
}, true);
|
|
|
269 |
},
|
|
|
270 |
|
|
|
271 |
/**
|
|
|
272 |
* Work out the number of the section we have been dropped on to, from the section element
|
|
|
273 |
* @param DOMElement section the selected section
|
|
|
274 |
* @return int the section number
|
|
|
275 |
*/
|
|
|
276 |
get_section_number: function(section) {
|
|
|
277 |
var sectionid = section.get('id').split('-');
|
|
|
278 |
if (sectionid.length < 2 || sectionid[0] != 'section') {
|
|
|
279 |
return false;
|
|
|
280 |
}
|
|
|
281 |
return parseInt(sectionid[1]);
|
|
|
282 |
},
|
|
|
283 |
|
|
|
284 |
/**
|
|
|
285 |
* Check if the event includes data of the given type
|
|
|
286 |
* @param e the event details
|
|
|
287 |
* @param type the data type to check for
|
|
|
288 |
* @return true if the data type is found in the event data
|
|
|
289 |
*/
|
|
|
290 |
types_includes: function(e, type) {
|
|
|
291 |
var i;
|
|
|
292 |
var types = e._event.dataTransfer.types;
|
|
|
293 |
type = type.toLowerCase();
|
|
|
294 |
for (i=0; i<types.length; i++) {
|
|
|
295 |
if (!types.hasOwnProperty(i)) {
|
|
|
296 |
continue;
|
|
|
297 |
}
|
|
|
298 |
if (types[i].toLowerCase() === type) {
|
|
|
299 |
return true;
|
|
|
300 |
}
|
|
|
301 |
}
|
|
|
302 |
return false;
|
|
|
303 |
},
|
|
|
304 |
|
|
|
305 |
/**
|
|
|
306 |
* Check if the event includes only data of the Files type
|
|
|
307 |
*
|
|
|
308 |
* Chrome drag page images as files. To differentiate a real file from a page
|
|
|
309 |
* image we need to check if all the dataTransfers types are files.
|
|
|
310 |
*
|
|
|
311 |
* @param {Event} e the event details
|
|
|
312 |
* @return true if the data types contains only Files related type
|
|
|
313 |
*/
|
|
|
314 |
typesIncludesFilesOnly: function(e) {
|
|
|
315 |
return e._event.dataTransfer.types.every(function(currentType) {
|
|
|
316 |
return (currentType.toLowerCase() != 'text/uri-list'
|
|
|
317 |
&& currentType.toLowerCase() != 'text/html'
|
|
|
318 |
&& currentType.toLowerCase() != 'text/plain'
|
|
|
319 |
);
|
|
|
320 |
});
|
|
|
321 |
},
|
|
|
322 |
|
|
|
323 |
/**
|
|
|
324 |
* Look through the event data, checking it against the registered data types
|
|
|
325 |
* (in order of priority) and return details of the first matching data type
|
|
|
326 |
* @param e the event details
|
|
|
327 |
* @return object|false - false if not found or an object {
|
|
|
328 |
* realtype: the type as given by the browser
|
|
|
329 |
* addmessage: the message to show to the user during dragging
|
|
|
330 |
* namemessage: the message for requesting a name for the resource from the user
|
|
|
331 |
* type: the identifier of the type (may match several 'realtype's)
|
|
|
332 |
* }
|
|
|
333 |
*/
|
|
|
334 |
drag_type: function(e) {
|
|
|
335 |
// Check there is some data attached.
|
|
|
336 |
if (e._event.dataTransfer === null) {
|
|
|
337 |
return false;
|
|
|
338 |
}
|
|
|
339 |
if (e._event.dataTransfer.types === null) {
|
|
|
340 |
return false;
|
|
|
341 |
}
|
|
|
342 |
if (e._event.dataTransfer.types.length == 0) {
|
|
|
343 |
return false;
|
|
|
344 |
}
|
|
|
345 |
|
|
|
346 |
// Check for files first.
|
|
|
347 |
if (this.types_includes(e, 'Files') && this.typesIncludesFilesOnly(e)) {
|
|
|
348 |
if (e.type != 'drop' || e._event.dataTransfer.files.length != 0) {
|
|
|
349 |
if (this.handlers.filehandlers.length == 0) {
|
|
|
350 |
return false; // No available file handlers - ignore this drag.
|
|
|
351 |
}
|
|
|
352 |
return {
|
|
|
353 |
realtype: 'Files',
|
|
|
354 |
addmessage: M.util.get_string('addfilehere', 'moodle'),
|
|
|
355 |
namemessage: null, // Should not be asked for anyway
|
|
|
356 |
type: 'Files'
|
|
|
357 |
};
|
|
|
358 |
}
|
|
|
359 |
}
|
|
|
360 |
|
|
|
361 |
// Check each of the registered types.
|
|
|
362 |
var types = this.handlers.types;
|
|
|
363 |
for (var i=0; i<types.length; i++) {
|
|
|
364 |
// Check each of the different identifiers for this type
|
|
|
365 |
var dttypes = types[i].datatransfertypes;
|
|
|
366 |
for (var j=0; j<dttypes.length; j++) {
|
|
|
367 |
if (this.types_includes(e, dttypes[j])) {
|
|
|
368 |
return {
|
|
|
369 |
realtype: dttypes[j],
|
|
|
370 |
addmessage: types[i].addmessage,
|
|
|
371 |
namemessage: types[i].namemessage,
|
|
|
372 |
handlermessage: types[i].handlermessage,
|
|
|
373 |
type: types[i].identifier,
|
|
|
374 |
handlers: types[i].handlers
|
|
|
375 |
};
|
|
|
376 |
}
|
|
|
377 |
}
|
|
|
378 |
}
|
|
|
379 |
return false; // No types we can handle
|
|
|
380 |
},
|
|
|
381 |
|
|
|
382 |
/**
|
|
|
383 |
* Check the content of the drag/drop includes a type we can handle, then, if
|
|
|
384 |
* it is, notify the browser that we want to handle it
|
|
|
385 |
* @param event e
|
|
|
386 |
* @return string type of the event or false
|
|
|
387 |
*/
|
|
|
388 |
check_drag: function(e) {
|
|
|
389 |
var type = this.drag_type(e);
|
|
|
390 |
if (type) {
|
|
|
391 |
// Notify browser that we will handle this drag/drop
|
|
|
392 |
e.stopPropagation();
|
|
|
393 |
e.preventDefault();
|
|
|
394 |
}
|
|
|
395 |
return type;
|
|
|
396 |
},
|
|
|
397 |
|
|
|
398 |
/**
|
|
|
399 |
* Handle a dragenter event: add a suitable 'add here' message
|
|
|
400 |
* when a drag event occurs, containing a registered data type
|
|
|
401 |
* @param e event data
|
|
|
402 |
* @return false to prevent the event from continuing to be processed
|
|
|
403 |
*/
|
|
|
404 |
drag_enter: function(e) {
|
|
|
405 |
if (!(type = this.check_drag(e))) {
|
|
|
406 |
return false;
|
|
|
407 |
}
|
|
|
408 |
|
|
|
409 |
var section = this.get_section(e.currentTarget);
|
|
|
410 |
if (!section) {
|
|
|
411 |
return false;
|
|
|
412 |
}
|
|
|
413 |
|
|
|
414 |
if (this.currentsection && this.currentsection != section) {
|
|
|
415 |
this.currentsection = section;
|
|
|
416 |
this.entercount = 1;
|
|
|
417 |
} else {
|
|
|
418 |
this.entercount++;
|
|
|
419 |
if (this.entercount > 2) {
|
|
|
420 |
this.entercount = 2;
|
|
|
421 |
return false;
|
|
|
422 |
}
|
|
|
423 |
}
|
|
|
424 |
|
|
|
425 |
this.showPreviewInfoElement(section);
|
|
|
426 |
|
|
|
427 |
return false;
|
|
|
428 |
},
|
|
|
429 |
|
|
|
430 |
/**
|
|
|
431 |
* Handle a dragleave event: remove the 'add here' message (if present)
|
|
|
432 |
* @param e event data
|
|
|
433 |
* @return false to prevent the event from continuing to be processed
|
|
|
434 |
*/
|
|
|
435 |
drag_leave: function(e) {
|
|
|
436 |
if (!this.check_drag(e)) {
|
|
|
437 |
return false;
|
|
|
438 |
}
|
|
|
439 |
|
|
|
440 |
this.entercount--;
|
|
|
441 |
if (this.entercount == 1) {
|
|
|
442 |
return false;
|
|
|
443 |
}
|
|
|
444 |
this.entercount = 0;
|
|
|
445 |
this.currentsection = null;
|
|
|
446 |
|
|
|
447 |
this.hide_preview_element();
|
|
|
448 |
return false;
|
|
|
449 |
},
|
|
|
450 |
|
|
|
451 |
/**
|
|
|
452 |
* Handle a dragover event: just prevent the browser default (necessary
|
|
|
453 |
* to allow drag and drop handling to work)
|
|
|
454 |
* @param e event data
|
|
|
455 |
* @return false to prevent the event from continuing to be processed
|
|
|
456 |
*/
|
|
|
457 |
drag_over: function(e) {
|
|
|
458 |
this.check_drag(e);
|
|
|
459 |
return false;
|
|
|
460 |
},
|
|
|
461 |
|
|
|
462 |
/**
|
|
|
463 |
* Handle a drop event: hide the 'add here' message, check the attached
|
|
|
464 |
* data type and start the upload process
|
|
|
465 |
* @param e event data
|
|
|
466 |
* @return false to prevent the event from continuing to be processed
|
|
|
467 |
*/
|
|
|
468 |
drop: function(e) {
|
|
|
469 |
this.hide_preview_element();
|
|
|
470 |
|
|
|
471 |
if (!(type = this.check_drag(e))) {
|
|
|
472 |
return false;
|
|
|
473 |
}
|
|
|
474 |
|
|
|
475 |
// Work out the number of the section we are on (from its id)
|
|
|
476 |
var section = this.get_section(e.currentTarget);
|
|
|
477 |
var sectionnumber = this.get_section_number(section);
|
|
|
478 |
|
|
|
479 |
// Process the file or the included data
|
|
|
480 |
if (type.type == 'Files') {
|
|
|
481 |
var files = e._event.dataTransfer.files;
|
|
|
482 |
for (var i=0, f; f=files[i]; i++) {
|
|
|
483 |
this.handle_file(f, section, sectionnumber);
|
|
|
484 |
}
|
|
|
485 |
} else {
|
|
|
486 |
var contents = e._event.dataTransfer.getData(type.realtype);
|
|
|
487 |
if (contents) {
|
|
|
488 |
this.handle_item(type, contents, section, sectionnumber);
|
|
|
489 |
}
|
|
|
490 |
}
|
|
|
491 |
|
|
|
492 |
return false;
|
|
|
493 |
},
|
|
|
494 |
|
|
|
495 |
/**
|
|
|
496 |
* Find or create the 'ul' element that contains all of the module
|
|
|
497 |
* instances in this section
|
|
|
498 |
* @param section the DOM element representing the section
|
|
|
499 |
* @return false to prevent the event from continuing to be processed
|
|
|
500 |
*/
|
|
|
501 |
get_mods_element: function(section) {
|
|
|
502 |
// Find the 'ul' containing the list of mods
|
|
|
503 |
var modsel = section.one(this.modslistselector);
|
|
|
504 |
if (!modsel) {
|
|
|
505 |
// Create the above 'ul' if it doesn't exist
|
|
|
506 |
modsel = document.createElement('ul');
|
|
|
507 |
modsel.className = 'section img-text';
|
|
|
508 |
var contentel = section.get('children').pop();
|
|
|
509 |
var brel = contentel.get('children').pop();
|
|
|
510 |
contentel.insertBefore(modsel, brel);
|
|
|
511 |
modsel = this.Y.one(modsel);
|
|
|
512 |
}
|
|
|
513 |
|
|
|
514 |
return modsel;
|
|
|
515 |
},
|
|
|
516 |
|
|
|
517 |
/**
|
|
|
518 |
* Add a new dummy item to the list of mods, to be replaced by a real
|
|
|
519 |
* item & link once the AJAX upload call has completed
|
|
|
520 |
* @param name the label to show in the element
|
|
|
521 |
* @param section the DOM element reperesenting the course section
|
|
|
522 |
* @return DOM element containing the new item
|
|
|
523 |
*/
|
|
|
524 |
add_resource_element: function(name, section, module) {
|
|
|
525 |
var modsel = this.get_mods_element(section);
|
|
|
526 |
|
|
|
527 |
var resel = {
|
|
|
528 |
parent: modsel,
|
|
|
529 |
li: document.createElement('li'),
|
|
|
530 |
div: document.createElement('div'),
|
|
|
531 |
indentdiv: document.createElement('div'),
|
|
|
532 |
a: document.createElement('a'),
|
|
|
533 |
icon: document.createElement('img'),
|
|
|
534 |
namespan: document.createElement('span'),
|
|
|
535 |
groupingspan: document.createElement('span'),
|
|
|
536 |
progressouter: document.createElement('span'),
|
|
|
537 |
progress: document.createElement('span')
|
|
|
538 |
};
|
|
|
539 |
|
|
|
540 |
resel.li.className = 'activity ' + module + ' modtype_' + module;
|
|
|
541 |
|
|
|
542 |
resel.indentdiv.className = 'mod-indent';
|
|
|
543 |
resel.li.appendChild(resel.indentdiv);
|
|
|
544 |
|
|
|
545 |
resel.div.className = 'activityinstance';
|
|
|
546 |
resel.indentdiv.appendChild(resel.div);
|
|
|
547 |
|
|
|
548 |
resel.a.href = '#';
|
|
|
549 |
resel.div.appendChild(resel.a);
|
|
|
550 |
|
|
|
551 |
resel.icon.src = M.util.image_url('i/ajaxloader');
|
|
|
552 |
resel.icon.className = 'activityicon iconlarge';
|
|
|
553 |
resel.a.appendChild(resel.icon);
|
|
|
554 |
|
|
|
555 |
resel.namespan.className = 'instancename';
|
|
|
556 |
resel.namespan.innerHTML = name;
|
|
|
557 |
resel.a.appendChild(resel.namespan);
|
|
|
558 |
|
|
|
559 |
resel.groupingspan.className = 'groupinglabel';
|
|
|
560 |
resel.div.appendChild(resel.groupingspan);
|
|
|
561 |
|
|
|
562 |
resel.progressouter.className = 'dndupload-progress-outer';
|
|
|
563 |
resel.progress.className = 'dndupload-progress-inner';
|
|
|
564 |
resel.progress.innerHTML = ' ';
|
|
|
565 |
resel.progressouter.appendChild(resel.progress);
|
|
|
566 |
resel.div.appendChild(resel.progressouter);
|
|
|
567 |
|
|
|
568 |
modsel.insertBefore(resel.li, modsel.get('children')); // Leave the 'preview element' at the bottom
|
|
|
569 |
|
|
|
570 |
return resel;
|
|
|
571 |
},
|
|
|
572 |
|
|
|
573 |
/**
|
|
|
574 |
* Hide any visible dndupload-preview elements on the page
|
|
|
575 |
*/
|
|
|
576 |
hide_preview_element: function() {
|
|
|
577 |
this.Y.all('.dndupload-preview-wrapper').addClass('dndupload-hidden');
|
|
|
578 |
this.Y.all('.dndupload-over').removeClass('dndupload-over');
|
|
|
579 |
this.Y.all('.dndupload-dropzone').removeClass('dndupload-dropzone');
|
|
|
580 |
},
|
|
|
581 |
|
|
|
582 |
/**
|
|
|
583 |
* Unhide the preview element for the given section and set it to display
|
|
|
584 |
* the correct message
|
|
|
585 |
* @param section the YUI node representing the selected course section
|
|
|
586 |
* @param type the details of the data type detected in the drag (including the message to display)
|
|
|
587 |
* @deprecated Since Moodle 4.0. Please use showPreviewInfoElement() instead.
|
|
|
588 |
*/
|
|
|
589 |
show_preview_element: function(section, type) {
|
|
|
590 |
this.hide_preview_element();
|
|
|
591 |
var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden');
|
|
|
592 |
section.addClass('dndupload-over');
|
|
|
593 |
|
|
|
594 |
// Horrible work-around to allow the 'Add X here' text to be a drop target in Firefox.
|
|
|
595 |
var node = preview.one('span').getDOMNode();
|
|
|
596 |
node.firstChild.nodeValue = type.addmessage;
|
|
|
597 |
},
|
|
|
598 |
|
|
|
599 |
/**
|
|
|
600 |
* Unhide the preview information element for the given section and set it to display
|
|
|
601 |
* the correct message
|
|
|
602 |
* @param {Object} section the YUI node representing the selected course section
|
|
|
603 |
*/
|
|
|
604 |
showPreviewInfoElement: function(section) {
|
|
|
605 |
this.hide_preview_element();
|
|
|
606 |
section.one('.dndupload-preview-wrapper').removeClass('dndupload-hidden');
|
|
|
607 |
section.addClass('dndupload-dropzone');
|
|
|
608 |
this.rePositionPreviewInfoElement(section);
|
|
|
609 |
},
|
|
|
610 |
|
|
|
611 |
/**
|
|
|
612 |
* Add the preview element to a course section. Note: this needs to be done before 'addEventListener'
|
|
|
613 |
* is called, otherwise Firefox will ignore events generated when the mouse is over the preview
|
|
|
614 |
* element (instead of passing them up to the parent element)
|
|
|
615 |
* @param {Object} section the YUI node representing the selected course section
|
|
|
616 |
*/
|
|
|
617 |
add_preview_element: function(section) {
|
|
|
618 |
const modsEl = this.get_mods_element(section);
|
|
|
619 |
|
|
|
620 |
// Create overlay div.
|
|
|
621 |
const overlay = document.createElement('div');
|
|
|
622 |
overlay.className = 'dndupload-preview-overlay';
|
|
|
623 |
|
|
|
624 |
modsEl.get('parentNode').appendChild(overlay);
|
|
|
625 |
|
|
|
626 |
// Create preview div.
|
|
|
627 |
const preview = {
|
|
|
628 |
wrapper: document.createElement('div'),
|
|
|
629 |
div: document.createElement('div'),
|
|
|
630 |
iconSpan: document.createElement('span'),
|
|
|
631 |
nameSpan: document.createElement('span')
|
|
|
632 |
};
|
|
|
633 |
|
|
|
634 |
preview.wrapper.className = 'dndupload-preview-wrapper dndupload-hidden';
|
|
|
635 |
preview.wrapper.appendChild(preview.div);
|
|
|
636 |
preview.div.className = 'dndupload-preview';
|
|
|
637 |
preview.div.appendChild(document.createTextNode(' '));
|
|
|
638 |
preview.iconSpan.className = 'fa fa-arrow-circle-o-down';
|
|
|
639 |
preview.nameSpan.className = 'instancename';
|
|
|
640 |
preview.nameSpan.innerHTML = M.util.get_string('addfilehere', 'moodle');
|
|
|
641 |
preview.div.appendChild(preview.iconSpan);
|
|
|
642 |
preview.div.appendChild(preview.nameSpan);
|
|
|
643 |
|
|
|
644 |
modsEl.get('parentNode').appendChild(preview.wrapper);
|
|
|
645 |
},
|
|
|
646 |
|
|
|
647 |
/**
|
|
|
648 |
* Re-position the preview information element by calculating the section position.
|
|
|
649 |
*
|
|
|
650 |
* @param {Object} section the YUI node representing the selected course section
|
|
|
651 |
*/
|
|
|
652 |
rePositionPreviewInfoElement: function(section) {
|
|
|
653 |
const sectionElement = document.getElementById(section.get('id'));
|
|
|
654 |
const rect = sectionElement.getBoundingClientRect();
|
|
|
655 |
const sectionHeight = parseInt(window.getComputedStyle(sectionElement).height, 10);
|
|
|
656 |
const sectionOffset = rect.top;
|
|
|
657 |
const preview = sectionElement.querySelector('.dndupload-preview-wrapper');
|
|
|
658 |
const previewHeight = parseInt(window.getComputedStyle(preview).height, 10) +
|
|
|
659 |
(2 * parseInt(window.getComputedStyle(preview).padding, 10));
|
|
|
660 |
let top, bottom;
|
|
|
661 |
if (sectionOffset < 0) {
|
|
|
662 |
if (sectionHeight + sectionOffset >= previewHeight) {
|
|
|
663 |
// We have enough space here, just stick the preview to the top.
|
|
|
664 |
let offSetTop = 0 - sectionOffset;
|
|
|
665 |
const navBar = document.querySelector('nav.navbar.fixed-top');
|
|
|
666 |
if (navBar) {
|
|
|
667 |
offSetTop = offSetTop + navBar.offsetHeight;
|
|
|
668 |
}
|
|
|
669 |
top = offSetTop + 'px';
|
|
|
670 |
bottom = 'unset';
|
|
|
671 |
} else {
|
|
|
672 |
// We do not have enough space here, just stick the preview to the bottom.
|
|
|
673 |
top = 'unset';
|
|
|
674 |
bottom = 0;
|
|
|
675 |
}
|
|
|
676 |
} else {
|
|
|
677 |
top = 0;
|
|
|
678 |
bottom = 'unset';
|
|
|
679 |
}
|
|
|
680 |
|
|
|
681 |
preview.style.top = top
|
|
|
682 |
preview.style.bottom = bottom;
|
|
|
683 |
},
|
|
|
684 |
|
|
|
685 |
/**
|
|
|
686 |
* Find the registered handler for the given file type. If there is more than one, ask the
|
|
|
687 |
* user which one to use. Then upload the file to the server
|
|
|
688 |
* @param file the details of the file, taken from the FileList in the drop event
|
|
|
689 |
* @param section the DOM element representing the selected course section
|
|
|
690 |
* @param sectionnumber the number of the selected course section
|
|
|
691 |
*/
|
|
|
692 |
handle_file: function(file, section, sectionnumber) {
|
|
|
693 |
var handlers = new Array();
|
|
|
694 |
var filehandlers = this.handlers.filehandlers;
|
|
|
695 |
var extension = '';
|
|
|
696 |
var dotpos = file.name.lastIndexOf('.');
|
|
|
697 |
if (dotpos != -1) {
|
|
|
698 |
extension = file.name.substr(dotpos+1, file.name.length).toLowerCase();
|
|
|
699 |
}
|
|
|
700 |
|
|
|
701 |
for (var i=0; i<filehandlers.length; i++) {
|
|
|
702 |
if (filehandlers[i].extension == '*' || filehandlers[i].extension == extension) {
|
|
|
703 |
handlers.push(filehandlers[i]);
|
|
|
704 |
}
|
|
|
705 |
}
|
|
|
706 |
|
|
|
707 |
if (handlers.length == 0) {
|
|
|
708 |
// No handlers at all (not even 'resource'?)
|
|
|
709 |
return;
|
|
|
710 |
}
|
|
|
711 |
|
|
|
712 |
if (handlers.length == 1) {
|
|
|
713 |
this.upload_file(file, section, sectionnumber, handlers[0].module);
|
|
|
714 |
return;
|
|
|
715 |
}
|
|
|
716 |
|
|
|
717 |
this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
|
|
|
718 |
},
|
|
|
719 |
|
|
|
720 |
/**
|
|
|
721 |
* Show a dialog box, allowing the user to choose what to do with the file they are uploading
|
|
|
722 |
* @param handlers the available handlers to choose between
|
|
|
723 |
* @param extension the extension of the file being uploaded
|
|
|
724 |
* @param file the File object being uploaded
|
|
|
725 |
* @param section the DOM element of the section being uploaded to
|
|
|
726 |
* @param sectionnumber the number of the selected course section
|
|
|
727 |
*/
|
|
|
728 |
file_handler_dialog: function(handlers, extension, file, section, sectionnumber) {
|
|
|
729 |
if (this.uploaddialog) {
|
|
|
730 |
var details = new Object();
|
|
|
731 |
details.isfile = true;
|
|
|
732 |
details.handlers = handlers;
|
|
|
733 |
details.extension = extension;
|
|
|
734 |
details.file = file;
|
|
|
735 |
details.section = section;
|
|
|
736 |
details.sectionnumber = sectionnumber;
|
|
|
737 |
this.uploadqueue.push(details);
|
|
|
738 |
return;
|
|
|
739 |
}
|
|
|
740 |
this.uploaddialog = true;
|
|
|
741 |
|
|
|
742 |
var timestamp = new Date().getTime();
|
|
|
743 |
var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
|
|
|
744 |
var content = '';
|
|
|
745 |
var sel;
|
|
|
746 |
if (extension in this.lastselected) {
|
|
|
747 |
sel = this.lastselected[extension];
|
|
|
748 |
} else {
|
|
|
749 |
sel = handlers[0].module;
|
|
|
750 |
}
|
|
|
751 |
content += '<p>'+M.util.get_string('actionchoice', 'moodle', file.name)+'</p>';
|
|
|
752 |
content += '<div id="dndupload_handlers'+uploadid+'">';
|
|
|
753 |
for (var i=0; i<handlers.length; i++) {
|
|
|
754 |
var id = 'dndupload_handler'+uploadid+handlers[i].module;
|
|
|
755 |
var checked = (handlers[i].module == sel) ? 'checked="checked" ' : '';
|
|
|
756 |
content += '<input type="radio" name="handler" value="'+handlers[i].module+'" id="'+id+'" '+checked+'/>';
|
|
|
757 |
content += ' <label for="'+id+'">';
|
|
|
758 |
content += handlers[i].message;
|
|
|
759 |
content += '</label><br/>';
|
|
|
760 |
}
|
|
|
761 |
content += '</div>';
|
|
|
762 |
|
|
|
763 |
var Y = this.Y;
|
|
|
764 |
var self = this;
|
|
|
765 |
var panel = new M.core.dialogue({
|
|
|
766 |
bodyContent: content,
|
|
|
767 |
width: '350px',
|
|
|
768 |
modal: true,
|
|
|
769 |
visible: false,
|
|
|
770 |
render: true,
|
|
|
771 |
align: {
|
|
|
772 |
node: null,
|
|
|
773 |
points: [Y.WidgetPositionAlign.CC, Y.WidgetPositionAlign.CC]
|
|
|
774 |
}
|
|
|
775 |
});
|
|
|
776 |
panel.show();
|
|
|
777 |
// When the panel is hidden - destroy it and then check for other pending uploads
|
|
|
778 |
panel.after("visibleChange", function(e) {
|
|
|
779 |
if (!panel.get('visible')) {
|
|
|
780 |
panel.destroy(true);
|
|
|
781 |
self.check_upload_queue();
|
|
|
782 |
}
|
|
|
783 |
});
|
|
|
784 |
|
|
|
785 |
// Add the submit/cancel buttons to the bottom of the dialog.
|
|
|
786 |
panel.addButton({
|
|
|
787 |
label: M.util.get_string('upload', 'moodle'),
|
|
|
788 |
action: function(e) {
|
|
|
789 |
e.preventDefault();
|
|
|
790 |
// Find out which module was selected
|
|
|
791 |
var module = false;
|
|
|
792 |
var div = Y.one('#dndupload_handlers'+uploadid);
|
|
|
793 |
div.all('input').each(function(input) {
|
|
|
794 |
if (input.get('checked')) {
|
|
|
795 |
module = input.get('value');
|
|
|
796 |
}
|
|
|
797 |
});
|
|
|
798 |
if (!module) {
|
|
|
799 |
return;
|
|
|
800 |
}
|
|
|
801 |
panel.hide();
|
|
|
802 |
// Remember this selection for next time
|
|
|
803 |
self.lastselected[extension] = module;
|
|
|
804 |
// Do the upload
|
|
|
805 |
self.upload_file(file, section, sectionnumber, module);
|
|
|
806 |
},
|
|
|
807 |
section: Y.WidgetStdMod.FOOTER
|
|
|
808 |
});
|
|
|
809 |
panel.addButton({
|
|
|
810 |
label: M.util.get_string('cancel', 'moodle'),
|
|
|
811 |
action: function(e) {
|
|
|
812 |
e.preventDefault();
|
|
|
813 |
panel.hide();
|
|
|
814 |
},
|
|
|
815 |
section: Y.WidgetStdMod.FOOTER
|
|
|
816 |
});
|
|
|
817 |
},
|
|
|
818 |
|
|
|
819 |
/**
|
|
|
820 |
* Check to see if there are any other dialog boxes to show, now that the current one has
|
|
|
821 |
* been dealt with
|
|
|
822 |
*/
|
|
|
823 |
check_upload_queue: function() {
|
|
|
824 |
this.uploaddialog = false;
|
|
|
825 |
if (this.uploadqueue.length == 0) {
|
|
|
826 |
return;
|
|
|
827 |
}
|
|
|
828 |
|
|
|
829 |
var details = this.uploadqueue.shift();
|
|
|
830 |
if (details.isfile) {
|
|
|
831 |
this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber);
|
|
|
832 |
} else {
|
|
|
833 |
this.handle_item(details.type, details.contents, details.section, details.sectionnumber);
|
|
|
834 |
}
|
|
|
835 |
},
|
|
|
836 |
|
|
|
837 |
/**
|
|
|
838 |
* Do the file upload: show the dummy element, use an AJAX call to send the data
|
|
|
839 |
* to the server, update the progress bar for the file, then replace the dummy
|
|
|
840 |
* element with the real information once the AJAX call completes
|
|
|
841 |
* @param file the details of the file, taken from the FileList in the drop event
|
|
|
842 |
* @param section the DOM element representing the selected course section
|
|
|
843 |
* @param sectionnumber the number of the selected course section
|
|
|
844 |
*/
|
|
|
845 |
upload_file: function(file, section, sectionnumber, module) {
|
|
|
846 |
|
|
|
847 |
// This would be an ideal place to use the Y.io function
|
|
|
848 |
// however, this does not support data encoded using the
|
|
|
849 |
// FormData object, which is needed to transfer data from
|
|
|
850 |
// the DataTransfer object into an XMLHTTPRequest
|
|
|
851 |
// This can be converted when the YUI issue has been integrated:
|
|
|
852 |
// http://yuilibrary.com/projects/yui3/ticket/2531274
|
|
|
853 |
var xhr = new XMLHttpRequest();
|
|
|
854 |
var self = this;
|
|
|
855 |
|
|
|
856 |
if (this.maxbytes > 0 && file.size > this.maxbytes) {
|
|
|
857 |
new M.core.alert({message: M.util.get_string('namedfiletoolarge', 'moodle', {filename: file.name})});
|
|
|
858 |
return;
|
|
|
859 |
}
|
|
|
860 |
|
|
|
861 |
// Add the file to the display
|
|
|
862 |
var resel = this.add_resource_element(file.name, section, module);
|
|
|
863 |
|
|
|
864 |
// Update the progress bar as the file is uploaded
|
|
|
865 |
xhr.upload.addEventListener('progress', function(e) {
|
|
|
866 |
if (e.lengthComputable) {
|
|
|
867 |
var percentage = Math.round((e.loaded * 100) / e.total);
|
|
|
868 |
resel.progress.style.width = percentage + '%';
|
|
|
869 |
}
|
|
|
870 |
}, false);
|
|
|
871 |
|
|
|
872 |
// Wait for the AJAX call to complete, then update the
|
|
|
873 |
// dummy element with the returned details
|
|
|
874 |
xhr.onreadystatechange = function() {
|
|
|
875 |
if (xhr.readyState == 1) {
|
|
|
876 |
this.originalUnloadEvent = window.onbeforeunload;
|
|
|
877 |
// Trigger form upload start events.
|
|
|
878 |
require(['core_form/events'], function(FormEvent) {
|
|
|
879 |
FormEvent.notifyUploadStarted(section.get('id'));
|
|
|
880 |
});
|
|
|
881 |
}
|
|
|
882 |
if (xhr.readyState == 4) {
|
|
|
883 |
if (xhr.status == 200) {
|
|
|
884 |
var result = JSON.parse(xhr.responseText);
|
|
|
885 |
if (result) {
|
|
|
886 |
if (result.error == 0) {
|
|
|
887 |
// All OK - replace the dummy element.
|
|
|
888 |
resel.li.outerHTML = result.fullcontent;
|
|
|
889 |
if (self.Y.UA.gecko > 0) {
|
|
|
890 |
// Fix a Firefox bug which makes sites with a '~' in their wwwroot
|
|
|
891 |
// log the user out when clicking on the link (before refreshing the page).
|
|
|
892 |
resel.li.outerHTML = unescape(resel.li.outerHTML);
|
|
|
893 |
}
|
|
|
894 |
self.add_editing(result.elementid);
|
|
|
895 |
// Once done, send any new course module id to the courseeditor to update de course state.
|
|
|
896 |
self.courseeditor.dispatch('cmState', [result.cmid]);
|
|
|
897 |
// Fire the content updated event.
|
|
|
898 |
require(['core/event', 'jquery'], function(event, $) {
|
|
|
899 |
event.notifyFilterContentUpdated($(result.fullcontent));
|
|
|
900 |
});
|
|
|
901 |
} else {
|
|
|
902 |
// Error - remove the dummy element
|
|
|
903 |
resel.parent.removeChild(resel.li);
|
|
|
904 |
new M.core.alert({message: result.error});
|
|
|
905 |
}
|
|
|
906 |
}
|
|
|
907 |
} else {
|
|
|
908 |
new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
|
|
|
909 |
}
|
|
|
910 |
// Trigger form upload complete events.
|
|
|
911 |
require(['core_form/events'], function(FormEvent) {
|
|
|
912 |
FormEvent.notifyUploadCompleted(section.get('id'));
|
|
|
913 |
});
|
|
|
914 |
}
|
|
|
915 |
};
|
|
|
916 |
|
|
|
917 |
// Prepare the data to send
|
|
|
918 |
var formData = new FormData();
|
|
|
919 |
try {
|
|
|
920 |
formData.append('repo_upload_file', file);
|
|
|
921 |
} catch (e) {
|
|
|
922 |
// Edge throws an error at this point if we try to upload a folder.
|
|
|
923 |
resel.parent.removeChild(resel.li);
|
|
|
924 |
new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
|
|
|
925 |
return;
|
|
|
926 |
}
|
|
|
927 |
formData.append('sesskey', M.cfg.sesskey);
|
|
|
928 |
formData.append('course', this.courseid);
|
|
|
929 |
formData.append('section', sectionnumber);
|
|
|
930 |
formData.append('module', module);
|
|
|
931 |
formData.append('type', 'Files');
|
|
|
932 |
|
|
|
933 |
// Try reading the file to check it is not a folder, before sending it to the server.
|
|
|
934 |
var reader = new FileReader();
|
|
|
935 |
reader.onload = function() {
|
|
|
936 |
// File was read OK - send it to the server.
|
|
|
937 |
xhr.open("POST", self.url, true);
|
|
|
938 |
xhr.send(formData);
|
|
|
939 |
};
|
|
|
940 |
reader.onerror = function() {
|
|
|
941 |
// Unable to read the file (it is probably a folder) - display an error message.
|
|
|
942 |
resel.parent.removeChild(resel.li);
|
|
|
943 |
new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
|
|
|
944 |
};
|
|
|
945 |
if (file.size > 0) {
|
|
|
946 |
// If this is a non-empty file, try reading the first few bytes.
|
|
|
947 |
// This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
|
|
|
948 |
reader.readAsText(file.slice(0, 5));
|
|
|
949 |
} else {
|
|
|
950 |
// If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
|
|
|
951 |
// instead of reader.onerror().
|
|
|
952 |
// So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
|
|
|
953 |
reader.readAsText(file);
|
|
|
954 |
}
|
|
|
955 |
},
|
|
|
956 |
|
|
|
957 |
/**
|
|
|
958 |
* Show a dialog box to gather the name of the resource / activity to be created
|
|
|
959 |
* from the uploaded content
|
|
|
960 |
* @param type the details of the type of content
|
|
|
961 |
* @param contents the contents to be uploaded
|
|
|
962 |
* @section the DOM element for the section being uploaded to
|
|
|
963 |
* @sectionnumber the number of the section being uploaded to
|
|
|
964 |
*/
|
|
|
965 |
handle_item: function(type, contents, section, sectionnumber) {
|
|
|
966 |
if (type.handlers.length == 0) {
|
|
|
967 |
// Nothing to handle this - should not have got here
|
|
|
968 |
return;
|
|
|
969 |
}
|
|
|
970 |
|
|
|
971 |
if (type.handlers.length == 1 && type.handlers[0].noname) {
|
|
|
972 |
// Only one handler and it doesn't need a name (i.e. a label).
|
|
|
973 |
this.upload_item('', type.type, contents, section, sectionnumber, type.handlers[0].module);
|
|
|
974 |
this.check_upload_queue();
|
|
|
975 |
return;
|
|
|
976 |
}
|
|
|
977 |
|
|
|
978 |
if (this.uploaddialog) {
|
|
|
979 |
var details = new Object();
|
|
|
980 |
details.isfile = false;
|
|
|
981 |
details.type = type;
|
|
|
982 |
details.contents = contents;
|
|
|
983 |
details.section = section;
|
|
|
984 |
details.setcionnumber = sectionnumber;
|
|
|
985 |
this.uploadqueue.push(details);
|
|
|
986 |
return;
|
|
|
987 |
}
|
|
|
988 |
this.uploaddialog = true;
|
|
|
989 |
|
|
|
990 |
var timestamp = new Date().getTime();
|
|
|
991 |
var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
|
|
|
992 |
var nameid = 'dndupload_handler_name'+uploadid;
|
|
|
993 |
var content = '';
|
|
|
994 |
if (type.handlers.length > 1) {
|
|
|
995 |
content += '<p>'+type.handlermessage+'</p>';
|
|
|
996 |
content += '<div id="dndupload_handlers'+uploadid+'">';
|
|
|
997 |
var sel = type.handlers[0].module;
|
|
|
998 |
for (var i=0; i<type.handlers.length; i++) {
|
|
|
999 |
var id = 'dndupload_handler'+uploadid+type.handlers[i].module;
|
|
|
1000 |
var checked = (type.handlers[i].module == sel) ? 'checked="checked" ' : '';
|
|
|
1001 |
content += '<input type="radio" name="handler" value="'+i+'" id="'+id+'" '+checked+'/>';
|
|
|
1002 |
content += ' <label for="'+id+'">';
|
|
|
1003 |
content += type.handlers[i].message;
|
|
|
1004 |
content += '</label><br/>';
|
|
|
1005 |
}
|
|
|
1006 |
content += '</div>';
|
|
|
1007 |
}
|
|
|
1008 |
var disabled = (type.handlers[0].noname) ? ' disabled = "disabled" ' : '';
|
|
|
1009 |
content += '<label for="'+nameid+'">'+type.namemessage+'</label>';
|
|
|
1010 |
content += ' <input type="text" id="'+nameid+'" value="" '+disabled+' />';
|
|
|
1011 |
|
|
|
1012 |
var Y = this.Y;
|
|
|
1013 |
var self = this;
|
|
|
1014 |
var panel = new M.core.dialogue({
|
|
|
1015 |
bodyContent: content,
|
|
|
1016 |
width: '350px',
|
|
|
1017 |
modal: true,
|
|
|
1018 |
visible: true,
|
|
|
1019 |
render: true,
|
|
|
1020 |
align: {
|
|
|
1021 |
node: null,
|
|
|
1022 |
points: [Y.WidgetPositionAlign.CC, Y.WidgetPositionAlign.CC]
|
|
|
1023 |
}
|
|
|
1024 |
});
|
|
|
1025 |
|
|
|
1026 |
// When the panel is hidden - destroy it and then check for other pending uploads
|
|
|
1027 |
panel.after("visibleChange", function(e) {
|
|
|
1028 |
if (!panel.get('visible')) {
|
|
|
1029 |
panel.destroy(true);
|
|
|
1030 |
self.check_upload_queue();
|
|
|
1031 |
}
|
|
|
1032 |
});
|
|
|
1033 |
|
|
|
1034 |
var namefield = Y.one('#'+nameid);
|
|
|
1035 |
var submit = function(e) {
|
|
|
1036 |
e.preventDefault();
|
|
|
1037 |
var name = Y.Lang.trim(namefield.get('value'));
|
|
|
1038 |
var module = false;
|
|
|
1039 |
var noname = false;
|
|
|
1040 |
if (type.handlers.length > 1) {
|
|
|
1041 |
// Find out which module was selected
|
|
|
1042 |
var div = Y.one('#dndupload_handlers'+uploadid);
|
|
|
1043 |
div.all('input').each(function(input) {
|
|
|
1044 |
if (input.get('checked')) {
|
|
|
1045 |
var idx = input.get('value');
|
|
|
1046 |
module = type.handlers[idx].module;
|
|
|
1047 |
noname = type.handlers[idx].noname;
|
|
|
1048 |
}
|
|
|
1049 |
});
|
|
|
1050 |
if (!module) {
|
|
|
1051 |
return;
|
|
|
1052 |
}
|
|
|
1053 |
} else {
|
|
|
1054 |
module = type.handlers[0].module;
|
|
|
1055 |
noname = type.handlers[0].noname;
|
|
|
1056 |
}
|
|
|
1057 |
if (name == '' && !noname) {
|
|
|
1058 |
return;
|
|
|
1059 |
}
|
|
|
1060 |
if (noname) {
|
|
|
1061 |
name = '';
|
|
|
1062 |
}
|
|
|
1063 |
panel.hide();
|
|
|
1064 |
// Do the upload
|
|
|
1065 |
self.upload_item(name, type.type, contents, section, sectionnumber, module);
|
|
|
1066 |
};
|
|
|
1067 |
|
|
|
1068 |
// Add the submit/cancel buttons to the bottom of the dialog.
|
|
|
1069 |
panel.addButton({
|
|
|
1070 |
label: M.util.get_string('upload', 'moodle'),
|
|
|
1071 |
action: submit,
|
|
|
1072 |
section: Y.WidgetStdMod.FOOTER,
|
|
|
1073 |
name: 'submit'
|
|
|
1074 |
});
|
|
|
1075 |
panel.addButton({
|
|
|
1076 |
label: M.util.get_string('cancel', 'moodle'),
|
|
|
1077 |
action: function(e) {
|
|
|
1078 |
e.preventDefault();
|
|
|
1079 |
panel.hide();
|
|
|
1080 |
},
|
|
|
1081 |
section: Y.WidgetStdMod.FOOTER
|
|
|
1082 |
});
|
|
|
1083 |
var submitbutton = panel.getButton('submit').button;
|
|
|
1084 |
namefield.on('key', submit, 'enter'); // Submit the form if 'enter' pressed
|
|
|
1085 |
namefield.after('keyup', function() {
|
|
|
1086 |
if (Y.Lang.trim(namefield.get('value')) == '') {
|
|
|
1087 |
submitbutton.disable();
|
|
|
1088 |
} else {
|
|
|
1089 |
submitbutton.enable();
|
|
|
1090 |
}
|
|
|
1091 |
});
|
|
|
1092 |
|
|
|
1093 |
// Enable / disable the 'name' box, depending on the handler selected.
|
|
|
1094 |
for (i=0; i<type.handlers.length; i++) {
|
|
|
1095 |
if (type.handlers[i].noname) {
|
|
|
1096 |
Y.one('#dndupload_handler'+uploadid+type.handlers[i].module).on('click', function (e) {
|
|
|
1097 |
namefield.set('disabled', 'disabled');
|
|
|
1098 |
submitbutton.enable();
|
|
|
1099 |
});
|
|
|
1100 |
} else {
|
|
|
1101 |
Y.one('#dndupload_handler'+uploadid+type.handlers[i].module).on('click', function (e) {
|
|
|
1102 |
namefield.removeAttribute('disabled');
|
|
|
1103 |
namefield.focus();
|
|
|
1104 |
if (Y.Lang.trim(namefield.get('value')) == '') {
|
|
|
1105 |
submitbutton.disable();
|
|
|
1106 |
}
|
|
|
1107 |
});
|
|
|
1108 |
}
|
|
|
1109 |
}
|
|
|
1110 |
|
|
|
1111 |
// Focus on the 'name' box
|
|
|
1112 |
Y.one('#'+nameid).focus();
|
|
|
1113 |
},
|
|
|
1114 |
|
|
|
1115 |
/**
|
|
|
1116 |
* Upload any data types that are not files: display a dummy resource element, send
|
|
|
1117 |
* the data to the server, update the progress bar for the file, then replace the
|
|
|
1118 |
* dummy element with the real information once the AJAX call completes
|
|
|
1119 |
* @param name the display name for the resource / activity to create
|
|
|
1120 |
* @param type the details of the data type found in the drop event
|
|
|
1121 |
* @param contents the actual data that was dropped
|
|
|
1122 |
* @param section the DOM element representing the selected course section
|
|
|
1123 |
* @param sectionnumber the number of the selected course section
|
|
|
1124 |
* @param module the module chosen to handle this upload
|
|
|
1125 |
*/
|
|
|
1126 |
upload_item: function(name, type, contents, section, sectionnumber, module) {
|
|
|
1127 |
|
|
|
1128 |
// This would be an ideal place to use the Y.io function
|
|
|
1129 |
// however, this does not support data encoded using the
|
|
|
1130 |
// FormData object, which is needed to transfer data from
|
|
|
1131 |
// the DataTransfer object into an XMLHTTPRequest
|
|
|
1132 |
// This can be converted when the YUI issue has been integrated:
|
|
|
1133 |
// http://yuilibrary.com/projects/yui3/ticket/2531274
|
|
|
1134 |
var xhr = new XMLHttpRequest();
|
|
|
1135 |
var self = this;
|
|
|
1136 |
|
|
|
1137 |
// Add the item to the display
|
|
|
1138 |
var resel = this.add_resource_element(name, section, module);
|
|
|
1139 |
|
|
|
1140 |
// Wait for the AJAX call to complete, then update the
|
|
|
1141 |
// dummy element with the returned details
|
|
|
1142 |
xhr.onreadystatechange = function() {
|
|
|
1143 |
if (xhr.readyState == 1) {
|
|
|
1144 |
this.originalUnloadEvent = window.onbeforeunload;
|
|
|
1145 |
// Trigger form upload start events.
|
|
|
1146 |
require(['core_form/events'], function(FormEvent) {
|
|
|
1147 |
FormEvent.notifyUploadStarted(section.get('id'));
|
|
|
1148 |
});
|
|
|
1149 |
}
|
|
|
1150 |
if (xhr.readyState == 4) {
|
|
|
1151 |
if (xhr.status == 200) {
|
|
|
1152 |
var result = JSON.parse(xhr.responseText);
|
|
|
1153 |
if (result) {
|
|
|
1154 |
if (result.error == 0) {
|
|
|
1155 |
// All OK - replace the dummy element.
|
|
|
1156 |
resel.li.outerHTML = result.fullcontent;
|
|
|
1157 |
if (self.Y.UA.gecko > 0) {
|
|
|
1158 |
// Fix a Firefox bug which makes sites with a '~' in their wwwroot
|
|
|
1159 |
// log the user out when clicking on the link (before refreshing the page).
|
|
|
1160 |
resel.li.outerHTML = unescape(resel.li.outerHTML);
|
|
|
1161 |
}
|
|
|
1162 |
self.add_editing(result.elementid);
|
|
|
1163 |
// Once done, send any new course module id to the courseeditor to update de course state.
|
|
|
1164 |
self.courseeditor.dispatch('cmState', [result.cmid]);
|
|
|
1165 |
} else {
|
|
|
1166 |
// Error - remove the dummy element
|
|
|
1167 |
resel.parent.removeChild(resel.li);
|
|
|
1168 |
// Trigger form upload complete events.
|
|
|
1169 |
require(['core_form/events'], function(FormEvent) {
|
|
|
1170 |
FormEvent.notifyUploadCompleted(section.get('id'));
|
|
|
1171 |
});
|
|
|
1172 |
new M.core.alert({message: result.error});
|
|
|
1173 |
}
|
|
|
1174 |
}
|
|
|
1175 |
} else {
|
|
|
1176 |
// Trigger form upload complete events.
|
|
|
1177 |
require(['core_form/events'], function(FormEvent) {
|
|
|
1178 |
FormEvent.notifyUploadCompleted(section.get('id'));
|
|
|
1179 |
});
|
|
|
1180 |
new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
|
|
|
1181 |
}
|
|
|
1182 |
// Trigger form upload complete events.
|
|
|
1183 |
require(['core_form/events'], function(FormEvent) {
|
|
|
1184 |
FormEvent.notifyUploadCompleted(section.get('id'));
|
|
|
1185 |
});
|
|
|
1186 |
}
|
|
|
1187 |
};
|
|
|
1188 |
|
|
|
1189 |
// Prepare the data to send
|
|
|
1190 |
var formData = new FormData();
|
|
|
1191 |
formData.append('contents', contents);
|
|
|
1192 |
formData.append('displayname', name);
|
|
|
1193 |
formData.append('sesskey', M.cfg.sesskey);
|
|
|
1194 |
formData.append('course', this.courseid);
|
|
|
1195 |
formData.append('section', sectionnumber);
|
|
|
1196 |
formData.append('type', type);
|
|
|
1197 |
formData.append('module', module);
|
|
|
1198 |
|
|
|
1199 |
// Send the data
|
|
|
1200 |
xhr.open("POST", this.url, true);
|
|
|
1201 |
xhr.send(formData);
|
|
|
1202 |
},
|
|
|
1203 |
|
|
|
1204 |
/**
|
|
|
1205 |
* Call the AJAX course editing initialisation to add the editing tools
|
|
|
1206 |
* to the newly-created resource link
|
|
|
1207 |
* @param elementid the id of the DOM element containing the new resource link
|
|
|
1208 |
* @param sectionnumber the number of the selected course section
|
|
|
1209 |
*/
|
|
|
1210 |
add_editing: function(elementid) {
|
|
|
1211 |
var node = Y.one('#' + elementid);
|
|
|
1212 |
YUI().use('moodle-course-coursebase', function(Y) {
|
|
|
1213 |
Y.log("Invoking setup_for_resource", 'debug', 'coursedndupload');
|
|
|
1214 |
M.course.coursebase.invoke_function('setup_for_resource', node);
|
|
|
1215 |
});
|
|
|
1216 |
if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
|
|
|
1217 |
M.core.actionmenu.newDOMNode(node);
|
|
|
1218 |
}
|
|
|
1219 |
}
|
|
|
1220 |
};
|