1(function( $ ) { 2 var nfRadio = Backbone.Radio; 3 var radioChannel = nfRadio.channel( 'file_upload' ); 4 5 var fileModel = Backbone.Model.extend( { 6 id: 0, 7 name: '', 8 tmpName: '', 9 fieldID: 0 10 } ); 11 12 var FileCollection = Backbone.Collection.extend( { 13 model: fileModel 14 } ); 15 16 var fileView = Marionette.ItemView.extend( { 17 tagName: 'nf-section', 18 template: '#tmpl-nf-field-file-row', 19 20 events: { 21 'click .delete': 'clickDelete' 22 }, 23 24 clickDelete: function( event ) { 25 radioChannel.trigger( 'click:deleteFile', event, this.model ); 26 } 27 28 } ); 29 30 var fileCollectionView = Marionette.CollectionView.extend( { 31 childView: fileView 32 } ); 33 34 var uploadController = Marionette.Object.extend( { 35 36 jqXHR: [], 37 $progress_bars: [], 38 activeUploads: {}, 39 40 initialize: function() { 41 this.listenTo( radioChannel, 'init:model', this.initFile ); 42 this.listenTo( radioChannel, 'render:view', this.initFileUpload ); 43 this.listenTo( radioChannel, 'click:deleteFile', this.deleteFile ); 44 radioChannel.reply( 'validate:required', this.validateRequired ); 45 radioChannel.reply( 'get:submitData', this.getSubmitData ); 46 }, 47 48 initFile: function( model ) { 49 model.set( 'uploadMulti', 1 != model.get( 'upload_multi_count' ) ) ; 50 }, 51 52 renderView: function( view ) { 53 var el = $( view.el ).find( '.files_uploaded' ); 54 view.fileCollectionView = new fileCollectionView( { 55 el: el, 56 collection: view.model.get( 'files' ), 57 thisModel: this.model 58 } ); 59 60 view.model.bind( 'change:files', this.changeCollection, view ); 61 62 /* 63 * This radio responder is only necessary if we have Multi-Part Forms active. 64 * Thankfully, it won't fire if the add-on isn't active. 65 * 66 * When we change our parts in a Multi-Part Form, re-render our file collection. 67 */ 68 view.listenTo( nfRadio.channel( 'nfMP' ), 'change:part', this.changeCollection, view ); 69 70 /* 71 * This will be triggered when a repeater fieldset is adding a fieldset 72 * It prevents the files data already loaded not to be removed from the view 73 */ 74 if(this.isInRepeaterFieldset(view.el) && view.model.attributes.files.models.length > 0){ 75 view.fileCollectionView.render(); 76 } 77 }, 78 79 isInRepeaterFieldset: function( element ) { 80 return element.dataset.fieldId.includes('.') && element.dataset.fieldId.includes('_'); 81 }, 82 83 changeCollection: function() { 84 this.fileCollectionView.render(); 85 //Set values in repeater when refreshing the files data view 86 nfRadio.channel( "field-repeater" ).trigger( 'set:value' ); 87 }, 88 89 getFieldID: function( e ) { 90 var $parent = $( e.target ).parents( '.field-wrap' ); 91 92 return $parent.data( 'field-id' ); 93 }, 94 95 getProgressBar: function( e ) { 96 var fieldID = this.getFieldID( e ); 97 98 return this.$progress_bars[ fieldID ]; 99 }, 100 101 resetProgress: function( e ) { 102 var self = this; 103 setTimeout( function() { 104 self.getProgressBar( e ).css( 'width', 0 ); 105 }, 1500 ); 106 }, 107 108 /** 109 * Keep a selection inside the field's maximum file count. 110 * 111 * Files that do not fit are dropped from the selection before anything is 112 * uploaded, and the visitor is told inline. A refused file is not an error 113 * on the field: the field is still holding a valid number of files, so it 114 * must neither disable the submit button nor raise the form's "correct 115 * errors" notice (#727). 116 */ 117 checkFilesLimit: function( view, e, data ) { 118 var limit = view.model.get( 'upload_multi_count' ); 119 120 if ( 1 == limit ) { 121 return true; 122 } 123 124 var files = view.model.get( 'files' ); 125 var held = files ? files.length : 0; 126 var remaining = limit - held; 127 128 if ( remaining <= 0 ) { 129 this.showLimitNotice( view, limit ); 130 131 return false; 132 } 133 134 if ( data.files.length > remaining ) { 135 // Same array reference the plugin hands to _onAdd, so trimming it 136 // here keeps the surplus files from ever being uploaded. 137 data.files.splice( remaining, data.files.length - remaining ); 138 this.showLimitNotice( view, limit ); 139 140 return true; 141 } 142 143 this.clearLimitNotice( view ); 144 145 return true; 146 }, 147 148 /** 149 * Inline, non-blocking notice that some files were refused by the limit. 150 * Deliberately not routed through showError: that registers a field error, 151 * which is what used to strand visitors on a perfectly valid field. 152 */ 153 showLimitNotice: function( view, limit ) { 154 var message = nf_upload.strings.file_limit_notice.replace( '%n', limit ); 155 var $field = $( view.el ); 156 var $notice = $field.find( '.nf-fu-limit-notice' ); 157
158 if ( ! $notice.length ) { 159 $notice = $( '<div class="nf-error-msg nf-fu-limit-notice" role="status" aria-live="polite"></div>' ); 160 $field.find( '.files_uploaded' ).after( $notice ); 161 } 162 163 $notice.text( message ); 164 }, 165 166 clearLimitNotice: function( view ) { 167 $( view.el ).find( '.nf-fu-limit-notice' ).remove(); 168 }, 169 170 addActiveUpload: function( formID, fieldID ) { 171 if ( !this.activeUploads[ formID ] ) { 172 this.activeUploads[ formID ] = {}; 173 } 174 if ( !this.activeUploads[ formID ][ fieldID ] ) { 175 this.activeUploads[ formID ][ fieldID ] = 1; 176 } else { 177 this.activeUploads[ formID ][ fieldID ] += 1; 178 } 179 }, 180 181 removeActiveUpload: function( formID, fieldID ) { 182 if ( this.activeUploads[ formID ] && this.activeUploads[ formID ][ fieldID ] ) { 183 this.activeUploads[ formID ][ fieldID ] -= 1; 184 if ( this.activeUploads[ formID ][ fieldID ] < 1 ) { 185 delete this.activeUploads[ formID ][ fieldID ]; 186 var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); 187 if ( fieldModel ) { 188 var errors = fieldModel.get( 'errors' ); 189 var targetError = errors.get( 'upload-in-progress' ); 190 if ( targetError ) { 191 errors.remove( targetError ); 192 fieldModel.set( 'errors', errors ); 193 fieldModel.trigger( 'change:errors', fieldModel ); 194 } 195 } 196 var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID ); 197 if ( formModel ) { 198 var fieldErrors = formModel.get( 'fieldErrors' ); 199 if ( fieldErrors[ fieldID ] ) { 200 delete fieldErrors[ fieldID ][ 'upload-in-progress' ]; 201 if ( 0 === _.size( fieldErrors[ fieldID ] ) ) { 202 delete fieldErrors[ fieldID ]; 203 } 204 } 205 } 206 } 207 208 // If no more uploads for this form, enable submit 209 if ( Object.keys( this.activeUploads[ formID ] ).length === 0 ) { 210 delete this.activeUploads[ formID ]; 211 nfRadio.channel( 'form-' + formID ).trigger( 'enable:submit' ); 212 } 213 } 214 }, 215 216 hasActiveUploads: function( formID ) { 217 return this.activeUploads[ formID ] && Object.keys( this.activeUploads[ formID ] ).length > 0; 218 }, 219 220 maybeSubmitFieldData: function( data ) { 221 if ( data.paramName === 'files-' + data.formData.field_id || data.paramName === 'files-' + data.formData.field_id + '[]' ) { 222 this.jqXHR[ data.formData.field_id ] = data.submit(); 223 } 224 }, 225 226 showError: function( error, view ) { 227 nfRadio.channel( 'fields' ).request( 'add:error', view.model.id, 'upload-file-error', error ); 228 }, 229 230 resetError: function( view, fieldID ) { 231 nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'upload-file-error' ); 232 this.$progress_bars[ fieldID ].css( 'width', 0 ); 233 $( view.el ).find( '.nf-fu-button-cancel' ).hide(); 234 var formID = view.model.get( 'formID' ); 235 this.removeActiveUpload( formID, fieldID ); 236 }, 237 238 cancelUpload: function( e ) { 239 e.preventDefault(); 240 var fieldID = e.data.view.model.id; 241 e.data.controller.jqXHR[ fieldID ].abort(); 242 // Cleanup is handled by the fileuploadfail event handler on abort 243 }, 244 245 isNonceValid: function( view ) { 246 var nonceExpiry = view.model.get( 'uploadNonceExpiry' ); 247 if ( typeof nonceExpiry === 'undefined' || nonceExpiry === null ) { 248 return false 249 } 250 251 var now = Math.round( (new Date()).getTime() / 1000 ); 252 253 return now < nonceExpiry; 254 }, 255 256 getValidNonce: function( field_id ) { 257 return jQuery.post( { 258 url: nfFrontEnd.adminAjax + '?action=nf_fu_get_new_nonce', 259 type: 'POST', 260 data: { 261 'field_id': field_id 262 }, 263 cache: false 264 } ); 265 }, 266 267 initFileUpload: function( view ) { 268 var fieldID = view.model.id; 269 var formID = view.model.get( 'formID' ); 270 var nonce = view.model.get( 'uploadNonce' ); 271 var $file = $( view.el ).find( '.nf-element' ); 272 var $files_uploaded = $( view.el ).find( '.files_uploaded' ); 273 this.$progress_bars[ fieldID ] = $( view.el ).find( '.nf-fu-progress-bar' ); 274 var url = nfFrontEnd.adminAjax + '?action=nf_fu_upload'; 275 var self = this; 276 var files = view.model.get( 'files' ); 277 278 $( view.el ).find( 'button.nf-fu-button-cancel' ).on( 'click', { view: view, controller: self }, self.cancelUpload ); 279 280 $( view.el ).find( 'button.nf-fu-fileinput-button' ).on( 'click', function() { 281 // Don't show required validation error on click of the file upload button 282 view.model.set( 'firstTouch', false ); 283 } ); 284 285 /* 286 * Make sure that our files array isn't undefined. 287 * If it is, set it to an empty array. 288 */ 289 files = files || []; 290 291 /* 292 * If "files" isn't a collection, turn it into one. 293 */ 294 if ( ! ( files instanceof FileCollection ) ) { 295 files = new FileCollection( files ); 296 view.model.set( 'files', files ); 297 } 298 299 this.renderView( view ); 300 301 var formData = { 302 form_id: formID, 303 field_id: fieldID, 304 nonce: nonce, 305 abort: false 306 }; 307 308 $file.fileupload( { 309 url: url, 310 dataType: 'json', 311 messages: { 312 maxFileSize: nf_upload.strings.max_file_size_error.replace( '%n', view.model.get( 'max_file_size_mb' ) ), 313 minFileSize: nf_upload.strings.min_file_size_error.replace( '%n', view.model.get( 'min_file_size_mb' ) ) 314 }, 315 maxChunkSize: view.model.get( 'max_chunk_size' ), 316 dropZone: $( view.el ), 317 maxFileSize: view.model.get( 'max_file_size' ), 318 minFileSize: view.model.get( 'min_file_size' ), 319 add: function (e, data) { 320 $( view.el ).find( '.nf-fu-button-cancel' ).show(); 321 formData.abort = false;
322 data.formData = formData; 323 if ( self.isNonceValid( view ) ) { 324 self.maybeSubmitFieldData( data ); 325 326 return; 327 } 328 329 self.getValidNonce( formData.field_id ).done( function( response ) { 330 if ( !response.success ) { 331 self.showError( nf_upload.strings.upload_nonce_error, view ); 332 return false; 333 } 334 335 view.model.set( 'uploadNonce', response.data.nonce ); 336 view.model.set( 'uploadNonceExpiry', response.data.nonce_expiry ); 337 338 data.formData.nonce = response.data.nonce; 339 self.maybeSubmitFieldData( data ); 340 } ); 341 }, 342 change: function( e, data ) { 343 if ( !self.checkFilesLimit( view, e, data ) ) { 344 return false; 345 } 346 // Remove any errors on this field. 347 nfRadio.channel( 'fields' ).request( 'remove:error', view.model.get( 'id' ), 'required-error' ); 348 }, 349 drop: function( e, data ) { 350 if ( !self.checkFilesLimit( view, e, data ) ) { 351 return false; 352 } 353 }, 354 done: function( e, data ) { 355 self.removeActiveUpload( formID, fieldID ); 356 357 if ( !data.result || data.result === undefined ) { 358 self.showError( nf_upload.strings.unknown_upload_error, view ); 359 self.resetProgress( e ); 360 return; 361 } 362 363 if ( -1 === data.result ) { 364 self.showError( nf_upload.strings.upload_error, view ); 365 self.resetProgress( e ); 366 return; 367 } 368 369 // Check for errors 370 if ( data.result.errors.length ) { 371 $.each( data.result.errors, function( index, error ) { 372 self.showError( error, view ); 373 } ); 374 } 375 376 if ( data.result.data.files === undefined || !data.result.data.files.length ) { 377 self.resetProgress( e ); 378 379 return; 380 } 381 382 var allowed = view.model.get( 'upload_multi_count' ); 383 var limit = 1; 384 385 if ( 1 != allowed ) { 386 var uploaded = view.model.get( 'files' ).length; 387 limit = allowed - uploaded; 388 389 if ( limit <= 0 ) { 390 // The field is already holding its maximum, so this file 391 // simply is not added. That is not an error state (#727). 392 self.showLimitNotice( view, allowed ); 393 self.resetProgress( e ); 394 return; 395 } 396 } 397 398 $( view.el ).find( '.nf-fu-button-cancel' ).hide(); 399 400 var count = 0; 401 $.each( data.result.data.files, function( index, file ) { 402 count++; 403 if ( count > limit ) { 404 return false; 405 } 406 files.add( new fileModel( { name: file.name, tmp_name: file.tmp_name, fieldID: fieldID } ) ); 407 } ); 408 409 view.model.set( 'files', files ); 410 view.model.set( 'value', 1 ); 411 view.model.trigger( 'change:files', view.model ); 412 413 self.resetProgress( e ); 414 415 nfRadio.channel( 'fields' ).trigger( 'change:field', view.el, view.model ); 416 }, 417 start: function() { 418 if ( 1 == view.model.get( 'upload_multi_count' ) ) { 419 // Remove the files uploaded display and reset the collection 420 $files_uploaded.empty(); 421 files.reset(); 422 } 423 nfRadio.channel( 'fields' ).request( 'remove:error', view.model.id, 'upload-file-error' ); 424 self.addActiveUpload( formID, fieldID ); 425 var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); 426 if ( fieldModel ) { 427 var errors = fieldModel.get( 'errors' ); 428 errors.add( { 'id': 'upload-in-progress', 'msg': nf_upload.strings.upload_in_progress } ); 429 fieldModel.set( 'errors', errors ); 430 fieldModel.trigger( 'change:errors', fieldModel ); 431 } 432 var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID ); 433 if ( formModel ) { 434 var fieldErrors = formModel.get( 'fieldErrors' ); 435 if ( 'undefined' === typeof fieldErrors[ fieldID ] ) { 436 fieldErrors[ fieldID ] = {}; 437 } 438 fieldErrors[ fieldID ][ 'upload-in-progress' ] = nf_upload.strings.upload_in_progress; 439 } 440 nfRadio.channel( 'form-' + formID ).trigger( 'disable:submit', view.model ); 441 }, 442 progressall: function( e, data ) { 443 var progress = parseInt( data.loaded / data.total * 100, 10 ); 444 self.getProgressBar( e ).css( 'width', progress + '%' ); 445 } 446 } ).on( 'fileuploadprocessalways', function( e, data ) { 447 var index = data.index, 448 file = data.files[ index ]; 449 if ( file.error ) { 450 nfRadio.channel( 'fields' ).request( 'add:error', view.model.id, 'upload-file-error', file.error ); 451 self.removeActiveUpload( formID, fieldID ); 452 } 453 } ).on( 'fileuploadchunkdone', function( e, data ) { 454 // Check for errors 455 if ( data.result.errors.length ) { 456 $.each( data.result.errors, function( index, error ) { 457 self.showError( error, view ); 458 } ); 459 460 self.jqXHR[ formData.field_id ].abort(); 461 self.$progress_bars[ formData.field_id ].css( 'width', 0 ); 462 self.removeActiveUpload( formID, fieldID ); 463 formData.abort = true; 464 return; 465 } 466 467 var key = data.result.data.files[ 0 ][ 'new_tmp_key' ]; 468 formData[ key ] = data.result.data.files[ 0 ][ 'tmp_name' ]; 469 } ).on( "fileuploadchunksend", function( e, data ) { 470 if ( formData.abort === true ) { 471 return false;
472 } 473 } ).on( 'fileuploadfail', function( e, data ) { 474 if ( data.errorThrown === 'abort' ) { 475 // Upload was cancelled via the Cancel button â perform targeted cleanup. 476 // resetError() handles progress bar, cancel button visibility, and active upload tracking. 477 self.resetError( view, fieldID ); 478 return false; 479 } 480 // Genuine upload failure â clean up tracking and progress display. 481 self.removeActiveUpload( formID, fieldID ); 482 self.resetProgress( e ); 483 } ) 484 .prop( 'disabled', !$.support.fileInput ) 485 .parent().addClass( $.support.fileInput ? undefined : 'disabled' ); 486 }, 487 488 getSubmitData: function( fieldData, field ) { 489 fieldData.files = field.get( 'files' ); 490 491 return fieldData; 492 }, 493 494 deleteFile: function( event, model ) { 495 event.preventDefault(); 496 // Resolve the field before removing the model: removing it re-renders 497 // the uploaded-files list, which detaches event.target from the 498 // document and leaves closest() with nothing to walk up to (#727). 499 var $field = $( event.target ).closest( '.field-wrap' ); 500 model.collection.remove( model ); 501 // Removing a file puts the field back inside its limit, so any notice 502 // about refused files no longer describes the field (#727). 503 $field.find( '.nf-fu-limit-notice' ).remove(); 504 // send off AJAX request to delete temp file or uploaded file 505 506 var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'fieldID' ) ); 507 nfRadio.channel( 'fields' ).trigger( 'change:field', '', fieldModel ); 508 // Remove any errors on this field. 509 nfRadio.channel( 'fields' ).request( 'remove:error', fieldModel, 'required-error' ); 510 }, 511 512 /** 513 * Check files have been submitted successfully for required field check 514 * 515 * @param el 516 * @param model 517 * @returns {boolean} 518 */ 519 validateRequired: function( el, model ) { 520 if ( !model.get( 'firstTouch' ) ) { 521 model.set( 'firstTouch', true ); 522 return true; 523 } 524 525 var files = model.get( 'files' ); 526 if ( typeof files === 'undefined' || !files.length ) { 527 model.set( 'value', '' ); 528 return false; 529 } 530 531 return true; 532 } 533 534 } ); 535 536 new uploadController(); 537 538 $( document ).ready( function() { 539 $( 'body' ).on( 'click', 'button.nf-fu-fileinput-button', function( e ) { 540 $( this ).parent().find( 'input.nf-element' ).click(); 541 } ); 542 543 $( document ).bind( 'drop dragover', function( e ) { 544 e.preventDefault(); 545 } ); 546 } ); 547})( 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.