1// jQuery plugin to display a custom jQuery File Uploader interface. 2// (C) 2020 CubicleSoft. All Rights Reserved. 3 4(function($) { 5 var EscapeHTML = function(text) { 6 var map = { 7 '&': '&', 8 '<': '<', 9 '>': '>', 10 '"': '"', 11 "'": ''' 12 }; 13 14 return text.replace(/[&<>"']/g, function(m) { return map[m]; }); 15 } 16 17 var FormatStr = function(format) { 18 var args = Array.prototype.slice.call(arguments, 1); 19 20 return format.replace(/{(\d+)}/g, function(match, number) { 21 return (typeof args[number] != 'undefined' ? args[number] : match); 22 }); 23 }; 24 25 var GetDisplayFilesize = function(numbytes, adjustprecision, units) { 26 if (numbytes == 0) return '0 Bytes'; 27 if (numbytes == 1) return '1 Byte'; 28 29 numbytes = Math.abs(numbytes); 30 var magnitude, abbreviations; 31 if (units && units.toLowerCase() === 'iec_formal') 32 { 33 magnitude = Math.pow(2, 10); 34 abbreviations = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 35 } 36 else if (units && units.toLowerCase() === 'si') 37 { 38 magnitude = Math.pow(10, 3); 39 abbreviations = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 40 } 41 else 42 { 43 magnitude = Math.pow(2, 10); 44 abbreviations = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 45 } 46 47 var pos = Math.floor(Math.log(numbytes) / Math.log(magnitude)); 48 var result = (numbytes / Math.pow(magnitude, pos)); 49 50 return (pos == 0 || (adjustprecision && result >= 99.995) ? result.toFixed(0) : result.toFixed(2)) + ' ' + abbreviations[pos]; 51 }; 52 53 var DisplayPreviewDialog = function(preview, endelem, inforow, data, settings) { 54 var previewbackground = $('<div>').addClass('ff_fileupload_dialog_background'); 55 var previewclone = preview.clone(true, true).click(function(e) { 56 e.stopPropagation(); 57 }); 58 var previewdialog = $('<div>').addClass('ff_fileupload_dialog_main').append(previewclone); 59 60 var HidePreviewDialog = function() { 61 $(document).off('keyup.fancy_fileupload'); 62 63 previewbackground.remove(); 64 endelem.focus(); 65 66 if (settings.hidepreview) settings.hidepreview.call(inforow, data, preview, previewclone); 67 }; 68 69 $(document).on('keyup.fancy_fileupload', function(e) { 70 if (e.keyCode == 27) { 71 HidePreviewDialog(); 72 } 73 }); 74 75 previewbackground.append(previewdialog).click(function() { 76 HidePreviewDialog(); 77 }); 78 79 $('body').append(previewbackground); 80 previewclone.focus(); 81 82 if (settings.showpreview) settings.showpreview.call(inforow, data, preview, previewclone); 83 }; 84 85 var InitShowAriaLabelInfo = function(inforow) { 86 inforow.find('button').hover(function() { 87 var val = $(this).attr('aria-label'); 88 89 if (val) 90 { 91 inforow.find('.ff_fileupload_buttoninfo').text(val).removeClass('ff_fileupload_hidden'); 92 inforow.find('.ff_fileupload_fileinfo').addClass('ff_fileupload_hidden'); 93 } 94 }, function() { 95 inforow.find('.ff_fileupload_fileinfo').removeClass('ff_fileupload_hidden'); 96 inforow.find('.ff_fileupload_buttoninfo').addClass('ff_fileupload_hidden'); 97 }); 98 }; 99 100 $.fn.FancyFileUpload = function(options) { 101 this.each(function() { 102 var $this = $(this); 103 104 // Remove the previous file uploader. 105 if ($this.data('fancy-fileupload') && typeof($this.data('fancy-fileupload')) === 'object') 106 { 107 $this.removeClass('ff_fileupload_hidden'); 108 109 var data = $this.data('fancy-fileupload'); 110 111 data.form.find('input[type=file]').fileupload('destroy'); 112 data.form.remove(); 113 data.fileuploadwrap.remove(); 114 115 $this.removeData('fancy-fileupload'); 116 } 117 }); 118 119 if (!$('.ff_fileupload_hidden').length) 120 { 121 $(document).off('drop.fancy_fileupload dragover.fancy_fileupload'); 122 $(window).off('beforeunload.fancy_fileupload'); 123 } 124 125 if (typeof(options) === 'string' && options === 'destroy') return this; 126 127 var settings = $.extend({}, $.fn.FancyFileUpload.defaults, options); 128 129 // Let custom callbacks make last second changes to the finalized settings. 130 if (settings.preinit) settings.preinit(settings); 131 132 // Prevent default file drag-and-drop operations. 133 $(document).off('drop.fancy_fileupload dragover.fancy_fileupload'); 134 $(document).on('drop.fancy_fileupload dragover.fancy_fileupload', function (e) { 135 e.preventDefault(); 136 }); 137 138 // Some useful functions. 139 var Translate = function(str) { 140 return (settings.langmap[str] ? settings.langmap[str] : str); 141 }; 142 143 // Prevent the user from leaving the page if there is an active upload. 144 // Most browsers won't show the custom message. So make the relevant UI elements bounce using CSS. 145 $(window).on('beforeunload.fancy_fileupload', function(e) { 146 var active = $('.ff_fileupload_uploading, .ff_fileupload_starting'); 147 var queued = $('.ff_fileupload_queued'); 148 149 if (active.length || queued.length) 150 { 151 active.removeClass('ff_fileupload_bounce'); 152 setTimeout(function() { active.addClass('ff_fileupload_bounce') }, 250); 153 154 queued.removeClass('ff_fileupload_bounce'); 155 setTimeout(function() { queued.addClass('ff_fileupload_bounce') }, 250); 156 157 if (active.length) return Translate('There is a file upload still in progress. Leaving the page will cancel the upload.\n\nAre you sure you want to leave this page?'); 158 if (queued.length) return Translate('There is a file that was added to the queue but the upload has not been started. Leaving the page will clear the queue and not upload the file.\n\nAre you sure you want to leave this page?'); 159 } 160 }); 161 162 // Create some extra DOM nodes for preview checking. 163 var audioelem = document.createElement('audio'); 164 var videoelem = document.createElement('video'); 165 166 var AddFile = function(uploads, e, data) { 167 var inforow = $('<tr><td class="ff_fileupload_preview"><button class="ff_fileupload_preview_image" type="button"><span class="ff_fileupload_preview_text"></span></button><div class="ff_fileupload_actions_mobile"></div></td><td class="ff_fileupload_summary"><div class="ff_fileupload_filename"></div><div class="ff_fileupload_fileinfo"></div><div class="ff_fileupload_buttoninfo ff_fileupload_hidden"></div><div class="ff_fileupload_errors ff_fileupload_hidden"></div><div class="ff_fileupload_progress_background ff_fileupload_hidden"><div class="ff_fileupload_progress_bar"></div></div></td><td class="ff_fileupload_actions"></td></tr>'); 168 var pos = data.files[0].name.lastIndexOf('.'); 169 var filename = (pos > -1 ? data.files[0].name.substring(0, pos) : data.files[0].name); 170 var fileext = (pos > -1 ? data.files[0].name.substring(pos + 1).toLowerCase() : ''); 171 var alphanum = 'abcdefghijklmnopqrstuvwxyz0123456789'; 172 pos = (fileext == '' ? -1 : alphanum.indexOf(fileext.charAt(0))); 173 var fileextclass = alphanum.charAt((pos > -1 ? pos : Math.floor(Math.random() * alphanum.length))); 174 175 // Initialize necessary callback options. 176 data.ff_info = {}; 177 data.ff_info.errors = []; 178 data.ff_info.retries = 0; 179 data.ff_info.retrydelay = settings.retrydelay; 180 data.ff_info.removewidget = false; 181 data.ff_info.inforow = inforow; 182 data.ff_info.displayfilesize = GetDisplayFilesize(data.files[0].size, settings.adjustprecision, settings.displayunits); 183 data.context = inforow; 184 185 // A couple of functions for handling actions. 186 var StartUpload = function(e) { 187 e.preventDefault(); 188 189 // Set filename. 190 if (settings.edit && !data.ff_info.errors.length) 191 { 192 var fileinput = inforow.find('.ff_fileupload_filename input'); 193 if (fileinput.length) 194 { 195 var newfilename = fileinput.val(); 196 if (fileext != '') newfilename += '.' + fileext; 197 198 inforow.find('.ff_fileupload_filename').text(newfilename); 199 data.files[0].uploadName = newfilename; 200 } 201 } 202 203 // Remove start upload buttons. 204 inforow.find('button.ff_fileupload_start_upload').remove(); 205 206 // Reset hover status. 207 inforow.find('.ff_fileupload_fileinfo').removeClass('ff_fileupload_hidden'); 208 inforow.find('.ff_fileupload_buttoninfo').addClass('ff_fileupload_hidden'); 209 210 // Set the status. 211 inforow.find('.ff_fileupload_fileinfo').text(data.ff_info.displayfilesize + ' | ' + Translate('Starting upload...')); 212 213 // Display progress bar. 214 inforow.find('.ff_fileupload_progress_background').removeClass('ff_fileupload_hidden'); 215 216 // Alter remove buttons. 217 inforow.find('button.ff_fileupload_remove_file').attr('aria-label', Translate('Cancel upload and remove from list'));
218 219 // Begin the actual upload. 220 inforow.removeClass('ff_fileupload_queued'); 221 inforow.addClass('ff_fileupload_starting'); 222 223 var SubmitUpload = function() { 224 inforow.removeClass('ff_fileupload_starting'); 225 inforow.addClass('ff_fileupload_uploading'); 226 data.submit(); 227 }; 228 229 if (settings.startupload) settings.startupload.call(inforow, SubmitUpload, e, data); 230 else SubmitUpload(); 231 }; 232 233 var RemoveFile = function(e) { 234 e.preventDefault(); 235 236 if (inforow.hasClass('ff_fileupload_uploading')) 237 { 238 if (!confirm(Translate('This file is currently being uploaded.\n\nStop the upload and remove the file from the list?'))) return; 239 240 data.ff_info.removewidget = true; 241 data.abort(); 242 } 243 else 244 { 245 if (inforow.hasClass('ff_fileupload_starting')) 246 { 247 if (!confirm(Translate('This file is waiting to start.\n\nCancel the operation and remove the file from the list?'))) return; 248 249 if (settings.uploadcancelled) settings.uploadcancelled.call(data.ff_info.inforow, e, data); 250 } 251 252 inforow.remove(); 253 254 delete data.ff_info; 255 } 256 }; 257 258 data.ff_info.RemoveFile = function() { 259 if (inforow.hasClass('ff_fileupload_uploading')) 260 { 261 data.ff_info.removewidget = true; 262 data.abort(); 263 } 264 else 265 { 266 if (inforow.hasClass('ff_fileupload_starting')) 267 { 268 if (settings.uploadcancelled) settings.uploadcancelled.call(data.ff_info.inforow, e, data); 269 } 270 271 inforow.remove(); 272 273 delete data.ff_info; 274 } 275 }; 276 277 // Thumbnail preview. 278 var haspreview = false; 279 var preview; 280 var hasimage = false; 281 if (URL && URL.createObjectURL) 282 { 283 var url = URL.createObjectURL(data.files[0]); 284 if (url) 285 { 286 if (data.files[0].type === 'image/gif' || data.files[0].type === 'image/jpeg' || data.files[0].type === 'image/png') 287 { 288 inforow.find('.ff_fileupload_preview_image').css('background-image', 'url("' + url + '")'); 289 290 haspreview = true; 291 preview = $('<img>').attr('src', url); 292 hasimage = true; 293 } 294 else if (data.files[0].type.lastIndexOf('audio/', 0) > -1 && audioelem.canPlayType && audioelem.canPlayType(data.files[0].type)) 295 { 296 haspreview = true; 297 preview = $('<audio>').attr('src', url).prop('controls', true); 298 } 299 else if (data.files[0].type.lastIndexOf('video/', 0) > -1 && videoelem.canPlayType && videoelem.canPlayType(data.files[0].type)) 300 { 301 haspreview = true; 302 preview = $('<video>').attr('src', url).prop('controls', true); 303 } 304 } 305 } 306 307 if (haspreview) 308 { 309 inforow.find('.ff_fileupload_preview_image').addClass('ff_fileupload_preview_image_has_preview').attr('aria-label', Translate('Preview')).click(function(e) { 310 e.preventDefault(); 311 312 this.blur(); 313 DisplayPreviewDialog(preview, this, inforow, data, settings); 314 }); 315 } 316 else 317 { 318 inforow.find('.ff_fileupload_preview_image').prop('disabled', true).attr('aria-label', Translate('No preview available')).click(function(e) { 319 e.preventDefault(); 320 }); 321 } 322 323 if (!hasimage) inforow.find('.ff_fileupload_preview_image').addClass('ff_fileupload_preview_text_with_color').addClass('ff_fileupload_preview_text_' + fileextclass).text(fileext); 324 325 // Validate inputs. 326 if (settings.accept) 327 { 328 var found = false; 329 for (var x = 0; x < settings.accept.length && !found; x++) 330 { 331 if (settings.accept[x] === fileext || settings.accept[x] === data.files[0].type) found = true; 332 } 333 334 if (!found) data.ff_info.errors.push(Translate('Invalid file extension.')); 335 } 336 337 if (settings.maxfilesize > -1 && data.files[0].size > settings.maxfilesize) data.ff_info.errors.push(FormatStr(Translate('File is too large. Maximum file size is {0}.'), GetDisplayFilesize(settings.maxfilesize, settings.adjustprecision, settings.displayunits))); 338 339 // Filename text field/display. 340 if (settings.edit && !data.ff_info.errors.length) 341 { 342 inforow.find('.ff_fileupload_filename').append($('<input>').attr('type', 'text').val(filename).keydown(function(e) { 343 // Start uploading if someone presses enter. 344 if (e.keyCode == 13) StartUpload(e); 345 })); 346 } 347 else 348 { 349 inforow.find('.ff_fileupload_filename').text(data.files[0].name); 350 } 351
352 // File/Upload information. 353 inforow.find('.ff_fileupload_fileinfo').text(data.ff_info.displayfilesize + (hasimage && settings.edit && !data.ff_info.errors.length ? ' | .' + fileext : '')); 354 355 // Errors. 356 if (data.ff_info.errors.length) inforow.find('.ff_fileupload_errors').html(data.ff_info.errors.join('<br>')).removeClass('ff_fileupload_hidden'); 357 358 // Action buttons. 359 if (!data.ff_info.errors.length) 360 { 361 inforow.find('.ff_fileupload_actions').append($('<button>').addClass('ff_fileupload_start_upload').attr('type', 'button').attr('aria-label', Translate('Start uploading')).click(StartUpload)); 362 inforow.find('.ff_fileupload_actions_mobile').append($('<button>').addClass('ff_fileupload_start_upload').attr('type', 'button').attr('aria-label', Translate('Start uploading')).click(StartUpload)); 363 364 inforow.addClass('ff_fileupload_queued'); 365 } 366 367 inforow.find('.ff_fileupload_actions').append($('<button>').addClass('ff_fileupload_remove_file').attr('type', 'button').attr('aria-label', Translate('Remove from list')).click(RemoveFile)); 368 inforow.find('.ff_fileupload_actions_mobile').append($('<button>').addClass('ff_fileupload_remove_file').attr('type', 'button').attr('aria-label', Translate('Remove from list')).click(RemoveFile)); 369 370 // Handle button hover. 371 InitShowAriaLabelInfo(inforow); 372 373 // Improve progress bar performance during upload. 374 data.ff_info.fileinfo = inforow.find('.ff_fileupload_fileinfo'); 375 data.ff_info.progressbar = inforow.find('.ff_fileupload_progress_bar'); 376 377 uploads.append(inforow); 378 379 if (settings.added) settings.added.call(inforow, e, data); 380 }; 381 382 var UploadProgress = function(e, data) { 383 var progress = (data.total < 1 ? 0 : data.loaded / data.total * 100); 384 385 data.ff_info.fileinfo.text(FormatStr(Translate('{0} of {1} | {2}%'), GetDisplayFilesize(data.loaded, settings.adjustprecision, settings.displayunits), data.ff_info.displayfilesize, progress.toFixed(0))); 386 data.ff_info.progressbar.css('width', progress + '%'); 387 388 if (settings.continueupload && settings.continueupload.call(data.ff_info.inforow, e, data) === false) data.abort(); 389 }; 390 391 var UploadFailed = function(e, data) { 392 // For handling chunked upload termination. 393 if (data.ff_info.lastresult && !data.ff_info.lastresult.success) 394 { 395 data.result = data.ff_info.lastresult; 396 data.errorThrown = 'failed_with_msg'; 397 } 398 399 if (data.errorThrown !== 'abort' && data.errorThrown !== 'failed_with_msg' && data.uploadedBytes < data.files[0].size && data.ff_info.retries < settings.retries) 400 { 401 data.ff_info.fileinfo.text(FormatStr(Translate('{0} | Network error, retrying in a moment... ({1})'), data.ff_info.displayfilesize, data.errorThrown)); 402 403 data.ff_info.inforow.removeClass('ff_fileupload_uploading'); 404 data.ff_info.inforow.addClass('ff_fileupload_starting'); 405 406 setTimeout(function() { 407 data.ff_info.inforow.removeClass('ff_fileupload_starting'); 408 data.ff_info.inforow.addClass('ff_fileupload_uploading'); 409 data.data = null; 410 data.submit(); 411 }, data.ff_info.retrydelay); 412 413 data.ff_info.retries++; 414 data.ff_info.retrydelay *= 2; 415 416 return; 417 } 418 419 data.ff_info.inforow.removeClass('ff_fileupload_uploading'); 420 421 if (settings.uploadcancelled) settings.uploadcancelled.call(data.ff_info.inforow, e, data); 422 423 if (data.ff_info.removewidget) 424 { 425 data.ff_info.inforow.remove(); 426 427 delete data.ff_info; 428 } 429 else 430 { 431 // Set the error info. 432 if (data.errorThrown === 'abort') data.ff_info.errors.push(Translate('The upload was cancelled.')); 433 else if (data.errorThrown === 'failed_with_msg') data.ff_info.errors.push(FormatStr(Translate('The upload failed. {0} ({1})'), EscapeHTML(data.result.error), EscapeHTML(data.result.errorcode))); 434 else data.ff_info.errors.push(Translate('The upload failed.')); 435 data.ff_info.inforow.find('.ff_fileupload_errors').html(data.ff_info.errors.join('<br>')).removeClass('ff_fileupload_hidden'); 436 437 // Hide the progress bar. 438 data.ff_info.inforow.find('.ff_fileupload_progress_background').addClass('ff_fileupload_hidden'); 439 440 // Alter remove buttons. 441 data.ff_info.inforow.find('button.ff_fileupload_remove_file').attr('aria-label', Translate('Remove from list'));
442 } 443 }; 444 445 var UploadDone = function(e, data) { 446 if (!data.result.success) 447 { 448 if (typeof(data.result.error) !== 'string') data.result.error = Translate('The server indicated that the upload was not successful. No additional information available.'); 449 if (typeof(data.result.errorcode) !== 'string') data.result.errorcode = 'server_response'; 450 451 data.errorThrown = 'failed_with_msg'; 452 data.ff_info.removewidget = false; 453 454 UploadFailed(e, data); 455 456 return; 457 } 458 459 data.ff_info.inforow.removeClass('ff_fileupload_uploading'); 460 461 if (settings.uploadcompleted) settings.uploadcompleted.call(data.ff_info.inforow, e, data); 462 463 if (data.ff_info.removewidget) 464 { 465 data.ff_info.inforow.remove(); 466 467 delete data.ff_info; 468 } 469 else 470 { 471 // Set the status. 472 data.ff_info.inforow.find('.ff_fileupload_fileinfo').text(data.ff_info.displayfilesize + ' | ' + Translate('Upload completed')); 473 474 // Hide the progress bar. 475 data.ff_info.inforow.find('.ff_fileupload_progress_background').addClass('ff_fileupload_hidden'); 476 477 // Alter remove buttons. 478 data.ff_info.inforow.find('button.ff_fileupload_remove_file').attr('aria-label', Translate('Remove from list')); 479 } 480 }; 481 482 var UploadChunkSend = function(e, data) { 483 if (data.ff_info) 484 { 485 if (settings.continueupload && settings.continueupload.call(data.ff_info.inforow, e, data) === false) 486 { 487 if (!data.ff_info.lastresult || data.ff_info.lastresult.success) 488 { 489 data.ff_info.lastresult = { 490 'success' : false 491 }; 492 } 493 } 494 495 if (data.ff_info.lastresult && !data.ff_info.lastresult.success) 496 { 497 data.result = data.ff_info.lastresult; 498 499 if (typeof(data.ff_info.lastresult.error) !== 'string') data.ff_info.lastresult.error = Translate('The server indicated that the upload was not successful. No additional information available.'); 500 if (typeof(data.ff_info.lastresult.errorcode) !== 'string') data.ff_info.lastresult.errorcode = 'server_response'; 501 502 data.ff_info.removewidget = false; 503 504 return false; 505 } 506 } 507 }; 508 509 var UploadChunkDone = function(e, data) { 510 // Reset retries for successful chunked uploads. 511 data.ff_info.retries = 0; 512 data.ff_info.retrydelay = settings.retrydelay; 513 514 // Save for the next UploadChunkSend() call. 515 data.ff_info.lastresult = data.result; 516 }; 517 518 519 return this.each(function() { 520 var $this = $(this); 521 522 // Calculate the action URL. 523 if (settings.url === '') 524 { 525 var url = $this.closest('form').attr('action'); 526 if (url) settings.url = url; 527 } 528 529 // Create a separate, hidden form on the page for handling file uploads. 530 var form = $('<form>').addClass('ff_fileupload_hidden').attr({ 531 'action' : settings.url, 532 'method' : 'post', 533 'enctype' : 'multipart/form-data' 534 }); 535 $('body').append(form); 536 537 // Append hidden input elements. 538 for (var x in settings.params) 539 { 540 if (settings.params.hasOwnProperty(x)) 541 { 542 var input = $('<input>').attr({ 543 'type' : 'hidden', 544 'name' : x, 545 'value' : settings.params[x] 546 }); 547 548 form.append(input); 549 } 550 } 551 552 // Append a file input element. 553 var fileinputname = $this.attr('name'); 554 var fileinput = $('<input>').attr({ 555 'type' : 'file', 556 'name' : (fileinputname ? fileinputname : 'file') 557 }); 558 if ($this.prop('multiple')) fileinput.prop('multiple', true); 559 560 // Process the accepted file extensions. 561 if ($this.attr('accept')) 562 { 563 fileinput.attr('accept', $this.attr('accept')); 564 565 if (!settings.accept) 566 { 567 var accept = $this.attr('accept').split(','); 568 569 settings.accept = []; 570 for (var x = 0; x < accept.length; x++) 571 { 572 var opt = $.trim(accept[x]).toLowerCase(); 573 settings.accept.push(opt.indexOf('/') < 0 && opt.lastIndexOf('.') > -1 ? opt.substring(opt.lastIndexOf('.') + 1) : opt); 574 } 575 } 576 } 577 578 form.append(fileinput); 579 580 // Insert the widget wrapper. 581 var fileuploadwrap = $('<div>').addClass('ff_fileupload_wrap'); 582 $this.after(fileuploadwrap); 583 584 // Insert a new dropzone. Using a button allows for standard keyboard and mouse navigation to the element. The wrapper is for paste support. 585 var dropzonewrap = $('<div>').addClass('ff_fileupload_dropzone_wrap'); 586 var dropzone = $('<button>').addClass('ff_fileupload_dropzone').attr('type', 'button').attr('aria-label', Translate('Browse, drag-and-drop, or paste files to upload')); 587 dropzonewrap.append(dropzone); 588 fileuploadwrap.append(dropzonewrap); 589 dropzone.on('click.fancy_fileupload', function(e) { 590 e.preventDefault(); 591 592 form.find('input[type=file]').click(); 593 }); 594 595 // Add special recording buttons (if enabled). 596 var dropzonetools = $('<div>').addClass('ff_fileupload_dropzone_tools'); 597 dropzonewrap.append(dropzonetools); 598 599 // Record audio. 600 if (settings.recordaudio && navigator.mediaDevices && window.MediaRecorder) 601 { 602 var audiobutton = $('<button>').addClass('ff_fileupload_dropzone_tool').addClass('ff_fileupload_recordaudio').attr('type', 'button').attr('aria-label', Translate('Record audio using a microphone')); 603 dropzonetools.append(audiobutton); 604 605 var audiorec = null;
606 var audiochunks = []; 607 audiobutton.click(function(e) { 608 e.preventDefault(); 609 610 if (!audiorec) 611 { 612 navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) { 613 audiorec = new MediaRecorder(stream, settings.audiosettings); 614 615 audiorec.addEventListener('dataavailable', function(e) { 616 if (e.data.size > 0) audiochunks.push(e.data); 617 618 if (audiorec.state === 'inactive') 619 { 620 var blob = new Blob(audiochunks, { type: 'audio/mp3' }); 621 blob.lastModifiedDate = new Date(); 622 blob.lastModified = Math.floor(blob.lastModifiedDate.getTime() / 1000); 623 blob.name = FormatStr(Translate('Audio recording - {0}.mp3'), blob.lastModifiedDate.toLocaleString()); 624 625 fileinput.fileupload('add', { files: [blob] }); 626 627 stream.getTracks().forEach(function(track) { 628 track.stop(); 629 }); 630 631 audiobutton.removeClass('ff_fileupload_recording'); 632 audiochunks = []; 633 audiorec = null; 634 } 635 }); 636 637 audiorec.start(); 638 audiobutton.addClass('ff_fileupload_recording'); 639 }).catch(function(e) { 640 alert(Translate('Unable to record audio. Either a microphone was not found or access was denied.')); 641 }); 642 } 643 else 644 { 645 audiorec.stop(); 646 } 647 }); 648 } 649 650 // Record video. 651 if (settings.recordvideo && navigator.mediaDevices && window.MediaRecorder) 652 { 653 var videobutton = $('<button>').addClass('ff_fileupload_dropzone_tool').addClass('ff_fileupload_recordvideo').attr('type', 'button').attr('aria-label', Translate('Record video using a camera')); 654 dropzonetools.append(videobutton); 655 656 var videorecpreview = $('<video>').prop('muted', true).prop('autoplay', true).addClass('ff_fileupload_recordvideo_preview').addClass('ff_fileupload_hidden'); 657 dropzonewrap.append(videorecpreview); 658 659 var videorec = null; 660 var videochunks = []; 661 videobutton.click(function(e) { 662 e.preventDefault(); 663 664 if (!videorec) 665 { 666 var streamhandler = function(stream) { 667 videorec = new MediaRecorder(stream, settings.videosettings); 668 669 videorec.addEventListener('dataavailable', function(e) { 670 if (e.data.size > 0) videochunks.push(e.data); 671 672 if (videorec.state === 'inactive') 673 { 674 var blob = new Blob(videochunks, { type: 'video/mp4' }); 675 blob.lastModifiedDate = new Date(); 676 blob.lastModified = Math.floor(blob.lastModifiedDate.getTime() / 1000); 677 blob.name = FormatStr(Translate('Video recording - {0}.mp4'), blob.lastModifiedDate.toLocaleString()); 678 679 fileinput.fileupload('add', { files: [blob] }); 680 681 stream.getTracks().forEach(function(track) { 682 track.stop(); 683 }); 684 685 videobutton.removeClass('ff_fileupload_recording'); 686 videorecpreview.addClass('ff_fileupload_hidden'); 687 if (videorecpreview[0].src !== '') videorecpreview[0].src = ''; 688 videorecpreview[0].srcObject = null; 689 videochunks = []; 690 videorec = null; 691 } 692 }); 693 694 videorec.start(); 695 videobutton.addClass('ff_fileupload_recording'); 696 697 // Display a preview box with just the video stream. 698 try { videorecpreview[0].src = URL.createObjectURL(stream); } catch(e) { videorecpreview[0].srcObject = stream; } 699 700 videorecpreview.removeClass('ff_fileupload_hidden'); 701 }; 702 703 // Video with audio (e.g. webcam) with fallback to video only (e.g. some screen recording codecs). 704 navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(streamhandler).catch(function(e) { 705 navigator.mediaDevices.getUserMedia({ video: true }).then(streamhandler).catch(function(e) { 706 alert(Translate('Unable to record video. Either a camera was not found or access was denied.')); 707 }); 708 }); 709 } 710 else 711 { 712 videorec.stop(); 713 } 714 }); 715 } 716 717 // Add a table to track unprocessed and in-progress uploads. 718 var uploads = $('<table>').addClass('ff_fileupload_uploads'); 719 fileuploadwrap.append(uploads); 720 721 // Hide the starting element. 722 $this.addClass('ff_fileupload_hidden'); 723 724 // Initialize jQuery File Upload using the hidden form and visible dropzone. 725 var baseoptions = { 726 url: settings.url, 727 dataType: 'json', 728 pasteZone: dropzonewrap, 729 limitConcurrentUploads: 2 730 }; 731 732 // Immutable options. 733 var immutableoptions = { 734 singleFileUploads: true, 735 dropZone: dropzone, 736 add: function(e, data) { AddFile(uploads, e, data) }, 737 progress: UploadProgress, 738 fail: UploadFailed, 739 done: UploadDone,
740 chunksend: UploadChunkSend, 741 chunkdone: UploadChunkDone 742 }; 743 744 // The user interface requires certain options to be set correctly. 745 fileinput.fileupload($.extend(baseoptions, settings.fileupload, immutableoptions)); 746 747 // Save necessary information in case the uploader is destroyed later. 748 $this.data('fancy-fileupload', { 749 'fileuploadwrap' : fileuploadwrap, 750 'form' : form, 751 'settings': settings 752 }); 753 754 // Post-initialization callback. 755 if (settings.postinit) settings.postinit.call($this); 756 }); 757 } 758 759 $.fn.FancyFileUpload.defaults = { 760 'url' : '', 761 'params' : {}, 762 'edit' : true, 763 'maxfilesize' : -1, 764 'accept' : null, 765 'displayunits' : 'iec_windows', 766 'adjustprecision' : true, 767 'retries' : 5, 768 'retrydelay' : 500, 769 'recordaudio' : false, 770 'audiosettings' : {}, 771 'recordvideo' : false, 772 'videosettings' : {}, 773 'preinit' : null, 774 'postinit' : null, 775 'added' : null, 776 'showpreview' : null, 777 'hidepreview' : null, 778 'startupload' : null, 779 'continueupload' : null, 780 'uploadcancelled' : null, 781 'uploadcompleted' : null, 782 'fileupload' : {}, 783 'langmap' : {} 784 }; 785}(jQuery));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.