Ticket #28510: browserify.diff
File browserify.diff, 1014.4 KB (added by , 11 years ago) |
---|
-
Gruntfile.js
114 114 } 115 115 } 116 116 }, 117 browserify: { 118 media: { 119 files: { 120 'src/wp-includes/js/media/models.js' : [ SOURCE_DIR + 'wp-includes/js/media/models.manifest.js' ], 121 'src/wp-includes/js/media/views.js' : [ SOURCE_DIR + 'wp-includes/js/media/views.manifest.js' ], 122 'src/wp-includes/js/media/audio-video.js' : [ SOURCE_DIR + 'wp-includes/js/media/audio-video.manifest.js' ], 123 'src/wp-includes/js/media/grid.js' : [ SOURCE_DIR + 'wp-includes/js/media/grid.manifest.js' ] 124 }, 125 options: { debug: true } 126 } 127 }, 117 128 sass: { 118 129 colors: { 119 130 expand: true, … … 358 369 '!wp-includes/js/zxcvbn.min.js' 359 370 ] 360 371 }, 372 media: { 373 expand: true, 374 cwd: SOURCE_DIR, 375 dest: BUILD_DIR, 376 ext: '.min.js', 377 src: [ 378 'wp-includes/js/media/audio-video.js', 379 'wp-includes/js/media/grid.js', 380 'wp-includes/js/media/models.js', 381 'wp-includes/js/media/views.js' 382 ] 383 }, 361 384 jqueryui: { 362 385 options: { 363 386 preserveComments: 'some' … … 435 458 interval: 2000 436 459 } 437 460 }, 461 browserify: { 462 files: [ 463 SOURCE_DIR + 'wp-includes/js/media/**/*.js', 464 '!' + SOURCE_DIR + 'wp-includes/js/media/audio-video.js', 465 '!' + SOURCE_DIR + 'wp-includes/js/media/grid.js', 466 '!' + SOURCE_DIR + 'wp-includes/js/media/models.js', 467 '!' + SOURCE_DIR + 'wp-includes/js/media/views.js' 468 ], 469 tasks: ['browserify', 'uglify:media'] 470 }, 438 471 config: { 439 472 files: 'Gruntfile.js' 440 473 }, … … 483 516 484 517 // Build task. 485 518 grunt.registerTask('build', ['clean:all', 'copy:all', 'cssmin:core', 'colors', 'rtl', 'cssmin:rtl', 'cssmin:colors', 486 'uglify:core', 'uglify:jqueryui', 'concat:tinymce', 'compress:tinymce', 'clean:tinymce', 'jsvalidate:build']); 519 'browserify:media', 'uglify:core', 'uglify:media', 'uglify:jqueryui', 'concat:tinymce', 'compress:tinymce', 520 'clean:tinymce', 'jsvalidate:build']); 487 521 488 522 // Testing tasks. 489 523 grunt.registerMultiTask('phpunit', 'Runs PHPUnit tests, including the ajax, external-http, and multisite tests.', function() { -
package.json
11 11 "devDependencies": { 12 12 "grunt": "~0.4.5", 13 13 "grunt-autoprefixer": "~1.0.1", 14 "grunt-browserify": "~3.2.1", 14 15 "grunt-contrib-clean": "~0.6.0", 15 16 "grunt-contrib-compress": "~0.12.0", 16 17 "grunt-contrib-concat": "~0.5.0", -
src/wp-includes/js/media/audio-video.js
1 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 2 /* global _wpMediaViewsL10n, _wpmejsSettings, MediaElementPlayer */ 3 4 (function($, _, Backbone) { 5 var media = wp.media, 6 baseSettings = {}, 7 l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n; 8 9 if ( ! _.isUndefined( window._wpmejsSettings ) ) { 10 baseSettings = _wpmejsSettings; 11 } 12 13 /** 14 * @mixin 15 */ 16 wp.media.mixin = { 17 mejsSettings: baseSettings, 18 19 removeAllPlayers: function() { 20 var p; 21 22 if ( window.mejs && window.mejs.players ) { 23 for ( p in window.mejs.players ) { 24 window.mejs.players[p].pause(); 25 this.removePlayer( window.mejs.players[p] ); 26 } 27 } 28 }, 29 30 /** 31 * Override the MediaElement method for removing a player. 32 * MediaElement tries to pull the audio/video tag out of 33 * its container and re-add it to the DOM. 34 */ 35 removePlayer: function(t) { 36 var featureIndex, feature; 37 38 if ( ! t.options ) { 39 return; 40 } 41 42 // invoke features cleanup 43 for ( featureIndex in t.options.features ) { 44 feature = t.options.features[featureIndex]; 45 if ( t['clean' + feature] ) { 46 try { 47 t['clean' + feature](t); 48 } catch (e) {} 49 } 50 } 51 52 if ( ! t.isDynamic ) { 53 t.$node.remove(); 54 } 55 56 if ( 'native' !== t.media.pluginType ) { 57 t.media.remove(); 58 } 59 60 delete window.mejs.players[t.id]; 61 62 t.container.remove(); 63 t.globalUnbind(); 64 delete t.node.player; 65 }, 66 67 /** 68 * Allows any class that has set 'player' to a MediaElementPlayer 69 * instance to remove the player when listening to events. 70 * 71 * Examples: modal closes, shortcode properties are removed, etc. 72 */ 73 unsetPlayers : function() { 74 if ( this.players && this.players.length ) { 75 _.each( this.players, function (player) { 76 player.pause(); 77 wp.media.mixin.removePlayer( player ); 78 } ); 79 this.players = []; 80 } 81 } 82 }; 83 84 /** 85 * Autowire "collection"-type shortcodes 86 */ 87 wp.media.playlist = new wp.media.collection({ 88 tag: 'playlist', 89 editTitle : l10n.editPlaylistTitle, 90 defaults : { 91 id: wp.media.view.settings.post.id, 92 style: 'light', 93 tracklist: true, 94 tracknumbers: true, 95 images: true, 96 artists: true, 97 type: 'audio' 98 } 99 }); 100 101 /** 102 * Shortcode modeling for audio 103 * `edit()` prepares the shortcode for the media modal 104 * `shortcode()` builds the new shortcode after update 105 * 106 * @namespace 107 */ 108 wp.media.audio = { 109 coerce : wp.media.coerce, 110 111 defaults : { 112 id : wp.media.view.settings.post.id, 113 src : '', 114 loop : false, 115 autoplay : false, 116 preload : 'none', 117 width : 400 118 }, 119 120 edit : function( data ) { 121 var frame, shortcode = wp.shortcode.next( 'audio', data ).shortcode; 122 console.log( 'woo' ); 123 124 frame = wp.media({ 125 frame: 'audio', 126 state: 'audio-details', 127 metadata: _.defaults( shortcode.attrs.named, this.defaults ) 128 }); 129 130 console.log( frame ); 131 132 return frame; 133 }, 134 135 shortcode : function( model ) { 136 var self = this, content; 137 138 _.each( this.defaults, function( value, key ) { 139 model[ key ] = self.coerce( model, key ); 140 141 if ( value === model[ key ] ) { 142 delete model[ key ]; 143 } 144 }); 145 146 content = model.content; 147 delete model.content; 148 149 return new wp.shortcode({ 150 tag: 'audio', 151 attrs: model, 152 content: content 153 }); 154 } 155 }; 156 157 /** 158 * Shortcode modeling for video 159 * `edit()` prepares the shortcode for the media modal 160 * `shortcode()` builds the new shortcode after update 161 * 162 * @namespace 163 */ 164 wp.media.video = { 165 coerce : wp.media.coerce, 166 167 defaults : { 168 id : wp.media.view.settings.post.id, 169 src : '', 170 poster : '', 171 loop : false, 172 autoplay : false, 173 preload : 'metadata', 174 content : '', 175 width : 640, 176 height : 360 177 }, 178 179 edit : function( data ) { 180 var frame, 181 shortcode = wp.shortcode.next( 'video', data ).shortcode, 182 attrs; 183 184 attrs = shortcode.attrs.named; 185 attrs.content = shortcode.content; 186 187 frame = wp.media({ 188 frame: 'video', 189 state: 'video-details', 190 metadata: _.defaults( attrs, this.defaults ) 191 }); 192 193 return frame; 194 }, 195 196 shortcode : function( model ) { 197 var self = this, content; 198 199 _.each( this.defaults, function( value, key ) { 200 model[ key ] = self.coerce( model, key ); 201 202 if ( value === model[ key ] ) { 203 delete model[ key ]; 204 } 205 }); 206 207 content = model.content; 208 delete model.content; 209 210 return new wp.shortcode({ 211 tag: 'video', 212 attrs: model, 213 content: content 214 }); 215 } 216 }; 217 218 media.model.PostMedia = require( './models/post-media.js' ); 219 media.controller.AudioDetails = require( './controllers/audio-details.js' ); 220 media.controller.VideoDetails = require( './controllers/video-details.js' ); 221 media.view.MediaFrame.MediaDetails = require( './views/frame/media-details.js' ); 222 media.view.MediaFrame.AudioDetails = require( './views/frame/audio-details.js' ); 223 media.view.MediaFrame.VideoDetails = require( './views/frame/video-details.js' ); 224 media.view.MediaDetails = require( './views/media-details.js' ); 225 media.view.AudioDetails = require( './views/audio-details.js' ); 226 media.view.VideoDetails = require( './views/video-details.js' ); 227 228 }(jQuery, _, Backbone)); 229 230 },{"./controllers/audio-details.js":2,"./controllers/video-details.js":8,"./models/post-media.js":11,"./views/audio-details.js":25,"./views/frame/audio-details.js":29,"./views/frame/media-details.js":30,"./views/frame/video-details.js":32,"./views/media-details.js":35,"./views/video-details.js":54}],2:[function(require,module,exports){ 231 /** 232 * The controller for the Audio Details state 233 * 234 * @constructor 235 * @augments wp.media.controller.State 236 * @augments Backbone.Model 237 */ 238 var State = require( './state.js' ), 239 l10n = wp.media.view.l10n, 240 AudioDetails; 241 242 AudioDetails = State.extend({ 243 defaults: { 244 id: 'audio-details', 245 toolbar: 'audio-details', 246 title: l10n.audioDetailsTitle, 247 content: 'audio-details', 248 menu: 'audio-details', 249 router: false, 250 priority: 60 251 }, 252 253 initialize: function( options ) { 254 this.media = options.media; 255 State.prototype.initialize.apply( this, arguments ); 256 } 257 }); 258 259 module.exports = AudioDetails; 260 261 },{"./state.js":7}],3:[function(require,module,exports){ 262 /** 263 * wp.media.controller.Library 264 * 265 * A state for choosing an attachment or group of attachments from the media library. 266 * 267 * @class 268 * @augments wp.media.controller.State 269 * @augments Backbone.Model 270 * @mixes media.selectionSync 271 * 272 * @param {object} [attributes] The attributes hash passed to the state. 273 * @param {string} [attributes.id=library] Unique identifier. 274 * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region. 275 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 276 * If one is not supplied, a collection of all attachments will be created. 277 * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state. 278 * If the 'selection' attribute is a plain JS object, 279 * a Selection will be created using its values as the selection instance's `props` model. 280 * Otherwise, it will copy the library's `props` model. 281 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 282 * @param {string} [attributes.content=upload] Initial mode for the content region. 283 * Overridden by persistent user setting if 'contentUserSetting' is true. 284 * @param {string} [attributes.menu=default] Initial mode for the menu region. 285 * @param {string} [attributes.router=browse] Initial mode for the router region. 286 * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region. 287 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 288 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 289 * Accepts 'all', 'uploaded', or 'unattached'. 290 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 291 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 292 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 293 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 294 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 295 */ 296 var selectionSync = require( '../utils/selection-sync.js' ), 297 SelectionModel = require( '../models/selection.js' ), 298 State = require( './state.js' ), 299 l10n = wp.media.view.l10n, 300 Library; 301 302 Library = State.extend({ 303 defaults: { 304 id: 'library', 305 title: l10n.mediaLibraryTitle, 306 multiple: false, 307 content: 'upload', 308 menu: 'default', 309 router: 'browse', 310 toolbar: 'select', 311 searchable: true, 312 filterable: false, 313 sortable: true, 314 autoSelect: true, 315 describe: false, 316 contentUserSetting: true, 317 syncSelection: true 318 }, 319 320 /** 321 * If a library isn't provided, query all media items. 322 * If a selection instance isn't provided, create one. 323 * 324 * @since 3.5.0 325 */ 326 initialize: function() { 327 var selection = this.get('selection'), 328 props; 329 330 if ( ! this.get('library') ) { 331 this.set( 'library', wp.media.query() ); 332 } 333 334 if ( ! (selection instanceof SelectionModel) ) { 335 props = selection; 336 337 if ( ! props ) { 338 props = this.get('library').props.toJSON(); 339 props = _.omit( props, 'orderby', 'query' ); 340 } 341 342 this.set( 'selection', new SelectionModel( null, { 343 multiple: this.get('multiple'), 344 props: props 345 }) ); 346 } 347 348 this.resetDisplays(); 349 }, 350 351 /** 352 * @since 3.5.0 353 */ 354 activate: function() { 355 this.syncSelection(); 356 357 wp.Uploader.queue.on( 'add', this.uploading, this ); 358 359 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 360 361 if ( this.get( 'router' ) && this.get('contentUserSetting') ) { 362 this.frame.on( 'content:activate', this.saveContentMode, this ); 363 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 364 } 365 }, 366 367 /** 368 * @since 3.5.0 369 */ 370 deactivate: function() { 371 this.recordSelection(); 372 373 this.frame.off( 'content:activate', this.saveContentMode, this ); 374 375 // Unbind all event handlers that use this state as the context 376 // from the selection. 377 this.get('selection').off( null, null, this ); 378 379 wp.Uploader.queue.off( null, null, this ); 380 }, 381 382 /** 383 * Reset the library to its initial state. 384 * 385 * @since 3.5.0 386 */ 387 reset: function() { 388 this.get('selection').reset(); 389 this.resetDisplays(); 390 this.refreshContent(); 391 }, 392 393 /** 394 * Reset the attachment display settings defaults to the site options. 395 * 396 * If site options don't define them, fall back to a persistent user setting. 397 * 398 * @since 3.5.0 399 */ 400 resetDisplays: function() { 401 var defaultProps = wp.media.view.settings.defaultProps; 402 this._displays = []; 403 this._defaultDisplaySettings = { 404 align: defaultProps.align || getUserSetting( 'align', 'none' ), 405 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 406 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 407 }; 408 }, 409 410 /** 411 * Create a model to represent display settings (alignment, etc.) for an attachment. 412 * 413 * @since 3.5.0 414 * 415 * @param {wp.media.model.Attachment} attachment 416 * @returns {Backbone.Model} 417 */ 418 display: function( attachment ) { 419 var displays = this._displays; 420 421 if ( ! displays[ attachment.cid ] ) { 422 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 423 } 424 return displays[ attachment.cid ]; 425 }, 426 427 /** 428 * Given an attachment, create attachment display settings properties. 429 * 430 * @since 3.6.0 431 * 432 * @param {wp.media.model.Attachment} attachment 433 * @returns {Object} 434 */ 435 defaultDisplaySettings: function( attachment ) { 436 var settings = this._defaultDisplaySettings; 437 if ( settings.canEmbed = this.canEmbed( attachment ) ) { 438 settings.link = 'embed'; 439 } 440 return settings; 441 }, 442 443 /** 444 * Whether an attachment can be embedded (audio or video). 445 * 446 * @since 3.6.0 447 * 448 * @param {wp.media.model.Attachment} attachment 449 * @returns {Boolean} 450 */ 451 canEmbed: function( attachment ) { 452 // If uploading, we know the filename but not the mime type. 453 if ( ! attachment.get('uploading') ) { 454 var type = attachment.get('type'); 455 if ( type !== 'audio' && type !== 'video' ) { 456 return false; 457 } 458 } 459 460 return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 461 }, 462 463 464 /** 465 * If the state is active, no items are selected, and the current 466 * content mode is not an option in the state's router (provided 467 * the state has a router), reset the content mode to the default. 468 * 469 * @since 3.5.0 470 */ 471 refreshContent: function() { 472 var selection = this.get('selection'), 473 frame = this.frame, 474 router = frame.router.get(), 475 mode = frame.content.mode(); 476 477 if ( this.active && ! selection.length && router && ! router.get( mode ) ) { 478 this.frame.content.render( this.get('content') ); 479 } 480 }, 481 482 /** 483 * Callback handler when an attachment is uploaded. 484 * 485 * Switch to the Media Library if uploaded from the 'Upload Files' tab. 486 * 487 * Adds any uploading attachments to the selection. 488 * 489 * If the state only supports one attachment to be selected and multiple 490 * attachments are uploaded, the last attachment in the upload queue will 491 * be selected. 492 * 493 * @since 3.5.0 494 * 495 * @param {wp.media.model.Attachment} attachment 496 */ 497 uploading: function( attachment ) { 498 var content = this.frame.content; 499 500 if ( 'upload' === content.mode() ) { 501 this.frame.content.mode('browse'); 502 } 503 504 if ( this.get( 'autoSelect' ) ) { 505 this.get('selection').add( attachment ); 506 this.frame.trigger( 'library:selection:add' ); 507 } 508 }, 509 510 /** 511 * Persist the mode of the content region as a user setting. 512 * 513 * @since 3.5.0 514 */ 515 saveContentMode: function() { 516 if ( 'browse' !== this.get('router') ) { 517 return; 518 } 519 520 var mode = this.frame.content.mode(), 521 view = this.frame.router.get(); 522 523 if ( view && view.get( mode ) ) { 524 setUserSetting( 'libraryContent', mode ); 525 } 526 } 527 }); 528 529 // Make selectionSync available on any Media Library state. 530 _.extend( Library.prototype, selectionSync ); 531 532 module.exports = Library; 533 },{"../models/selection.js":13,"../utils/selection-sync.js":14,"./state.js":7}],4:[function(require,module,exports){ 534 /** 535 * wp.media.controller.MediaLibrary 536 * 537 * @class 538 * @augments wp.media.controller.Library 539 * @augments wp.media.controller.State 540 * @augments Backbone.Model 541 */ 542 var Library = require( './library.js' ), 543 MediaLibrary; 544 545 MediaLibrary = Library.extend({ 546 defaults: _.defaults({ 547 // Attachments browser defaults. @see media.view.AttachmentsBrowser 548 filterable: 'uploaded', 549 550 displaySettings: false, 551 priority: 80, 552 syncSelection: false 553 }, Library.prototype.defaults ), 554 555 /** 556 * @since 3.9.0 557 * 558 * @param options 559 */ 560 initialize: function( options ) { 561 this.media = options.media; 562 this.type = options.type; 563 this.set( 'library', wp.media.query({ type: this.type }) ); 564 565 Library.prototype.initialize.apply( this, arguments ); 566 }, 567 568 /** 569 * @since 3.9.0 570 */ 571 activate: function() { 572 // @todo this should use this.frame. 573 if ( wp.media.frame.lastMime ) { 574 this.set( 'library', wp.media.query({ type: wp.media.frame.lastMime }) ); 575 delete wp.media.frame.lastMime; 576 } 577 Library.prototype.activate.apply( this, arguments ); 578 } 579 }); 580 581 module.exports = MediaLibrary; 582 },{"./library.js":3}],5:[function(require,module,exports){ 583 /** 584 * wp.media.controller.Region 585 * 586 * A region is a persistent application layout area. 587 * 588 * A region assumes one mode at any time, and can be switched to another. 589 * 590 * When mode changes, events are triggered on the region's parent view. 591 * The parent view will listen to specific events and fill the region with an 592 * appropriate view depending on mode. For example, a frame listens for the 593 * 'browse' mode t be activated on the 'content' view and then fills the region 594 * with an AttachmentsBrowser view. 595 * 596 * @class 597 * 598 * @param {object} options Options hash for the region. 599 * @param {string} options.id Unique identifier for the region. 600 * @param {Backbone.View} options.view A parent view the region exists within. 601 * @param {string} options.selector jQuery selector for the region within the parent view. 602 */ 603 var Region = function( options ) { 604 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); 605 }; 606 607 // Use Backbone's self-propagating `extend` inheritance method. 608 Region.extend = Backbone.Model.extend; 609 610 _.extend( Region.prototype, { 611 /** 612 * Activate a mode. 613 * 614 * @since 3.5.0 615 * 616 * @param {string} mode 617 * 618 * @fires this.view#{this.id}:activate:{this._mode} 619 * @fires this.view#{this.id}:activate 620 * @fires this.view#{this.id}:deactivate:{this._mode} 621 * @fires this.view#{this.id}:deactivate 622 * 623 * @returns {wp.media.controller.Region} Returns itself to allow chaining. 624 */ 625 mode: function( mode ) { 626 if ( ! mode ) { 627 return this._mode; 628 } 629 // Bail if we're trying to change to the current mode. 630 if ( mode === this._mode ) { 631 return this; 632 } 633 634 /** 635 * Region mode deactivation event. 636 * 637 * @event this.view#{this.id}:deactivate:{this._mode} 638 * @event this.view#{this.id}:deactivate 639 */ 640 this.trigger('deactivate'); 641 642 this._mode = mode; 643 this.render( mode ); 644 645 /** 646 * Region mode activation event. 647 * 648 * @event this.view#{this.id}:activate:{this._mode} 649 * @event this.view#{this.id}:activate 650 */ 651 this.trigger('activate'); 652 return this; 653 }, 654 /** 655 * Render a mode. 656 * 657 * @since 3.5.0 658 * 659 * @param {string} mode 660 * 661 * @fires this.view#{this.id}:create:{this._mode} 662 * @fires this.view#{this.id}:create 663 * @fires this.view#{this.id}:render:{this._mode} 664 * @fires this.view#{this.id}:render 665 * 666 * @returns {wp.media.controller.Region} Returns itself to allow chaining 667 */ 668 render: function( mode ) { 669 // If the mode isn't active, activate it. 670 if ( mode && mode !== this._mode ) { 671 return this.mode( mode ); 672 } 673 674 var set = { view: null }, 675 view; 676 677 /** 678 * Create region view event. 679 * 680 * Region view creation takes place in an event callback on the frame. 681 * 682 * @event this.view#{this.id}:create:{this._mode} 683 * @event this.view#{this.id}:create 684 */ 685 this.trigger( 'create', set ); 686 view = set.view; 687 688 /** 689 * Render region view event. 690 * 691 * Region view creation takes place in an event callback on the frame. 692 * 693 * @event this.view#{this.id}:create:{this._mode} 694 * @event this.view#{this.id}:create 695 */ 696 this.trigger( 'render', view ); 697 if ( view ) { 698 this.set( view ); 699 } 700 return this; 701 }, 702 703 /** 704 * Get the region's view. 705 * 706 * @since 3.5.0 707 * 708 * @returns {wp.media.View} 709 */ 710 get: function() { 711 return this.view.views.first( this.selector ); 712 }, 713 714 /** 715 * Set the region's view as a subview of the frame. 716 * 717 * @since 3.5.0 718 * 719 * @param {Array|Object} views 720 * @param {Object} [options={}] 721 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining 722 */ 723 set: function( views, options ) { 724 if ( options ) { 725 options.add = false; 726 } 727 return this.view.views.set( this.selector, views, options ); 728 }, 729 730 /** 731 * Trigger regional view events on the frame. 732 * 733 * @since 3.5.0 734 * 735 * @param {string} event 736 * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining. 737 */ 738 trigger: function( event ) { 739 var base, args; 740 741 if ( ! this._mode ) { 742 return; 743 } 744 745 args = _.toArray( arguments ); 746 base = this.id + ':' + event; 747 748 // Trigger `{this.id}:{event}:{this._mode}` event on the frame. 749 args[0] = base + ':' + this._mode; 750 this.view.trigger.apply( this.view, args ); 751 752 // Trigger `{this.id}:{event}` event on the frame. 753 args[0] = base; 754 this.view.trigger.apply( this.view, args ); 755 return this; 756 } 757 }); 758 759 module.exports = Region; 760 },{}],6:[function(require,module,exports){ 761 /** 762 * wp.media.controller.StateMachine 763 * 764 * A state machine keeps track of state. It is in one state at a time, 765 * and can change from one state to another. 766 * 767 * States are stored as models in a Backbone collection. 768 * 769 * @since 3.5.0 770 * 771 * @class 772 * @augments Backbone.Model 773 * @mixin 774 * @mixes Backbone.Events 775 * 776 * @param {Array} states 777 */ 778 var StateMachine = function( states ) { 779 // @todo This is dead code. The states collection gets created in media.view.Frame._createStates. 780 this.states = new Backbone.Collection( states ); 781 }; 782 783 // Use Backbone's self-propagating `extend` inheritance method. 784 StateMachine.extend = Backbone.Model.extend; 785 786 _.extend( StateMachine.prototype, Backbone.Events, { 787 /** 788 * Fetch a state. 789 * 790 * If no `id` is provided, returns the active state. 791 * 792 * Implicitly creates states. 793 * 794 * Ensure that the `states` collection exists so the `StateMachine` 795 * can be used as a mixin. 796 * 797 * @since 3.5.0 798 * 799 * @param {string} id 800 * @returns {wp.media.controller.State} Returns a State model 801 * from the StateMachine collection 802 */ 803 state: function( id ) { 804 this.states = this.states || new Backbone.Collection(); 805 806 // Default to the active state. 807 id = id || this._state; 808 809 if ( id && ! this.states.get( id ) ) { 810 this.states.add({ id: id }); 811 } 812 return this.states.get( id ); 813 }, 814 815 /** 816 * Sets the active state. 817 * 818 * Bail if we're trying to select the current state, if we haven't 819 * created the `states` collection, or are trying to select a state 820 * that does not exist. 821 * 822 * @since 3.5.0 823 * 824 * @param {string} id 825 * 826 * @fires wp.media.controller.State#deactivate 827 * @fires wp.media.controller.State#activate 828 * 829 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining 830 */ 831 setState: function( id ) { 832 var previous = this.state(); 833 834 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) { 835 return this; 836 } 837 838 if ( previous ) { 839 previous.trigger('deactivate'); 840 this._lastState = previous.id; 841 } 842 843 this._state = id; 844 this.state().trigger('activate'); 845 846 return this; 847 }, 848 849 /** 850 * Returns the previous active state. 851 * 852 * Call the `state()` method with no parameters to retrieve the current 853 * active state. 854 * 855 * @since 3.5.0 856 * 857 * @returns {wp.media.controller.State} Returns a State model 858 * from the StateMachine collection 859 */ 860 lastState: function() { 861 if ( this._lastState ) { 862 return this.state( this._lastState ); 863 } 864 } 865 }); 866 867 // Map all event binding and triggering on a StateMachine to its `states` collection. 868 _.each([ 'on', 'off', 'trigger' ], function( method ) { 869 /** 870 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. 871 */ 872 StateMachine.prototype[ method ] = function() { 873 // Ensure that the `states` collection exists so the `StateMachine` 874 // can be used as a mixin. 875 this.states = this.states || new Backbone.Collection(); 876 // Forward the method to the `states` collection. 877 this.states[ method ].apply( this.states, arguments ); 878 return this; 879 }; 880 }); 881 882 module.exports = StateMachine; 883 },{}],7:[function(require,module,exports){ 884 /** 885 * wp.media.controller.State 886 * 887 * A state is a step in a workflow that when set will trigger the controllers 888 * for the regions to be updated as specified in the frame. 889 * 890 * A state has an event-driven lifecycle: 891 * 892 * 'ready' triggers when a state is added to a state machine's collection. 893 * 'activate' triggers when a state is activated by a state machine. 894 * 'deactivate' triggers when a state is deactivated by a state machine. 895 * 'reset' is not triggered automatically. It should be invoked by the 896 * proper controller to reset the state to its default. 897 * 898 * @class 899 * @augments Backbone.Model 900 */ 901 var State = Backbone.Model.extend({ 902 /** 903 * Constructor. 904 * 905 * @since 3.5.0 906 */ 907 constructor: function() { 908 this.on( 'activate', this._preActivate, this ); 909 this.on( 'activate', this.activate, this ); 910 this.on( 'activate', this._postActivate, this ); 911 this.on( 'deactivate', this._deactivate, this ); 912 this.on( 'deactivate', this.deactivate, this ); 913 this.on( 'reset', this.reset, this ); 914 this.on( 'ready', this._ready, this ); 915 this.on( 'ready', this.ready, this ); 916 /** 917 * Call parent constructor with passed arguments 918 */ 919 Backbone.Model.apply( this, arguments ); 920 this.on( 'change:menu', this._updateMenu, this ); 921 }, 922 /** 923 * Ready event callback. 924 * 925 * @abstract 926 * @since 3.5.0 927 */ 928 ready: function() {}, 929 930 /** 931 * Activate event callback. 932 * 933 * @abstract 934 * @since 3.5.0 935 */ 936 activate: function() {}, 937 938 /** 939 * Deactivate event callback. 940 * 941 * @abstract 942 * @since 3.5.0 943 */ 944 deactivate: function() {}, 945 946 /** 947 * Reset event callback. 948 * 949 * @abstract 950 * @since 3.5.0 951 */ 952 reset: function() {}, 953 954 /** 955 * @access private 956 * @since 3.5.0 957 */ 958 _ready: function() { 959 this._updateMenu(); 960 }, 961 962 /** 963 * @access private 964 * @since 3.5.0 965 */ 966 _preActivate: function() { 967 this.active = true; 968 }, 969 970 /** 971 * @access private 972 * @since 3.5.0 973 */ 974 _postActivate: function() { 975 this.on( 'change:menu', this._menu, this ); 976 this.on( 'change:titleMode', this._title, this ); 977 this.on( 'change:content', this._content, this ); 978 this.on( 'change:toolbar', this._toolbar, this ); 979 980 this.frame.on( 'title:render:default', this._renderTitle, this ); 981 982 this._title(); 983 this._menu(); 984 this._toolbar(); 985 this._content(); 986 this._router(); 987 }, 988 989 /** 990 * @access private 991 * @since 3.5.0 992 */ 993 _deactivate: function() { 994 this.active = false; 995 996 this.frame.off( 'title:render:default', this._renderTitle, this ); 997 998 this.off( 'change:menu', this._menu, this ); 999 this.off( 'change:titleMode', this._title, this ); 1000 this.off( 'change:content', this._content, this ); 1001 this.off( 'change:toolbar', this._toolbar, this ); 1002 }, 1003 1004 /** 1005 * @access private 1006 * @since 3.5.0 1007 */ 1008 _title: function() { 1009 this.frame.title.render( this.get('titleMode') || 'default' ); 1010 }, 1011 1012 /** 1013 * @access private 1014 * @since 3.5.0 1015 */ 1016 _renderTitle: function( view ) { 1017 view.$el.text( this.get('title') || '' ); 1018 }, 1019 1020 /** 1021 * @access private 1022 * @since 3.5.0 1023 */ 1024 _router: function() { 1025 var router = this.frame.router, 1026 mode = this.get('router'), 1027 view; 1028 1029 this.frame.$el.toggleClass( 'hide-router', ! mode ); 1030 if ( ! mode ) { 1031 return; 1032 } 1033 1034 this.frame.router.render( mode ); 1035 1036 view = router.get(); 1037 if ( view && view.select ) { 1038 view.select( this.frame.content.mode() ); 1039 } 1040 }, 1041 1042 /** 1043 * @access private 1044 * @since 3.5.0 1045 */ 1046 _menu: function() { 1047 var menu = this.frame.menu, 1048 mode = this.get('menu'), 1049 view; 1050 1051 this.frame.$el.toggleClass( 'hide-menu', ! mode ); 1052 if ( ! mode ) { 1053 return; 1054 } 1055 1056 menu.mode( mode ); 1057 1058 view = menu.get(); 1059 if ( view && view.select ) { 1060 view.select( this.id ); 1061 } 1062 }, 1063 1064 /** 1065 * @access private 1066 * @since 3.5.0 1067 */ 1068 _updateMenu: function() { 1069 var previous = this.previous('menu'), 1070 menu = this.get('menu'); 1071 1072 if ( previous ) { 1073 this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); 1074 } 1075 1076 if ( menu ) { 1077 this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); 1078 } 1079 }, 1080 1081 /** 1082 * Create a view in the media menu for the state. 1083 * 1084 * @access private 1085 * @since 3.5.0 1086 * 1087 * @param {media.view.Menu} view The menu view. 1088 */ 1089 _renderMenu: function( view ) { 1090 var menuItem = this.get('menuItem'), 1091 title = this.get('title'), 1092 priority = this.get('priority'); 1093 1094 if ( ! menuItem && title ) { 1095 menuItem = { text: title }; 1096 1097 if ( priority ) { 1098 menuItem.priority = priority; 1099 } 1100 } 1101 1102 if ( ! menuItem ) { 1103 return; 1104 } 1105 1106 view.set( this.id, menuItem ); 1107 } 1108 }); 1109 1110 _.each(['toolbar','content'], function( region ) { 1111 /** 1112 * @access private 1113 */ 1114 State.prototype[ '_' + region ] = function() { 1115 var mode = this.get( region ); 1116 if ( mode ) { 1117 this.frame[ region ].render( mode ); 1118 } 1119 }; 1120 }); 1121 1122 module.exports = State; 1123 },{}],8:[function(require,module,exports){ 1124 /** 1125 * The controller for the Video Details state 1126 * 1127 * @constructor 1128 * @augments wp.media.controller.State 1129 * @augments Backbone.Model 1130 */ 1131 var State = require( './state.js' ), 1132 l10n = wp.media.view.l10n, 1133 VideoDetails; 1134 1135 VideoDetails = State.extend({ 1136 defaults: { 1137 id: 'video-details', 1138 toolbar: 'video-details', 1139 title: l10n.videoDetailsTitle, 1140 content: 'video-details', 1141 menu: 'video-details', 1142 router: false, 1143 priority: 60 1144 }, 1145 1146 initialize: function( options ) { 1147 this.media = options.media; 1148 State.prototype.initialize.apply( this, arguments ); 1149 } 1150 }); 1151 1152 module.exports = VideoDetails; 1153 },{"./state.js":7}],9:[function(require,module,exports){ 1154 /** 1155 * wp.media.model.Attachment 1156 * 1157 * @class 1158 * @augments Backbone.Model 1159 */ 1160 var $ = jQuery, 1161 Attachment; 1162 1163 Attachment = Backbone.Model.extend({ 1164 /** 1165 * Triggered when attachment details change 1166 * Overrides Backbone.Model.sync 1167 * 1168 * @param {string} method 1169 * @param {wp.media.model.Attachment} model 1170 * @param {Object} [options={}] 1171 * 1172 * @returns {Promise} 1173 */ 1174 sync: function( method, model, options ) { 1175 // If the attachment does not yet have an `id`, return an instantly 1176 // rejected promise. Otherwise, all of our requests will fail. 1177 if ( _.isUndefined( this.id ) ) { 1178 return $.Deferred().rejectWith( this ).promise(); 1179 } 1180 1181 // Overload the `read` request so Attachment.fetch() functions correctly. 1182 if ( 'read' === method ) { 1183 options = options || {}; 1184 options.context = this; 1185 options.data = _.extend( options.data || {}, { 1186 action: 'get-attachment', 1187 id: this.id 1188 }); 1189 return wp.media.ajax( options ); 1190 1191 // Overload the `update` request so properties can be saved. 1192 } else if ( 'update' === method ) { 1193 // If we do not have the necessary nonce, fail immeditately. 1194 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 1195 return $.Deferred().rejectWith( this ).promise(); 1196 } 1197 1198 options = options || {}; 1199 options.context = this; 1200 1201 // Set the action and ID. 1202 options.data = _.extend( options.data || {}, { 1203 action: 'save-attachment', 1204 id: this.id, 1205 nonce: this.get('nonces').update, 1206 post_id: wp.media.model.settings.post.id 1207 }); 1208 1209 // Record the values of the changed attributes. 1210 if ( model.hasChanged() ) { 1211 options.data.changes = {}; 1212 1213 _.each( model.changed, function( value, key ) { 1214 options.data.changes[ key ] = this.get( key ); 1215 }, this ); 1216 } 1217 1218 return wp.media.ajax( options ); 1219 1220 // Overload the `delete` request so attachments can be removed. 1221 // This will permanently delete an attachment. 1222 } else if ( 'delete' === method ) { 1223 options = options || {}; 1224 1225 if ( ! options.wait ) { 1226 this.destroyed = true; 1227 } 1228 1229 options.context = this; 1230 options.data = _.extend( options.data || {}, { 1231 action: 'delete-post', 1232 id: this.id, 1233 _wpnonce: this.get('nonces')['delete'] 1234 }); 1235 1236 return wp.media.ajax( options ).done( function() { 1237 this.destroyed = true; 1238 }).fail( function() { 1239 this.destroyed = false; 1240 }); 1241 1242 // Otherwise, fall back to `Backbone.sync()`. 1243 } else { 1244 /** 1245 * Call `sync` directly on Backbone.Model 1246 */ 1247 return Backbone.Model.prototype.sync.apply( this, arguments ); 1248 } 1249 }, 1250 /** 1251 * Convert date strings into Date objects. 1252 * 1253 * @param {Object} resp The raw response object, typically returned by fetch() 1254 * @returns {Object} The modified response object, which is the attributes hash 1255 * to be set on the model. 1256 */ 1257 parse: function( resp ) { 1258 if ( ! resp ) { 1259 return resp; 1260 } 1261 1262 resp.date = new Date( resp.date ); 1263 resp.modified = new Date( resp.modified ); 1264 return resp; 1265 }, 1266 /** 1267 * @param {Object} data The properties to be saved. 1268 * @param {Object} options Sync options. e.g. patch, wait, success, error. 1269 * 1270 * @this Backbone.Model 1271 * 1272 * @returns {Promise} 1273 */ 1274 saveCompat: function( data, options ) { 1275 var model = this; 1276 1277 // If we do not have the necessary nonce, fail immeditately. 1278 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 1279 return $.Deferred().rejectWith( this ).promise(); 1280 } 1281 1282 return media.post( 'save-attachment-compat', _.defaults({ 1283 id: this.id, 1284 nonce: this.get('nonces').update, 1285 post_id: wp.media.model.settings.post.id 1286 }, data ) ).done( function( resp, status, xhr ) { 1287 model.set( model.parse( resp, xhr ), options ); 1288 }); 1289 } 1290 }, { 1291 /** 1292 * Create a new model on the static 'all' attachments collection and return it. 1293 * 1294 * @static 1295 * @param {Object} attrs 1296 * @returns {wp.media.model.Attachment} 1297 */ 1298 create: function( attrs ) { 1299 var Attachments = require( './attachments.js' ); 1300 return Attachments.all.push( attrs ); 1301 }, 1302 /** 1303 * Create a new model on the static 'all' attachments collection and return it. 1304 * 1305 * If this function has already been called for the id, 1306 * it returns the specified attachment. 1307 * 1308 * @static 1309 * @param {string} id A string used to identify a model. 1310 * @param {Backbone.Model|undefined} attachment 1311 * @returns {wp.media.model.Attachment} 1312 */ 1313 get: _.memoize( function( id, attachment ) { 1314 var Attachments = require( './attachments.js' ); 1315 return Attachments.all.push( attachment || { id: id } ); 1316 }) 1317 }); 1318 1319 module.exports = Attachment; 1320 },{"./attachments.js":10}],10:[function(require,module,exports){ 1321 /** 1322 * wp.media.model.Attachments 1323 * 1324 * A collection of attachments. 1325 * 1326 * This collection has no persistence with the server without supplying 1327 * 'options.props.query = true', which will mirror the collection 1328 * to an Attachments Query collection - @see wp.media.model.Attachments.mirror(). 1329 * 1330 * @class 1331 * @augments Backbone.Collection 1332 * 1333 * @param {array} [models] Models to initialize with the collection. 1334 * @param {object} [options] Options hash for the collection. 1335 * @param {string} [options.props] Options hash for the initial query properties. 1336 * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection. 1337 * @param {string} [options.props.orderby] Initial attribute key to order the collection by. 1338 * @param {string} [options.props.query] Whether the collection is linked to an attachments query. 1339 * @param {string} [options.observe] 1340 * @param {string} [options.filters] 1341 * 1342 */ 1343 var Attachment = require( './attachment.js' ), 1344 Attachments; 1345 1346 Attachments = Backbone.Collection.extend({ 1347 /** 1348 * @type {wp.media.model.Attachment} 1349 */ 1350 model: Attachment, 1351 /** 1352 * @param {Array} [models=[]] Array of models used to populate the collection. 1353 * @param {Object} [options={}] 1354 */ 1355 initialize: function( models, options ) { 1356 options = options || {}; 1357 1358 this.props = new Backbone.Model(); 1359 this.filters = options.filters || {}; 1360 1361 // Bind default `change` events to the `props` model. 1362 this.props.on( 'change', this._changeFilteredProps, this ); 1363 1364 this.props.on( 'change:order', this._changeOrder, this ); 1365 this.props.on( 'change:orderby', this._changeOrderby, this ); 1366 this.props.on( 'change:query', this._changeQuery, this ); 1367 1368 this.props.set( _.defaults( options.props || {} ) ); 1369 1370 if ( options.observe ) { 1371 this.observe( options.observe ); 1372 } 1373 }, 1374 /** 1375 * Sort the collection when the order attribute changes. 1376 * 1377 * @access private 1378 */ 1379 _changeOrder: function() { 1380 if ( this.comparator ) { 1381 this.sort(); 1382 } 1383 }, 1384 /** 1385 * Set the default comparator only when the `orderby` property is set. 1386 * 1387 * @access private 1388 * 1389 * @param {Backbone.Model} model 1390 * @param {string} orderby 1391 */ 1392 _changeOrderby: function( model, orderby ) { 1393 // If a different comparator is defined, bail. 1394 if ( this.comparator && this.comparator !== Attachments.comparator ) { 1395 return; 1396 } 1397 1398 if ( orderby && 'post__in' !== orderby ) { 1399 this.comparator = Attachments.comparator; 1400 } else { 1401 delete this.comparator; 1402 } 1403 }, 1404 /** 1405 * If the `query` property is set to true, query the server using 1406 * the `props` values, and sync the results to this collection. 1407 * 1408 * @access private 1409 * 1410 * @param {Backbone.Model} model 1411 * @param {Boolean} query 1412 */ 1413 _changeQuery: function( model, query ) { 1414 if ( query ) { 1415 this.props.on( 'change', this._requery, this ); 1416 this._requery(); 1417 } else { 1418 this.props.off( 'change', this._requery, this ); 1419 } 1420 }, 1421 /** 1422 * @access private 1423 * 1424 * @param {Backbone.Model} model 1425 */ 1426 _changeFilteredProps: function( model ) { 1427 // If this is a query, updating the collection will be handled by 1428 // `this._requery()`. 1429 if ( this.props.get('query') ) { 1430 return; 1431 } 1432 1433 var changed = _.chain( model.changed ).map( function( t, prop ) { 1434 var filter = Attachments.filters[ prop ], 1435 term = model.get( prop ); 1436 1437 if ( ! filter ) { 1438 return; 1439 } 1440 1441 if ( term && ! this.filters[ prop ] ) { 1442 this.filters[ prop ] = filter; 1443 } else if ( ! term && this.filters[ prop ] === filter ) { 1444 delete this.filters[ prop ]; 1445 } else { 1446 return; 1447 } 1448 1449 // Record the change. 1450 return true; 1451 }, this ).any().value(); 1452 1453 if ( ! changed ) { 1454 return; 1455 } 1456 1457 // If no `Attachments` model is provided to source the searches 1458 // from, then automatically generate a source from the existing 1459 // models. 1460 if ( ! this._source ) { 1461 this._source = new Attachments( this.models ); 1462 } 1463 1464 this.reset( this._source.filter( this.validator, this ) ); 1465 }, 1466 1467 validateDestroyed: false, 1468 /** 1469 * Checks whether an attachment is valid. 1470 * 1471 * @param {wp.media.model.Attachment} attachment 1472 * @returns {Boolean} 1473 */ 1474 validator: function( attachment ) { 1475 if ( ! this.validateDestroyed && attachment.destroyed ) { 1476 return false; 1477 } 1478 return _.all( this.filters, function( filter ) { 1479 return !! filter.call( this, attachment ); 1480 }, this ); 1481 }, 1482 /** 1483 * Add or remove an attachment to the collection depending on its validity. 1484 * 1485 * @param {wp.media.model.Attachment} attachment 1486 * @param {Object} options 1487 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1488 */ 1489 validate: function( attachment, options ) { 1490 var valid = this.validator( attachment ), 1491 hasAttachment = !! this.get( attachment.cid ); 1492 1493 if ( ! valid && hasAttachment ) { 1494 this.remove( attachment, options ); 1495 } else if ( valid && ! hasAttachment ) { 1496 this.add( attachment, options ); 1497 } 1498 1499 return this; 1500 }, 1501 1502 /** 1503 * Add or remove all attachments from another collection depending on each one's validity. 1504 * 1505 * @param {wp.media.model.Attachments} attachments 1506 * @param {object} [options={}] 1507 * 1508 * @fires wp.media.model.Attachments#reset 1509 * 1510 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1511 */ 1512 validateAll: function( attachments, options ) { 1513 options = options || {}; 1514 1515 _.each( attachments.models, function( attachment ) { 1516 this.validate( attachment, { silent: true }); 1517 }, this ); 1518 1519 if ( ! options.silent ) { 1520 this.trigger( 'reset', this, options ); 1521 } 1522 return this; 1523 }, 1524 /** 1525 * Start observing another attachments collection change events 1526 * and replicate them on this collection. 1527 * 1528 * @param {wp.media.model.Attachments} The attachments collection to observe. 1529 * @returns {wp.media.model.Attachments} Returns itself to allow chaining. 1530 */ 1531 observe: function( attachments ) { 1532 this.observers = this.observers || []; 1533 this.observers.push( attachments ); 1534 1535 attachments.on( 'add change remove', this._validateHandler, this ); 1536 attachments.on( 'reset', this._validateAllHandler, this ); 1537 this.validateAll( attachments ); 1538 return this; 1539 }, 1540 /** 1541 * Stop replicating collection change events from another attachments collection. 1542 * 1543 * @param {wp.media.model.Attachments} The attachments collection to stop observing. 1544 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1545 */ 1546 unobserve: function( attachments ) { 1547 if ( attachments ) { 1548 attachments.off( null, null, this ); 1549 this.observers = _.without( this.observers, attachments ); 1550 1551 } else { 1552 _.each( this.observers, function( attachments ) { 1553 attachments.off( null, null, this ); 1554 }, this ); 1555 delete this.observers; 1556 } 1557 1558 return this; 1559 }, 1560 /** 1561 * @access private 1562 * 1563 * @param {wp.media.model.Attachments} attachment 1564 * @param {wp.media.model.Attachments} attachments 1565 * @param {Object} options 1566 * 1567 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1568 */ 1569 _validateHandler: function( attachment, attachments, options ) { 1570 // If we're not mirroring this `attachments` collection, 1571 // only retain the `silent` option. 1572 options = attachments === this.mirroring ? options : { 1573 silent: options && options.silent 1574 }; 1575 1576 return this.validate( attachment, options ); 1577 }, 1578 /** 1579 * @access private 1580 * 1581 * @param {wp.media.model.Attachments} attachments 1582 * @param {Object} options 1583 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1584 */ 1585 _validateAllHandler: function( attachments, options ) { 1586 return this.validateAll( attachments, options ); 1587 }, 1588 /** 1589 * Start mirroring another attachments collection, clearing out any models already 1590 * in the collection. 1591 * 1592 * @param {wp.media.model.Attachments} The attachments collection to mirror. 1593 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1594 */ 1595 mirror: function( attachments ) { 1596 if ( this.mirroring && this.mirroring === attachments ) { 1597 return this; 1598 } 1599 1600 this.unmirror(); 1601 this.mirroring = attachments; 1602 1603 // Clear the collection silently. A `reset` event will be fired 1604 // when `observe()` calls `validateAll()`. 1605 this.reset( [], { silent: true } ); 1606 this.observe( attachments ); 1607 1608 return this; 1609 }, 1610 /** 1611 * Stop mirroring another attachments collection. 1612 */ 1613 unmirror: function() { 1614 if ( ! this.mirroring ) { 1615 return; 1616 } 1617 1618 this.unobserve( this.mirroring ); 1619 delete this.mirroring; 1620 }, 1621 /** 1622 * Retrive more attachments from the server for the collection. 1623 * 1624 * Only works if the collection is mirroring a Query Attachments collection, 1625 * and forwards to its `more` method. This collection class doesn't have 1626 * server persistence by itself. 1627 * 1628 * @param {object} options 1629 * @returns {Promise} 1630 */ 1631 more: function( options ) { 1632 var deferred = jQuery.Deferred(), 1633 mirroring = this.mirroring, 1634 attachments = this; 1635 1636 if ( ! mirroring || ! mirroring.more ) { 1637 return deferred.resolveWith( this ).promise(); 1638 } 1639 // If we're mirroring another collection, forward `more` to 1640 // the mirrored collection. Account for a race condition by 1641 // checking if we're still mirroring that collection when 1642 // the request resolves. 1643 mirroring.more( options ).done( function() { 1644 if ( this === attachments.mirroring ) 1645 deferred.resolveWith( this ); 1646 }); 1647 1648 return deferred.promise(); 1649 }, 1650 /** 1651 * Whether there are more attachments that haven't been sync'd from the server 1652 * that match the collection's query. 1653 * 1654 * Only works if the collection is mirroring a Query Attachments collection, 1655 * and forwards to its `hasMore` method. This collection class doesn't have 1656 * server persistence by itself. 1657 * 1658 * @returns {boolean} 1659 */ 1660 hasMore: function() { 1661 return this.mirroring ? this.mirroring.hasMore() : false; 1662 }, 1663 /** 1664 * A custom AJAX-response parser. 1665 * 1666 * See trac ticket #24753 1667 * 1668 * @param {Object|Array} resp The raw response Object/Array. 1669 * @param {Object} xhr 1670 * @returns {Array} The array of model attributes to be added to the collection 1671 */ 1672 parse: function( resp, xhr ) { 1673 if ( ! _.isArray( resp ) ) { 1674 resp = [resp]; 1675 } 1676 1677 return _.map( resp, function( attrs ) { 1678 var id, attachment, newAttributes; 1679 1680 if ( attrs instanceof Backbone.Model ) { 1681 id = attrs.get( 'id' ); 1682 attrs = attrs.attributes; 1683 } else { 1684 id = attrs.id; 1685 } 1686 1687 attachment = Attachment.get( id ); 1688 newAttributes = attachment.parse( attrs, xhr ); 1689 1690 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { 1691 attachment.set( newAttributes ); 1692 } 1693 1694 return attachment; 1695 }); 1696 }, 1697 /** 1698 * If the collection is a query, create and mirror an Attachments Query collection. 1699 * 1700 * @access private 1701 */ 1702 _requery: function( refresh ) { 1703 var props, Query; 1704 if ( this.props.get('query') ) { 1705 Query = require( './query.js' ); 1706 props = this.props.toJSON(); 1707 props.cache = ( true !== refresh ); 1708 this.mirror( Query.get( props ) ); 1709 } 1710 }, 1711 /** 1712 * If this collection is sorted by `menuOrder`, recalculates and saves 1713 * the menu order to the database. 1714 * 1715 * @returns {undefined|Promise} 1716 */ 1717 saveMenuOrder: function() { 1718 if ( 'menuOrder' !== this.props.get('orderby') ) { 1719 return; 1720 } 1721 1722 // Removes any uploading attachments, updates each attachment's 1723 // menu order, and returns an object with an { id: menuOrder } 1724 // mapping to pass to the request. 1725 var attachments = this.chain().filter( function( attachment ) { 1726 return ! _.isUndefined( attachment.id ); 1727 }).map( function( attachment, index ) { 1728 // Indices start at 1. 1729 index = index + 1; 1730 attachment.set( 'menuOrder', index ); 1731 return [ attachment.id, index ]; 1732 }).object().value(); 1733 1734 if ( _.isEmpty( attachments ) ) { 1735 return; 1736 } 1737 1738 return wp.media.post( 'save-attachment-order', { 1739 nonce: wp.media.model.settings.post.nonce, 1740 post_id: wp.media.model.settings.post.id, 1741 attachments: attachments 1742 }); 1743 } 1744 }, { 1745 /** 1746 * A function to compare two attachment models in an attachments collection. 1747 * 1748 * Used as the default comparator for instances of wp.media.model.Attachments 1749 * and its subclasses. @see wp.media.model.Attachments._changeOrderby(). 1750 * 1751 * @static 1752 * 1753 * @param {Backbone.Model} a 1754 * @param {Backbone.Model} b 1755 * @param {Object} options 1756 * @returns {Number} -1 if the first model should come before the second, 1757 * 0 if they are of the same rank and 1758 * 1 if the first model should come after. 1759 */ 1760 comparator: function( a, b, options ) { 1761 var key = this.props.get('orderby'), 1762 order = this.props.get('order') || 'DESC', 1763 ac = a.cid, 1764 bc = b.cid; 1765 1766 a = a.get( key ); 1767 b = b.get( key ); 1768 1769 if ( 'date' === key || 'modified' === key ) { 1770 a = a || new Date(); 1771 b = b || new Date(); 1772 } 1773 1774 // If `options.ties` is set, don't enforce the `cid` tiebreaker. 1775 if ( options && options.ties ) { 1776 ac = bc = null; 1777 } 1778 1779 return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac ); 1780 }, 1781 /** 1782 * @namespace 1783 */ 1784 filters: { 1785 /** 1786 * @static 1787 * Note that this client-side searching is *not* equivalent 1788 * to our server-side searching. 1789 * 1790 * @param {wp.media.model.Attachment} attachment 1791 * 1792 * @this wp.media.model.Attachments 1793 * 1794 * @returns {Boolean} 1795 */ 1796 search: function( attachment ) { 1797 if ( ! this.props.get('search') ) { 1798 return true; 1799 } 1800 1801 return _.any(['title','filename','description','caption','name'], function( key ) { 1802 var value = attachment.get( key ); 1803 return value && -1 !== value.search( this.props.get('search') ); 1804 }, this ); 1805 }, 1806 /** 1807 * @static 1808 * @param {wp.media.model.Attachment} attachment 1809 * 1810 * @this wp.media.model.Attachments 1811 * 1812 * @returns {Boolean} 1813 */ 1814 type: function( attachment ) { 1815 var type = this.props.get('type'); 1816 return ! type || -1 !== type.indexOf( attachment.get('type') ); 1817 }, 1818 /** 1819 * @static 1820 * @param {wp.media.model.Attachment} attachment 1821 * 1822 * @this wp.media.model.Attachments 1823 * 1824 * @returns {Boolean} 1825 */ 1826 uploadedTo: function( attachment ) { 1827 var uploadedTo = this.props.get('uploadedTo'); 1828 if ( _.isUndefined( uploadedTo ) ) { 1829 return true; 1830 } 1831 1832 return uploadedTo === attachment.get('uploadedTo'); 1833 }, 1834 /** 1835 * @static 1836 * @param {wp.media.model.Attachment} attachment 1837 * 1838 * @this wp.media.model.Attachments 1839 * 1840 * @returns {Boolean} 1841 */ 1842 status: function( attachment ) { 1843 var status = this.props.get('status'); 1844 if ( _.isUndefined( status ) ) { 1845 return true; 1846 } 1847 1848 return status === attachment.get('status'); 1849 } 1850 } 1851 }); 1852 1853 module.exports = Attachments; 1854 },{"./attachment.js":9,"./query.js":12}],11:[function(require,module,exports){ 1855 /** 1856 * Shared model class for audio and video. Updates the model after 1857 * "Add Audio|Video Source" and "Replace Audio|Video" states return 1858 * 1859 * @constructor 1860 * @augments Backbone.Model 1861 */ 1862 var PostMedia = Backbone.Model.extend({ 1863 initialize: function() { 1864 this.attachment = false; 1865 }, 1866 1867 setSource: function( attachment ) { 1868 this.attachment = attachment; 1869 this.extension = attachment.get( 'filename' ).split('.').pop(); 1870 1871 if ( this.get( 'src' ) && this.extension === this.get( 'src' ).split('.').pop() ) { 1872 this.unset( 'src' ); 1873 } 1874 1875 if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) { 1876 this.set( this.extension, this.attachment.get( 'url' ) ); 1877 } else { 1878 this.unset( this.extension ); 1879 } 1880 }, 1881 1882 changeAttachment: function( attachment ) { 1883 var self = this; 1884 1885 this.setSource( attachment ); 1886 1887 this.unset( 'src' ); 1888 _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function( ext ) { 1889 self.unset( ext ); 1890 } ); 1891 } 1892 }); 1893 1894 module.exports = PostMedia; 1895 },{}],12:[function(require,module,exports){ 1896 /** 1897 * wp.media.model.Query 1898 * 1899 * A collection of attachments that match the supplied query arguments. 1900 * 1901 * Note: Do NOT change this.args after the query has been initialized. 1902 * Things will break. 1903 * 1904 * @class 1905 * @augments wp.media.model.Attachments 1906 * @augments Backbone.Collection 1907 * 1908 * @param {array} [models] Models to initialize with the collection. 1909 * @param {object} [options] Options hash. 1910 * @param {object} [options.args] Attachments query arguments. 1911 * @param {object} [options.args.posts_per_page] 1912 */ 1913 var Attachments = require( './attachments.js' ), 1914 Query; 1915 1916 Query = Attachments.extend({ 1917 /** 1918 * @global wp.Uploader 1919 * 1920 * @param {array} [models=[]] Array of initial models to populate the collection. 1921 * @param {object} [options={}] 1922 */ 1923 initialize: function( models, options ) { 1924 var allowed; 1925 1926 options = options || {}; 1927 Attachments.prototype.initialize.apply( this, arguments ); 1928 1929 this.args = options.args; 1930 this._hasMore = true; 1931 this.created = new Date(); 1932 1933 this.filters.order = function( attachment ) { 1934 var orderby = this.props.get('orderby'), 1935 order = this.props.get('order'); 1936 1937 if ( ! this.comparator ) { 1938 return true; 1939 } 1940 1941 // We want any items that can be placed before the last 1942 // item in the set. If we add any items after the last 1943 // item, then we can't guarantee the set is complete. 1944 if ( this.length ) { 1945 return 1 !== this.comparator( attachment, this.last(), { ties: true }); 1946 1947 // Handle the case where there are no items yet and 1948 // we're sorting for recent items. In that case, we want 1949 // changes that occurred after we created the query. 1950 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { 1951 return attachment.get( orderby ) >= this.created; 1952 1953 // If we're sorting by menu order and we have no items, 1954 // accept any items that have the default menu order (0). 1955 } else if ( 'ASC' === order && 'menuOrder' === orderby ) { 1956 return attachment.get( orderby ) === 0; 1957 } 1958 1959 // Otherwise, we don't want any items yet. 1960 return false; 1961 }; 1962 1963 // Observe the central `wp.Uploader.queue` collection to watch for 1964 // new matches for the query. 1965 // 1966 // Only observe when a limited number of query args are set. There 1967 // are no filters for other properties, so observing will result in 1968 // false positives in those queries. 1969 allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; 1970 if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { 1971 this.observe( wp.Uploader.queue ); 1972 } 1973 }, 1974 /** 1975 * Whether there are more attachments that haven't been sync'd from the server 1976 * that match the collection's query. 1977 * 1978 * @returns {boolean} 1979 */ 1980 hasMore: function() { 1981 return this._hasMore; 1982 }, 1983 /** 1984 * Fetch more attachments from the server for the collection. 1985 * 1986 * @param {object} [options={}] 1987 * @returns {Promise} 1988 */ 1989 more: function( options ) { 1990 var query = this; 1991 1992 // If there is already a request pending, return early with the Deferred object. 1993 if ( this._more && 'pending' === this._more.state() ) { 1994 return this._more; 1995 } 1996 1997 if ( ! this.hasMore() ) { 1998 return jQuery.Deferred().resolveWith( this ).promise(); 1999 } 2000 2001 options = options || {}; 2002 options.remove = false; 2003 2004 return this._more = this.fetch( options ).done( function( resp ) { 2005 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { 2006 query._hasMore = false; 2007 } 2008 }); 2009 }, 2010 /** 2011 * Overrides Backbone.Collection.sync 2012 * Overrides wp.media.model.Attachments.sync 2013 * 2014 * @param {String} method 2015 * @param {Backbone.Model} model 2016 * @param {Object} [options={}] 2017 * @returns {Promise} 2018 */ 2019 sync: function( method, model, options ) { 2020 var args, fallback; 2021 2022 // Overload the read method so Attachment.fetch() functions correctly. 2023 if ( 'read' === method ) { 2024 options = options || {}; 2025 options.context = this; 2026 options.data = _.extend( options.data || {}, { 2027 action: 'query-attachments', 2028 post_id: wp.media.model.settings.post.id 2029 }); 2030 2031 // Clone the args so manipulation is non-destructive. 2032 args = _.clone( this.args ); 2033 2034 // Determine which page to query. 2035 if ( -1 !== args.posts_per_page ) { 2036 args.paged = Math.floor( this.length / args.posts_per_page ) + 1; 2037 } 2038 2039 options.data.query = args; 2040 return wp.media.ajax( options ); 2041 2042 // Otherwise, fall back to Backbone.sync() 2043 } else { 2044 /** 2045 * Call wp.media.model.Attachments.sync or Backbone.sync 2046 */ 2047 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; 2048 return fallback.sync.apply( this, arguments ); 2049 } 2050 } 2051 }, { 2052 /** 2053 * @readonly 2054 */ 2055 defaultProps: { 2056 orderby: 'date', 2057 order: 'DESC' 2058 }, 2059 /** 2060 * @readonly 2061 */ 2062 defaultArgs: { 2063 posts_per_page: 40 2064 }, 2065 /** 2066 * @readonly 2067 */ 2068 orderby: { 2069 allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], 2070 /** 2071 * A map of JavaScript orderby values to their WP_Query equivalents. 2072 * @type {Object} 2073 */ 2074 valuemap: { 2075 'id': 'ID', 2076 'uploadedTo': 'parent', 2077 'menuOrder': 'menu_order ID' 2078 } 2079 }, 2080 /** 2081 * A map of JavaScript query properties to their WP_Query equivalents. 2082 * 2083 * @readonly 2084 */ 2085 propmap: { 2086 'search': 's', 2087 'type': 'post_mime_type', 2088 'perPage': 'posts_per_page', 2089 'menuOrder': 'menu_order', 2090 'uploadedTo': 'post_parent', 2091 'status': 'post_status', 2092 'include': 'post__in', 2093 'exclude': 'post__not_in' 2094 }, 2095 /** 2096 * Creates and returns an Attachments Query collection given the properties. 2097 * 2098 * Caches query objects and reuses where possible. 2099 * 2100 * @static 2101 * @method 2102 * 2103 * @param {object} [props] 2104 * @param {Object} [props.cache=true] Whether to use the query cache or not. 2105 * @param {Object} [props.order] 2106 * @param {Object} [props.orderby] 2107 * @param {Object} [props.include] 2108 * @param {Object} [props.exclude] 2109 * @param {Object} [props.s] 2110 * @param {Object} [props.post_mime_type] 2111 * @param {Object} [props.posts_per_page] 2112 * @param {Object} [props.menu_order] 2113 * @param {Object} [props.post_parent] 2114 * @param {Object} [props.post_status] 2115 * @param {Object} [options] 2116 * 2117 * @returns {wp.media.model.Query} A new Attachments Query collection. 2118 */ 2119 get: (function(){ 2120 /** 2121 * @static 2122 * @type Array 2123 */ 2124 var queries = []; 2125 2126 /** 2127 * @returns {Query} 2128 */ 2129 return function( props, options ) { 2130 var args = {}, 2131 orderby = Query.orderby, 2132 defaults = Query.defaultProps, 2133 query, 2134 cache = !! props.cache || _.isUndefined( props.cache ); 2135 2136 // Remove the `query` property. This isn't linked to a query, 2137 // this *is* the query. 2138 delete props.query; 2139 delete props.cache; 2140 2141 // Fill default args. 2142 _.defaults( props, defaults ); 2143 2144 // Normalize the order. 2145 props.order = props.order.toUpperCase(); 2146 if ( 'DESC' !== props.order && 'ASC' !== props.order ) { 2147 props.order = defaults.order.toUpperCase(); 2148 } 2149 2150 // Ensure we have a valid orderby value. 2151 if ( ! _.contains( orderby.allowed, props.orderby ) ) { 2152 props.orderby = defaults.orderby; 2153 } 2154 2155 _.each( [ 'include', 'exclude' ], function( prop ) { 2156 if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { 2157 props[ prop ] = [ props[ prop ] ]; 2158 } 2159 } ); 2160 2161 // Generate the query `args` object. 2162 // Correct any differing property names. 2163 _.each( props, function( value, prop ) { 2164 if ( _.isNull( value ) ) { 2165 return; 2166 } 2167 2168 args[ Query.propmap[ prop ] || prop ] = value; 2169 }); 2170 2171 // Fill any other default query args. 2172 _.defaults( args, Query.defaultArgs ); 2173 2174 // `props.orderby` does not always map directly to `args.orderby`. 2175 // Substitute exceptions specified in orderby.keymap. 2176 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; 2177 2178 // Search the query cache for a matching query. 2179 if ( cache ) { 2180 query = _.find( queries, function( query ) { 2181 return _.isEqual( query.args, args ); 2182 }); 2183 } else { 2184 queries = []; 2185 } 2186 2187 // Otherwise, create a new query and add it to the cache. 2188 if ( ! query ) { 2189 query = new Query( [], _.extend( options || {}, { 2190 props: props, 2191 args: args 2192 } ) ); 2193 queries.push( query ); 2194 } 2195 2196 return query; 2197 }; 2198 }()) 2199 }); 2200 2201 module.exports = Query; 2202 },{"./attachments.js":10}],13:[function(require,module,exports){ 2203 /** 2204 * wp.media.model.Selection 2205 * 2206 * A selection of attachments. 2207 * 2208 * @class 2209 * @augments wp.media.model.Attachments 2210 * @augments Backbone.Collection 2211 */ 2212 var Attachments = require( './attachments.js' ), 2213 Selection; 2214 2215 Selection = Attachments.extend({ 2216 /** 2217 * Refresh the `single` model whenever the selection changes. 2218 * Binds `single` instead of using the context argument to ensure 2219 * it receives no parameters. 2220 * 2221 * @param {Array} [models=[]] Array of models used to populate the collection. 2222 * @param {Object} [options={}] 2223 */ 2224 initialize: function( models, options ) { 2225 /** 2226 * call 'initialize' directly on the parent class 2227 */ 2228 Attachments.prototype.initialize.apply( this, arguments ); 2229 this.multiple = options && options.multiple; 2230 2231 this.on( 'add remove reset', _.bind( this.single, this, false ) ); 2232 }, 2233 2234 /** 2235 * If the workflow does not support multi-select, clear out the selection 2236 * before adding a new attachment to it. 2237 * 2238 * @param {Array} models 2239 * @param {Object} options 2240 * @returns {wp.media.model.Attachment[]} 2241 */ 2242 add: function( models, options ) { 2243 if ( ! this.multiple ) { 2244 this.remove( this.models ); 2245 } 2246 /** 2247 * call 'add' directly on the parent class 2248 */ 2249 return Attachments.prototype.add.call( this, models, options ); 2250 }, 2251 2252 /** 2253 * Fired when toggling (clicking on) an attachment in the modal. 2254 * 2255 * @param {undefined|boolean|wp.media.model.Attachment} model 2256 * 2257 * @fires wp.media.model.Selection#selection:single 2258 * @fires wp.media.model.Selection#selection:unsingle 2259 * 2260 * @returns {Backbone.Model} 2261 */ 2262 single: function( model ) { 2263 var previous = this._single; 2264 2265 // If a `model` is provided, use it as the single model. 2266 if ( model ) { 2267 this._single = model; 2268 } 2269 // If the single model isn't in the selection, remove it. 2270 if ( this._single && ! this.get( this._single.cid ) ) { 2271 delete this._single; 2272 } 2273 2274 this._single = this._single || this.last(); 2275 2276 // If single has changed, fire an event. 2277 if ( this._single !== previous ) { 2278 if ( previous ) { 2279 previous.trigger( 'selection:unsingle', previous, this ); 2280 2281 // If the model was already removed, trigger the collection 2282 // event manually. 2283 if ( ! this.get( previous.cid ) ) { 2284 this.trigger( 'selection:unsingle', previous, this ); 2285 } 2286 } 2287 if ( this._single ) { 2288 this._single.trigger( 'selection:single', this._single, this ); 2289 } 2290 } 2291 2292 // Return the single model, or the last model as a fallback. 2293 return this._single; 2294 } 2295 }); 2296 2297 module.exports = Selection; 2298 },{"./attachments.js":10}],14:[function(require,module,exports){ 2299 /** 2300 * wp.media.selectionSync 2301 * 2302 * Sync an attachments selection in a state with another state. 2303 * 2304 * Allows for selecting multiple images in the Insert Media workflow, and then 2305 * switching to the Insert Gallery workflow while preserving the attachments selection. 2306 * 2307 * @mixin 2308 */ 2309 var selectionSync = { 2310 /** 2311 * @since 3.5.0 2312 */ 2313 syncSelection: function() { 2314 var selection = this.get('selection'), 2315 manager = this.frame._selection; 2316 2317 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 2318 return; 2319 } 2320 2321 // If the selection supports multiple items, validate the stored 2322 // attachments based on the new selection's conditions. Record 2323 // the attachments that are not included; we'll maintain a 2324 // reference to those. Other attachments are considered in flux. 2325 if ( selection.multiple ) { 2326 selection.reset( [], { silent: true }); 2327 selection.validateAll( manager.attachments ); 2328 manager.difference = _.difference( manager.attachments.models, selection.models ); 2329 } 2330 2331 // Sync the selection's single item with the master. 2332 selection.single( manager.single ); 2333 }, 2334 2335 /** 2336 * Record the currently active attachments, which is a combination 2337 * of the selection's attachments and the set of selected 2338 * attachments that this specific selection considered invalid. 2339 * Reset the difference and record the single attachment. 2340 * 2341 * @since 3.5.0 2342 */ 2343 recordSelection: function() { 2344 var selection = this.get('selection'), 2345 manager = this.frame._selection; 2346 2347 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 2348 return; 2349 } 2350 2351 if ( selection.multiple ) { 2352 manager.attachments.reset( selection.toArray().concat( manager.difference ) ); 2353 manager.difference = []; 2354 } else { 2355 manager.attachments.add( selection.toArray() ); 2356 } 2357 2358 manager.single = selection._single; 2359 } 2360 }; 2361 2362 module.exports = selectionSync; 2363 },{}],15:[function(require,module,exports){ 2364 /** 2365 * wp.media.view.AttachmentCompat 2366 * 2367 * A view to display fields added via the `attachment_fields_to_edit` filter. 2368 * 2369 * @class 2370 * @augments wp.media.View 2371 * @augments wp.Backbone.View 2372 * @augments Backbone.View 2373 */ 2374 var View = require( './view.js' ), 2375 AttachmentCompat; 2376 2377 AttachmentCompat = View.extend({ 2378 tagName: 'form', 2379 className: 'compat-item', 2380 2381 events: { 2382 'submit': 'preventDefault', 2383 'change input': 'save', 2384 'change select': 'save', 2385 'change textarea': 'save' 2386 }, 2387 2388 initialize: function() { 2389 this.model.on( 'change:compat', this.render, this ); 2390 }, 2391 /** 2392 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 2393 */ 2394 dispose: function() { 2395 if ( this.$(':focus').length ) { 2396 this.save(); 2397 } 2398 /** 2399 * call 'dispose' directly on the parent class 2400 */ 2401 return View.prototype.dispose.apply( this, arguments ); 2402 }, 2403 /** 2404 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 2405 */ 2406 render: function() { 2407 var compat = this.model.get('compat'); 2408 if ( ! compat || ! compat.item ) { 2409 return; 2410 } 2411 2412 this.views.detach(); 2413 this.$el.html( compat.item ); 2414 this.views.render(); 2415 return this; 2416 }, 2417 /** 2418 * @param {Object} event 2419 */ 2420 preventDefault: function( event ) { 2421 event.preventDefault(); 2422 }, 2423 /** 2424 * @param {Object} event 2425 */ 2426 save: function( event ) { 2427 var data = {}; 2428 2429 if ( event ) { 2430 event.preventDefault(); 2431 } 2432 2433 _.each( this.$el.serializeArray(), function( pair ) { 2434 data[ pair.name ] = pair.value; 2435 }); 2436 2437 this.controller.trigger( 'attachment:compat:waiting', ['waiting'] ); 2438 this.model.saveCompat( data ).always( _.bind( this.postSave, this ) ); 2439 }, 2440 2441 postSave: function() { 2442 this.controller.trigger( 'attachment:compat:ready', ['ready'] ); 2443 } 2444 }); 2445 2446 module.exports = AttachmentCompat; 2447 },{"./view.js":55}],16:[function(require,module,exports){ 2448 /** 2449 * wp.media.view.AttachmentFilters 2450 * 2451 * @class 2452 * @augments wp.media.View 2453 * @augments wp.Backbone.View 2454 * @augments Backbone.View 2455 */ 2456 var View = require( './view.js' ), 2457 $ = jQuery, 2458 AttachmentFilters; 2459 2460 AttachmentFilters = View.extend({ 2461 tagName: 'select', 2462 className: 'attachment-filters', 2463 id: 'media-attachment-filters', 2464 2465 events: { 2466 change: 'change' 2467 }, 2468 2469 keys: [], 2470 2471 initialize: function() { 2472 this.createFilters(); 2473 _.extend( this.filters, this.options.filters ); 2474 2475 // Build `<option>` elements. 2476 this.$el.html( _.chain( this.filters ).map( function( filter, value ) { 2477 return { 2478 el: $( '<option></option>' ).val( value ).html( filter.text )[0], 2479 priority: filter.priority || 50 2480 }; 2481 }, this ).sortBy('priority').pluck('el').value() ); 2482 2483 this.model.on( 'change', this.select, this ); 2484 this.select(); 2485 }, 2486 2487 /** 2488 * @abstract 2489 */ 2490 createFilters: function() { 2491 this.filters = {}; 2492 }, 2493 2494 /** 2495 * When the selected filter changes, update the Attachment Query properties to match. 2496 */ 2497 change: function() { 2498 var filter = this.filters[ this.el.value ]; 2499 if ( filter ) { 2500 this.model.set( filter.props ); 2501 } 2502 }, 2503 2504 select: function() { 2505 var model = this.model, 2506 value = 'all', 2507 props = model.toJSON(); 2508 2509 _.find( this.filters, function( filter, id ) { 2510 var equal = _.all( filter.props, function( prop, key ) { 2511 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); 2512 }); 2513 2514 if ( equal ) { 2515 return value = id; 2516 } 2517 }); 2518 2519 this.$el.val( value ); 2520 } 2521 }); 2522 2523 module.exports = AttachmentFilters; 2524 },{"./view.js":55}],17:[function(require,module,exports){ 2525 /** 2526 * wp.media.view.AttachmentFilters.All 2527 * 2528 * @class 2529 * @augments wp.media.view.AttachmentFilters 2530 * @augments wp.media.View 2531 * @augments wp.Backbone.View 2532 * @augments Backbone.View 2533 */ 2534 var AttachmentFilters = require( '../attachment-filters.js' ), 2535 l10n = wp.media.view.l10n, 2536 All; 2537 2538 All = AttachmentFilters.extend({ 2539 createFilters: function() { 2540 var filters = {}; 2541 2542 _.each( wp.media.view.settings.mimeTypes || {}, function( text, key ) { 2543 filters[ key ] = { 2544 text: text, 2545 props: { 2546 status: null, 2547 type: key, 2548 uploadedTo: null, 2549 orderby: 'date', 2550 order: 'DESC' 2551 } 2552 }; 2553 }); 2554 2555 filters.all = { 2556 text: l10n.allMediaItems, 2557 props: { 2558 status: null, 2559 type: null, 2560 uploadedTo: null, 2561 orderby: 'date', 2562 order: 'DESC' 2563 }, 2564 priority: 10 2565 }; 2566 2567 if ( wp.media.view.settings.post.id ) { 2568 filters.uploaded = { 2569 text: l10n.uploadedToThisPost, 2570 props: { 2571 status: null, 2572 type: null, 2573 uploadedTo: wp.media.view.settings.post.id, 2574 orderby: 'menuOrder', 2575 order: 'ASC' 2576 }, 2577 priority: 20 2578 }; 2579 } 2580 2581 filters.unattached = { 2582 text: l10n.unattached, 2583 props: { 2584 status: null, 2585 uploadedTo: 0, 2586 type: null, 2587 orderby: 'menuOrder', 2588 order: 'ASC' 2589 }, 2590 priority: 50 2591 }; 2592 2593 if ( wp.media.view.settings.mediaTrash && 2594 this.controller.isModeActive( 'grid' ) ) { 2595 2596 filters.trash = { 2597 text: l10n.trash, 2598 props: { 2599 uploadedTo: null, 2600 status: 'trash', 2601 type: null, 2602 orderby: 'date', 2603 order: 'DESC' 2604 }, 2605 priority: 50 2606 }; 2607 } 2608 2609 this.filters = filters; 2610 } 2611 }); 2612 2613 module.exports = All; 2614 },{"../attachment-filters.js":16}],18:[function(require,module,exports){ 2615 /** 2616 * A filter dropdown for month/dates. 2617 * 2618 * @class 2619 * @augments wp.media.view.AttachmentFilters 2620 * @augments wp.media.View 2621 * @augments wp.Backbone.View 2622 * @augments Backbone.View 2623 */ 2624 var AttachmentFilters = require( '../attachment-filters.js' ), 2625 l10n = wp.media.view.l10n, 2626 DateFilter; 2627 2628 DateFilter = AttachmentFilters.extend({ 2629 id: 'media-attachment-date-filters', 2630 2631 createFilters: function() { 2632 var filters = {}; 2633 _.each( wp.media.view.settings.months || {}, function( value, index ) { 2634 filters[ index ] = { 2635 text: value.text, 2636 props: { 2637 year: value.year, 2638 monthnum: value.month 2639 } 2640 }; 2641 }); 2642 filters.all = { 2643 text: l10n.allDates, 2644 props: { 2645 monthnum: false, 2646 year: false 2647 }, 2648 priority: 10 2649 }; 2650 this.filters = filters; 2651 } 2652 }); 2653 2654 module.exports = DateFilter; 2655 },{"../attachment-filters.js":16}],19:[function(require,module,exports){ 2656 /** 2657 * wp.media.view.AttachmentFilters.Uploaded 2658 * 2659 * @class 2660 * @augments wp.media.view.AttachmentFilters 2661 * @augments wp.media.View 2662 * @augments wp.Backbone.View 2663 * @augments Backbone.View 2664 */ 2665 var AttachmentFilters = require( '../attachment-filters.js' ), 2666 l10n = wp.media.view.l10n, 2667 Uploaded; 2668 2669 Uploaded = AttachmentFilters.extend({ 2670 createFilters: function() { 2671 var type = this.model.get('type'), 2672 types = wp.media.view.settings.mimeTypes, 2673 text; 2674 2675 if ( types && type ) { 2676 text = types[ type ]; 2677 } 2678 2679 this.filters = { 2680 all: { 2681 text: text || l10n.allMediaItems, 2682 props: { 2683 uploadedTo: null, 2684 orderby: 'date', 2685 order: 'DESC' 2686 }, 2687 priority: 10 2688 }, 2689 2690 uploaded: { 2691 text: l10n.uploadedToThisPost, 2692 props: { 2693 uploadedTo: wp.media.view.settings.post.id, 2694 orderby: 'menuOrder', 2695 order: 'ASC' 2696 }, 2697 priority: 20 2698 }, 2699 2700 unattached: { 2701 text: l10n.unattached, 2702 props: { 2703 uploadedTo: 0, 2704 orderby: 'menuOrder', 2705 order: 'ASC' 2706 }, 2707 priority: 50 2708 } 2709 }; 2710 } 2711 }); 2712 2713 module.exports = Uploaded; 2714 },{"../attachment-filters.js":16}],20:[function(require,module,exports){ 2715 /** 2716 * wp.media.view.Attachment 2717 * 2718 * @class 2719 * @augments wp.media.View 2720 * @augments wp.Backbone.View 2721 * @augments Backbone.View 2722 */ 2723 var View = require( './view.js' ), 2724 $ = jQuery, 2725 Attachment; 2726 2727 Attachment = View.extend({ 2728 tagName: 'li', 2729 className: 'attachment', 2730 template: wp.template('attachment'), 2731 2732 attributes: function() { 2733 return { 2734 'tabIndex': 0, 2735 'role': 'checkbox', 2736 'aria-label': this.model.get( 'title' ), 2737 'aria-checked': false, 2738 'data-id': this.model.get( 'id' ) 2739 }; 2740 }, 2741 2742 events: { 2743 'click .js--select-attachment': 'toggleSelectionHandler', 2744 'change [data-setting]': 'updateSetting', 2745 'change [data-setting] input': 'updateSetting', 2746 'change [data-setting] select': 'updateSetting', 2747 'change [data-setting] textarea': 'updateSetting', 2748 'click .close': 'removeFromLibrary', 2749 'click .check': 'checkClickHandler', 2750 'click a': 'preventDefault', 2751 'keydown .close': 'removeFromLibrary', 2752 'keydown': 'toggleSelectionHandler' 2753 }, 2754 2755 buttons: {}, 2756 2757 initialize: function() { 2758 var selection = this.options.selection, 2759 options = _.defaults( this.options, { 2760 rerenderOnModelChange: true 2761 } ); 2762 2763 if ( options.rerenderOnModelChange ) { 2764 this.model.on( 'change', this.render, this ); 2765 } else { 2766 this.model.on( 'change:percent', this.progress, this ); 2767 } 2768 this.model.on( 'change:title', this._syncTitle, this ); 2769 this.model.on( 'change:caption', this._syncCaption, this ); 2770 this.model.on( 'change:artist', this._syncArtist, this ); 2771 this.model.on( 'change:album', this._syncAlbum, this ); 2772 2773 // Update the selection. 2774 this.model.on( 'add', this.select, this ); 2775 this.model.on( 'remove', this.deselect, this ); 2776 if ( selection ) { 2777 selection.on( 'reset', this.updateSelect, this ); 2778 // Update the model's details view. 2779 this.model.on( 'selection:single selection:unsingle', this.details, this ); 2780 this.details( this.model, this.controller.state().get('selection') ); 2781 } 2782 2783 this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); 2784 }, 2785 /** 2786 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2787 */ 2788 dispose: function() { 2789 var selection = this.options.selection; 2790 2791 // Make sure all settings are saved before removing the view. 2792 this.updateAll(); 2793 2794 if ( selection ) { 2795 selection.off( null, null, this ); 2796 } 2797 /** 2798 * call 'dispose' directly on the parent class 2799 */ 2800 View.prototype.dispose.apply( this, arguments ); 2801 return this; 2802 }, 2803 /** 2804 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2805 */ 2806 render: function() { 2807 var options = _.defaults( this.model.toJSON(), { 2808 orientation: 'landscape', 2809 uploading: false, 2810 type: '', 2811 subtype: '', 2812 icon: '', 2813 filename: '', 2814 caption: '', 2815 title: '', 2816 dateFormatted: '', 2817 width: '', 2818 height: '', 2819 compat: false, 2820 alt: '', 2821 description: '' 2822 }, this.options ); 2823 2824 options.buttons = this.buttons; 2825 options.describe = this.controller.state().get('describe'); 2826 2827 if ( 'image' === options.type ) { 2828 options.size = this.imageSize(); 2829 } 2830 2831 options.can = {}; 2832 if ( options.nonces ) { 2833 options.can.remove = !! options.nonces['delete']; 2834 options.can.save = !! options.nonces.update; 2835 } 2836 2837 if ( this.controller.state().get('allowLocalEdits') ) { 2838 options.allowLocalEdits = true; 2839 } 2840 2841 if ( options.uploading && ! options.percent ) { 2842 options.percent = 0; 2843 } 2844 2845 this.views.detach(); 2846 this.$el.html( this.template( options ) ); 2847 2848 this.$el.toggleClass( 'uploading', options.uploading ); 2849 2850 if ( options.uploading ) { 2851 this.$bar = this.$('.media-progress-bar div'); 2852 } else { 2853 delete this.$bar; 2854 } 2855 2856 // Check if the model is selected. 2857 this.updateSelect(); 2858 2859 // Update the save status. 2860 this.updateSave(); 2861 2862 this.views.render(); 2863 2864 return this; 2865 }, 2866 2867 progress: function() { 2868 if ( this.$bar && this.$bar.length ) { 2869 this.$bar.width( this.model.get('percent') + '%' ); 2870 } 2871 }, 2872 2873 /** 2874 * @param {Object} event 2875 */ 2876 toggleSelectionHandler: function( event ) { 2877 var method; 2878 2879 // Don't do anything inside inputs. 2880 if ( 'INPUT' === event.target.nodeName ) { 2881 return; 2882 } 2883 2884 // Catch arrow events 2885 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 2886 this.controller.trigger( 'attachment:keydown:arrow', event ); 2887 return; 2888 } 2889 2890 // Catch enter and space events 2891 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 2892 return; 2893 } 2894 2895 event.preventDefault(); 2896 2897 // In the grid view, bubble up an edit:attachment event to the controller. 2898 if ( this.controller.isModeActive( 'grid' ) ) { 2899 if ( this.controller.isModeActive( 'edit' ) ) { 2900 // Pass the current target to restore focus when closing 2901 this.controller.trigger( 'edit:attachment', this.model, event.currentTarget ); 2902 return; 2903 } 2904 2905 if ( this.controller.isModeActive( 'select' ) ) { 2906 method = 'toggle'; 2907 } 2908 } 2909 2910 if ( event.shiftKey ) { 2911 method = 'between'; 2912 } else if ( event.ctrlKey || event.metaKey ) { 2913 method = 'toggle'; 2914 } 2915 2916 this.toggleSelection({ 2917 method: method 2918 }); 2919 2920 this.controller.trigger( 'selection:toggle' ); 2921 }, 2922 /** 2923 * @param {Object} options 2924 */ 2925 toggleSelection: function( options ) { 2926 var collection = this.collection, 2927 selection = this.options.selection, 2928 model = this.model, 2929 method = options && options.method, 2930 single, models, singleIndex, modelIndex; 2931 2932 if ( ! selection ) { 2933 return; 2934 } 2935 2936 single = selection.single(); 2937 method = _.isUndefined( method ) ? selection.multiple : method; 2938 2939 // If the `method` is set to `between`, select all models that 2940 // exist between the current and the selected model. 2941 if ( 'between' === method && single && selection.multiple ) { 2942 // If the models are the same, short-circuit. 2943 if ( single === model ) { 2944 return; 2945 } 2946 2947 singleIndex = collection.indexOf( single ); 2948 modelIndex = collection.indexOf( this.model ); 2949 2950 if ( singleIndex < modelIndex ) { 2951 models = collection.models.slice( singleIndex, modelIndex + 1 ); 2952 } else { 2953 models = collection.models.slice( modelIndex, singleIndex + 1 ); 2954 } 2955 2956 selection.add( models ); 2957 selection.single( model ); 2958 return; 2959 2960 // If the `method` is set to `toggle`, just flip the selection 2961 // status, regardless of whether the model is the single model. 2962 } else if ( 'toggle' === method ) { 2963 selection[ this.selected() ? 'remove' : 'add' ]( model ); 2964 selection.single( model ); 2965 return; 2966 } else if ( 'add' === method ) { 2967 selection.add( model ); 2968 selection.single( model ); 2969 return; 2970 } 2971 2972 // Fixes bug that loses focus when selecting a featured image 2973 if ( ! method ) { 2974 method = 'add'; 2975 } 2976 2977 if ( method !== 'add' ) { 2978 method = 'reset'; 2979 } 2980 2981 if ( this.selected() ) { 2982 // If the model is the single model, remove it. 2983 // If it is not the same as the single model, 2984 // it now becomes the single model. 2985 selection[ single === model ? 'remove' : 'single' ]( model ); 2986 } else { 2987 // If the model is not selected, run the `method` on the 2988 // selection. By default, we `reset` the selection, but the 2989 // `method` can be set to `add` the model to the selection. 2990 selection[ method ]( model ); 2991 selection.single( model ); 2992 } 2993 }, 2994 2995 updateSelect: function() { 2996 this[ this.selected() ? 'select' : 'deselect' ](); 2997 }, 2998 /** 2999 * @returns {unresolved|Boolean} 3000 */ 3001 selected: function() { 3002 var selection = this.options.selection; 3003 if ( selection ) { 3004 return !! selection.get( this.model.cid ); 3005 } 3006 }, 3007 /** 3008 * @param {Backbone.Model} model 3009 * @param {Backbone.Collection} collection 3010 */ 3011 select: function( model, collection ) { 3012 var selection = this.options.selection, 3013 controller = this.controller; 3014 3015 // Check if a selection exists and if it's the collection provided. 3016 // If they're not the same collection, bail; we're in another 3017 // selection's event loop. 3018 if ( ! selection || ( collection && collection !== selection ) ) { 3019 return; 3020 } 3021 3022 // Bail if the model is already selected. 3023 if ( this.$el.hasClass( 'selected' ) ) { 3024 return; 3025 } 3026 3027 // Add 'selected' class to model, set aria-checked to true. 3028 this.$el.addClass( 'selected' ).attr( 'aria-checked', true ); 3029 // Make the checkbox tabable, except in media grid (bulk select mode). 3030 if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) { 3031 this.$( '.check' ).attr( 'tabindex', '0' ); 3032 } 3033 }, 3034 /** 3035 * @param {Backbone.Model} model 3036 * @param {Backbone.Collection} collection 3037 */ 3038 deselect: function( model, collection ) { 3039 var selection = this.options.selection; 3040 3041 // Check if a selection exists and if it's the collection provided. 3042 // If they're not the same collection, bail; we're in another 3043 // selection's event loop. 3044 if ( ! selection || ( collection && collection !== selection ) ) { 3045 return; 3046 } 3047 this.$el.removeClass( 'selected' ).attr( 'aria-checked', false ) 3048 .find( '.check' ).attr( 'tabindex', '-1' ); 3049 }, 3050 /** 3051 * @param {Backbone.Model} model 3052 * @param {Backbone.Collection} collection 3053 */ 3054 details: function( model, collection ) { 3055 var selection = this.options.selection, 3056 details; 3057 3058 if ( selection !== collection ) { 3059 return; 3060 } 3061 3062 details = selection.single(); 3063 this.$el.toggleClass( 'details', details === this.model ); 3064 }, 3065 /** 3066 * @param {Object} event 3067 */ 3068 preventDefault: function( event ) { 3069 event.preventDefault(); 3070 }, 3071 /** 3072 * @param {string} size 3073 * @returns {Object} 3074 */ 3075 imageSize: function( size ) { 3076 var sizes = this.model.get('sizes'); 3077 3078 size = size || 'medium'; 3079 3080 // Use the provided image size if possible. 3081 if ( sizes && sizes[ size ] ) { 3082 return _.clone( sizes[ size ] ); 3083 } else { 3084 return { 3085 url: this.model.get('url'), 3086 width: this.model.get('width'), 3087 height: this.model.get('height'), 3088 orientation: this.model.get('orientation') 3089 }; 3090 } 3091 }, 3092 /** 3093 * @param {Object} event 3094 */ 3095 updateSetting: function( event ) { 3096 var $setting = $( event.target ).closest('[data-setting]'), 3097 setting, value; 3098 3099 if ( ! $setting.length ) { 3100 return; 3101 } 3102 3103 setting = $setting.data('setting'); 3104 value = event.target.value; 3105 3106 if ( this.model.get( setting ) !== value ) { 3107 this.save( setting, value ); 3108 } 3109 }, 3110 3111 /** 3112 * Pass all the arguments to the model's save method. 3113 * 3114 * Records the aggregate status of all save requests and updates the 3115 * view's classes accordingly. 3116 */ 3117 save: function() { 3118 var view = this, 3119 save = this._save = this._save || { status: 'ready' }, 3120 request = this.model.save.apply( this.model, arguments ), 3121 requests = save.requests ? $.when( request, save.requests ) : request; 3122 3123 // If we're waiting to remove 'Saved.', stop. 3124 if ( save.savedTimer ) { 3125 clearTimeout( save.savedTimer ); 3126 } 3127 3128 this.updateSave('waiting'); 3129 save.requests = requests; 3130 requests.always( function() { 3131 // If we've performed another request since this one, bail. 3132 if ( save.requests !== requests ) { 3133 return; 3134 } 3135 3136 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' ); 3137 save.savedTimer = setTimeout( function() { 3138 view.updateSave('ready'); 3139 delete save.savedTimer; 3140 }, 2000 ); 3141 }); 3142 }, 3143 /** 3144 * @param {string} status 3145 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 3146 */ 3147 updateSave: function( status ) { 3148 var save = this._save = this._save || { status: 'ready' }; 3149 3150 if ( status && status !== save.status ) { 3151 this.$el.removeClass( 'save-' + save.status ); 3152 save.status = status; 3153 } 3154 3155 this.$el.addClass( 'save-' + save.status ); 3156 return this; 3157 }, 3158 3159 updateAll: function() { 3160 var $settings = this.$('[data-setting]'), 3161 model = this.model, 3162 changed; 3163 3164 changed = _.chain( $settings ).map( function( el ) { 3165 var $input = $('input, textarea, select, [value]', el ), 3166 setting, value; 3167 3168 if ( ! $input.length ) { 3169 return; 3170 } 3171 3172 setting = $(el).data('setting'); 3173 value = $input.val(); 3174 3175 // Record the value if it changed. 3176 if ( model.get( setting ) !== value ) { 3177 return [ setting, value ]; 3178 } 3179 }).compact().object().value(); 3180 3181 if ( ! _.isEmpty( changed ) ) { 3182 model.save( changed ); 3183 } 3184 }, 3185 /** 3186 * @param {Object} event 3187 */ 3188 removeFromLibrary: function( event ) { 3189 // Catch enter and space events 3190 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 3191 return; 3192 } 3193 3194 // Stop propagation so the model isn't selected. 3195 event.stopPropagation(); 3196 3197 this.collection.remove( this.model ); 3198 }, 3199 3200 /** 3201 * Add the model if it isn't in the selection, if it is in the selection, 3202 * remove it. 3203 * 3204 * @param {[type]} event [description] 3205 * @return {[type]} [description] 3206 */ 3207 checkClickHandler: function ( event ) { 3208 var selection = this.options.selection; 3209 if ( ! selection ) { 3210 return; 3211 } 3212 event.stopPropagation(); 3213 if ( selection.where( { id: this.model.get( 'id' ) } ).length ) { 3214 selection.remove( this.model ); 3215 // Move focus back to the attachment tile (from the check). 3216 this.$el.focus(); 3217 } else { 3218 selection.add( this.model ); 3219 } 3220 } 3221 }); 3222 3223 // Ensure settings remain in sync between attachment views. 3224 _.each({ 3225 caption: '_syncCaption', 3226 title: '_syncTitle', 3227 artist: '_syncArtist', 3228 album: '_syncAlbum' 3229 }, function( method, setting ) { 3230 /** 3231 * @param {Backbone.Model} model 3232 * @param {string} value 3233 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 3234 */ 3235 Attachment.prototype[ method ] = function( model, value ) { 3236 var $setting = this.$('[data-setting="' + setting + '"]'); 3237 3238 if ( ! $setting.length ) { 3239 return this; 3240 } 3241 3242 // If the updated value is in sync with the value in the DOM, there 3243 // is no need to re-render. If we're currently editing the value, 3244 // it will automatically be in sync, suppressing the re-render for 3245 // the view we're editing, while updating any others. 3246 if ( value === $setting.find('input, textarea, select, [value]').val() ) { 3247 return this; 3248 } 3249 3250 return this.render(); 3251 }; 3252 }); 3253 3254 module.exports = Attachment; 3255 },{"./view.js":55}],21:[function(require,module,exports){ 3256 /** 3257 * wp.media.view.Attachment.Details 3258 * 3259 * @class 3260 * @augments wp.media.view.Attachment 3261 * @augments wp.media.View 3262 * @augments wp.Backbone.View 3263 * @augments Backbone.View 3264 */ 3265 var Attachment = require( '../attachment.js' ), 3266 l10n = wp.media.view.l10n, 3267 Details; 3268 3269 Details = Attachment.extend({ 3270 tagName: 'div', 3271 className: 'attachment-details', 3272 template: wp.template('attachment-details'), 3273 3274 attributes: function() { 3275 return { 3276 'tabIndex': 0, 3277 'data-id': this.model.get( 'id' ) 3278 }; 3279 }, 3280 3281 events: { 3282 'change [data-setting]': 'updateSetting', 3283 'change [data-setting] input': 'updateSetting', 3284 'change [data-setting] select': 'updateSetting', 3285 'change [data-setting] textarea': 'updateSetting', 3286 'click .delete-attachment': 'deleteAttachment', 3287 'click .trash-attachment': 'trashAttachment', 3288 'click .untrash-attachment': 'untrashAttachment', 3289 'click .edit-attachment': 'editAttachment', 3290 'click .refresh-attachment': 'refreshAttachment', 3291 'keydown': 'toggleSelectionHandler' 3292 }, 3293 3294 initialize: function() { 3295 this.options = _.defaults( this.options, { 3296 rerenderOnModelChange: false 3297 }); 3298 3299 this.on( 'ready', this.initialFocus ); 3300 // Call 'initialize' directly on the parent class. 3301 Attachment.prototype.initialize.apply( this, arguments ); 3302 }, 3303 3304 initialFocus: function() { 3305 if ( ! wp.media.isTouchDevice ) { 3306 this.$( ':input' ).eq( 0 ).focus(); 3307 } 3308 }, 3309 /** 3310 * @param {Object} event 3311 */ 3312 deleteAttachment: function( event ) { 3313 event.preventDefault(); 3314 3315 if ( confirm( l10n.warnDelete ) ) { 3316 this.model.destroy(); 3317 // Keep focus inside media modal 3318 // after image is deleted 3319 this.controller.modal.focusManager.focus(); 3320 } 3321 }, 3322 /** 3323 * @param {Object} event 3324 */ 3325 trashAttachment: function( event ) { 3326 var library = this.controller.library; 3327 event.preventDefault(); 3328 3329 if ( wp.media.view.settings.mediaTrash && 3330 'edit-metadata' === this.controller.content.mode() ) { 3331 3332 this.model.set( 'status', 'trash' ); 3333 this.model.save().done( function() { 3334 library._requery( true ); 3335 } ); 3336 } else { 3337 this.model.destroy(); 3338 } 3339 }, 3340 /** 3341 * @param {Object} event 3342 */ 3343 untrashAttachment: function( event ) { 3344 var library = this.controller.library; 3345 event.preventDefault(); 3346 3347 this.model.set( 'status', 'inherit' ); 3348 this.model.save().done( function() { 3349 library._requery( true ); 3350 } ); 3351 }, 3352 /** 3353 * @param {Object} event 3354 */ 3355 editAttachment: function( event ) { 3356 var editState = this.controller.states.get( 'edit-image' ); 3357 if ( window.imageEdit && editState ) { 3358 event.preventDefault(); 3359 3360 editState.set( 'image', this.model ); 3361 this.controller.setState( 'edit-image' ); 3362 } else { 3363 this.$el.addClass('needs-refresh'); 3364 } 3365 }, 3366 /** 3367 * @param {Object} event 3368 */ 3369 refreshAttachment: function( event ) { 3370 this.$el.removeClass('needs-refresh'); 3371 event.preventDefault(); 3372 this.model.fetch(); 3373 }, 3374 /** 3375 * When reverse tabbing(shift+tab) out of the right details panel, deliver 3376 * the focus to the item in the list that was being edited. 3377 * 3378 * @param {Object} event 3379 */ 3380 toggleSelectionHandler: function( event ) { 3381 if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { 3382 this.controller.trigger( 'attachment:details:shift-tab', event ); 3383 return false; 3384 } 3385 3386 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 3387 this.controller.trigger( 'attachment:keydown:arrow', event ); 3388 return; 3389 } 3390 } 3391 }); 3392 3393 module.exports = Details; 3394 },{"../attachment.js":20}],22:[function(require,module,exports){ 3395 /** 3396 * wp.media.view.Attachment.Library 3397 * 3398 * @class 3399 * @augments wp.media.view.Attachment 3400 * @augments wp.media.View 3401 * @augments wp.Backbone.View 3402 * @augments Backbone.View 3403 */ 3404 var Attachment = require( '../attachment.js' ), 3405 Library; 3406 3407 Library = Attachment.extend({ 3408 buttons: { 3409 check: true 3410 } 3411 }); 3412 3413 module.exports = Library; 3414 },{"../attachment.js":20}],23:[function(require,module,exports){ 3415 /** 3416 * wp.media.view.Attachments 3417 * 3418 * @class 3419 * @augments wp.media.View 3420 * @augments wp.Backbone.View 3421 * @augments Backbone.View 3422 */ 3423 var View = require( './view.js' ), 3424 Attachment = require( './attachment.js' ), 3425 $ = jQuery, 3426 Attachments; 3427 3428 Attachments = View.extend({ 3429 tagName: 'ul', 3430 className: 'attachments', 3431 3432 attributes: { 3433 tabIndex: -1 3434 }, 3435 3436 initialize: function() { 3437 this.el.id = _.uniqueId('__attachments-view-'); 3438 3439 _.defaults( this.options, { 3440 refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, 3441 refreshThreshold: 3, 3442 AttachmentView: Attachment, 3443 sortable: false, 3444 resize: true, 3445 idealColumnWidth: $( window ).width() < 640 ? 135 : 150 3446 }); 3447 3448 this._viewsByCid = {}; 3449 this.$window = $( window ); 3450 this.resizeEvent = 'resize.media-modal-columns'; 3451 3452 this.collection.on( 'add', function( attachment ) { 3453 this.views.add( this.createAttachmentView( attachment ), { 3454 at: this.collection.indexOf( attachment ) 3455 }); 3456 }, this ); 3457 3458 this.collection.on( 'remove', function( attachment ) { 3459 var view = this._viewsByCid[ attachment.cid ]; 3460 delete this._viewsByCid[ attachment.cid ]; 3461 3462 if ( view ) { 3463 view.remove(); 3464 } 3465 }, this ); 3466 3467 this.collection.on( 'reset', this.render, this ); 3468 3469 this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); 3470 3471 // Throttle the scroll handler and bind this. 3472 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 3473 3474 this.options.scrollElement = this.options.scrollElement || this.el; 3475 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 3476 3477 this.initSortable(); 3478 3479 _.bindAll( this, 'setColumns' ); 3480 3481 if ( this.options.resize ) { 3482 this.on( 'ready', this.bindEvents ); 3483 this.controller.on( 'open', this.setColumns ); 3484 3485 // Call this.setColumns() after this view has been rendered in the DOM so 3486 // attachments get proper width applied. 3487 _.defer( this.setColumns, this ); 3488 } 3489 }, 3490 3491 bindEvents: function() { 3492 this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); 3493 }, 3494 3495 attachmentFocus: function() { 3496 this.$( 'li:first' ).focus(); 3497 }, 3498 3499 restoreFocus: function() { 3500 this.$( 'li.selected:first' ).focus(); 3501 }, 3502 3503 arrowEvent: function( event ) { 3504 var attachments = this.$el.children( 'li' ), 3505 perRow = this.columns, 3506 index = attachments.filter( ':focus' ).index(), 3507 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); 3508 3509 if ( index === -1 ) { 3510 return; 3511 } 3512 3513 // Left arrow 3514 if ( 37 === event.keyCode ) { 3515 if ( 0 === index ) { 3516 return; 3517 } 3518 attachments.eq( index - 1 ).focus(); 3519 } 3520 3521 // Up arrow 3522 if ( 38 === event.keyCode ) { 3523 if ( 1 === row ) { 3524 return; 3525 } 3526 attachments.eq( index - perRow ).focus(); 3527 } 3528 3529 // Right arrow 3530 if ( 39 === event.keyCode ) { 3531 if ( attachments.length === index ) { 3532 return; 3533 } 3534 attachments.eq( index + 1 ).focus(); 3535 } 3536 3537 // Down arrow 3538 if ( 40 === event.keyCode ) { 3539 if ( Math.ceil( attachments.length / perRow ) === row ) { 3540 return; 3541 } 3542 attachments.eq( index + perRow ).focus(); 3543 } 3544 }, 3545 3546 dispose: function() { 3547 this.collection.props.off( null, null, this ); 3548 if ( this.options.resize ) { 3549 this.$window.off( this.resizeEvent ); 3550 } 3551 3552 /** 3553 * call 'dispose' directly on the parent class 3554 */ 3555 View.prototype.dispose.apply( this, arguments ); 3556 }, 3557 3558 setColumns: function() { 3559 var prev = this.columns, 3560 width = this.$el.width(); 3561 3562 if ( width ) { 3563 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; 3564 3565 if ( ! prev || prev !== this.columns ) { 3566 this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); 3567 } 3568 } 3569 }, 3570 3571 initSortable: function() { 3572 var collection = this.collection; 3573 3574 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 3575 return; 3576 } 3577 3578 this.$el.sortable( _.extend({ 3579 // If the `collection` has a `comparator`, disable sorting. 3580 disabled: !! collection.comparator, 3581 3582 // Change the position of the attachment as soon as the 3583 // mouse pointer overlaps a thumbnail. 3584 tolerance: 'pointer', 3585 3586 // Record the initial `index` of the dragged model. 3587 start: function( event, ui ) { 3588 ui.item.data('sortableIndexStart', ui.item.index()); 3589 }, 3590 3591 // Update the model's index in the collection. 3592 // Do so silently, as the view is already accurate. 3593 update: function( event, ui ) { 3594 var model = collection.at( ui.item.data('sortableIndexStart') ), 3595 comparator = collection.comparator; 3596 3597 // Temporarily disable the comparator to prevent `add` 3598 // from re-sorting. 3599 delete collection.comparator; 3600 3601 // Silently shift the model to its new index. 3602 collection.remove( model, { 3603 silent: true 3604 }); 3605 collection.add( model, { 3606 silent: true, 3607 at: ui.item.index() 3608 }); 3609 3610 // Restore the comparator. 3611 collection.comparator = comparator; 3612 3613 // Fire the `reset` event to ensure other collections sync. 3614 collection.trigger( 'reset', collection ); 3615 3616 // If the collection is sorted by menu order, 3617 // update the menu order. 3618 collection.saveMenuOrder(); 3619 } 3620 }, this.options.sortable ) ); 3621 3622 // If the `orderby` property is changed on the `collection`, 3623 // check to see if we have a `comparator`. If so, disable sorting. 3624 collection.props.on( 'change:orderby', function() { 3625 this.$el.sortable( 'option', 'disabled', !! collection.comparator ); 3626 }, this ); 3627 3628 this.collection.props.on( 'change:orderby', this.refreshSortable, this ); 3629 this.refreshSortable(); 3630 }, 3631 3632 refreshSortable: function() { 3633 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 3634 return; 3635 } 3636 3637 // If the `collection` has a `comparator`, disable sorting. 3638 var collection = this.collection, 3639 orderby = collection.props.get('orderby'), 3640 enabled = 'menuOrder' === orderby || ! collection.comparator; 3641 3642 this.$el.sortable( 'option', 'disabled', ! enabled ); 3643 }, 3644 3645 /** 3646 * @param {wp.media.model.Attachment} attachment 3647 * @returns {wp.media.View} 3648 */ 3649 createAttachmentView: function( attachment ) { 3650 var view = new this.options.AttachmentView({ 3651 controller: this.controller, 3652 model: attachment, 3653 collection: this.collection, 3654 selection: this.options.selection 3655 }); 3656 3657 return this._viewsByCid[ attachment.cid ] = view; 3658 }, 3659 3660 prepare: function() { 3661 // Create all of the Attachment views, and replace 3662 // the list in a single DOM operation. 3663 if ( this.collection.length ) { 3664 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 3665 3666 // If there are no elements, clear the views and load some. 3667 } else { 3668 this.views.unset(); 3669 this.collection.more().done( this.scroll ); 3670 } 3671 }, 3672 3673 ready: function() { 3674 // Trigger the scroll event to check if we're within the 3675 // threshold to query for additional attachments. 3676 this.scroll(); 3677 }, 3678 3679 scroll: function() { 3680 var view = this, 3681 el = this.options.scrollElement, 3682 scrollTop = el.scrollTop, 3683 toolbar; 3684 3685 // The scroll event occurs on the document, but the element 3686 // that should be checked is the document body. 3687 if ( el == document ) { 3688 el = document.body; 3689 scrollTop = $(document).scrollTop(); 3690 } 3691 3692 if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { 3693 return; 3694 } 3695 3696 toolbar = this.views.parent.toolbar; 3697 3698 // Show the spinner only if we are close to the bottom. 3699 if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { 3700 toolbar.get('spinner').show(); 3701 } 3702 3703 if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { 3704 this.collection.more().done(function() { 3705 view.scroll(); 3706 toolbar.get('spinner').hide(); 3707 }); 3708 } 3709 } 3710 }); 3711 3712 module.exports = Attachments; 3713 },{"./attachment.js":20,"./view.js":55}],24:[function(require,module,exports){ 3714 /** 3715 * wp.media.view.AttachmentsBrowser 3716 * 3717 * @class 3718 * @augments wp.media.View 3719 * @augments wp.Backbone.View 3720 * @augments Backbone.View 3721 * 3722 * @param {object} options 3723 * @param {object} [options.filters=false] Which filters to show in the browser's toolbar. 3724 * Accepts 'uploaded' and 'all'. 3725 * @param {object} [options.search=true] Whether to show the search interface in the 3726 * browser's toolbar. 3727 * @param {object} [options.display=false] Whether to show the attachments display settings 3728 * view in the sidebar. 3729 * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser. 3730 * Accepts true, false, and 'errors'. 3731 */ 3732 var View = require( '../view.js' ), 3733 Library = require( '../attachment/library.js' ), 3734 Toolbar = require( '../toolbar.js' ), 3735 Spinner = require( '../spinner.js' ), 3736 Search = require( '../search.js' ), 3737 Label = require( '../label.js' ), 3738 Uploaded = require( '../attachment-filters/uploaded.js' ), 3739 All = require( '../attachment-filters/all.js' ), 3740 DateFilter = require( '../attachment-filters/date.js' ), 3741 UploaderInline = require( '../uploader/inline.js' ), 3742 Attachments = require( '../attachments.js' ), 3743 Sidebar = require( '../sidebar.js' ), 3744 UploaderStatus = require( '../uploader/status.js' ), 3745 Details = require( '../attachment/details.js' ), 3746 AttachmentCompat = require( '../attachment-compat.js' ), 3747 AttachmentDisplay = require( '../settings/attachment-display.js' ), 3748 mediaTrash = wp.media.view.settings.mediaTrash, 3749 l10n = wp.media.view.l10n, 3750 $ = jQuery, 3751 AttachmentsBrowser; 3752 3753 AttachmentsBrowser = View.extend({ 3754 tagName: 'div', 3755 className: 'attachments-browser', 3756 3757 initialize: function() { 3758 _.defaults( this.options, { 3759 filters: false, 3760 search: true, 3761 display: false, 3762 sidebar: true, 3763 AttachmentView: Library 3764 }); 3765 3766 this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) ); 3767 this.controller.on( 'edit:selection', this.editSelection ); 3768 this.createToolbar(); 3769 if ( this.options.sidebar ) { 3770 this.createSidebar(); 3771 } 3772 this.createUploader(); 3773 this.createAttachments(); 3774 this.updateContent(); 3775 3776 if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) { 3777 this.$el.addClass( 'hide-sidebar' ); 3778 3779 if ( 'errors' === this.options.sidebar ) { 3780 this.$el.addClass( 'sidebar-for-errors' ); 3781 } 3782 } 3783 3784 this.collection.on( 'add remove reset', this.updateContent, this ); 3785 }, 3786 3787 editSelection: function( modal ) { 3788 modal.$( '.media-button-backToLibrary' ).focus(); 3789 }, 3790 3791 /** 3792 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining 3793 */ 3794 dispose: function() { 3795 this.options.selection.off( null, null, this ); 3796 View.prototype.dispose.apply( this, arguments ); 3797 return this; 3798 }, 3799 3800 createToolbar: function() { 3801 var LibraryViewSwitcher, Filters, toolbarOptions; 3802 3803 toolbarOptions = { 3804 controller: this.controller 3805 }; 3806 3807 if ( this.controller.isModeActive( 'grid' ) ) { 3808 toolbarOptions.className = 'media-toolbar wp-filter'; 3809 } 3810 3811 /** 3812 * @member {wp.media.view.Toolbar} 3813 */ 3814 this.toolbar = new Toolbar( toolbarOptions ); 3815 3816 this.views.add( this.toolbar ); 3817 3818 this.toolbar.set( 'spinner', new Spinner({ 3819 priority: -60 3820 }) ); 3821 3822 if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { 3823 // "Filters" will return a <select>, need to render 3824 // screen reader text before 3825 this.toolbar.set( 'filtersLabel', new Label({ 3826 value: l10n.filterByType, 3827 attributes: { 3828 'for': 'media-attachment-filters' 3829 }, 3830 priority: -80 3831 }).render() ); 3832 3833 if ( 'uploaded' === this.options.filters ) { 3834 this.toolbar.set( 'filters', new Uploaded({ 3835 controller: this.controller, 3836 model: this.collection.props, 3837 priority: -80 3838 }).render() ); 3839 } else { 3840 Filters = new All({ 3841 controller: this.controller, 3842 model: this.collection.props, 3843 priority: -80 3844 }); 3845 3846 this.toolbar.set( 'filters', Filters.render() ); 3847 } 3848 } 3849 3850 // Feels odd to bring the global media library switcher into the Attachment 3851 // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); 3852 // which the controller can tap into and add this view? 3853 if ( this.controller.isModeActive( 'grid' ) ) { 3854 LibraryViewSwitcher = View.extend({ 3855 className: 'view-switch media-grid-view-switch', 3856 template: wp.template( 'media-library-view-switcher') 3857 }); 3858 3859 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({ 3860 controller: this.controller, 3861 priority: -90 3862 }).render() ); 3863 3864 // DateFilter is a <select>, screen reader text needs to be rendered before 3865 this.toolbar.set( 'dateFilterLabel', new Label({ 3866 value: l10n.filterByDate, 3867 attributes: { 3868 'for': 'media-attachment-date-filters' 3869 }, 3870 priority: -75 3871 }).render() ); 3872 this.toolbar.set( 'dateFilter', new DateFilter({ 3873 controller: this.controller, 3874 model: this.collection.props, 3875 priority: -75 3876 }).render() ); 3877 3878 // BulkSelection is a <div> with subviews, including screen reader text 3879 this.toolbar.set( 'selectModeToggleButton', new wp.media.view.SelectModeToggleButton({ 3880 text: l10n.bulkSelect, 3881 controller: this.controller, 3882 priority: -70 3883 }).render() ); 3884 3885 this.toolbar.set( 'deleteSelectedButton', new wp.media.view.DeleteSelectedButton({ 3886 filters: Filters, 3887 style: 'primary', 3888 disabled: true, 3889 text: mediaTrash ? l10n.trashSelected : l10n.deleteSelected, 3890 controller: this.controller, 3891 priority: -60, 3892 click: function() { 3893 var changed = [], removed = [], self = this, 3894 selection = this.controller.state().get( 'selection' ), 3895 library = this.controller.state().get( 'library' ); 3896 3897 if ( ! selection.length ) { 3898 return; 3899 } 3900 3901 if ( ! mediaTrash && ! confirm( l10n.warnBulkDelete ) ) { 3902 return; 3903 } 3904 3905 if ( mediaTrash && 3906 'trash' !== selection.at( 0 ).get( 'status' ) && 3907 ! confirm( l10n.warnBulkTrash ) ) { 3908 3909 return; 3910 } 3911 3912 selection.each( function( model ) { 3913 if ( ! model.get( 'nonces' )['delete'] ) { 3914 removed.push( model ); 3915 return; 3916 } 3917 3918 if ( mediaTrash && 'trash' === model.get( 'status' ) ) { 3919 model.set( 'status', 'inherit' ); 3920 changed.push( model.save() ); 3921 removed.push( model ); 3922 } else if ( mediaTrash ) { 3923 model.set( 'status', 'trash' ); 3924 changed.push( model.save() ); 3925 removed.push( model ); 3926 } else { 3927 model.destroy({wait: true}); 3928 } 3929 } ); 3930 3931 if ( changed.length ) { 3932 selection.remove( removed ); 3933 3934 $.when.apply( null, changed ).then( function() { 3935 library._requery( true ); 3936 self.controller.trigger( 'selection:action:done' ); 3937 } ); 3938 } else { 3939 this.controller.trigger( 'selection:action:done' ); 3940 } 3941 } 3942 }).render() ); 3943 3944 if ( mediaTrash ) { 3945 this.toolbar.set( 'deleteSelectedPermanentlyButton', new wp.media.view.DeleteSelectedPermanentlyButton({ 3946 filters: Filters, 3947 style: 'primary', 3948 disabled: true, 3949 text: l10n.deleteSelected, 3950 controller: this.controller, 3951 priority: -55, 3952 click: function() { 3953 var removed = [], selection = this.controller.state().get( 'selection' ); 3954 3955 if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) { 3956 return; 3957 } 3958 3959 selection.each( function( model ) { 3960 if ( ! model.get( 'nonces' )['delete'] ) { 3961 removed.push( model ); 3962 return; 3963 } 3964 3965 model.destroy(); 3966 } ); 3967 3968 selection.remove( removed ); 3969 this.controller.trigger( 'selection:action:done' ); 3970 } 3971 }).render() ); 3972 } 3973 3974 } else { 3975 // DateFilter is a <select>, screen reader text needs to be rendered before 3976 this.toolbar.set( 'dateFilterLabel', new Label({ 3977 value: l10n.filterByDate, 3978 attributes: { 3979 'for': 'media-attachment-date-filters' 3980 }, 3981 priority: -75 3982 }).render() ); 3983 this.toolbar.set( 'dateFilter', new DateFilter({ 3984 controller: this.controller, 3985 model: this.collection.props, 3986 priority: -75 3987 }).render() ); 3988 } 3989 3990 if ( this.options.search ) { 3991 // Search is an input, screen reader text needs to be rendered before 3992 this.toolbar.set( 'searchLabel', new Label({ 3993 value: l10n.searchMediaLabel, 3994 attributes: { 3995 'for': 'media-search-input' 3996 }, 3997 priority: 60 3998 }).render() ); 3999 this.toolbar.set( 'search', new Search({ 4000 controller: this.controller, 4001 model: this.collection.props, 4002 priority: 60 4003 }).render() ); 4004 } 4005 4006 if ( this.options.dragInfo ) { 4007 this.toolbar.set( 'dragInfo', new View({ 4008 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0], 4009 priority: -40 4010 }) ); 4011 } 4012 4013 if ( this.options.suggestedWidth && this.options.suggestedHeight ) { 4014 this.toolbar.set( 'suggestedDimensions', new View({ 4015 el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '</div>' )[0], 4016 priority: -40 4017 }) ); 4018 } 4019 }, 4020 4021 updateContent: function() { 4022 var view = this, 4023 noItemsView; 4024 4025 if ( this.controller.isModeActive( 'grid' ) ) { 4026 noItemsView = view.attachmentsNoResults; 4027 } else { 4028 noItemsView = view.uploader; 4029 } 4030 4031 if ( ! this.collection.length ) { 4032 this.toolbar.get( 'spinner' ).show(); 4033 this.dfd = this.collection.more().done( function() { 4034 if ( ! view.collection.length ) { 4035 noItemsView.$el.removeClass( 'hidden' ); 4036 } else { 4037 noItemsView.$el.addClass( 'hidden' ); 4038 } 4039 view.toolbar.get( 'spinner' ).hide(); 4040 } ); 4041 } else { 4042 noItemsView.$el.addClass( 'hidden' ); 4043 view.toolbar.get( 'spinner' ).hide(); 4044 } 4045 }, 4046 4047 createUploader: function() { 4048 this.uploader = new UploaderInline({ 4049 controller: this.controller, 4050 status: false, 4051 message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound, 4052 canClose: this.controller.isModeActive( 'grid' ) 4053 }); 4054 4055 this.uploader.hide(); 4056 this.views.add( this.uploader ); 4057 }, 4058 4059 toggleUploader: function() { 4060 if ( this.uploader.$el.hasClass( 'hidden' ) ) { 4061 this.uploader.show(); 4062 } else { 4063 this.uploader.hide(); 4064 } 4065 }, 4066 4067 createAttachments: function() { 4068 this.attachments = new Attachments({ 4069 controller: this.controller, 4070 collection: this.collection, 4071 selection: this.options.selection, 4072 model: this.model, 4073 sortable: this.options.sortable, 4074 scrollElement: this.options.scrollElement, 4075 idealColumnWidth: this.options.idealColumnWidth, 4076 4077 // The single `Attachment` view to be used in the `Attachments` view. 4078 AttachmentView: this.options.AttachmentView 4079 }); 4080 4081 // Add keydown listener to the instance of the Attachments view 4082 this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent ); 4083 this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus ); 4084 4085 this.views.add( this.attachments ); 4086 4087 4088 if ( this.controller.isModeActive( 'grid' ) ) { 4089 this.attachmentsNoResults = new View({ 4090 controller: this.controller, 4091 tagName: 'p' 4092 }); 4093 4094 this.attachmentsNoResults.$el.addClass( 'hidden no-media' ); 4095 this.attachmentsNoResults.$el.html( l10n.noMedia ); 4096 4097 this.views.add( this.attachmentsNoResults ); 4098 } 4099 }, 4100 4101 createSidebar: function() { 4102 var options = this.options, 4103 selection = options.selection, 4104 sidebar = this.sidebar = new Sidebar({ 4105 controller: this.controller 4106 }); 4107 4108 this.views.add( sidebar ); 4109 4110 if ( this.controller.uploader ) { 4111 sidebar.set( 'uploads', new UploaderStatus({ 4112 controller: this.controller, 4113 priority: 40 4114 }) ); 4115 } 4116 4117 selection.on( 'selection:single', this.createSingle, this ); 4118 selection.on( 'selection:unsingle', this.disposeSingle, this ); 4119 4120 if ( selection.single() ) { 4121 this.createSingle(); 4122 } 4123 }, 4124 4125 createSingle: function() { 4126 var sidebar = this.sidebar, 4127 single = this.options.selection.single(); 4128 4129 sidebar.set( 'details', new Details({ 4130 controller: this.controller, 4131 model: single, 4132 priority: 80 4133 }) ); 4134 4135 sidebar.set( 'compat', new AttachmentCompat({ 4136 controller: this.controller, 4137 model: single, 4138 priority: 120 4139 }) ); 4140 4141 if ( this.options.display ) { 4142 sidebar.set( 'display', new AttachmentDisplay({ 4143 controller: this.controller, 4144 model: this.model.display( single ), 4145 attachment: single, 4146 priority: 160, 4147 userSettings: this.model.get('displayUserSettings') 4148 }) ); 4149 } 4150 4151 // Show the sidebar on mobile 4152 if ( this.model.id === 'insert' ) { 4153 sidebar.$el.addClass( 'visible' ); 4154 } 4155 }, 4156 4157 disposeSingle: function() { 4158 var sidebar = this.sidebar; 4159 sidebar.unset('details'); 4160 sidebar.unset('compat'); 4161 sidebar.unset('display'); 4162 // Hide the sidebar on mobile 4163 sidebar.$el.removeClass( 'visible' ); 4164 } 4165 }); 4166 4167 module.exports = AttachmentsBrowser; 4168 },{"../attachment-compat.js":15,"../attachment-filters/all.js":17,"../attachment-filters/date.js":18,"../attachment-filters/uploaded.js":19,"../attachment/details.js":21,"../attachment/library.js":22,"../attachments.js":23,"../label.js":34,"../search.js":43,"../settings/attachment-display.js":45,"../sidebar.js":46,"../spinner.js":47,"../toolbar.js":48,"../uploader/inline.js":50,"../uploader/status.js":52,"../view.js":55}],25:[function(require,module,exports){ 4169 /** 4170 * wp.media.view.AudioDetails 4171 * 4172 * @constructor 4173 * @augments wp.media.view.MediaDetails 4174 * @augments wp.media.view.Settings.AttachmentDisplay 4175 * @augments wp.media.view.Settings 4176 * @augments wp.media.View 4177 * @augments wp.Backbone.View 4178 * @augments Backbone.View 4179 */ 4180 var MediaDetails = require( './media-details' ), 4181 AudioDetails; 4182 4183 AudioDetails = MediaDetails.extend({ 4184 className: 'audio-details', 4185 template: wp.template('audio-details'), 4186 4187 setMedia: function() { 4188 var audio = this.$('.wp-audio-shortcode'); 4189 4190 if ( audio.find( 'source' ).length ) { 4191 if ( audio.is(':hidden') ) { 4192 audio.show(); 4193 } 4194 this.media = MediaDetails.prepareSrc( audio.get(0) ); 4195 } else { 4196 audio.hide(); 4197 this.media = false; 4198 } 4199 4200 return this; 4201 } 4202 }); 4203 4204 module.exports = AudioDetails; 4205 },{"./media-details":35}],26:[function(require,module,exports){ 4206 /** 4207 * wp.media.view.Button 4208 * 4209 * @class 4210 * @augments wp.media.View 4211 * @augments wp.Backbone.View 4212 * @augments Backbone.View 4213 */ 4214 var View = require( './view.js' ), 4215 Button; 4216 4217 Button = View.extend({ 4218 tagName: 'a', 4219 className: 'media-button', 4220 attributes: { href: '#' }, 4221 4222 events: { 4223 'click': 'click' 4224 }, 4225 4226 defaults: { 4227 text: '', 4228 style: '', 4229 size: 'large', 4230 disabled: false 4231 }, 4232 4233 initialize: function() { 4234 /** 4235 * Create a model with the provided `defaults`. 4236 * 4237 * @member {Backbone.Model} 4238 */ 4239 this.model = new Backbone.Model( this.defaults ); 4240 4241 // If any of the `options` have a key from `defaults`, apply its 4242 // value to the `model` and remove it from the `options object. 4243 _.each( this.defaults, function( def, key ) { 4244 var value = this.options[ key ]; 4245 if ( _.isUndefined( value ) ) { 4246 return; 4247 } 4248 4249 this.model.set( key, value ); 4250 delete this.options[ key ]; 4251 }, this ); 4252 4253 this.model.on( 'change', this.render, this ); 4254 }, 4255 /** 4256 * @returns {wp.media.view.Button} Returns itself to allow chaining 4257 */ 4258 render: function() { 4259 var classes = [ 'button', this.className ], 4260 model = this.model.toJSON(); 4261 4262 if ( model.style ) { 4263 classes.push( 'button-' + model.style ); 4264 } 4265 4266 if ( model.size ) { 4267 classes.push( 'button-' + model.size ); 4268 } 4269 4270 classes = _.uniq( classes.concat( this.options.classes ) ); 4271 this.el.className = classes.join(' '); 4272 4273 this.$el.attr( 'disabled', model.disabled ); 4274 this.$el.text( this.model.get('text') ); 4275 4276 return this; 4277 }, 4278 /** 4279 * @param {Object} event 4280 */ 4281 click: function( event ) { 4282 if ( '#' === this.attributes.href ) { 4283 event.preventDefault(); 4284 } 4285 4286 if ( this.options.click && ! this.model.get('disabled') ) { 4287 this.options.click.apply( this, arguments ); 4288 } 4289 } 4290 }); 4291 4292 module.exports = Button; 4293 },{"./view.js":55}],27:[function(require,module,exports){ 4294 /** 4295 * wp.media.view.FocusManager 4296 * 4297 * @class 4298 * @augments wp.media.View 4299 * @augments wp.Backbone.View 4300 * @augments Backbone.View 4301 */ 4302 var View = require( './view.js' ), 4303 FocusManager; 4304 4305 FocusManager = View.extend({ 4306 4307 events: { 4308 'keydown': 'constrainTabbing' 4309 }, 4310 4311 focus: function() { // Reset focus on first left menu item 4312 this.$('.media-menu-item').first().focus(); 4313 }, 4314 /** 4315 * @param {Object} event 4316 */ 4317 constrainTabbing: function( event ) { 4318 var tabbables; 4319 4320 // Look for the tab key. 4321 if ( 9 !== event.keyCode ) { 4322 return; 4323 } 4324 4325 tabbables = this.$( ':tabbable' ); 4326 4327 // Keep tab focus within media modal while it's open 4328 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { 4329 tabbables.first().focus(); 4330 return false; 4331 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { 4332 tabbables.last().focus(); 4333 return false; 4334 } 4335 } 4336 4337 }); 4338 4339 module.exports = FocusManager; 4340 },{"./view.js":55}],28:[function(require,module,exports){ 4341 /** 4342 * wp.media.view.Frame 4343 * 4344 * A frame is a composite view consisting of one or more regions and one or more 4345 * states. 4346 * 4347 * @see wp.media.controller.State 4348 * @see wp.media.controller.Region 4349 * 4350 * @class 4351 * @augments wp.media.View 4352 * @augments wp.Backbone.View 4353 * @augments Backbone.View 4354 * @mixes wp.media.controller.StateMachine 4355 */ 4356 var StateMachine = require( '../controllers/state-machine.js' ), 4357 State = require( '../controllers/state.js' ), 4358 Region = require( '../controllers/region.js' ), 4359 View = require( './view.js' ), 4360 Frame; 4361 4362 Frame = View.extend({ 4363 initialize: function() { 4364 _.defaults( this.options, { 4365 mode: [ 'select' ] 4366 }); 4367 this._createRegions(); 4368 this._createStates(); 4369 this._createModes(); 4370 }, 4371 4372 _createRegions: function() { 4373 // Clone the regions array. 4374 this.regions = this.regions ? this.regions.slice() : []; 4375 4376 // Initialize regions. 4377 _.each( this.regions, function( region ) { 4378 this[ region ] = new Region({ 4379 view: this, 4380 id: region, 4381 selector: '.media-frame-' + region 4382 }); 4383 }, this ); 4384 }, 4385 /** 4386 * Create the frame's states. 4387 * 4388 * @see wp.media.controller.State 4389 * @see wp.media.controller.StateMachine 4390 * 4391 * @fires wp.media.controller.State#ready 4392 */ 4393 _createStates: function() { 4394 // Create the default `states` collection. 4395 this.states = new Backbone.Collection( null, { 4396 model: State 4397 }); 4398 4399 // Ensure states have a reference to the frame. 4400 this.states.on( 'add', function( model ) { 4401 model.frame = this; 4402 model.trigger('ready'); 4403 }, this ); 4404 4405 if ( this.options.states ) { 4406 this.states.add( this.options.states ); 4407 } 4408 }, 4409 4410 /** 4411 * A frame can be in a mode or multiple modes at one time. 4412 * 4413 * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode. 4414 */ 4415 _createModes: function() { 4416 // Store active "modes" that the frame is in. Unrelated to region modes. 4417 this.activeModes = new Backbone.Collection(); 4418 this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) ); 4419 4420 _.each( this.options.mode, function( mode ) { 4421 this.activateMode( mode ); 4422 }, this ); 4423 }, 4424 /** 4425 * Reset all states on the frame to their defaults. 4426 * 4427 * @returns {wp.media.view.Frame} Returns itself to allow chaining 4428 */ 4429 reset: function() { 4430 this.states.invoke( 'trigger', 'reset' ); 4431 return this; 4432 }, 4433 /** 4434 * Map activeMode collection events to the frame. 4435 */ 4436 triggerModeEvents: function( model, collection, options ) { 4437 var collectionEvent, 4438 modeEventMap = { 4439 add: 'activate', 4440 remove: 'deactivate' 4441 }, 4442 eventToTrigger; 4443 // Probably a better way to do this. 4444 _.each( options, function( value, key ) { 4445 if ( value ) { 4446 collectionEvent = key; 4447 } 4448 } ); 4449 4450 if ( ! _.has( modeEventMap, collectionEvent ) ) { 4451 return; 4452 } 4453 4454 eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent]; 4455 this.trigger( eventToTrigger ); 4456 }, 4457 /** 4458 * Activate a mode on the frame. 4459 * 4460 * @param string mode Mode ID. 4461 * @returns {this} Returns itself to allow chaining. 4462 */ 4463 activateMode: function( mode ) { 4464 // Bail if the mode is already active. 4465 if ( this.isModeActive( mode ) ) { 4466 return; 4467 } 4468 this.activeModes.add( [ { id: mode } ] ); 4469 // Add a CSS class to the frame so elements can be styled for the mode. 4470 this.$el.addClass( 'mode-' + mode ); 4471 4472 return this; 4473 }, 4474 /** 4475 * Deactivate a mode on the frame. 4476 * 4477 * @param string mode Mode ID. 4478 * @returns {this} Returns itself to allow chaining. 4479 */ 4480 deactivateMode: function( mode ) { 4481 // Bail if the mode isn't active. 4482 if ( ! this.isModeActive( mode ) ) { 4483 return this; 4484 } 4485 this.activeModes.remove( this.activeModes.where( { id: mode } ) ); 4486 this.$el.removeClass( 'mode-' + mode ); 4487 /** 4488 * Frame mode deactivation event. 4489 * 4490 * @event this#{mode}:deactivate 4491 */ 4492 this.trigger( mode + ':deactivate' ); 4493 4494 return this; 4495 }, 4496 /** 4497 * Check if a mode is enabled on the frame. 4498 * 4499 * @param string mode Mode ID. 4500 * @return bool 4501 */ 4502 isModeActive: function( mode ) { 4503 return Boolean( this.activeModes.where( { id: mode } ).length ); 4504 } 4505 }); 4506 4507 // Make the `Frame` a `StateMachine`. 4508 _.extend( Frame.prototype, StateMachine.prototype ); 4509 4510 module.exports = Frame; 4511 },{"../controllers/region.js":5,"../controllers/state-machine.js":6,"../controllers/state.js":7,"./view.js":55}],29:[function(require,module,exports){ 4512 /** 4513 * wp.media.view.MediaFrame.AudioDetails 4514 * 4515 * @constructor 4516 * @augments wp.media.view.MediaFrame.MediaDetails 4517 * @augments wp.media.view.MediaFrame.Select 4518 * @augments wp.media.view.MediaFrame 4519 * @augments wp.media.view.Frame 4520 * @augments wp.media.View 4521 * @augments wp.Backbone.View 4522 * @augments Backbone.View 4523 * @mixes wp.media.controller.StateMachine 4524 */ 4525 var MediaDetails = require( './media-details' ), 4526 MediaLibrary = require( '../../controllers/media-library.js' ), 4527 AudioDetailsView = require( '../audio-details.js' ), 4528 AudioDetailsController = require( '../../controllers/audio-details.js' ), 4529 l10n = wp.media.view.l10n, 4530 AudioDetails; 4531 4532 AudioDetails = MediaDetails.extend({ 4533 defaults: { 4534 id: 'audio', 4535 url: '', 4536 menu: 'audio-details', 4537 content: 'audio-details', 4538 toolbar: 'audio-details', 4539 type: 'link', 4540 title: l10n.audioDetailsTitle, 4541 priority: 120 4542 }, 4543 4544 initialize: function( options ) { 4545 options.DetailsView = AudioDetailsView; 4546 options.cancelText = l10n.audioDetailsCancel; 4547 options.addText = l10n.audioAddSourceTitle; 4548 4549 MediaDetails.prototype.initialize.call( this, options ); 4550 }, 4551 4552 bindHandlers: function() { 4553 MediaDetails.prototype.bindHandlers.apply( this, arguments ); 4554 4555 this.on( 'toolbar:render:replace-audio', this.renderReplaceToolbar, this ); 4556 this.on( 'toolbar:render:add-audio-source', this.renderAddSourceToolbar, this ); 4557 }, 4558 4559 createStates: function() { 4560 this.states.add([ 4561 new AudioDetailsController( { 4562 media: this.media 4563 } ), 4564 4565 new MediaLibrary( { 4566 type: 'audio', 4567 id: 'replace-audio', 4568 title: l10n.audioReplaceTitle, 4569 toolbar: 'replace-audio', 4570 media: this.media, 4571 menu: 'audio-details' 4572 } ), 4573 4574 new MediaLibrary( { 4575 type: 'audio', 4576 id: 'add-audio-source', 4577 title: l10n.audioAddSourceTitle, 4578 toolbar: 'add-audio-source', 4579 media: this.media, 4580 menu: false 4581 } ) 4582 ]); 4583 } 4584 }); 4585 4586 module.exports = AudioDetails; 4587 },{"../../controllers/audio-details.js":2,"../../controllers/media-library.js":4,"../audio-details.js":25,"./media-details":30}],30:[function(require,module,exports){ 4588 /** 4589 * wp.media.view.MediaFrame.MediaDetails 4590 * 4591 * @constructor 4592 * @augments wp.media.view.MediaFrame.Select 4593 * @augments wp.media.view.MediaFrame 4594 * @augments wp.media.view.Frame 4595 * @augments wp.media.View 4596 * @augments wp.Backbone.View 4597 * @augments Backbone.View 4598 * @mixes wp.media.controller.StateMachine 4599 */ 4600 var View = require( '../view.js' ), 4601 Toolbar = require( '../toolbar.js' ), 4602 Select = require( './select.js' ), 4603 Selection = require( '../../models/selection.js' ), 4604 PostMedia = require( '../../models/post-media.js' ), 4605 l10n = wp.media.view.l10n, 4606 MediaDetails; 4607 4608 MediaDetails = Select.extend({ 4609 defaults: { 4610 id: 'media', 4611 url: '', 4612 menu: 'media-details', 4613 content: 'media-details', 4614 toolbar: 'media-details', 4615 type: 'link', 4616 priority: 120 4617 }, 4618 4619 initialize: function( options ) { 4620 this.DetailsView = options.DetailsView; 4621 this.cancelText = options.cancelText; 4622 this.addText = options.addText; 4623 4624 this.media = new PostMedia( options.metadata ); 4625 this.options.selection = new Selection( this.media.attachment, { multiple: false } ); 4626 Select.prototype.initialize.apply( this, arguments ); 4627 }, 4628 4629 bindHandlers: function() { 4630 var menu = this.defaults.menu; 4631 4632 Select.prototype.bindHandlers.apply( this, arguments ); 4633 4634 this.on( 'menu:create:' + menu, this.createMenu, this ); 4635 this.on( 'content:render:' + menu, this.renderDetailsContent, this ); 4636 this.on( 'menu:render:' + menu, this.renderMenu, this ); 4637 this.on( 'toolbar:render:' + menu, this.renderDetailsToolbar, this ); 4638 }, 4639 4640 renderDetailsContent: function() { 4641 var view = new this.DetailsView({ 4642 controller: this, 4643 model: this.state().media, 4644 attachment: this.state().media.attachment 4645 }).render(); 4646 4647 this.content.set( view ); 4648 }, 4649 4650 renderMenu: function( view ) { 4651 var lastState = this.lastState(), 4652 previous = lastState && lastState.id, 4653 frame = this; 4654 4655 view.set({ 4656 cancel: { 4657 text: this.cancelText, 4658 priority: 20, 4659 click: function() { 4660 if ( previous ) { 4661 frame.setState( previous ); 4662 } else { 4663 frame.close(); 4664 } 4665 } 4666 }, 4667 separateCancel: new View({ 4668 className: 'separator', 4669 priority: 40 4670 }) 4671 }); 4672 4673 }, 4674 4675 setPrimaryButton: function(text, handler) { 4676 this.toolbar.set( new Toolbar({ 4677 controller: this, 4678 items: { 4679 button: { 4680 style: 'primary', 4681 text: text, 4682 priority: 80, 4683 click: function() { 4684 var controller = this.controller; 4685 handler.call( this, controller, controller.state() ); 4686 // Restore and reset the default state. 4687 controller.setState( controller.options.state ); 4688 controller.reset(); 4689 } 4690 } 4691 } 4692 }) ); 4693 }, 4694 4695 renderDetailsToolbar: function() { 4696 this.setPrimaryButton( l10n.update, function( controller, state ) { 4697 controller.close(); 4698 state.trigger( 'update', controller.media.toJSON() ); 4699 } ); 4700 }, 4701 4702 renderReplaceToolbar: function() { 4703 this.setPrimaryButton( l10n.replace, function( controller, state ) { 4704 var attachment = state.get( 'selection' ).single(); 4705 controller.media.changeAttachment( attachment ); 4706 state.trigger( 'replace', controller.media.toJSON() ); 4707 } ); 4708 }, 4709 4710 renderAddSourceToolbar: function() { 4711 this.setPrimaryButton( this.addText, function( controller, state ) { 4712 var attachment = state.get( 'selection' ).single(); 4713 controller.media.setSource( attachment ); 4714 state.trigger( 'add-source', controller.media.toJSON() ); 4715 } ); 4716 } 4717 }); 4718 4719 module.exports = MediaDetails; 4720 },{"../../models/post-media.js":11,"../../models/selection.js":13,"../toolbar.js":48,"../view.js":55,"./select.js":31}],31:[function(require,module,exports){ 4721 /** 4722 * wp.media.view.MediaFrame.Select 4723 * 4724 * A frame for selecting an item or items from the media library. 4725 * 4726 * @class 4727 * @augments wp.media.view.MediaFrame 4728 * @augments wp.media.view.Frame 4729 * @augments wp.media.View 4730 * @augments wp.Backbone.View 4731 * @augments Backbone.View 4732 * @mixes wp.media.controller.StateMachine 4733 */ 4734 4735 var MediaFrame = require( '../media-frame.js' ), 4736 Library = require( '../../controllers/library.js' ), 4737 AttachmentsModel = require( '../../models/attachments.js' ), 4738 SelectionModel = require( '../../models/selection.js' ), 4739 AttachmentsBrowser = require( '../attachments/browser.js' ), 4740 UploaderInline = require( '../uploader/inline.js' ), 4741 ToolbarSelect = require( '../toolbar/select.js' ), 4742 l10n = wp.media.view.l10n, 4743 Select; 4744 4745 Select = MediaFrame.extend({ 4746 initialize: function() { 4747 // Call 'initialize' directly on the parent class. 4748 MediaFrame.prototype.initialize.apply( this, arguments ); 4749 4750 _.defaults( this.options, { 4751 selection: [], 4752 library: {}, 4753 multiple: false, 4754 state: 'library' 4755 }); 4756 4757 this.createSelection(); 4758 this.createStates(); 4759 this.bindHandlers(); 4760 }, 4761 4762 /** 4763 * Attach a selection collection to the frame. 4764 * 4765 * A selection is a collection of attachments used for a specific purpose 4766 * by a media frame. e.g. Selecting an attachment (or many) to insert into 4767 * post content. 4768 * 4769 * @see media.model.Selection 4770 */ 4771 createSelection: function() { 4772 var selection = this.options.selection; 4773 4774 if ( ! (selection instanceof SelectionModel) ) { 4775 this.options.selection = new SelectionModel( selection, { 4776 multiple: this.options.multiple 4777 }); 4778 } 4779 4780 this._selection = { 4781 attachments: new AttachmentsModel(), 4782 difference: [] 4783 }; 4784 }, 4785 4786 /** 4787 * Create the default states on the frame. 4788 */ 4789 createStates: function() { 4790 var options = this.options; 4791 4792 if ( this.options.states ) { 4793 return; 4794 } 4795 4796 // Add the default states. 4797 this.states.add([ 4798 // Main states. 4799 new Library({ 4800 library: wp.media.query( options.library ), 4801 multiple: options.multiple, 4802 title: options.title, 4803 priority: 20 4804 }) 4805 ]); 4806 }, 4807 4808 /** 4809 * Bind region mode event callbacks. 4810 * 4811 * @see media.controller.Region.render 4812 */ 4813 bindHandlers: function() { 4814 this.on( 'router:create:browse', this.createRouter, this ); 4815 this.on( 'router:render:browse', this.browseRouter, this ); 4816 this.on( 'content:create:browse', this.browseContent, this ); 4817 this.on( 'content:render:upload', this.uploadContent, this ); 4818 this.on( 'toolbar:create:select', this.createSelectToolbar, this ); 4819 }, 4820 4821 /** 4822 * Render callback for the router region in the `browse` mode. 4823 * 4824 * @param {wp.media.view.Router} routerView 4825 */ 4826 browseRouter: function( routerView ) { 4827 routerView.set({ 4828 upload: { 4829 text: l10n.uploadFilesTitle, 4830 priority: 20 4831 }, 4832 browse: { 4833 text: l10n.mediaLibraryTitle, 4834 priority: 40 4835 } 4836 }); 4837 }, 4838 4839 /** 4840 * Render callback for the content region in the `browse` mode. 4841 * 4842 * @param {wp.media.controller.Region} contentRegion 4843 */ 4844 browseContent: function( contentRegion ) { 4845 var state = this.state(); 4846 4847 this.$el.removeClass('hide-toolbar'); 4848 4849 // Browse our library of attachments. 4850 contentRegion.view = new AttachmentsBrowser({ 4851 controller: this, 4852 collection: state.get('library'), 4853 selection: state.get('selection'), 4854 model: state, 4855 sortable: state.get('sortable'), 4856 search: state.get('searchable'), 4857 filters: state.get('filterable'), 4858 display: state.has('display') ? state.get('display') : state.get('displaySettings'), 4859 dragInfo: state.get('dragInfo'), 4860 4861 idealColumnWidth: state.get('idealColumnWidth'), 4862 suggestedWidth: state.get('suggestedWidth'), 4863 suggestedHeight: state.get('suggestedHeight'), 4864 4865 AttachmentView: state.get('AttachmentView') 4866 }); 4867 }, 4868 4869 /** 4870 * Render callback for the content region in the `upload` mode. 4871 */ 4872 uploadContent: function() { 4873 this.$el.removeClass( 'hide-toolbar' ); 4874 this.content.set( new UploaderInline({ 4875 controller: this 4876 }) ); 4877 }, 4878 4879 /** 4880 * Toolbars 4881 * 4882 * @param {Object} toolbar 4883 * @param {Object} [options={}] 4884 * @this wp.media.controller.Region 4885 */ 4886 createSelectToolbar: function( toolbar, options ) { 4887 options = options || this.options.button || {}; 4888 options.controller = this; 4889 4890 toolbar.view = new ToolbarSelect( options ); 4891 } 4892 }); 4893 4894 module.exports = Select; 4895 },{"../../controllers/library.js":3,"../../models/attachments.js":10,"../../models/selection.js":13,"../attachments/browser.js":24,"../media-frame.js":36,"../toolbar/select.js":49,"../uploader/inline.js":50}],32:[function(require,module,exports){ 4896 /** 4897 * wp.media.view.MediaFrame.VideoDetails 4898 * 4899 * @constructor 4900 * @augments wp.media.view.MediaFrame.MediaDetails 4901 * @augments wp.media.view.MediaFrame.Select 4902 * @augments wp.media.view.MediaFrame 4903 * @augments wp.media.view.Frame 4904 * @augments wp.media.View 4905 * @augments wp.Backbone.View 4906 * @augments Backbone.View 4907 * @mixes wp.media.controller.StateMachine 4908 */ 4909 var MediaDetails = require( './media-details' ), 4910 MediaLibrary = require( '../../controllers/media-library.js' ), 4911 VideoDetailsView = require( '../video-details.js' ), 4912 VideoDetailsController = require( '../../controllers/video-details.js' ), 4913 l10n = wp.media.view.l10n, 4914 VideoDetails; 4915 4916 VideoDetails = MediaDetails.extend({ 4917 defaults: { 4918 id: 'video', 4919 url: '', 4920 menu: 'video-details', 4921 content: 'video-details', 4922 toolbar: 'video-details', 4923 type: 'link', 4924 title: l10n.videoDetailsTitle, 4925 priority: 120 4926 }, 4927 4928 initialize: function( options ) { 4929 options.DetailsView = VideoDetailsView; 4930 options.cancelText = l10n.videoDetailsCancel; 4931 options.addText = l10n.videoAddSourceTitle; 4932 4933 MediaDetails.prototype.initialize.call( this, options ); 4934 }, 4935 4936 bindHandlers: function() { 4937 MediaDetails.prototype.bindHandlers.apply( this, arguments ); 4938 4939 this.on( 'toolbar:render:replace-video', this.renderReplaceToolbar, this ); 4940 this.on( 'toolbar:render:add-video-source', this.renderAddSourceToolbar, this ); 4941 this.on( 'toolbar:render:select-poster-image', this.renderSelectPosterImageToolbar, this ); 4942 this.on( 'toolbar:render:add-track', this.renderAddTrackToolbar, this ); 4943 }, 4944 4945 createStates: function() { 4946 this.states.add([ 4947 new VideoDetailsController({ 4948 media: this.media 4949 }), 4950 4951 new MediaLibrary( { 4952 type: 'video', 4953 id: 'replace-video', 4954 title: l10n.videoReplaceTitle, 4955 toolbar: 'replace-video', 4956 media: this.media, 4957 menu: 'video-details' 4958 } ), 4959 4960 new MediaLibrary( { 4961 type: 'video', 4962 id: 'add-video-source', 4963 title: l10n.videoAddSourceTitle, 4964 toolbar: 'add-video-source', 4965 media: this.media, 4966 menu: false 4967 } ), 4968 4969 new MediaLibrary( { 4970 type: 'image', 4971 id: 'select-poster-image', 4972 title: l10n.videoSelectPosterImageTitle, 4973 toolbar: 'select-poster-image', 4974 media: this.media, 4975 menu: 'video-details' 4976 } ), 4977 4978 new MediaLibrary( { 4979 type: 'text', 4980 id: 'add-track', 4981 title: l10n.videoAddTrackTitle, 4982 toolbar: 'add-track', 4983 media: this.media, 4984 menu: 'video-details' 4985 } ) 4986 ]); 4987 }, 4988 4989 renderSelectPosterImageToolbar: function() { 4990 this.setPrimaryButton( l10n.videoSelectPosterImageTitle, function( controller, state ) { 4991 var urls = [], attachment = state.get( 'selection' ).single(); 4992 4993 controller.media.set( 'poster', attachment.get( 'url' ) ); 4994 state.trigger( 'set-poster-image', controller.media.toJSON() ); 4995 4996 _.each( wp.media.view.settings.embedExts, function (ext) { 4997 if ( controller.media.get( ext ) ) { 4998 urls.push( controller.media.get( ext ) ); 4999 } 5000 } ); 5001 5002 wp.ajax.send( 'set-attachment-thumbnail', { 5003 data : { 5004 urls: urls, 5005 thumbnail_id: attachment.get( 'id' ) 5006 } 5007 } ); 5008 } ); 5009 }, 5010 5011 renderAddTrackToolbar: function() { 5012 this.setPrimaryButton( l10n.videoAddTrackTitle, function( controller, state ) { 5013 var attachment = state.get( 'selection' ).single(), 5014 content = controller.media.get( 'content' ); 5015 5016 if ( -1 === content.indexOf( attachment.get( 'url' ) ) ) { 5017 content += [ 5018 '<track srclang="en" label="English"kind="subtitles" src="', 5019 attachment.get( 'url' ), 5020 '" />' 5021 ].join(''); 5022 5023 controller.media.set( 'content', content ); 5024 } 5025 state.trigger( 'add-track', controller.media.toJSON() ); 5026 } ); 5027 } 5028 }); 5029 5030 module.exports = VideoDetails; 5031 },{"../../controllers/media-library.js":4,"../../controllers/video-details.js":8,"../video-details.js":54,"./media-details":30}],33:[function(require,module,exports){ 5032 /** 5033 * wp.media.view.Iframe 5034 * 5035 * @class 5036 * @augments wp.media.View 5037 * @augments wp.Backbone.View 5038 * @augments Backbone.View 5039 */ 5040 var View = require( './view.js' ), 5041 Iframe; 5042 5043 Iframe = View.extend({ 5044 className: 'media-iframe', 5045 /** 5046 * @returns {wp.media.view.Iframe} Returns itself to allow chaining 5047 */ 5048 render: function() { 5049 this.views.detach(); 5050 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' ); 5051 this.views.render(); 5052 return this; 5053 } 5054 }); 5055 5056 module.exports = Iframe; 5057 },{"./view.js":55}],34:[function(require,module,exports){ 5058 /** 5059 * @class 5060 * @augments wp.media.View 5061 * @augments wp.Backbone.View 5062 * @augments Backbone.View 5063 */ 5064 var View = require( './view.js' ), 5065 Label; 5066 5067 Label = View.extend({ 5068 tagName: 'label', 5069 className: 'screen-reader-text', 5070 5071 initialize: function() { 5072 this.value = this.options.value; 5073 }, 5074 5075 render: function() { 5076 this.$el.html( this.value ); 5077 5078 return this; 5079 } 5080 }); 5081 5082 module.exports = Label; 5083 },{"./view.js":55}],35:[function(require,module,exports){ 5084 /** 5085 * wp.media.view.MediaDetails 5086 * 5087 * @constructor 5088 * @augments wp.media.view.Settings.AttachmentDisplay 5089 * @augments wp.media.view.Settings 5090 * @augments wp.media.View 5091 * @augments wp.Backbone.View 5092 * @augments Backbone.View 5093 */ 5094 var AttachmentDisplay = require( './settings/attachment-display.js' ), 5095 $ = jQuery, 5096 MediaDetails; 5097 5098 MediaDetails = AttachmentDisplay.extend({ 5099 initialize: function() { 5100 _.bindAll(this, 'success'); 5101 this.players = []; 5102 this.listenTo( this.controller, 'close', wp.media.mixin.unsetPlayers ); 5103 this.on( 'ready', this.setPlayer ); 5104 this.on( 'media:setting:remove', wp.media.mixin.unsetPlayers, this ); 5105 this.on( 'media:setting:remove', this.render ); 5106 this.on( 'media:setting:remove', this.setPlayer ); 5107 this.events = _.extend( this.events, { 5108 'click .remove-setting' : 'removeSetting', 5109 'change .content-track' : 'setTracks', 5110 'click .remove-track' : 'setTracks', 5111 'click .add-media-source' : 'addSource' 5112 } ); 5113 5114 AttachmentDisplay.prototype.initialize.apply( this, arguments ); 5115 }, 5116 5117 prepare: function() { 5118 return _.defaults({ 5119 model: this.model.toJSON() 5120 }, this.options ); 5121 }, 5122 5123 /** 5124 * Remove a setting's UI when the model unsets it 5125 * 5126 * @fires wp.media.view.MediaDetails#media:setting:remove 5127 * 5128 * @param {Event} e 5129 */ 5130 removeSetting : function(e) { 5131 var wrap = $( e.currentTarget ).parent(), setting; 5132 setting = wrap.find( 'input' ).data( 'setting' ); 5133 5134 if ( setting ) { 5135 this.model.unset( setting ); 5136 this.trigger( 'media:setting:remove', this ); 5137 } 5138 5139 wrap.remove(); 5140 }, 5141 5142 /** 5143 * 5144 * @fires wp.media.view.MediaDetails#media:setting:remove 5145 */ 5146 setTracks : function() { 5147 var tracks = ''; 5148 5149 _.each( this.$('.content-track'), function(track) { 5150 tracks += $( track ).val(); 5151 } ); 5152 5153 this.model.set( 'content', tracks ); 5154 this.trigger( 'media:setting:remove', this ); 5155 }, 5156 5157 addSource : function( e ) { 5158 this.controller.lastMime = $( e.currentTarget ).data( 'mime' ); 5159 this.controller.setState( 'add-' + this.controller.defaults.id + '-source' ); 5160 }, 5161 5162 /** 5163 * @global MediaElementPlayer 5164 */ 5165 setPlayer : function() { 5166 if ( ! this.players.length && this.media ) { 5167 this.players.push( new MediaElementPlayer( this.media, this.settings ) ); 5168 } 5169 }, 5170 5171 /** 5172 * @abstract 5173 */ 5174 setMedia : function() { 5175 return this; 5176 }, 5177 5178 success : function(mejs) { 5179 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; 5180 5181 if ( 'flash' === mejs.pluginType && autoplay ) { 5182 mejs.addEventListener( 'canplay', function() { 5183 mejs.play(); 5184 }, false ); 5185 } 5186 5187 this.mejs = mejs; 5188 }, 5189 5190 /** 5191 * @returns {media.view.MediaDetails} Returns itself to allow chaining 5192 */ 5193 render: function() { 5194 var self = this; 5195 5196 AttachmentDisplay.prototype.render.apply( this, arguments ); 5197 setTimeout( function() { self.resetFocus(); }, 10 ); 5198 5199 this.settings = _.defaults( { 5200 success : this.success 5201 }, wp.media.mixin.mejsSettings ); 5202 5203 return this.setMedia(); 5204 }, 5205 5206 resetFocus: function() { 5207 this.$( '.embed-media-settings' ).scrollTop( 0 ); 5208 } 5209 }, { 5210 instances : 0, 5211 /** 5212 * When multiple players in the DOM contain the same src, things get weird. 5213 * 5214 * @param {HTMLElement} elem 5215 * @returns {HTMLElement} 5216 */ 5217 prepareSrc : function( elem ) { 5218 var i = MediaDetails.instances++; 5219 _.each( $( elem ).find( 'source' ), function( source ) { 5220 source.src = [ 5221 source.src, 5222 source.src.indexOf('?') > -1 ? '&' : '?', 5223 '_=', 5224 i 5225 ].join(''); 5226 } ); 5227 5228 return elem; 5229 } 5230 }); 5231 5232 module.exports = MediaDetails; 5233 },{"./settings/attachment-display.js":45}],36:[function(require,module,exports){ 5234 /** 5235 * wp.media.view.MediaFrame 5236 * 5237 * The frame used to create the media modal. 5238 * 5239 * @class 5240 * @augments wp.media.view.Frame 5241 * @augments wp.media.View 5242 * @augments wp.Backbone.View 5243 * @augments Backbone.View 5244 * @mixes wp.media.controller.StateMachine 5245 */ 5246 var View = require( './view.js' ), 5247 Frame = require( './frame.js' ), 5248 Modal = require( './modal.js' ), 5249 UploaderWindow = require( './uploader/window.js' ), 5250 Menu = require( './menu.js' ), 5251 Toolbar = require( './toolbar.js' ), 5252 Router = require( './router.js' ), 5253 Iframe = require( './iframe.js' ), 5254 $ = jQuery, 5255 MediaFrame; 5256 5257 MediaFrame = Frame.extend({ 5258 className: 'media-frame', 5259 template: wp.template('media-frame'), 5260 regions: ['menu','title','content','toolbar','router'], 5261 5262 events: { 5263 'click div.media-frame-title h1': 'toggleMenu' 5264 }, 5265 5266 /** 5267 * @global wp.Uploader 5268 */ 5269 initialize: function() { 5270 Frame.prototype.initialize.apply( this, arguments ); 5271 5272 _.defaults( this.options, { 5273 title: '', 5274 modal: true, 5275 uploader: true 5276 }); 5277 5278 // Ensure core UI is enabled. 5279 this.$el.addClass('wp-core-ui'); 5280 5281 // Initialize modal container view. 5282 if ( this.options.modal ) { 5283 this.modal = new Modal({ 5284 controller: this, 5285 title: this.options.title 5286 }); 5287 5288 this.modal.content( this ); 5289 } 5290 5291 // Force the uploader off if the upload limit has been exceeded or 5292 // if the browser isn't supported. 5293 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 5294 this.options.uploader = false; 5295 } 5296 5297 // Initialize window-wide uploader. 5298 if ( this.options.uploader ) { 5299 this.uploader = new UploaderWindow({ 5300 controller: this, 5301 uploader: { 5302 dropzone: this.modal ? this.modal.$el : this.$el, 5303 container: this.$el 5304 } 5305 }); 5306 this.views.set( '.media-frame-uploader', this.uploader ); 5307 } 5308 5309 this.on( 'attach', _.bind( this.views.ready, this.views ), this ); 5310 5311 // Bind default title creation. 5312 this.on( 'title:create:default', this.createTitle, this ); 5313 this.title.mode('default'); 5314 5315 this.on( 'title:render', function( view ) { 5316 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' ); 5317 }); 5318 5319 // Bind default menu. 5320 this.on( 'menu:create:default', this.createMenu, this ); 5321 }, 5322 /** 5323 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 5324 */ 5325 render: function() { 5326 // Activate the default state if no active state exists. 5327 if ( ! this.state() && this.options.state ) { 5328 this.setState( this.options.state ); 5329 } 5330 /** 5331 * call 'render' directly on the parent class 5332 */ 5333 return Frame.prototype.render.apply( this, arguments ); 5334 }, 5335 /** 5336 * @param {Object} title 5337 * @this wp.media.controller.Region 5338 */ 5339 createTitle: function( title ) { 5340 title.view = new View({ 5341 controller: this, 5342 tagName: 'h1' 5343 }); 5344 }, 5345 /** 5346 * @param {Object} menu 5347 * @this wp.media.controller.Region 5348 */ 5349 createMenu: function( menu ) { 5350 menu.view = new Menu({ 5351 controller: this 5352 }); 5353 }, 5354 5355 toggleMenu: function() { 5356 this.$el.find( '.media-menu' ).toggleClass( 'visible' ); 5357 }, 5358 5359 /** 5360 * @param {Object} toolbar 5361 * @this wp.media.controller.Region 5362 */ 5363 createToolbar: function( toolbar ) { 5364 toolbar.view = new Toolbar({ 5365 controller: this 5366 }); 5367 }, 5368 /** 5369 * @param {Object} router 5370 * @this wp.media.controller.Region 5371 */ 5372 createRouter: function( router ) { 5373 router.view = new Router({ 5374 controller: this 5375 }); 5376 }, 5377 /** 5378 * @param {Object} options 5379 */ 5380 createIframeStates: function( options ) { 5381 var settings = wp.media.view.settings, 5382 tabs = settings.tabs, 5383 tabUrl = settings.tabUrl, 5384 $postId; 5385 5386 if ( ! tabs || ! tabUrl ) { 5387 return; 5388 } 5389 5390 // Add the post ID to the tab URL if it exists. 5391 $postId = $('#post_ID'); 5392 if ( $postId.length ) { 5393 tabUrl += '&post_id=' + $postId.val(); 5394 } 5395 5396 // Generate the tab states. 5397 _.each( tabs, function( title, id ) { 5398 this.state( 'iframe:' + id ).set( _.defaults({ 5399 tab: id, 5400 src: tabUrl + '&tab=' + id, 5401 title: title, 5402 content: 'iframe', 5403 menu: 'default' 5404 }, options ) ); 5405 }, this ); 5406 5407 this.on( 'content:create:iframe', this.iframeContent, this ); 5408 this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this ); 5409 this.on( 'menu:render:default', this.iframeMenu, this ); 5410 this.on( 'open', this.hijackThickbox, this ); 5411 this.on( 'close', this.restoreThickbox, this ); 5412 }, 5413 5414 /** 5415 * @param {Object} content 5416 * @this wp.media.controller.Region 5417 */ 5418 iframeContent: function( content ) { 5419 this.$el.addClass('hide-toolbar'); 5420 content.view = new Iframe({ 5421 controller: this 5422 }); 5423 }, 5424 5425 iframeContentCleanup: function() { 5426 this.$el.removeClass('hide-toolbar'); 5427 }, 5428 5429 iframeMenu: function( view ) { 5430 var views = {}; 5431 5432 if ( ! view ) { 5433 return; 5434 } 5435 5436 _.each( wp.media.view.settings.tabs, function( title, id ) { 5437 views[ 'iframe:' + id ] = { 5438 text: this.state( 'iframe:' + id ).get('title'), 5439 priority: 200 5440 }; 5441 }, this ); 5442 5443 view.set( views ); 5444 }, 5445 5446 hijackThickbox: function() { 5447 var frame = this; 5448 5449 if ( ! window.tb_remove || this._tb_remove ) { 5450 return; 5451 } 5452 5453 this._tb_remove = window.tb_remove; 5454 window.tb_remove = function() { 5455 frame.close(); 5456 frame.reset(); 5457 frame.setState( frame.options.state ); 5458 frame._tb_remove.call( window ); 5459 }; 5460 }, 5461 5462 restoreThickbox: function() { 5463 if ( ! this._tb_remove ) { 5464 return; 5465 } 5466 5467 window.tb_remove = this._tb_remove; 5468 delete this._tb_remove; 5469 } 5470 }); 5471 5472 // Map some of the modal's methods to the frame. 5473 _.each(['open','close','attach','detach','escape'], function( method ) { 5474 /** 5475 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 5476 */ 5477 MediaFrame.prototype[ method ] = function() { 5478 if ( this.modal ) { 5479 this.modal[ method ].apply( this.modal, arguments ); 5480 } 5481 return this; 5482 }; 5483 }); 5484 5485 module.exports = MediaFrame; 5486 },{"./frame.js":28,"./iframe.js":33,"./menu.js":38,"./modal.js":39,"./router.js":42,"./toolbar.js":48,"./uploader/window.js":53,"./view.js":55}],37:[function(require,module,exports){ 5487 /** 5488 * wp.media.view.MenuItem 5489 * 5490 * @class 5491 * @augments wp.media.View 5492 * @augments wp.Backbone.View 5493 * @augments Backbone.View 5494 */ 5495 var View = require( './view.js' ), 5496 $ = jQuery, 5497 MenuItem; 5498 5499 MenuItem = View.extend({ 5500 tagName: 'a', 5501 className: 'media-menu-item', 5502 5503 attributes: { 5504 href: '#' 5505 }, 5506 5507 events: { 5508 'click': '_click' 5509 }, 5510 /** 5511 * @param {Object} event 5512 */ 5513 _click: function( event ) { 5514 var clickOverride = this.options.click; 5515 5516 if ( event ) { 5517 event.preventDefault(); 5518 } 5519 5520 if ( clickOverride ) { 5521 clickOverride.call( this ); 5522 } else { 5523 this.click(); 5524 } 5525 5526 // When selecting a tab along the left side, 5527 // focus should be transferred into the main panel 5528 if ( ! wp.media.isTouchDevice ) { 5529 $('.media-frame-content input').first().focus(); 5530 } 5531 }, 5532 5533 click: function() { 5534 var state = this.options.state; 5535 5536 if ( state ) { 5537 this.controller.setState( state ); 5538 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below 5539 } 5540 }, 5541 /** 5542 * @returns {wp.media.view.MenuItem} returns itself to allow chaining 5543 */ 5544 render: function() { 5545 var options = this.options; 5546 5547 if ( options.text ) { 5548 this.$el.text( options.text ); 5549 } else if ( options.html ) { 5550 this.$el.html( options.html ); 5551 } 5552 5553 return this; 5554 } 5555 }); 5556 5557 module.exports = MenuItem; 5558 },{"./view.js":55}],38:[function(require,module,exports){ 5559 /** 5560 * wp.media.view.Menu 5561 * 5562 * @class 5563 * @augments wp.media.view.PriorityList 5564 * @augments wp.media.View 5565 * @augments wp.Backbone.View 5566 * @augments Backbone.View 5567 */ 5568 var MenuItem = require( './menu-item.js' ), 5569 PriorityList = require( './priority-list.js' ), 5570 Menu; 5571 5572 Menu = PriorityList.extend({ 5573 tagName: 'div', 5574 className: 'media-menu', 5575 property: 'state', 5576 ItemView: MenuItem, 5577 region: 'menu', 5578 5579 /* TODO: alternatively hide on any click anywhere 5580 events: { 5581 'click': 'click' 5582 }, 5583 5584 click: function() { 5585 this.$el.removeClass( 'visible' ); 5586 }, 5587 */ 5588 5589 /** 5590 * @param {Object} options 5591 * @param {string} id 5592 * @returns {wp.media.View} 5593 */ 5594 toView: function( options, id ) { 5595 options = options || {}; 5596 options[ this.property ] = options[ this.property ] || id; 5597 return new this.ItemView( options ).render(); 5598 }, 5599 5600 ready: function() { 5601 /** 5602 * call 'ready' directly on the parent class 5603 */ 5604 PriorityList.prototype.ready.apply( this, arguments ); 5605 this.visibility(); 5606 }, 5607 5608 set: function() { 5609 /** 5610 * call 'set' directly on the parent class 5611 */ 5612 PriorityList.prototype.set.apply( this, arguments ); 5613 this.visibility(); 5614 }, 5615 5616 unset: function() { 5617 /** 5618 * call 'unset' directly on the parent class 5619 */ 5620 PriorityList.prototype.unset.apply( this, arguments ); 5621 this.visibility(); 5622 }, 5623 5624 visibility: function() { 5625 var region = this.region, 5626 view = this.controller[ region ].get(), 5627 views = this.views.get(), 5628 hide = ! views || views.length < 2; 5629 5630 if ( this === view ) { 5631 this.controller.$el.toggleClass( 'hide-' + region, hide ); 5632 } 5633 }, 5634 /** 5635 * @param {string} id 5636 */ 5637 select: function( id ) { 5638 var view = this.get( id ); 5639 5640 if ( ! view ) { 5641 return; 5642 } 5643 5644 this.deselect(); 5645 view.$el.addClass('active'); 5646 }, 5647 5648 deselect: function() { 5649 this.$el.children().removeClass('active'); 5650 }, 5651 5652 hide: function( id ) { 5653 var view = this.get( id ); 5654 5655 if ( ! view ) { 5656 return; 5657 } 5658 5659 view.$el.addClass('hidden'); 5660 }, 5661 5662 show: function( id ) { 5663 var view = this.get( id ); 5664 5665 if ( ! view ) { 5666 return; 5667 } 5668 5669 view.$el.removeClass('hidden'); 5670 } 5671 }); 5672 5673 module.exports = Menu; 5674 },{"./menu-item.js":37,"./priority-list.js":40}],39:[function(require,module,exports){ 5675 /** 5676 * wp.media.view.Modal 5677 * 5678 * A modal view, which the media modal uses as its default container. 5679 * 5680 * @class 5681 * @augments wp.media.View 5682 * @augments wp.Backbone.View 5683 * @augments Backbone.View 5684 */ 5685 var View = require( './view.js' ), 5686 FocusManager = require( './focus-manager.js' ), 5687 $ = jQuery, 5688 Modal; 5689 5690 Modal = View.extend({ 5691 tagName: 'div', 5692 template: wp.template('media-modal'), 5693 5694 attributes: { 5695 tabindex: 0 5696 }, 5697 5698 events: { 5699 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 5700 'keydown': 'keydown' 5701 }, 5702 5703 initialize: function() { 5704 _.defaults( this.options, { 5705 container: document.body, 5706 title: '', 5707 propagate: true, 5708 freeze: true 5709 }); 5710 5711 this.focusManager = new FocusManager({ 5712 el: this.el 5713 }); 5714 }, 5715 /** 5716 * @returns {Object} 5717 */ 5718 prepare: function() { 5719 return { 5720 title: this.options.title 5721 }; 5722 }, 5723 5724 /** 5725 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5726 */ 5727 attach: function() { 5728 if ( this.views.attached ) { 5729 return this; 5730 } 5731 5732 if ( ! this.views.rendered ) { 5733 this.render(); 5734 } 5735 5736 this.$el.appendTo( this.options.container ); 5737 5738 // Manually mark the view as attached and trigger ready. 5739 this.views.attached = true; 5740 this.views.ready(); 5741 5742 return this.propagate('attach'); 5743 }, 5744 5745 /** 5746 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5747 */ 5748 detach: function() { 5749 if ( this.$el.is(':visible') ) { 5750 this.close(); 5751 } 5752 5753 this.$el.detach(); 5754 this.views.attached = false; 5755 return this.propagate('detach'); 5756 }, 5757 5758 /** 5759 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5760 */ 5761 open: function() { 5762 var $el = this.$el, 5763 options = this.options, 5764 mceEditor; 5765 5766 if ( $el.is(':visible') ) { 5767 return this; 5768 } 5769 5770 if ( ! this.views.attached ) { 5771 this.attach(); 5772 } 5773 5774 // If the `freeze` option is set, record the window's scroll position. 5775 if ( options.freeze ) { 5776 this._freeze = { 5777 scrollTop: $( window ).scrollTop() 5778 }; 5779 } 5780 5781 // Disable page scrolling. 5782 $( 'body' ).addClass( 'modal-open' ); 5783 5784 $el.show(); 5785 5786 // Try to close the onscreen keyboard 5787 if ( 'ontouchend' in document ) { 5788 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) { 5789 mceEditor.iframeElement.focus(); 5790 mceEditor.iframeElement.blur(); 5791 5792 setTimeout( function() { 5793 mceEditor.iframeElement.blur(); 5794 }, 100 ); 5795 } 5796 } 5797 5798 this.$el.focus(); 5799 5800 return this.propagate('open'); 5801 }, 5802 5803 /** 5804 * @param {Object} options 5805 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5806 */ 5807 close: function( options ) { 5808 var freeze = this._freeze; 5809 5810 if ( ! this.views.attached || ! this.$el.is(':visible') ) { 5811 return this; 5812 } 5813 5814 // Enable page scrolling. 5815 $( 'body' ).removeClass( 'modal-open' ); 5816 5817 // Hide modal and remove restricted media modal tab focus once it's closed 5818 this.$el.hide().undelegate( 'keydown' ); 5819 5820 // Put focus back in useful location once modal is closed 5821 $('#wpbody-content').focus(); 5822 5823 this.propagate('close'); 5824 5825 // If the `freeze` option is set, restore the container's scroll position. 5826 if ( freeze ) { 5827 $( window ).scrollTop( freeze.scrollTop ); 5828 } 5829 5830 if ( options && options.escape ) { 5831 this.propagate('escape'); 5832 } 5833 5834 return this; 5835 }, 5836 /** 5837 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5838 */ 5839 escape: function() { 5840 return this.close({ escape: true }); 5841 }, 5842 /** 5843 * @param {Object} event 5844 */ 5845 escapeHandler: function( event ) { 5846 event.preventDefault(); 5847 this.escape(); 5848 }, 5849 5850 /** 5851 * @param {Array|Object} content Views to register to '.media-modal-content' 5852 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5853 */ 5854 content: function( content ) { 5855 this.views.set( '.media-modal-content', content ); 5856 return this; 5857 }, 5858 5859 /** 5860 * Triggers a modal event and if the `propagate` option is set, 5861 * forwards events to the modal's controller. 5862 * 5863 * @param {string} id 5864 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5865 */ 5866 propagate: function( id ) { 5867 this.trigger( id ); 5868 5869 if ( this.options.propagate ) { 5870 this.controller.trigger( id ); 5871 } 5872 5873 return this; 5874 }, 5875 /** 5876 * @param {Object} event 5877 */ 5878 keydown: function( event ) { 5879 // Close the modal when escape is pressed. 5880 if ( 27 === event.which && this.$el.is(':visible') ) { 5881 this.escape(); 5882 event.stopImmediatePropagation(); 5883 } 5884 } 5885 }); 5886 5887 module.exports = Modal; 5888 },{"./focus-manager.js":27,"./view.js":55}],40:[function(require,module,exports){ 5889 /** 5890 * wp.media.view.PriorityList 5891 * 5892 * @class 5893 * @augments wp.media.View 5894 * @augments wp.Backbone.View 5895 * @augments Backbone.View 5896 */ 5897 var View = require( './view.js' ), 5898 PriorityList; 5899 5900 PriorityList = View.extend({ 5901 tagName: 'div', 5902 5903 initialize: function() { 5904 this._views = {}; 5905 5906 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 5907 delete this.options.views; 5908 5909 if ( ! this.options.silent ) { 5910 this.render(); 5911 } 5912 }, 5913 /** 5914 * @param {string} id 5915 * @param {wp.media.View|Object} view 5916 * @param {Object} options 5917 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining 5918 */ 5919 set: function( id, view, options ) { 5920 var priority, views, index; 5921 5922 options = options || {}; 5923 5924 // Accept an object with an `id` : `view` mapping. 5925 if ( _.isObject( id ) ) { 5926 _.each( id, function( view, id ) { 5927 this.set( id, view ); 5928 }, this ); 5929 return this; 5930 } 5931 5932 if ( ! (view instanceof Backbone.View) ) { 5933 view = this.toView( view, id, options ); 5934 } 5935 view.controller = view.controller || this.controller; 5936 5937 this.unset( id ); 5938 5939 priority = view.options.priority || 10; 5940 views = this.views.get() || []; 5941 5942 _.find( views, function( existing, i ) { 5943 if ( existing.options.priority > priority ) { 5944 index = i; 5945 return true; 5946 } 5947 }); 5948 5949 this._views[ id ] = view; 5950 this.views.add( view, { 5951 at: _.isNumber( index ) ? index : views.length || 0 5952 }); 5953 5954 return this; 5955 }, 5956 /** 5957 * @param {string} id 5958 * @returns {wp.media.View} 5959 */ 5960 get: function( id ) { 5961 return this._views[ id ]; 5962 }, 5963 /** 5964 * @param {string} id 5965 * @returns {wp.media.view.PriorityList} 5966 */ 5967 unset: function( id ) { 5968 var view = this.get( id ); 5969 5970 if ( view ) { 5971 view.remove(); 5972 } 5973 5974 delete this._views[ id ]; 5975 return this; 5976 }, 5977 /** 5978 * @param {Object} options 5979 * @returns {wp.media.View} 5980 */ 5981 toView: function( options ) { 5982 return new View( options ); 5983 } 5984 }); 5985 5986 module.exports = PriorityList; 5987 },{"./view.js":55}],41:[function(require,module,exports){ 5988 /** 5989 * wp.media.view.RouterItem 5990 * 5991 * @class 5992 * @augments wp.media.view.MenuItem 5993 * @augments wp.media.View 5994 * @augments wp.Backbone.View 5995 * @augments Backbone.View 5996 */ 5997 var MenuItem = require( './menu-item.js' ), 5998 RouterItem; 5999 6000 RouterItem = MenuItem.extend({ 6001 /** 6002 * On click handler to activate the content region's corresponding mode. 6003 */ 6004 click: function() { 6005 var contentMode = this.options.contentMode; 6006 if ( contentMode ) { 6007 this.controller.content.mode( contentMode ); 6008 } 6009 } 6010 }); 6011 6012 module.exports = RouterItem; 6013 },{"./menu-item.js":37}],42:[function(require,module,exports){ 6014 /** 6015 * wp.media.view.Router 6016 * 6017 * @class 6018 * @augments wp.media.view.Menu 6019 * @augments wp.media.view.PriorityList 6020 * @augments wp.media.View 6021 * @augments wp.Backbone.View 6022 * @augments Backbone.View 6023 */ 6024 var Menu = require( './menu.js' ), 6025 RouterItem = require( './router-item.js' ), 6026 Router; 6027 6028 Router = Menu.extend({ 6029 tagName: 'div', 6030 className: 'media-router', 6031 property: 'contentMode', 6032 ItemView: RouterItem, 6033 region: 'router', 6034 6035 initialize: function() { 6036 this.controller.on( 'content:render', this.update, this ); 6037 // Call 'initialize' directly on the parent class. 6038 Menu.prototype.initialize.apply( this, arguments ); 6039 }, 6040 6041 update: function() { 6042 var mode = this.controller.content.mode(); 6043 if ( mode ) { 6044 this.select( mode ); 6045 } 6046 } 6047 }); 6048 6049 module.exports = Router; 6050 },{"./menu.js":38,"./router-item.js":41}],43:[function(require,module,exports){ 6051 /** 6052 * wp.media.view.Search 6053 * 6054 * @class 6055 * @augments wp.media.View 6056 * @augments wp.Backbone.View 6057 * @augments Backbone.View 6058 */ 6059 var View = require( './view.js' ), 6060 l10n = wp.media.view.l10n, 6061 Search; 6062 6063 Search = View.extend({ 6064 tagName: 'input', 6065 className: 'search', 6066 id: 'media-search-input', 6067 6068 attributes: { 6069 type: 'search', 6070 placeholder: l10n.search 6071 }, 6072 6073 events: { 6074 'input': 'search', 6075 'keyup': 'search', 6076 'change': 'search', 6077 'search': 'search' 6078 }, 6079 6080 /** 6081 * @returns {wp.media.view.Search} Returns itself to allow chaining 6082 */ 6083 render: function() { 6084 this.el.value = this.model.escape('search'); 6085 return this; 6086 }, 6087 6088 search: function( event ) { 6089 if ( event.target.value ) { 6090 this.model.set( 'search', event.target.value ); 6091 } else { 6092 this.model.unset('search'); 6093 } 6094 } 6095 }); 6096 6097 module.exports = Search; 6098 },{"./view.js":55}],44:[function(require,module,exports){ 6099 /** 6100 * wp.media.view.Settings 6101 * 6102 * @class 6103 * @augments wp.media.View 6104 * @augments wp.Backbone.View 6105 * @augments Backbone.View 6106 */ 6107 var View = require( './view.js' ), 6108 $ = jQuery, 6109 Settings; 6110 6111 Settings = View.extend({ 6112 events: { 6113 'click button': 'updateHandler', 6114 'change input': 'updateHandler', 6115 'change select': 'updateHandler', 6116 'change textarea': 'updateHandler' 6117 }, 6118 6119 initialize: function() { 6120 this.model = this.model || new Backbone.Model(); 6121 this.model.on( 'change', this.updateChanges, this ); 6122 }, 6123 6124 prepare: function() { 6125 return _.defaults({ 6126 model: this.model.toJSON() 6127 }, this.options ); 6128 }, 6129 /** 6130 * @returns {wp.media.view.Settings} Returns itself to allow chaining 6131 */ 6132 render: function() { 6133 View.prototype.render.apply( this, arguments ); 6134 // Select the correct values. 6135 _( this.model.attributes ).chain().keys().each( this.update, this ); 6136 return this; 6137 }, 6138 /** 6139 * @param {string} key 6140 */ 6141 update: function( key ) { 6142 var value = this.model.get( key ), 6143 $setting = this.$('[data-setting="' + key + '"]'), 6144 $buttons, $value; 6145 6146 // Bail if we didn't find a matching setting. 6147 if ( ! $setting.length ) { 6148 return; 6149 } 6150 6151 // Attempt to determine how the setting is rendered and update 6152 // the selected value. 6153 6154 // Handle dropdowns. 6155 if ( $setting.is('select') ) { 6156 $value = $setting.find('[value="' + value + '"]'); 6157 6158 if ( $value.length ) { 6159 $setting.find('option').prop( 'selected', false ); 6160 $value.prop( 'selected', true ); 6161 } else { 6162 // If we can't find the desired value, record what *is* selected. 6163 this.model.set( key, $setting.find(':selected').val() ); 6164 } 6165 6166 // Handle button groups. 6167 } else if ( $setting.hasClass('button-group') ) { 6168 $buttons = $setting.find('button').removeClass('active'); 6169 $buttons.filter( '[value="' + value + '"]' ).addClass('active'); 6170 6171 // Handle text inputs and textareas. 6172 } else if ( $setting.is('input[type="text"], textarea') ) { 6173 if ( ! $setting.is(':focus') ) { 6174 $setting.val( value ); 6175 } 6176 // Handle checkboxes. 6177 } else if ( $setting.is('input[type="checkbox"]') ) { 6178 $setting.prop( 'checked', !! value && 'false' !== value ); 6179 } 6180 }, 6181 /** 6182 * @param {Object} event 6183 */ 6184 updateHandler: function( event ) { 6185 var $setting = $( event.target ).closest('[data-setting]'), 6186 value = event.target.value, 6187 userSetting; 6188 6189 event.preventDefault(); 6190 6191 if ( ! $setting.length ) { 6192 return; 6193 } 6194 6195 // Use the correct value for checkboxes. 6196 if ( $setting.is('input[type="checkbox"]') ) { 6197 value = $setting[0].checked; 6198 } 6199 6200 // Update the corresponding setting. 6201 this.model.set( $setting.data('setting'), value ); 6202 6203 // If the setting has a corresponding user setting, 6204 // update that as well. 6205 if ( userSetting = $setting.data('userSetting') ) { 6206 setUserSetting( userSetting, value ); 6207 } 6208 }, 6209 6210 updateChanges: function( model ) { 6211 if ( model.hasChanged() ) { 6212 _( model.changed ).chain().keys().each( this.update, this ); 6213 } 6214 } 6215 }); 6216 6217 module.exports = Settings; 6218 },{"./view.js":55}],45:[function(require,module,exports){ 6219 /** 6220 * wp.media.view.Settings.AttachmentDisplay 6221 * 6222 * @class 6223 * @augments wp.media.view.Settings 6224 * @augments wp.media.View 6225 * @augments wp.Backbone.View 6226 * @augments Backbone.View 6227 */ 6228 var Settings = require( '../settings.js' ), 6229 AttachmentDisplay; 6230 6231 AttachmentDisplay = Settings.extend({ 6232 className: 'attachment-display-settings', 6233 template: wp.template('attachment-display-settings'), 6234 6235 initialize: function() { 6236 var attachment = this.options.attachment; 6237 6238 _.defaults( this.options, { 6239 userSettings: false 6240 }); 6241 // Call 'initialize' directly on the parent class. 6242 Settings.prototype.initialize.apply( this, arguments ); 6243 this.model.on( 'change:link', this.updateLinkTo, this ); 6244 6245 if ( attachment ) { 6246 attachment.on( 'change:uploading', this.render, this ); 6247 } 6248 }, 6249 6250 dispose: function() { 6251 var attachment = this.options.attachment; 6252 if ( attachment ) { 6253 attachment.off( null, null, this ); 6254 } 6255 /** 6256 * call 'dispose' directly on the parent class 6257 */ 6258 Settings.prototype.dispose.apply( this, arguments ); 6259 }, 6260 /** 6261 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining 6262 */ 6263 render: function() { 6264 var attachment = this.options.attachment; 6265 if ( attachment ) { 6266 _.extend( this.options, { 6267 sizes: attachment.get('sizes'), 6268 type: attachment.get('type') 6269 }); 6270 } 6271 /** 6272 * call 'render' directly on the parent class 6273 */ 6274 Settings.prototype.render.call( this ); 6275 this.updateLinkTo(); 6276 return this; 6277 }, 6278 6279 updateLinkTo: function() { 6280 var linkTo = this.model.get('link'), 6281 $input = this.$('.link-to-custom'), 6282 attachment = this.options.attachment; 6283 6284 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { 6285 $input.addClass( 'hidden' ); 6286 return; 6287 } 6288 6289 if ( attachment ) { 6290 if ( 'post' === linkTo ) { 6291 $input.val( attachment.get('link') ); 6292 } else if ( 'file' === linkTo ) { 6293 $input.val( attachment.get('url') ); 6294 } else if ( ! this.model.get('linkUrl') ) { 6295 $input.val('http://'); 6296 } 6297 6298 $input.prop( 'readonly', 'custom' !== linkTo ); 6299 } 6300 6301 $input.removeClass( 'hidden' ); 6302 6303 // If the input is visible, focus and select its contents. 6304 if ( ! wp.media.isTouchDevice && $input.is(':visible') ) { 6305 $input.focus()[0].select(); 6306 } 6307 } 6308 }); 6309 6310 module.exports = AttachmentDisplay; 6311 },{"../settings.js":44}],46:[function(require,module,exports){ 6312 /** 6313 * wp.media.view.Sidebar 6314 * 6315 * @class 6316 * @augments wp.media.view.PriorityList 6317 * @augments wp.media.View 6318 * @augments wp.Backbone.View 6319 * @augments Backbone.View 6320 */ 6321 var PriorityList = require( './priority-list.js' ), 6322 Sidebar; 6323 6324 Sidebar = PriorityList.extend({ 6325 className: 'media-sidebar' 6326 }); 6327 6328 module.exports = Sidebar; 6329 },{"./priority-list.js":40}],47:[function(require,module,exports){ 6330 /** 6331 * wp.media.view.Spinner 6332 * 6333 * @class 6334 * @augments wp.media.View 6335 * @augments wp.Backbone.View 6336 * @augments Backbone.View 6337 */ 6338 var View = require( './view.js' ), 6339 Spinner; 6340 6341 Spinner = View.extend({ 6342 tagName: 'span', 6343 className: 'spinner', 6344 spinnerTimeout: false, 6345 delay: 400, 6346 6347 show: function() { 6348 if ( ! this.spinnerTimeout ) { 6349 this.spinnerTimeout = _.delay(function( $el ) { 6350 $el.show(); 6351 }, this.delay, this.$el ); 6352 } 6353 6354 return this; 6355 }, 6356 6357 hide: function() { 6358 this.$el.hide(); 6359 this.spinnerTimeout = clearTimeout( this.spinnerTimeout ); 6360 6361 return this; 6362 } 6363 }); 6364 6365 module.exports = Spinner; 6366 },{"./view.js":55}],48:[function(require,module,exports){ 6367 /** 6368 * wp.media.view.Toolbar 6369 * 6370 * A toolbar which consists of a primary and a secondary section. Each sections 6371 * can be filled with views. 6372 * 6373 * @class 6374 * @augments wp.media.View 6375 * @augments wp.Backbone.View 6376 * @augments Backbone.View 6377 */ 6378 var View = require( './view.js' ), 6379 Button = require( './button.js' ), 6380 PriorityList = require( './priority-list.js' ), 6381 Toolbar; 6382 6383 Toolbar = View.extend({ 6384 tagName: 'div', 6385 className: 'media-toolbar', 6386 6387 initialize: function() { 6388 var state = this.controller.state(), 6389 selection = this.selection = state.get('selection'), 6390 library = this.library = state.get('library'); 6391 6392 this._views = {}; 6393 6394 // The toolbar is composed of two `PriorityList` views. 6395 this.primary = new PriorityList(); 6396 this.secondary = new PriorityList(); 6397 this.primary.$el.addClass('media-toolbar-primary search-form'); 6398 this.secondary.$el.addClass('media-toolbar-secondary'); 6399 6400 this.views.set([ this.secondary, this.primary ]); 6401 6402 if ( this.options.items ) { 6403 this.set( this.options.items, { silent: true }); 6404 } 6405 6406 if ( ! this.options.silent ) { 6407 this.render(); 6408 } 6409 6410 if ( selection ) { 6411 selection.on( 'add remove reset', this.refresh, this ); 6412 } 6413 6414 if ( library ) { 6415 library.on( 'add remove reset', this.refresh, this ); 6416 } 6417 }, 6418 /** 6419 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining 6420 */ 6421 dispose: function() { 6422 if ( this.selection ) { 6423 this.selection.off( null, null, this ); 6424 } 6425 6426 if ( this.library ) { 6427 this.library.off( null, null, this ); 6428 } 6429 /** 6430 * call 'dispose' directly on the parent class 6431 */ 6432 return View.prototype.dispose.apply( this, arguments ); 6433 }, 6434 6435 ready: function() { 6436 this.refresh(); 6437 }, 6438 6439 /** 6440 * @param {string} id 6441 * @param {Backbone.View|Object} view 6442 * @param {Object} [options={}] 6443 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 6444 */ 6445 set: function( id, view, options ) { 6446 var list; 6447 options = options || {}; 6448 6449 // Accept an object with an `id` : `view` mapping. 6450 if ( _.isObject( id ) ) { 6451 _.each( id, function( view, id ) { 6452 this.set( id, view, { silent: true }); 6453 }, this ); 6454 6455 } else { 6456 if ( ! ( view instanceof Backbone.View ) ) { 6457 view.classes = [ 'media-button-' + id ].concat( view.classes || [] ); 6458 view = new Button( view ).render(); 6459 } 6460 6461 view.controller = view.controller || this.controller; 6462 6463 this._views[ id ] = view; 6464 6465 list = view.options.priority < 0 ? 'secondary' : 'primary'; 6466 this[ list ].set( id, view, options ); 6467 } 6468 6469 if ( ! options.silent ) { 6470 this.refresh(); 6471 } 6472 6473 return this; 6474 }, 6475 /** 6476 * @param {string} id 6477 * @returns {wp.media.view.Button} 6478 */ 6479 get: function( id ) { 6480 return this._views[ id ]; 6481 }, 6482 /** 6483 * @param {string} id 6484 * @param {Object} options 6485 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 6486 */ 6487 unset: function( id, options ) { 6488 delete this._views[ id ]; 6489 this.primary.unset( id, options ); 6490 this.secondary.unset( id, options ); 6491 6492 if ( ! options || ! options.silent ) { 6493 this.refresh(); 6494 } 6495 return this; 6496 }, 6497 6498 refresh: function() { 6499 var state = this.controller.state(), 6500 library = state.get('library'), 6501 selection = state.get('selection'); 6502 6503 _.each( this._views, function( button ) { 6504 if ( ! button.model || ! button.options || ! button.options.requires ) { 6505 return; 6506 } 6507 6508 var requires = button.options.requires, 6509 disabled = false; 6510 6511 // Prevent insertion of attachments if any of them are still uploading 6512 disabled = _.some( selection.models, function( attachment ) { 6513 return attachment.get('uploading') === true; 6514 }); 6515 6516 if ( requires.selection && selection && ! selection.length ) { 6517 disabled = true; 6518 } else if ( requires.library && library && ! library.length ) { 6519 disabled = true; 6520 } 6521 button.model.set( 'disabled', disabled ); 6522 }); 6523 } 6524 }); 6525 6526 module.exports = Toolbar; 6527 },{"./button.js":26,"./priority-list.js":40,"./view.js":55}],49:[function(require,module,exports){ 6528 /** 6529 * wp.media.view.Toolbar.Select 6530 * 6531 * @class 6532 * @augments wp.media.view.Toolbar 6533 * @augments wp.media.View 6534 * @augments wp.Backbone.View 6535 * @augments Backbone.View 6536 */ 6537 var Toolbar = require( '../toolbar.js' ), 6538 l10n = wp.media.view.l10n, 6539 Select; 6540 6541 Select = Toolbar.extend({ 6542 initialize: function() { 6543 var options = this.options; 6544 6545 _.bindAll( this, 'clickSelect' ); 6546 6547 _.defaults( options, { 6548 event: 'select', 6549 state: false, 6550 reset: true, 6551 close: true, 6552 text: l10n.select, 6553 6554 // Does the button rely on the selection? 6555 requires: { 6556 selection: true 6557 } 6558 }); 6559 6560 options.items = _.defaults( options.items || {}, { 6561 select: { 6562 style: 'primary', 6563 text: options.text, 6564 priority: 80, 6565 click: this.clickSelect, 6566 requires: options.requires 6567 } 6568 }); 6569 // Call 'initialize' directly on the parent class. 6570 Toolbar.prototype.initialize.apply( this, arguments ); 6571 }, 6572 6573 clickSelect: function() { 6574 var options = this.options, 6575 controller = this.controller; 6576 6577 if ( options.close ) { 6578 controller.close(); 6579 } 6580 6581 if ( options.event ) { 6582 controller.state().trigger( options.event ); 6583 } 6584 6585 if ( options.state ) { 6586 controller.setState( options.state ); 6587 } 6588 6589 if ( options.reset ) { 6590 controller.reset(); 6591 } 6592 } 6593 }); 6594 6595 module.exports = Select; 6596 },{"../toolbar.js":48}],50:[function(require,module,exports){ 6597 /** 6598 * wp.media.view.UploaderInline 6599 * 6600 * The inline uploader that shows up in the 'Upload Files' tab. 6601 * 6602 * @class 6603 * @augments wp.media.View 6604 * @augments wp.Backbone.View 6605 * @augments Backbone.View 6606 */ 6607 var View = require( '../view.js' ), 6608 UploaderStatus = require( './status.js' ), 6609 UploaderInline; 6610 6611 UploaderInline = View.extend({ 6612 tagName: 'div', 6613 className: 'uploader-inline', 6614 template: wp.template('uploader-inline'), 6615 6616 events: { 6617 'click .close': 'hide' 6618 }, 6619 6620 initialize: function() { 6621 _.defaults( this.options, { 6622 message: '', 6623 status: true, 6624 canClose: false 6625 }); 6626 6627 if ( ! this.options.$browser && this.controller.uploader ) { 6628 this.options.$browser = this.controller.uploader.$browser; 6629 } 6630 6631 if ( _.isUndefined( this.options.postId ) ) { 6632 this.options.postId = wp.media.view.settings.post.id; 6633 } 6634 6635 if ( this.options.status ) { 6636 this.views.set( '.upload-inline-status', new UploaderStatus({ 6637 controller: this.controller 6638 }) ); 6639 } 6640 }, 6641 6642 prepare: function() { 6643 var suggestedWidth = this.controller.state().get('suggestedWidth'), 6644 suggestedHeight = this.controller.state().get('suggestedHeight'), 6645 data = {}; 6646 6647 data.message = this.options.message; 6648 data.canClose = this.options.canClose; 6649 6650 if ( suggestedWidth && suggestedHeight ) { 6651 data.suggestedWidth = suggestedWidth; 6652 data.suggestedHeight = suggestedHeight; 6653 } 6654 6655 return data; 6656 }, 6657 /** 6658 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 6659 */ 6660 dispose: function() { 6661 if ( this.disposing ) { 6662 /** 6663 * call 'dispose' directly on the parent class 6664 */ 6665 return View.prototype.dispose.apply( this, arguments ); 6666 } 6667 6668 // Run remove on `dispose`, so we can be sure to refresh the 6669 // uploader with a view-less DOM. Track whether we're disposing 6670 // so we don't trigger an infinite loop. 6671 this.disposing = true; 6672 return this.remove(); 6673 }, 6674 /** 6675 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 6676 */ 6677 remove: function() { 6678 /** 6679 * call 'remove' directly on the parent class 6680 */ 6681 var result = View.prototype.remove.apply( this, arguments ); 6682 6683 _.defer( _.bind( this.refresh, this ) ); 6684 return result; 6685 }, 6686 6687 refresh: function() { 6688 var uploader = this.controller.uploader; 6689 6690 if ( uploader ) { 6691 uploader.refresh(); 6692 } 6693 }, 6694 /** 6695 * @returns {wp.media.view.UploaderInline} 6696 */ 6697 ready: function() { 6698 var $browser = this.options.$browser, 6699 $placeholder; 6700 6701 if ( this.controller.uploader ) { 6702 $placeholder = this.$('.browser'); 6703 6704 // Check if we've already replaced the placeholder. 6705 if ( $placeholder[0] === $browser[0] ) { 6706 return; 6707 } 6708 6709 $browser.detach().text( $placeholder.text() ); 6710 $browser[0].className = $placeholder[0].className; 6711 $placeholder.replaceWith( $browser.show() ); 6712 } 6713 6714 this.refresh(); 6715 return this; 6716 }, 6717 show: function() { 6718 this.$el.removeClass( 'hidden' ); 6719 }, 6720 hide: function() { 6721 this.$el.addClass( 'hidden' ); 6722 } 6723 6724 }); 6725 6726 module.exports = UploaderInline; 6727 },{"../view.js":55,"./status.js":52}],51:[function(require,module,exports){ 6728 /** 6729 * wp.media.view.UploaderStatusError 6730 * 6731 * @class 6732 * @augments wp.media.View 6733 * @augments wp.Backbone.View 6734 * @augments Backbone.View 6735 */ 6736 var View = require( '../view.js' ), 6737 UploaderStatusError; 6738 6739 UploaderStatusError = View.extend({ 6740 className: 'upload-error', 6741 template: wp.template('uploader-status-error') 6742 }); 6743 6744 module.exports = UploaderStatusError; 6745 },{"../view.js":55}],52:[function(require,module,exports){ 6746 /** 6747 * wp.media.view.UploaderStatus 6748 * 6749 * An uploader status for on-going uploads. 6750 * 6751 * @class 6752 * @augments wp.media.View 6753 * @augments wp.Backbone.View 6754 * @augments Backbone.View 6755 */ 6756 var View = require( '../view.js' ), 6757 UploaderStatusError = require( './status-error.js' ), 6758 UploaderStatus; 6759 6760 UploaderStatus = View.extend({ 6761 className: 'media-uploader-status', 6762 template: wp.template('uploader-status'), 6763 6764 events: { 6765 'click .upload-dismiss-errors': 'dismiss' 6766 }, 6767 6768 initialize: function() { 6769 this.queue = wp.Uploader.queue; 6770 this.queue.on( 'add remove reset', this.visibility, this ); 6771 this.queue.on( 'add remove reset change:percent', this.progress, this ); 6772 this.queue.on( 'add remove reset change:uploading', this.info, this ); 6773 6774 this.errors = wp.Uploader.errors; 6775 this.errors.reset(); 6776 this.errors.on( 'add remove reset', this.visibility, this ); 6777 this.errors.on( 'add', this.error, this ); 6778 }, 6779 /** 6780 * @global wp.Uploader 6781 * @returns {wp.media.view.UploaderStatus} 6782 */ 6783 dispose: function() { 6784 wp.Uploader.queue.off( null, null, this ); 6785 /** 6786 * call 'dispose' directly on the parent class 6787 */ 6788 View.prototype.dispose.apply( this, arguments ); 6789 return this; 6790 }, 6791 6792 visibility: function() { 6793 this.$el.toggleClass( 'uploading', !! this.queue.length ); 6794 this.$el.toggleClass( 'errors', !! this.errors.length ); 6795 this.$el.toggle( !! this.queue.length || !! this.errors.length ); 6796 }, 6797 6798 ready: function() { 6799 _.each({ 6800 '$bar': '.media-progress-bar div', 6801 '$index': '.upload-index', 6802 '$total': '.upload-total', 6803 '$filename': '.upload-filename' 6804 }, function( selector, key ) { 6805 this[ key ] = this.$( selector ); 6806 }, this ); 6807 6808 this.visibility(); 6809 this.progress(); 6810 this.info(); 6811 }, 6812 6813 progress: function() { 6814 var queue = this.queue, 6815 $bar = this.$bar; 6816 6817 if ( ! $bar || ! queue.length ) { 6818 return; 6819 } 6820 6821 $bar.width( ( queue.reduce( function( memo, attachment ) { 6822 if ( ! attachment.get('uploading') ) { 6823 return memo + 100; 6824 } 6825 6826 var percent = attachment.get('percent'); 6827 return memo + ( _.isNumber( percent ) ? percent : 100 ); 6828 }, 0 ) / queue.length ) + '%' ); 6829 }, 6830 6831 info: function() { 6832 var queue = this.queue, 6833 index = 0, active; 6834 6835 if ( ! queue.length ) { 6836 return; 6837 } 6838 6839 active = this.queue.find( function( attachment, i ) { 6840 index = i; 6841 return attachment.get('uploading'); 6842 }); 6843 6844 this.$index.text( index + 1 ); 6845 this.$total.text( queue.length ); 6846 this.$filename.html( active ? this.filename( active.get('filename') ) : '' ); 6847 }, 6848 /** 6849 * @param {string} filename 6850 * @returns {string} 6851 */ 6852 filename: function( filename ) { 6853 return wp.media.truncate( _.escape( filename ), 24 ); 6854 }, 6855 /** 6856 * @param {Backbone.Model} error 6857 */ 6858 error: function( error ) { 6859 this.views.add( '.upload-errors', new UploaderStatusError({ 6860 filename: this.filename( error.get('file').name ), 6861 message: error.get('message') 6862 }), { at: 0 }); 6863 }, 6864 6865 /** 6866 * @global wp.Uploader 6867 * 6868 * @param {Object} event 6869 */ 6870 dismiss: function( event ) { 6871 var errors = this.views.get('.upload-errors'); 6872 6873 event.preventDefault(); 6874 6875 if ( errors ) { 6876 _.invoke( errors, 'remove' ); 6877 } 6878 wp.Uploader.errors.reset(); 6879 } 6880 }); 6881 6882 module.exports = UploaderStatus; 6883 },{"../view.js":55,"./status-error.js":51}],53:[function(require,module,exports){ 6884 /** 6885 * wp.media.view.UploaderWindow 6886 * 6887 * An uploader window that allows for dragging and dropping media. 6888 * 6889 * @class 6890 * @augments wp.media.View 6891 * @augments wp.Backbone.View 6892 * @augments Backbone.View 6893 * 6894 * @param {object} [options] Options hash passed to the view. 6895 * @param {object} [options.uploader] Uploader properties. 6896 * @param {jQuery} [options.uploader.browser] 6897 * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone. 6898 * @param {object} [options.uploader.params] 6899 */ 6900 var View = require( '../view.js' ), 6901 $ = jQuery, 6902 UploaderWindow; 6903 6904 UploaderWindow = View.extend({ 6905 tagName: 'div', 6906 className: 'uploader-window', 6907 template: wp.template('uploader-window'), 6908 6909 initialize: function() { 6910 var uploader; 6911 6912 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body'); 6913 6914 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { 6915 dropzone: this.$el, 6916 browser: this.$browser, 6917 params: {} 6918 }); 6919 6920 // Ensure the dropzone is a jQuery collection. 6921 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) { 6922 uploader.dropzone = $( uploader.dropzone ); 6923 } 6924 6925 this.controller.on( 'activate', this.refresh, this ); 6926 6927 this.controller.on( 'detach', function() { 6928 this.$browser.remove(); 6929 }, this ); 6930 }, 6931 6932 refresh: function() { 6933 if ( this.uploader ) { 6934 this.uploader.refresh(); 6935 } 6936 }, 6937 6938 ready: function() { 6939 var postId = wp.media.view.settings.post.id, 6940 dropzone; 6941 6942 // If the uploader already exists, bail. 6943 if ( this.uploader ) { 6944 return; 6945 } 6946 6947 if ( postId ) { 6948 this.options.uploader.params.post_id = postId; 6949 } 6950 this.uploader = new wp.Uploader( this.options.uploader ); 6951 6952 dropzone = this.uploader.dropzone; 6953 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) ); 6954 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) ); 6955 6956 $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) ); 6957 }, 6958 6959 _ready: function() { 6960 this.controller.trigger( 'uploader:ready' ); 6961 }, 6962 6963 show: function() { 6964 var $el = this.$el.show(); 6965 6966 // Ensure that the animation is triggered by waiting until 6967 // the transparent element is painted into the DOM. 6968 _.defer( function() { 6969 $el.css({ opacity: 1 }); 6970 }); 6971 }, 6972 6973 hide: function() { 6974 var $el = this.$el.css({ opacity: 0 }); 6975 6976 wp.media.transition( $el ).done( function() { 6977 // Transition end events are subject to race conditions. 6978 // Make sure that the value is set as intended. 6979 if ( '0' === $el.css('opacity') ) { 6980 $el.hide(); 6981 } 6982 }); 6983 6984 // https://core.trac.wordpress.org/ticket/27341 6985 _.delay( function() { 6986 if ( '0' === $el.css('opacity') && $el.is(':visible') ) { 6987 $el.hide(); 6988 } 6989 }, 500 ); 6990 } 6991 }); 6992 6993 module.exports = UploaderWindow; 6994 },{"../view.js":55}],54:[function(require,module,exports){ 6995 /** 6996 * wp.media.view.VideoDetails 6997 * 6998 * @constructor 6999 * @augments wp.media.view.MediaDetails 7000 * @augments wp.media.view.Settings.AttachmentDisplay 7001 * @augments wp.media.view.Settings 7002 * @augments wp.media.View 7003 * @augments wp.Backbone.View 7004 * @augments Backbone.View 7005 */ 7006 var MediaDetails = require( './media-details' ), 7007 VideoDetails; 7008 7009 VideoDetails = MediaDetails.extend({ 7010 className: 'video-details', 7011 template: wp.template('video-details'), 7012 7013 setMedia: function() { 7014 var video = this.$('.wp-video-shortcode'); 7015 7016 if ( video.find( 'source' ).length ) { 7017 if ( video.is(':hidden') ) { 7018 video.show(); 7019 } 7020 7021 if ( ! video.hasClass('youtube-video') ) { 7022 this.media = MediaDetails.prepareSrc( video.get(0) ); 7023 } else { 7024 this.media = video.get(0); 7025 } 7026 } else { 7027 video.hide(); 7028 this.media = false; 7029 } 7030 7031 return this; 7032 } 7033 }); 7034 7035 module.exports = VideoDetails; 7036 },{"./media-details":35}],55:[function(require,module,exports){ 7037 /** 7038 * wp.media.View 7039 * 7040 * The base view class for media. 7041 * 7042 * Undelegating events, removing events from the model, and 7043 * removing events from the controller mirror the code for 7044 * `Backbone.View.dispose` in Backbone 0.9.8 development. 7045 * 7046 * This behavior has since been removed, and should not be used 7047 * outside of the media manager. 7048 * 7049 * @class 7050 * @augments wp.Backbone.View 7051 * @augments Backbone.View 7052 */ 7053 var View = wp.Backbone.View.extend({ 7054 constructor: function( options ) { 7055 if ( options && options.controller ) { 7056 this.controller = options.controller; 7057 } 7058 wp.Backbone.View.apply( this, arguments ); 7059 }, 7060 /** 7061 * @todo The internal comment mentions this might have been a stop-gap 7062 * before Backbone 0.9.8 came out. Figure out if Backbone core takes 7063 * care of this in Backbone.View now. 7064 * 7065 * @returns {wp.media.View} Returns itself to allow chaining 7066 */ 7067 dispose: function() { 7068 // Undelegating events, removing events from the model, and 7069 // removing events from the controller mirror the code for 7070 // `Backbone.View.dispose` in Backbone 0.9.8 development. 7071 this.undelegateEvents(); 7072 7073 if ( this.model && this.model.off ) { 7074 this.model.off( null, null, this ); 7075 } 7076 7077 if ( this.collection && this.collection.off ) { 7078 this.collection.off( null, null, this ); 7079 } 7080 7081 // Unbind controller events. 7082 if ( this.controller && this.controller.off ) { 7083 this.controller.off( null, null, this ); 7084 } 7085 7086 return this; 7087 }, 7088 /** 7089 * @returns {wp.media.View} Returns itself to allow chaining 7090 */ 7091 remove: function() { 7092 this.dispose(); 7093 /** 7094 * call 'remove' directly on the parent class 7095 */ 7096 return wp.Backbone.View.prototype.remove.apply( this, arguments ); 7097 } 7098 }); 7099 7100 module.exports = View; 7101 },{}]},{},[1]); -
src/wp-includes/js/media/audio-video.manifest.js
1 /* global _wpMediaViewsL10n, _wpmejsSettings, MediaElementPlayer */ 2 3 (function($, _, Backbone) { 4 var media = wp.media, 5 baseSettings = {}, 6 l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n; 7 8 if ( ! _.isUndefined( window._wpmejsSettings ) ) { 9 baseSettings = _wpmejsSettings; 10 } 11 12 /** 13 * @mixin 14 */ 15 wp.media.mixin = { 16 mejsSettings: baseSettings, 17 18 removeAllPlayers: function() { 19 var p; 20 21 if ( window.mejs && window.mejs.players ) { 22 for ( p in window.mejs.players ) { 23 window.mejs.players[p].pause(); 24 this.removePlayer( window.mejs.players[p] ); 25 } 26 } 27 }, 28 29 /** 30 * Override the MediaElement method for removing a player. 31 * MediaElement tries to pull the audio/video tag out of 32 * its container and re-add it to the DOM. 33 */ 34 removePlayer: function(t) { 35 var featureIndex, feature; 36 37 if ( ! t.options ) { 38 return; 39 } 40 41 // invoke features cleanup 42 for ( featureIndex in t.options.features ) { 43 feature = t.options.features[featureIndex]; 44 if ( t['clean' + feature] ) { 45 try { 46 t['clean' + feature](t); 47 } catch (e) {} 48 } 49 } 50 51 if ( ! t.isDynamic ) { 52 t.$node.remove(); 53 } 54 55 if ( 'native' !== t.media.pluginType ) { 56 t.media.remove(); 57 } 58 59 delete window.mejs.players[t.id]; 60 61 t.container.remove(); 62 t.globalUnbind(); 63 delete t.node.player; 64 }, 65 66 /** 67 * Allows any class that has set 'player' to a MediaElementPlayer 68 * instance to remove the player when listening to events. 69 * 70 * Examples: modal closes, shortcode properties are removed, etc. 71 */ 72 unsetPlayers : function() { 73 if ( this.players && this.players.length ) { 74 _.each( this.players, function (player) { 75 player.pause(); 76 wp.media.mixin.removePlayer( player ); 77 } ); 78 this.players = []; 79 } 80 } 81 }; 82 83 /** 84 * Autowire "collection"-type shortcodes 85 */ 86 wp.media.playlist = new wp.media.collection({ 87 tag: 'playlist', 88 editTitle : l10n.editPlaylistTitle, 89 defaults : { 90 id: wp.media.view.settings.post.id, 91 style: 'light', 92 tracklist: true, 93 tracknumbers: true, 94 images: true, 95 artists: true, 96 type: 'audio' 97 } 98 }); 99 100 /** 101 * Shortcode modeling for audio 102 * `edit()` prepares the shortcode for the media modal 103 * `shortcode()` builds the new shortcode after update 104 * 105 * @namespace 106 */ 107 wp.media.audio = { 108 coerce : wp.media.coerce, 109 110 defaults : { 111 id : wp.media.view.settings.post.id, 112 src : '', 113 loop : false, 114 autoplay : false, 115 preload : 'none', 116 width : 400 117 }, 118 119 edit : function( data ) { 120 var frame, shortcode = wp.shortcode.next( 'audio', data ).shortcode; 121 console.log( 'woo' ); 122 123 frame = wp.media({ 124 frame: 'audio', 125 state: 'audio-details', 126 metadata: _.defaults( shortcode.attrs.named, this.defaults ) 127 }); 128 129 console.log( frame ); 130 131 return frame; 132 }, 133 134 shortcode : function( model ) { 135 var self = this, content; 136 137 _.each( this.defaults, function( value, key ) { 138 model[ key ] = self.coerce( model, key ); 139 140 if ( value === model[ key ] ) { 141 delete model[ key ]; 142 } 143 }); 144 145 content = model.content; 146 delete model.content; 147 148 return new wp.shortcode({ 149 tag: 'audio', 150 attrs: model, 151 content: content 152 }); 153 } 154 }; 155 156 /** 157 * Shortcode modeling for video 158 * `edit()` prepares the shortcode for the media modal 159 * `shortcode()` builds the new shortcode after update 160 * 161 * @namespace 162 */ 163 wp.media.video = { 164 coerce : wp.media.coerce, 165 166 defaults : { 167 id : wp.media.view.settings.post.id, 168 src : '', 169 poster : '', 170 loop : false, 171 autoplay : false, 172 preload : 'metadata', 173 content : '', 174 width : 640, 175 height : 360 176 }, 177 178 edit : function( data ) { 179 var frame, 180 shortcode = wp.shortcode.next( 'video', data ).shortcode, 181 attrs; 182 183 attrs = shortcode.attrs.named; 184 attrs.content = shortcode.content; 185 186 frame = wp.media({ 187 frame: 'video', 188 state: 'video-details', 189 metadata: _.defaults( attrs, this.defaults ) 190 }); 191 192 return frame; 193 }, 194 195 shortcode : function( model ) { 196 var self = this, content; 197 198 _.each( this.defaults, function( value, key ) { 199 model[ key ] = self.coerce( model, key ); 200 201 if ( value === model[ key ] ) { 202 delete model[ key ]; 203 } 204 }); 205 206 content = model.content; 207 delete model.content; 208 209 return new wp.shortcode({ 210 tag: 'video', 211 attrs: model, 212 content: content 213 }); 214 } 215 }; 216 217 media.model.PostMedia = require( './models/post-media.js' ); 218 media.controller.AudioDetails = require( './controllers/audio-details.js' ); 219 media.controller.VideoDetails = require( './controllers/video-details.js' ); 220 media.view.MediaFrame.MediaDetails = require( './views/frame/media-details.js' ); 221 media.view.MediaFrame.AudioDetails = require( './views/frame/audio-details.js' ); 222 media.view.MediaFrame.VideoDetails = require( './views/frame/video-details.js' ); 223 media.view.MediaDetails = require( './views/media-details.js' ); 224 media.view.AudioDetails = require( './views/audio-details.js' ); 225 media.view.VideoDetails = require( './views/video-details.js' ); 226 227 }(jQuery, _, Backbone)); -
src/wp-includes/js/media/controllers/audio-details.js
1 /** 2 * The controller for the Audio Details state 3 * 4 * @constructor 5 * @augments wp.media.controller.State 6 * @augments Backbone.Model 7 */ 8 var State = require( './state.js' ), 9 l10n = wp.media.view.l10n, 10 AudioDetails; 11 12 AudioDetails = State.extend({ 13 defaults: { 14 id: 'audio-details', 15 toolbar: 'audio-details', 16 title: l10n.audioDetailsTitle, 17 content: 'audio-details', 18 menu: 'audio-details', 19 router: false, 20 priority: 60 21 }, 22 23 initialize: function( options ) { 24 this.media = options.media; 25 State.prototype.initialize.apply( this, arguments ); 26 } 27 }); 28 29 module.exports = AudioDetails; -
src/wp-includes/js/media/controllers/collection-add.js
1 /** 2 * wp.media.controller.CollectionAdd 3 * 4 * A state for adding attachments to a collection (e.g. video playlist). 5 * 6 * @class 7 * @augments wp.media.controller.Library 8 * @augments wp.media.controller.State 9 * @augments Backbone.Model 10 * 11 * @param {object} [attributes] The attributes hash passed to the state. 12 * @param {string} [attributes.id=library] Unique identifier. 13 * @param {string} attributes.title Title for the state. Displays in the frame's title region. 14 * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. 15 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 16 * If one is not supplied, a collection of attachments of the specified type will be created. 17 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 18 * Accepts 'all', 'uploaded', or 'unattached'. 19 * @param {string} [attributes.menu=gallery] Initial mode for the menu region. 20 * @param {string} [attributes.content=upload] Initial mode for the content region. 21 * Overridden by persistent user setting if 'contentUserSetting' is true. 22 * @param {string} [attributes.router=browse] Initial mode for the router region. 23 * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region. 24 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 25 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 26 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 27 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 28 * @param {int} [attributes.priority=100] The priority for the state link in the media menu. 29 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 30 * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. 31 * @param {string} attributes.type The collection's media type. (e.g. 'video'). 32 * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). 33 */ 34 var Selection = require( '../models/selection.js' ), 35 Library = require( './library.js' ), 36 CollectionAdd; 37 38 CollectionAdd = Library.extend({ 39 defaults: _.defaults( { 40 // Selection defaults. @see media.model.Selection 41 multiple: 'add', 42 // Attachments browser defaults. @see media.view.AttachmentsBrowser 43 filterable: 'uploaded', 44 45 priority: 100, 46 syncSelection: false 47 }, Library.prototype.defaults ), 48 49 /** 50 * @since 3.9.0 51 */ 52 initialize: function() { 53 var collectionType = this.get('collectionType'); 54 55 if ( 'video' === this.get( 'type' ) ) { 56 collectionType = 'video-' + collectionType; 57 } 58 59 this.set( 'id', collectionType + '-library' ); 60 this.set( 'toolbar', collectionType + '-add' ); 61 this.set( 'menu', collectionType ); 62 63 // If we haven't been provided a `library`, create a `Selection`. 64 if ( ! this.get('library') ) { 65 this.set( 'library', wp.media.query({ type: this.get('type') }) ); 66 } 67 Library.prototype.initialize.apply( this, arguments ); 68 }, 69 70 /** 71 * @since 3.9.0 72 */ 73 activate: function() { 74 var library = this.get('library'), 75 editLibrary = this.get('editLibrary'), 76 edit = this.frame.state( this.get('collectionType') + '-edit' ).get('library'); 77 78 if ( editLibrary && editLibrary !== edit ) { 79 library.unobserve( editLibrary ); 80 } 81 82 // Accepts attachments that exist in the original library and 83 // that do not exist in gallery's library. 84 library.validator = function( attachment ) { 85 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && Selection.prototype.validator.apply( this, arguments ); 86 }; 87 88 // Reset the library to ensure that all attachments are re-added 89 // to the collection. Do so silently, as calling `observe` will 90 // trigger the `reset` event. 91 library.reset( library.mirroring.models, { silent: true }); 92 library.observe( edit ); 93 this.set('editLibrary', edit); 94 95 Library.prototype.activate.apply( this, arguments ); 96 } 97 }); 98 99 module.exports = CollectionAdd; 100 No newline at end of file -
src/wp-includes/js/media/controllers/collection-edit.js
1 /** 2 * wp.media.controller.CollectionEdit 3 * 4 * A state for editing a collection, which is used by audio and video playlists, 5 * and can be used for other collections. 6 * 7 * @class 8 * @augments wp.media.controller.Library 9 * @augments wp.media.controller.State 10 * @augments Backbone.Model 11 * 12 * @param {object} [attributes] The attributes hash passed to the state. 13 * @param {string} attributes.title Title for the state. Displays in the media menu and the frame's title region. 14 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to edit. 15 * If one is not supplied, an empty media.model.Selection collection is created. 16 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 17 * @param {string} [attributes.content=browse] Initial mode for the content region. 18 * @param {string} attributes.menu Initial mode for the menu region. @todo this needs a better explanation. 19 * @param {boolean} [attributes.searchable=false] Whether the library is searchable. 20 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 21 * @param {boolean} [attributes.describe=true] Whether to offer UI to describe the attachments - e.g. captioning images in a gallery. 22 * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable. 23 * @param {boolean} [attributes.dragInfoText] Instructional text about the attachments being sortable. 24 * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments. 25 * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance. 26 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 27 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 28 * Defaults to false for this state, because the library passed in *is* the selection. 29 * @param {view} [attributes.SettingsView] The view to edit the collection instance settings (e.g. Playlist settings with "Show tracklist" checkbox). 30 * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`. 31 * If none supplied, defaults to wp.media.view.Attachment.EditLibrary. 32 * @param {string} attributes.type The collection's media type. (e.g. 'video'). 33 * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). 34 */ 35 var SelectionModel = require( '../models/selection.js' ), 36 Library = require( './library.js' ), 37 View = require( '../views/view.js' ), 38 EditLibraryView = require( '../views/attachment/edit-library.js' ), 39 l10n = wp.media.view.l10n, 40 CollectionEdit; 41 42 CollectionEdit = Library.extend({ 43 defaults: { 44 multiple: false, 45 sortable: true, 46 searchable: false, 47 content: 'browse', 48 describe: true, 49 dragInfo: true, 50 idealColumnWidth: 170, 51 editing: false, 52 priority: 60, 53 SettingsView: false, 54 syncSelection: false 55 }, 56 57 /** 58 * @since 3.9.0 59 */ 60 initialize: function() { 61 var collectionType = this.get('collectionType'); 62 63 if ( 'video' === this.get( 'type' ) ) { 64 collectionType = 'video-' + collectionType; 65 } 66 67 this.set( 'id', collectionType + '-edit' ); 68 this.set( 'toolbar', collectionType + '-edit' ); 69 70 // If we haven't been provided a `library`, create a `Selection`. 71 if ( ! this.get('library') ) { 72 this.set( 'library', new SelectionModel() ); 73 } 74 // The single `Attachment` view to be used in the `Attachments` view. 75 if ( ! this.get('AttachmentView') ) { 76 this.set( 'AttachmentView', EditLibraryView ); 77 } 78 Library.prototype.initialize.apply( this, arguments ); 79 }, 80 81 /** 82 * @since 3.9.0 83 */ 84 activate: function() { 85 var library = this.get('library'); 86 87 // Limit the library to images only. 88 library.props.set( 'type', this.get( 'type' ) ); 89 90 // Watch for uploaded attachments. 91 this.get('library').observe( wp.Uploader.queue ); 92 93 this.frame.on( 'content:render:browse', this.renderSettings, this ); 94 95 Library.prototype.activate.apply( this, arguments ); 96 }, 97 98 /** 99 * @since 3.9.0 100 */ 101 deactivate: function() { 102 // Stop watching for uploaded attachments. 103 this.get('library').unobserve( wp.Uploader.queue ); 104 105 this.frame.off( 'content:render:browse', this.renderSettings, this ); 106 107 Library.prototype.deactivate.apply( this, arguments ); 108 }, 109 110 /** 111 * Render the collection embed settings view in the browser sidebar. 112 * 113 * @todo This is against the pattern elsewhere in media. Typically the frame 114 * is responsible for adding region mode callbacks. Explain. 115 * 116 * @since 3.9.0 117 * 118 * @param {wp.media.view.attachmentsBrowser} The attachments browser view. 119 */ 120 renderSettings: function( attachmentsBrowserView ) { 121 var library = this.get('library'), 122 collectionType = this.get('collectionType'), 123 dragInfoText = this.get('dragInfoText'), 124 SettingsView = this.get('SettingsView'), 125 obj = {}; 126 127 if ( ! library || ! attachmentsBrowserView ) { 128 return; 129 } 130 131 library[ collectionType ] = library[ collectionType ] || new Backbone.Model(); 132 133 obj[ collectionType ] = new SettingsView({ 134 controller: this, 135 model: library[ collectionType ], 136 priority: 40 137 }); 138 139 attachmentsBrowserView.sidebar.set( obj ); 140 141 if ( dragInfoText ) { 142 attachmentsBrowserView.toolbar.set( 'dragInfo', new View({ 143 el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0], 144 priority: -40 145 }) ); 146 } 147 148 // Add the 'Reverse order' button to the toolbar. 149 attachmentsBrowserView.toolbar.set( 'reverse', { 150 text: l10n.reverseOrder, 151 priority: 80, 152 153 click: function() { 154 library.reset( library.toArray().reverse() ); 155 } 156 }); 157 } 158 }); 159 160 module.exports = CollectionEdit; 161 No newline at end of file -
src/wp-includes/js/media/controllers/cropper.js
1 /** 2 * wp.media.controller.Cropper 3 * 4 * A state for cropping an image. 5 * 6 * @class 7 * @augments wp.media.controller.State 8 * @augments Backbone.Model 9 */ 10 var State = require( './state.js' ), 11 ToolbarView = require( '../views/toolbar.js' ), 12 CropperView = require( '../views/cropper.js' ), 13 l10n = wp.media.view.l10n, 14 Cropper; 15 16 Cropper = State.extend({ 17 defaults: { 18 id: 'cropper', 19 title: l10n.cropImage, 20 // Region mode defaults. 21 toolbar: 'crop', 22 content: 'crop', 23 router: false, 24 25 canSkipCrop: false 26 }, 27 28 activate: function() { 29 this.frame.on( 'content:create:crop', this.createCropContent, this ); 30 this.frame.on( 'close', this.removeCropper, this ); 31 this.set('selection', new Backbone.Collection(this.frame._selection.single)); 32 }, 33 34 deactivate: function() { 35 this.frame.toolbar.mode('browse'); 36 }, 37 38 createCropContent: function() { 39 this.cropperView = new CropperView({ 40 controller: this, 41 attachment: this.get('selection').first() 42 }); 43 this.cropperView.on('image-loaded', this.createCropToolbar, this); 44 this.frame.content.set(this.cropperView); 45 46 }, 47 removeCropper: function() { 48 this.imgSelect.cancelSelection(); 49 this.imgSelect.setOptions({remove: true}); 50 this.imgSelect.update(); 51 this.cropperView.remove(); 52 }, 53 createCropToolbar: function() { 54 var canSkipCrop, toolbarOptions; 55 56 canSkipCrop = this.get('canSkipCrop') || false; 57 58 toolbarOptions = { 59 controller: this.frame, 60 items: { 61 insert: { 62 style: 'primary', 63 text: l10n.cropImage, 64 priority: 80, 65 requires: { library: false, selection: false }, 66 67 click: function() { 68 var self = this, 69 selection = this.controller.state().get('selection').first(); 70 71 selection.set({cropDetails: this.controller.state().imgSelect.getSelection()}); 72 73 this.$el.text(l10n.cropping); 74 this.$el.attr('disabled', true); 75 this.controller.state().doCrop( selection ).done( function( croppedImage ) { 76 self.controller.trigger('cropped', croppedImage ); 77 self.controller.close(); 78 }).fail( function() { 79 self.controller.trigger('content:error:crop'); 80 }); 81 } 82 } 83 } 84 }; 85 86 if ( canSkipCrop ) { 87 _.extend( toolbarOptions.items, { 88 skip: { 89 style: 'secondary', 90 text: l10n.skipCropping, 91 priority: 70, 92 requires: { library: false, selection: false }, 93 click: function() { 94 var selection = this.controller.state().get('selection').first(); 95 this.controller.state().cropperView.remove(); 96 this.controller.trigger('skippedcrop', selection); 97 this.controller.close(); 98 } 99 } 100 }); 101 } 102 103 this.frame.toolbar.set( new ToolbarView(toolbarOptions) ); 104 }, 105 106 doCrop: function( attachment ) { 107 return wp.ajax.post( 'custom-header-crop', { 108 nonce: attachment.get('nonces').edit, 109 id: attachment.get('id'), 110 cropDetails: attachment.get('cropDetails') 111 } ); 112 } 113 }); 114 115 module.exports = Cropper; 116 No newline at end of file -
src/wp-includes/js/media/controllers/edit-attachment-metadata.js
1 /** 2 * wp.media.controller.EditAttachmentMetadata 3 * 4 * A state for editing an attachment's metadata. 5 * 6 * @constructor 7 * @augments wp.media.controller.State 8 * @augments Backbone.Model 9 */ 10 var State = require( './state.js' ), 11 l10n = wp.media.view.l10n, 12 EditAttachmentMetadata; 13 14 EditAttachmentMetadata = State.extend({ 15 defaults: { 16 id: 'edit-attachment', 17 // Title string passed to the frame's title region view. 18 title: l10n.attachmentDetails, 19 // Region mode defaults. 20 content: 'edit-metadata', 21 menu: false, 22 toolbar: false, 23 router: false 24 } 25 }); 26 27 module.exports = EditAttachmentMetadata; 28 No newline at end of file -
src/wp-includes/js/media/controllers/edit-image.js
1 /** 2 * wp.media.controller.EditImage 3 * 4 * A state for editing (cropping, etc.) an image. 5 * 6 * @class 7 * @augments wp.media.controller.State 8 * @augments Backbone.Model 9 * 10 * @param {object} attributes The attributes hash passed to the state. 11 * @param {wp.media.model.Attachment} attributes.model The attachment. 12 * @param {string} [attributes.id=edit-image] Unique identifier. 13 * @param {string} [attributes.title=Edit Image] Title for the state. Displays in the media menu and the frame's title region. 14 * @param {string} [attributes.content=edit-image] Initial mode for the content region. 15 * @param {string} [attributes.toolbar=edit-image] Initial mode for the toolbar region. 16 * @param {string} [attributes.menu=false] Initial mode for the menu region. 17 * @param {string} [attributes.url] Unused. @todo Consider removal. 18 */ 19 var State = require( './state.js' ), 20 ToolbarView = require( '../views/toolbar.js' ), 21 l10n = wp.media.view.l10n, 22 EditImage; 23 24 EditImage = State.extend({ 25 defaults: { 26 id: 'edit-image', 27 title: l10n.editImage, 28 menu: false, 29 toolbar: 'edit-image', 30 content: 'edit-image', 31 url: '' 32 }, 33 34 /** 35 * @since 3.9.0 36 */ 37 activate: function() { 38 this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar ); 39 }, 40 41 /** 42 * @since 3.9.0 43 */ 44 deactivate: function() { 45 this.stopListening( this.frame ); 46 }, 47 48 /** 49 * @since 3.9.0 50 */ 51 toolbar: function() { 52 var frame = this.frame, 53 lastState = frame.lastState(), 54 previous = lastState && lastState.id; 55 56 frame.toolbar.set( new ToolbarView({ 57 controller: frame, 58 items: { 59 back: { 60 style: 'primary', 61 text: l10n.back, 62 priority: 20, 63 click: function() { 64 if ( previous ) { 65 frame.setState( previous ); 66 } else { 67 frame.close(); 68 } 69 } 70 } 71 } 72 }) ); 73 } 74 }); 75 76 module.exports = EditImage; 77 No newline at end of file -
src/wp-includes/js/media/controllers/embed.js
1 /** 2 * wp.media.controller.Embed 3 * 4 * A state for embedding media from a URL. 5 * 6 * @class 7 * @augments wp.media.controller.State 8 * @augments Backbone.Model 9 * 10 * @param {object} attributes The attributes hash passed to the state. 11 * @param {string} [attributes.id=embed] Unique identifier. 12 * @param {string} [attributes.title=Insert From URL] Title for the state. Displays in the media menu and the frame's title region. 13 * @param {string} [attributes.content=embed] Initial mode for the content region. 14 * @param {string} [attributes.menu=default] Initial mode for the menu region. 15 * @param {string} [attributes.toolbar=main-embed] Initial mode for the toolbar region. 16 * @param {string} [attributes.menu=false] Initial mode for the menu region. 17 * @param {int} [attributes.priority=120] The priority for the state link in the media menu. 18 * @param {string} [attributes.type=link] The type of embed. Currently only link is supported. 19 * @param {string} [attributes.url] The embed URL. 20 * @param {object} [attributes.metadata={}] Properties of the embed, which will override attributes.url if set. 21 */ 22 var State = require( './state.js' ), 23 l10n = wp.media.view.l10n, 24 Embed; 25 26 Embed = State.extend({ 27 defaults: { 28 id: 'embed', 29 title: l10n.insertFromUrlTitle, 30 content: 'embed', 31 menu: 'default', 32 toolbar: 'main-embed', 33 priority: 120, 34 type: 'link', 35 url: '', 36 metadata: {} 37 }, 38 39 // The amount of time used when debouncing the scan. 40 sensitivity: 200, 41 42 initialize: function(options) { 43 this.metadata = options.metadata; 44 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity ); 45 this.props = new Backbone.Model( this.metadata || { url: '' }); 46 this.props.on( 'change:url', this.debouncedScan, this ); 47 this.props.on( 'change:url', this.refresh, this ); 48 this.on( 'scan', this.scanImage, this ); 49 }, 50 51 /** 52 * Trigger a scan of the embedded URL's content for metadata required to embed. 53 * 54 * @fires wp.media.controller.Embed#scan 55 */ 56 scan: function() { 57 var scanners, 58 embed = this, 59 attributes = { 60 type: 'link', 61 scanners: [] 62 }; 63 64 // Scan is triggered with the list of `attributes` to set on the 65 // state, useful for the 'type' attribute and 'scanners' attribute, 66 // an array of promise objects for asynchronous scan operations. 67 if ( this.props.get('url') ) { 68 this.trigger( 'scan', attributes ); 69 } 70 71 if ( attributes.scanners.length ) { 72 scanners = attributes.scanners = $.when.apply( $, attributes.scanners ); 73 scanners.always( function() { 74 if ( embed.get('scanners') === scanners ) { 75 embed.set( 'loading', false ); 76 } 77 }); 78 } else { 79 attributes.scanners = null; 80 } 81 82 attributes.loading = !! attributes.scanners; 83 this.set( attributes ); 84 }, 85 /** 86 * Try scanning the embed as an image to discover its dimensions. 87 * 88 * @param {Object} attributes 89 */ 90 scanImage: function( attributes ) { 91 var frame = this.frame, 92 state = this, 93 url = this.props.get('url'), 94 image = new Image(), 95 deferred = $.Deferred(); 96 97 attributes.scanners.push( deferred.promise() ); 98 99 // Try to load the image and find its width/height. 100 image.onload = function() { 101 deferred.resolve(); 102 103 if ( state !== frame.state() || url !== state.props.get('url') ) { 104 return; 105 } 106 107 state.set({ 108 type: 'image' 109 }); 110 111 state.props.set({ 112 width: image.width, 113 height: image.height 114 }); 115 }; 116 117 image.onerror = deferred.reject; 118 image.src = url; 119 }, 120 121 refresh: function() { 122 this.frame.toolbar.get().refresh(); 123 }, 124 125 reset: function() { 126 this.props.clear().set({ url: '' }); 127 128 if ( this.active ) { 129 this.refresh(); 130 } 131 } 132 }); 133 134 module.exports = Embed; 135 No newline at end of file -
src/wp-includes/js/media/controllers/featured-image.js
1 /** 2 * wp.media.controller.FeaturedImage 3 * 4 * A state for selecting a featured image for a post. 5 * 6 * @class 7 * @augments wp.media.controller.Library 8 * @augments wp.media.controller.State 9 * @augments Backbone.Model 10 * 11 * @param {object} [attributes] The attributes hash passed to the state. 12 * @param {string} [attributes.id=featured-image] Unique identifier. 13 * @param {string} [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region. 14 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 15 * If one is not supplied, a collection of all images will be created. 16 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 17 * @param {string} [attributes.content=upload] Initial mode for the content region. 18 * Overridden by persistent user setting if 'contentUserSetting' is true. 19 * @param {string} [attributes.menu=default] Initial mode for the menu region. 20 * @param {string} [attributes.router=browse] Initial mode for the router region. 21 * @param {string} [attributes.toolbar=featured-image] Initial mode for the toolbar region. 22 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 23 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 24 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 25 * Accepts 'all', 'uploaded', or 'unattached'. 26 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 27 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 28 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 29 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 30 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 31 */ 32 var Attachment = require( '../models/attachment.js' ), 33 Library = require( './library.js' ), 34 l10n = wp.media.view.l10n, 35 FeaturedImage; 36 37 FeaturedImage = Library.extend({ 38 defaults: _.defaults({ 39 id: 'featured-image', 40 title: l10n.setFeaturedImageTitle, 41 multiple: false, 42 filterable: 'uploaded', 43 toolbar: 'featured-image', 44 priority: 60, 45 syncSelection: true 46 }, Library.prototype.defaults ), 47 48 /** 49 * @since 3.5.0 50 */ 51 initialize: function() { 52 var library, comparator; 53 54 // If we haven't been provided a `library`, create a `Selection`. 55 if ( ! this.get('library') ) { 56 this.set( 'library', wp.media.query({ type: 'image' }) ); 57 } 58 59 Library.prototype.initialize.apply( this, arguments ); 60 61 library = this.get('library'); 62 comparator = library.comparator; 63 64 // Overload the library's comparator to push items that are not in 65 // the mirrored query to the front of the aggregate collection. 66 library.comparator = function( a, b ) { 67 var aInQuery = !! this.mirroring.get( a.cid ), 68 bInQuery = !! this.mirroring.get( b.cid ); 69 70 if ( ! aInQuery && bInQuery ) { 71 return -1; 72 } else if ( aInQuery && ! bInQuery ) { 73 return 1; 74 } else { 75 return comparator.apply( this, arguments ); 76 } 77 }; 78 79 // Add all items in the selection to the library, so any featured 80 // images that are not initially loaded still appear. 81 library.observe( this.get('selection') ); 82 }, 83 84 /** 85 * @since 3.5.0 86 */ 87 activate: function() { 88 this.updateSelection(); 89 this.frame.on( 'open', this.updateSelection, this ); 90 91 Library.prototype.activate.apply( this, arguments ); 92 }, 93 94 /** 95 * @since 3.5.0 96 */ 97 deactivate: function() { 98 this.frame.off( 'open', this.updateSelection, this ); 99 100 Library.prototype.deactivate.apply( this, arguments ); 101 }, 102 103 /** 104 * @since 3.5.0 105 */ 106 updateSelection: function() { 107 var selection = this.get('selection'), 108 id = wp.media.view.settings.post.featuredImageId, 109 attachment; 110 111 if ( '' !== id && -1 !== id ) { 112 attachment = Attachment.get( id ); 113 attachment.fetch(); 114 } 115 116 selection.reset( attachment ? [ attachment ] : [] ); 117 } 118 }); 119 120 module.exports = FeaturedImage; 121 No newline at end of file -
src/wp-includes/js/media/controllers/gallery-add.js
1 /** 2 * A state for selecting more images to add to a gallery. 3 * 4 * @class 5 * @augments wp.media.controller.Library 6 * @augments wp.media.controller.State 7 * @augments Backbone.Model 8 * 9 * @param {object} [attributes] The attributes hash passed to the state. 10 * @param {string} [attributes.id=gallery-library] Unique identifier. 11 * @param {string} [attributes.title=Add to Gallery] Title for the state. Displays in the frame's title region. 12 * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. 13 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 14 * If one is not supplied, a collection of all images will be created. 15 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 16 * Accepts 'all', 'uploaded', or 'unattached'. 17 * @param {string} [attributes.menu=gallery] Initial mode for the menu region. 18 * @param {string} [attributes.content=upload] Initial mode for the content region. 19 * Overridden by persistent user setting if 'contentUserSetting' is true. 20 * @param {string} [attributes.router=browse] Initial mode for the router region. 21 * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region. 22 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 23 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 24 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 25 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 26 * @param {int} [attributes.priority=100] The priority for the state link in the media menu. 27 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 28 * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. 29 */ 30 var Selection = require( '../models/selection.js' ), 31 Library = require( './library.js' ), 32 l10n = wp.media.view.l10n, 33 GalleryAdd; 34 35 GalleryAdd = Library.extend({ 36 defaults: _.defaults({ 37 id: 'gallery-library', 38 title: l10n.addToGalleryTitle, 39 multiple: 'add', 40 filterable: 'uploaded', 41 menu: 'gallery', 42 toolbar: 'gallery-add', 43 priority: 100, 44 syncSelection: false 45 }, Library.prototype.defaults ), 46 47 /** 48 * @since 3.5.0 49 */ 50 initialize: function() { 51 // If a library wasn't supplied, create a library of images. 52 if ( ! this.get('library') ) 53 this.set( 'library', wp.media.query({ type: 'image' }) ); 54 55 Library.prototype.initialize.apply( this, arguments ); 56 }, 57 58 /** 59 * @since 3.5.0 60 */ 61 activate: function() { 62 var library = this.get('library'), 63 edit = this.frame.state('gallery-edit').get('library'); 64 65 if ( this.editLibrary && this.editLibrary !== edit ) 66 library.unobserve( this.editLibrary ); 67 68 // Accepts attachments that exist in the original library and 69 // that do not exist in gallery's library. 70 library.validator = function( attachment ) { 71 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && Selection.prototype.validator.apply( this, arguments ); 72 }; 73 74 // Reset the library to ensure that all attachments are re-added 75 // to the collection. Do so silently, as calling `observe` will 76 // trigger the `reset` event. 77 library.reset( library.mirroring.models, { silent: true }); 78 library.observe( edit ); 79 this.editLibrary = edit; 80 81 Library.prototype.activate.apply( this, arguments ); 82 } 83 }); 84 85 module.exports = GalleryAdd; 86 No newline at end of file -
src/wp-includes/js/media/controllers/gallery-edit.js
1 /** 2 * wp.media.controller.GalleryEdit 3 * 4 * A state for editing a gallery's images and settings. 5 * 6 * @class 7 * @augments wp.media.controller.Library 8 * @augments wp.media.controller.State 9 * @augments Backbone.Model 10 * 11 * @param {object} [attributes] The attributes hash passed to the state. 12 * @param {string} [attributes.id=gallery-edit] Unique identifier. 13 * @param {string} [attributes.title=Edit Gallery] Title for the state. Displays in the frame's title region. 14 * @param {wp.media.model.Attachments} [attributes.library] The collection of attachments in the gallery. 15 * If one is not supplied, an empty media.model.Selection collection is created. 16 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 17 * @param {boolean} [attributes.searchable=false] Whether the library is searchable. 18 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 19 * @param {string|false} [attributes.content=browse] Initial mode for the content region. 20 * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region. 21 * @param {boolean} [attributes.describe=true] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 22 * @param {boolean} [attributes.displaySettings=true] Whether to show the attachment display settings interface. 23 * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable. 24 * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments. 25 * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance. 26 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 27 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 28 * Defaults to false for this state, because the library passed in *is* the selection. 29 * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`. 30 * If none supplied, defaults to wp.media.view.Attachment.EditLibrary. 31 */ 32 var Selection = require( '../models/selection.js' ), 33 Library = require( './library.js' ), 34 EditLibraryView = require( '../views/attachment/edit-library.js' ), 35 GallerySettingsView = require( '../views/settings/gallery.js' ), 36 l10n = wp.media.view.l10n, 37 GalleryEdit; 38 39 GalleryEdit = Library.extend({ 40 defaults: { 41 id: 'gallery-edit', 42 title: l10n.editGalleryTitle, 43 multiple: false, 44 searchable: false, 45 sortable: true, 46 display: false, 47 content: 'browse', 48 toolbar: 'gallery-edit', 49 describe: true, 50 displaySettings: true, 51 dragInfo: true, 52 idealColumnWidth: 170, 53 editing: false, 54 priority: 60, 55 syncSelection: false 56 }, 57 58 /** 59 * @since 3.5.0 60 */ 61 initialize: function() { 62 // If we haven't been provided a `library`, create a `Selection`. 63 if ( ! this.get('library') ) 64 this.set( 'library', new Selection() ); 65 66 // The single `Attachment` view to be used in the `Attachments` view. 67 if ( ! this.get('AttachmentView') ) 68 this.set( 'AttachmentView', EditLibraryView ); 69 Library.prototype.initialize.apply( this, arguments ); 70 }, 71 72 /** 73 * @since 3.5.0 74 */ 75 activate: function() { 76 var library = this.get('library'); 77 78 // Limit the library to images only. 79 library.props.set( 'type', 'image' ); 80 81 // Watch for uploaded attachments. 82 this.get('library').observe( wp.Uploader.queue ); 83 84 this.frame.on( 'content:render:browse', this.gallerySettings, this ); 85 86 Library.prototype.activate.apply( this, arguments ); 87 }, 88 89 /** 90 * @since 3.5.0 91 */ 92 deactivate: function() { 93 // Stop watching for uploaded attachments. 94 this.get('library').unobserve( wp.Uploader.queue ); 95 96 this.frame.off( 'content:render:browse', this.gallerySettings, this ); 97 98 Library.prototype.deactivate.apply( this, arguments ); 99 }, 100 101 /** 102 * @since 3.5.0 103 * 104 * @param browser 105 */ 106 gallerySettings: function( browser ) { 107 if ( ! this.get('displaySettings') ) { 108 return; 109 } 110 111 var library = this.get('library'); 112 113 if ( ! library || ! browser ) { 114 return; 115 } 116 117 library.gallery = library.gallery || new Backbone.Model(); 118 119 browser.sidebar.set({ 120 gallery: new GallerySettingsView({ 121 controller: this, 122 model: library.gallery, 123 priority: 40 124 }) 125 }); 126 127 browser.toolbar.set( 'reverse', { 128 text: l10n.reverseOrder, 129 priority: 80, 130 131 click: function() { 132 library.reset( library.toArray().reverse() ); 133 } 134 }); 135 } 136 }); 137 138 module.exports = GalleryEdit; 139 No newline at end of file -
src/wp-includes/js/media/controllers/image-details.js
1 /** 2 * wp.media.controller.ImageDetails 3 * 4 * A state for editing the attachment display settings of an image that's been 5 * inserted into the editor. 6 * 7 * @class 8 * @augments wp.media.controller.State 9 * @augments Backbone.Model 10 * 11 * @param {object} [attributes] The attributes hash passed to the state. 12 * @param {string} [attributes.id=image-details] Unique identifier. 13 * @param {string} [attributes.title=Image Details] Title for the state. Displays in the frame's title region. 14 * @param {wp.media.model.Attachment} attributes.image The image's model. 15 * @param {string|false} [attributes.content=image-details] Initial mode for the content region. 16 * @param {string|false} [attributes.menu=false] Initial mode for the menu region. 17 * @param {string|false} [attributes.router=false] Initial mode for the router region. 18 * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region. 19 * @param {boolean} [attributes.editing=false] Unused. 20 * @param {int} [attributes.priority=60] Unused. 21 * 22 * @todo This state inherits some defaults from media.controller.Library.prototype.defaults, 23 * however this may not do anything. 24 */ 25 var State = require( './state.js' ), 26 Library = require( './library.js' ), 27 l10n = wp.media.view.l10n, 28 ImageDetails; 29 30 ImageDetails = State.extend({ 31 defaults: _.defaults({ 32 id: 'image-details', 33 title: l10n.imageDetailsTitle, 34 content: 'image-details', 35 menu: false, 36 router: false, 37 toolbar: 'image-details', 38 editing: false, 39 priority: 60 40 }, Library.prototype.defaults ), 41 42 /** 43 * @since 3.9.0 44 * 45 * @param options Attributes 46 */ 47 initialize: function( options ) { 48 this.image = options.image; 49 State.prototype.initialize.apply( this, arguments ); 50 }, 51 52 /** 53 * @since 3.9.0 54 */ 55 activate: function() { 56 this.frame.modal.$el.addClass('image-details'); 57 } 58 }); 59 60 module.exports = ImageDetails; 61 No newline at end of file -
src/wp-includes/js/media/controllers/library.js
1 /** 2 * wp.media.controller.Library 3 * 4 * A state for choosing an attachment or group of attachments from the media library. 5 * 6 * @class 7 * @augments wp.media.controller.State 8 * @augments Backbone.Model 9 * @mixes media.selectionSync 10 * 11 * @param {object} [attributes] The attributes hash passed to the state. 12 * @param {string} [attributes.id=library] Unique identifier. 13 * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region. 14 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 15 * If one is not supplied, a collection of all attachments will be created. 16 * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state. 17 * If the 'selection' attribute is a plain JS object, 18 * a Selection will be created using its values as the selection instance's `props` model. 19 * Otherwise, it will copy the library's `props` model. 20 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 21 * @param {string} [attributes.content=upload] Initial mode for the content region. 22 * Overridden by persistent user setting if 'contentUserSetting' is true. 23 * @param {string} [attributes.menu=default] Initial mode for the menu region. 24 * @param {string} [attributes.router=browse] Initial mode for the router region. 25 * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region. 26 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 27 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 28 * Accepts 'all', 'uploaded', or 'unattached'. 29 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 30 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 31 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 32 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 33 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 34 */ 35 var selectionSync = require( '../utils/selection-sync.js' ), 36 SelectionModel = require( '../models/selection.js' ), 37 State = require( './state.js' ), 38 l10n = wp.media.view.l10n, 39 Library; 40 41 Library = State.extend({ 42 defaults: { 43 id: 'library', 44 title: l10n.mediaLibraryTitle, 45 multiple: false, 46 content: 'upload', 47 menu: 'default', 48 router: 'browse', 49 toolbar: 'select', 50 searchable: true, 51 filterable: false, 52 sortable: true, 53 autoSelect: true, 54 describe: false, 55 contentUserSetting: true, 56 syncSelection: true 57 }, 58 59 /** 60 * If a library isn't provided, query all media items. 61 * If a selection instance isn't provided, create one. 62 * 63 * @since 3.5.0 64 */ 65 initialize: function() { 66 var selection = this.get('selection'), 67 props; 68 69 if ( ! this.get('library') ) { 70 this.set( 'library', wp.media.query() ); 71 } 72 73 if ( ! (selection instanceof SelectionModel) ) { 74 props = selection; 75 76 if ( ! props ) { 77 props = this.get('library').props.toJSON(); 78 props = _.omit( props, 'orderby', 'query' ); 79 } 80 81 this.set( 'selection', new SelectionModel( null, { 82 multiple: this.get('multiple'), 83 props: props 84 }) ); 85 } 86 87 this.resetDisplays(); 88 }, 89 90 /** 91 * @since 3.5.0 92 */ 93 activate: function() { 94 this.syncSelection(); 95 96 wp.Uploader.queue.on( 'add', this.uploading, this ); 97 98 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 99 100 if ( this.get( 'router' ) && this.get('contentUserSetting') ) { 101 this.frame.on( 'content:activate', this.saveContentMode, this ); 102 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 103 } 104 }, 105 106 /** 107 * @since 3.5.0 108 */ 109 deactivate: function() { 110 this.recordSelection(); 111 112 this.frame.off( 'content:activate', this.saveContentMode, this ); 113 114 // Unbind all event handlers that use this state as the context 115 // from the selection. 116 this.get('selection').off( null, null, this ); 117 118 wp.Uploader.queue.off( null, null, this ); 119 }, 120 121 /** 122 * Reset the library to its initial state. 123 * 124 * @since 3.5.0 125 */ 126 reset: function() { 127 this.get('selection').reset(); 128 this.resetDisplays(); 129 this.refreshContent(); 130 }, 131 132 /** 133 * Reset the attachment display settings defaults to the site options. 134 * 135 * If site options don't define them, fall back to a persistent user setting. 136 * 137 * @since 3.5.0 138 */ 139 resetDisplays: function() { 140 var defaultProps = wp.media.view.settings.defaultProps; 141 this._displays = []; 142 this._defaultDisplaySettings = { 143 align: defaultProps.align || getUserSetting( 'align', 'none' ), 144 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 145 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 146 }; 147 }, 148 149 /** 150 * Create a model to represent display settings (alignment, etc.) for an attachment. 151 * 152 * @since 3.5.0 153 * 154 * @param {wp.media.model.Attachment} attachment 155 * @returns {Backbone.Model} 156 */ 157 display: function( attachment ) { 158 var displays = this._displays; 159 160 if ( ! displays[ attachment.cid ] ) { 161 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 162 } 163 return displays[ attachment.cid ]; 164 }, 165 166 /** 167 * Given an attachment, create attachment display settings properties. 168 * 169 * @since 3.6.0 170 * 171 * @param {wp.media.model.Attachment} attachment 172 * @returns {Object} 173 */ 174 defaultDisplaySettings: function( attachment ) { 175 var settings = this._defaultDisplaySettings; 176 if ( settings.canEmbed = this.canEmbed( attachment ) ) { 177 settings.link = 'embed'; 178 } 179 return settings; 180 }, 181 182 /** 183 * Whether an attachment can be embedded (audio or video). 184 * 185 * @since 3.6.0 186 * 187 * @param {wp.media.model.Attachment} attachment 188 * @returns {Boolean} 189 */ 190 canEmbed: function( attachment ) { 191 // If uploading, we know the filename but not the mime type. 192 if ( ! attachment.get('uploading') ) { 193 var type = attachment.get('type'); 194 if ( type !== 'audio' && type !== 'video' ) { 195 return false; 196 } 197 } 198 199 return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 200 }, 201 202 203 /** 204 * If the state is active, no items are selected, and the current 205 * content mode is not an option in the state's router (provided 206 * the state has a router), reset the content mode to the default. 207 * 208 * @since 3.5.0 209 */ 210 refreshContent: function() { 211 var selection = this.get('selection'), 212 frame = this.frame, 213 router = frame.router.get(), 214 mode = frame.content.mode(); 215 216 if ( this.active && ! selection.length && router && ! router.get( mode ) ) { 217 this.frame.content.render( this.get('content') ); 218 } 219 }, 220 221 /** 222 * Callback handler when an attachment is uploaded. 223 * 224 * Switch to the Media Library if uploaded from the 'Upload Files' tab. 225 * 226 * Adds any uploading attachments to the selection. 227 * 228 * If the state only supports one attachment to be selected and multiple 229 * attachments are uploaded, the last attachment in the upload queue will 230 * be selected. 231 * 232 * @since 3.5.0 233 * 234 * @param {wp.media.model.Attachment} attachment 235 */ 236 uploading: function( attachment ) { 237 var content = this.frame.content; 238 239 if ( 'upload' === content.mode() ) { 240 this.frame.content.mode('browse'); 241 } 242 243 if ( this.get( 'autoSelect' ) ) { 244 this.get('selection').add( attachment ); 245 this.frame.trigger( 'library:selection:add' ); 246 } 247 }, 248 249 /** 250 * Persist the mode of the content region as a user setting. 251 * 252 * @since 3.5.0 253 */ 254 saveContentMode: function() { 255 if ( 'browse' !== this.get('router') ) { 256 return; 257 } 258 259 var mode = this.frame.content.mode(), 260 view = this.frame.router.get(); 261 262 if ( view && view.get( mode ) ) { 263 setUserSetting( 'libraryContent', mode ); 264 } 265 } 266 }); 267 268 // Make selectionSync available on any Media Library state. 269 _.extend( Library.prototype, selectionSync ); 270 271 module.exports = Library; 272 No newline at end of file -
src/wp-includes/js/media/controllers/media-library.js
1 /** 2 * wp.media.controller.MediaLibrary 3 * 4 * @class 5 * @augments wp.media.controller.Library 6 * @augments wp.media.controller.State 7 * @augments Backbone.Model 8 */ 9 var Library = require( './library.js' ), 10 MediaLibrary; 11 12 MediaLibrary = Library.extend({ 13 defaults: _.defaults({ 14 // Attachments browser defaults. @see media.view.AttachmentsBrowser 15 filterable: 'uploaded', 16 17 displaySettings: false, 18 priority: 80, 19 syncSelection: false 20 }, Library.prototype.defaults ), 21 22 /** 23 * @since 3.9.0 24 * 25 * @param options 26 */ 27 initialize: function( options ) { 28 this.media = options.media; 29 this.type = options.type; 30 this.set( 'library', wp.media.query({ type: this.type }) ); 31 32 Library.prototype.initialize.apply( this, arguments ); 33 }, 34 35 /** 36 * @since 3.9.0 37 */ 38 activate: function() { 39 // @todo this should use this.frame. 40 if ( wp.media.frame.lastMime ) { 41 this.set( 'library', wp.media.query({ type: wp.media.frame.lastMime }) ); 42 delete wp.media.frame.lastMime; 43 } 44 Library.prototype.activate.apply( this, arguments ); 45 } 46 }); 47 48 module.exports = MediaLibrary; 49 No newline at end of file -
src/wp-includes/js/media/controllers/region.js
1 /** 2 * wp.media.controller.Region 3 * 4 * A region is a persistent application layout area. 5 * 6 * A region assumes one mode at any time, and can be switched to another. 7 * 8 * When mode changes, events are triggered on the region's parent view. 9 * The parent view will listen to specific events and fill the region with an 10 * appropriate view depending on mode. For example, a frame listens for the 11 * 'browse' mode t be activated on the 'content' view and then fills the region 12 * with an AttachmentsBrowser view. 13 * 14 * @class 15 * 16 * @param {object} options Options hash for the region. 17 * @param {string} options.id Unique identifier for the region. 18 * @param {Backbone.View} options.view A parent view the region exists within. 19 * @param {string} options.selector jQuery selector for the region within the parent view. 20 */ 21 var Region = function( options ) { 22 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); 23 }; 24 25 // Use Backbone's self-propagating `extend` inheritance method. 26 Region.extend = Backbone.Model.extend; 27 28 _.extend( Region.prototype, { 29 /** 30 * Activate a mode. 31 * 32 * @since 3.5.0 33 * 34 * @param {string} mode 35 * 36 * @fires this.view#{this.id}:activate:{this._mode} 37 * @fires this.view#{this.id}:activate 38 * @fires this.view#{this.id}:deactivate:{this._mode} 39 * @fires this.view#{this.id}:deactivate 40 * 41 * @returns {wp.media.controller.Region} Returns itself to allow chaining. 42 */ 43 mode: function( mode ) { 44 if ( ! mode ) { 45 return this._mode; 46 } 47 // Bail if we're trying to change to the current mode. 48 if ( mode === this._mode ) { 49 return this; 50 } 51 52 /** 53 * Region mode deactivation event. 54 * 55 * @event this.view#{this.id}:deactivate:{this._mode} 56 * @event this.view#{this.id}:deactivate 57 */ 58 this.trigger('deactivate'); 59 60 this._mode = mode; 61 this.render( mode ); 62 63 /** 64 * Region mode activation event. 65 * 66 * @event this.view#{this.id}:activate:{this._mode} 67 * @event this.view#{this.id}:activate 68 */ 69 this.trigger('activate'); 70 return this; 71 }, 72 /** 73 * Render a mode. 74 * 75 * @since 3.5.0 76 * 77 * @param {string} mode 78 * 79 * @fires this.view#{this.id}:create:{this._mode} 80 * @fires this.view#{this.id}:create 81 * @fires this.view#{this.id}:render:{this._mode} 82 * @fires this.view#{this.id}:render 83 * 84 * @returns {wp.media.controller.Region} Returns itself to allow chaining 85 */ 86 render: function( mode ) { 87 // If the mode isn't active, activate it. 88 if ( mode && mode !== this._mode ) { 89 return this.mode( mode ); 90 } 91 92 var set = { view: null }, 93 view; 94 95 /** 96 * Create region view event. 97 * 98 * Region view creation takes place in an event callback on the frame. 99 * 100 * @event this.view#{this.id}:create:{this._mode} 101 * @event this.view#{this.id}:create 102 */ 103 this.trigger( 'create', set ); 104 view = set.view; 105 106 /** 107 * Render region view event. 108 * 109 * Region view creation takes place in an event callback on the frame. 110 * 111 * @event this.view#{this.id}:create:{this._mode} 112 * @event this.view#{this.id}:create 113 */ 114 this.trigger( 'render', view ); 115 if ( view ) { 116 this.set( view ); 117 } 118 return this; 119 }, 120 121 /** 122 * Get the region's view. 123 * 124 * @since 3.5.0 125 * 126 * @returns {wp.media.View} 127 */ 128 get: function() { 129 return this.view.views.first( this.selector ); 130 }, 131 132 /** 133 * Set the region's view as a subview of the frame. 134 * 135 * @since 3.5.0 136 * 137 * @param {Array|Object} views 138 * @param {Object} [options={}] 139 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining 140 */ 141 set: function( views, options ) { 142 if ( options ) { 143 options.add = false; 144 } 145 return this.view.views.set( this.selector, views, options ); 146 }, 147 148 /** 149 * Trigger regional view events on the frame. 150 * 151 * @since 3.5.0 152 * 153 * @param {string} event 154 * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining. 155 */ 156 trigger: function( event ) { 157 var base, args; 158 159 if ( ! this._mode ) { 160 return; 161 } 162 163 args = _.toArray( arguments ); 164 base = this.id + ':' + event; 165 166 // Trigger `{this.id}:{event}:{this._mode}` event on the frame. 167 args[0] = base + ':' + this._mode; 168 this.view.trigger.apply( this.view, args ); 169 170 // Trigger `{this.id}:{event}` event on the frame. 171 args[0] = base; 172 this.view.trigger.apply( this.view, args ); 173 return this; 174 } 175 }); 176 177 module.exports = Region; 178 No newline at end of file -
src/wp-includes/js/media/controllers/replace-image.js
1 /** 2 * wp.media.controller.ReplaceImage 3 * 4 * A state for replacing an image. 5 * 6 * @class 7 * @augments wp.media.controller.Library 8 * @augments wp.media.controller.State 9 * @augments Backbone.Model 10 * 11 * @param {object} [attributes] The attributes hash passed to the state. 12 * @param {string} [attributes.id=replace-image] Unique identifier. 13 * @param {string} [attributes.title=Replace Image] Title for the state. Displays in the media menu and the frame's title region. 14 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 15 * If one is not supplied, a collection of all images will be created. 16 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 17 * @param {string} [attributes.content=upload] Initial mode for the content region. 18 * Overridden by persistent user setting if 'contentUserSetting' is true. 19 * @param {string} [attributes.menu=default] Initial mode for the menu region. 20 * @param {string} [attributes.router=browse] Initial mode for the router region. 21 * @param {string} [attributes.toolbar=replace] Initial mode for the toolbar region. 22 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 23 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 24 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 25 * Accepts 'all', 'uploaded', or 'unattached'. 26 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 27 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 28 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 29 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 30 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 31 */ 32 var Library = require( './library.js' ), 33 l10n = wp.media.view.l10n, 34 ReplaceImage; 35 36 ReplaceImage = Library.extend({ 37 defaults: _.defaults({ 38 id: 'replace-image', 39 title: l10n.replaceImageTitle, 40 multiple: false, 41 filterable: 'uploaded', 42 toolbar: 'replace', 43 menu: false, 44 priority: 60, 45 syncSelection: true 46 }, Library.prototype.defaults ), 47 48 /** 49 * @since 3.9.0 50 * 51 * @param options 52 */ 53 initialize: function( options ) { 54 var library, comparator; 55 56 this.image = options.image; 57 // If we haven't been provided a `library`, create a `Selection`. 58 if ( ! this.get('library') ) { 59 this.set( 'library', wp.media.query({ type: 'image' }) ); 60 } 61 62 Library.prototype.initialize.apply( this, arguments ); 63 64 library = this.get('library'); 65 comparator = library.comparator; 66 67 // Overload the library's comparator to push items that are not in 68 // the mirrored query to the front of the aggregate collection. 69 library.comparator = function( a, b ) { 70 var aInQuery = !! this.mirroring.get( a.cid ), 71 bInQuery = !! this.mirroring.get( b.cid ); 72 73 if ( ! aInQuery && bInQuery ) { 74 return -1; 75 } else if ( aInQuery && ! bInQuery ) { 76 return 1; 77 } else { 78 return comparator.apply( this, arguments ); 79 } 80 }; 81 82 // Add all items in the selection to the library, so any featured 83 // images that are not initially loaded still appear. 84 library.observe( this.get('selection') ); 85 }, 86 87 /** 88 * @since 3.9.0 89 */ 90 activate: function() { 91 this.updateSelection(); 92 Library.prototype.activate.apply( this, arguments ); 93 }, 94 95 /** 96 * @since 3.9.0 97 */ 98 updateSelection: function() { 99 var selection = this.get('selection'), 100 attachment = this.image.attachment; 101 102 selection.reset( attachment ? [ attachment ] : [] ); 103 } 104 }); 105 106 module.exports = ReplaceImage; 107 No newline at end of file -
src/wp-includes/js/media/controllers/state-machine.js
1 /** 2 * wp.media.controller.StateMachine 3 * 4 * A state machine keeps track of state. It is in one state at a time, 5 * and can change from one state to another. 6 * 7 * States are stored as models in a Backbone collection. 8 * 9 * @since 3.5.0 10 * 11 * @class 12 * @augments Backbone.Model 13 * @mixin 14 * @mixes Backbone.Events 15 * 16 * @param {Array} states 17 */ 18 var StateMachine = function( states ) { 19 // @todo This is dead code. The states collection gets created in media.view.Frame._createStates. 20 this.states = new Backbone.Collection( states ); 21 }; 22 23 // Use Backbone's self-propagating `extend` inheritance method. 24 StateMachine.extend = Backbone.Model.extend; 25 26 _.extend( StateMachine.prototype, Backbone.Events, { 27 /** 28 * Fetch a state. 29 * 30 * If no `id` is provided, returns the active state. 31 * 32 * Implicitly creates states. 33 * 34 * Ensure that the `states` collection exists so the `StateMachine` 35 * can be used as a mixin. 36 * 37 * @since 3.5.0 38 * 39 * @param {string} id 40 * @returns {wp.media.controller.State} Returns a State model 41 * from the StateMachine collection 42 */ 43 state: function( id ) { 44 this.states = this.states || new Backbone.Collection(); 45 46 // Default to the active state. 47 id = id || this._state; 48 49 if ( id && ! this.states.get( id ) ) { 50 this.states.add({ id: id }); 51 } 52 return this.states.get( id ); 53 }, 54 55 /** 56 * Sets the active state. 57 * 58 * Bail if we're trying to select the current state, if we haven't 59 * created the `states` collection, or are trying to select a state 60 * that does not exist. 61 * 62 * @since 3.5.0 63 * 64 * @param {string} id 65 * 66 * @fires wp.media.controller.State#deactivate 67 * @fires wp.media.controller.State#activate 68 * 69 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining 70 */ 71 setState: function( id ) { 72 var previous = this.state(); 73 74 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) { 75 return this; 76 } 77 78 if ( previous ) { 79 previous.trigger('deactivate'); 80 this._lastState = previous.id; 81 } 82 83 this._state = id; 84 this.state().trigger('activate'); 85 86 return this; 87 }, 88 89 /** 90 * Returns the previous active state. 91 * 92 * Call the `state()` method with no parameters to retrieve the current 93 * active state. 94 * 95 * @since 3.5.0 96 * 97 * @returns {wp.media.controller.State} Returns a State model 98 * from the StateMachine collection 99 */ 100 lastState: function() { 101 if ( this._lastState ) { 102 return this.state( this._lastState ); 103 } 104 } 105 }); 106 107 // Map all event binding and triggering on a StateMachine to its `states` collection. 108 _.each([ 'on', 'off', 'trigger' ], function( method ) { 109 /** 110 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. 111 */ 112 StateMachine.prototype[ method ] = function() { 113 // Ensure that the `states` collection exists so the `StateMachine` 114 // can be used as a mixin. 115 this.states = this.states || new Backbone.Collection(); 116 // Forward the method to the `states` collection. 117 this.states[ method ].apply( this.states, arguments ); 118 return this; 119 }; 120 }); 121 122 module.exports = StateMachine; 123 No newline at end of file -
src/wp-includes/js/media/controllers/state.js
1 /** 2 * wp.media.controller.State 3 * 4 * A state is a step in a workflow that when set will trigger the controllers 5 * for the regions to be updated as specified in the frame. 6 * 7 * A state has an event-driven lifecycle: 8 * 9 * 'ready' triggers when a state is added to a state machine's collection. 10 * 'activate' triggers when a state is activated by a state machine. 11 * 'deactivate' triggers when a state is deactivated by a state machine. 12 * 'reset' is not triggered automatically. It should be invoked by the 13 * proper controller to reset the state to its default. 14 * 15 * @class 16 * @augments Backbone.Model 17 */ 18 var State = Backbone.Model.extend({ 19 /** 20 * Constructor. 21 * 22 * @since 3.5.0 23 */ 24 constructor: function() { 25 this.on( 'activate', this._preActivate, this ); 26 this.on( 'activate', this.activate, this ); 27 this.on( 'activate', this._postActivate, this ); 28 this.on( 'deactivate', this._deactivate, this ); 29 this.on( 'deactivate', this.deactivate, this ); 30 this.on( 'reset', this.reset, this ); 31 this.on( 'ready', this._ready, this ); 32 this.on( 'ready', this.ready, this ); 33 /** 34 * Call parent constructor with passed arguments 35 */ 36 Backbone.Model.apply( this, arguments ); 37 this.on( 'change:menu', this._updateMenu, this ); 38 }, 39 /** 40 * Ready event callback. 41 * 42 * @abstract 43 * @since 3.5.0 44 */ 45 ready: function() {}, 46 47 /** 48 * Activate event callback. 49 * 50 * @abstract 51 * @since 3.5.0 52 */ 53 activate: function() {}, 54 55 /** 56 * Deactivate event callback. 57 * 58 * @abstract 59 * @since 3.5.0 60 */ 61 deactivate: function() {}, 62 63 /** 64 * Reset event callback. 65 * 66 * @abstract 67 * @since 3.5.0 68 */ 69 reset: function() {}, 70 71 /** 72 * @access private 73 * @since 3.5.0 74 */ 75 _ready: function() { 76 this._updateMenu(); 77 }, 78 79 /** 80 * @access private 81 * @since 3.5.0 82 */ 83 _preActivate: function() { 84 this.active = true; 85 }, 86 87 /** 88 * @access private 89 * @since 3.5.0 90 */ 91 _postActivate: function() { 92 this.on( 'change:menu', this._menu, this ); 93 this.on( 'change:titleMode', this._title, this ); 94 this.on( 'change:content', this._content, this ); 95 this.on( 'change:toolbar', this._toolbar, this ); 96 97 this.frame.on( 'title:render:default', this._renderTitle, this ); 98 99 this._title(); 100 this._menu(); 101 this._toolbar(); 102 this._content(); 103 this._router(); 104 }, 105 106 /** 107 * @access private 108 * @since 3.5.0 109 */ 110 _deactivate: function() { 111 this.active = false; 112 113 this.frame.off( 'title:render:default', this._renderTitle, this ); 114 115 this.off( 'change:menu', this._menu, this ); 116 this.off( 'change:titleMode', this._title, this ); 117 this.off( 'change:content', this._content, this ); 118 this.off( 'change:toolbar', this._toolbar, this ); 119 }, 120 121 /** 122 * @access private 123 * @since 3.5.0 124 */ 125 _title: function() { 126 this.frame.title.render( this.get('titleMode') || 'default' ); 127 }, 128 129 /** 130 * @access private 131 * @since 3.5.0 132 */ 133 _renderTitle: function( view ) { 134 view.$el.text( this.get('title') || '' ); 135 }, 136 137 /** 138 * @access private 139 * @since 3.5.0 140 */ 141 _router: function() { 142 var router = this.frame.router, 143 mode = this.get('router'), 144 view; 145 146 this.frame.$el.toggleClass( 'hide-router', ! mode ); 147 if ( ! mode ) { 148 return; 149 } 150 151 this.frame.router.render( mode ); 152 153 view = router.get(); 154 if ( view && view.select ) { 155 view.select( this.frame.content.mode() ); 156 } 157 }, 158 159 /** 160 * @access private 161 * @since 3.5.0 162 */ 163 _menu: function() { 164 var menu = this.frame.menu, 165 mode = this.get('menu'), 166 view; 167 168 this.frame.$el.toggleClass( 'hide-menu', ! mode ); 169 if ( ! mode ) { 170 return; 171 } 172 173 menu.mode( mode ); 174 175 view = menu.get(); 176 if ( view && view.select ) { 177 view.select( this.id ); 178 } 179 }, 180 181 /** 182 * @access private 183 * @since 3.5.0 184 */ 185 _updateMenu: function() { 186 var previous = this.previous('menu'), 187 menu = this.get('menu'); 188 189 if ( previous ) { 190 this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); 191 } 192 193 if ( menu ) { 194 this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); 195 } 196 }, 197 198 /** 199 * Create a view in the media menu for the state. 200 * 201 * @access private 202 * @since 3.5.0 203 * 204 * @param {media.view.Menu} view The menu view. 205 */ 206 _renderMenu: function( view ) { 207 var menuItem = this.get('menuItem'), 208 title = this.get('title'), 209 priority = this.get('priority'); 210 211 if ( ! menuItem && title ) { 212 menuItem = { text: title }; 213 214 if ( priority ) { 215 menuItem.priority = priority; 216 } 217 } 218 219 if ( ! menuItem ) { 220 return; 221 } 222 223 view.set( this.id, menuItem ); 224 } 225 }); 226 227 _.each(['toolbar','content'], function( region ) { 228 /** 229 * @access private 230 */ 231 State.prototype[ '_' + region ] = function() { 232 var mode = this.get( region ); 233 if ( mode ) { 234 this.frame[ region ].render( mode ); 235 } 236 }; 237 }); 238 239 module.exports = State; 240 No newline at end of file -
src/wp-includes/js/media/controllers/video-details.js
1 /** 2 * The controller for the Video Details state 3 * 4 * @constructor 5 * @augments wp.media.controller.State 6 * @augments Backbone.Model 7 */ 8 var State = require( './state.js' ), 9 l10n = wp.media.view.l10n, 10 VideoDetails; 11 12 VideoDetails = State.extend({ 13 defaults: { 14 id: 'video-details', 15 toolbar: 'video-details', 16 title: l10n.videoDetailsTitle, 17 content: 'video-details', 18 menu: 'video-details', 19 router: false, 20 priority: 60 21 }, 22 23 initialize: function( options ) { 24 this.media = options.media; 25 State.prototype.initialize.apply( this, arguments ); 26 } 27 }); 28 29 module.exports = VideoDetails; 30 No newline at end of file -
src/wp-includes/js/media/grid.js
1 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 2 /** 3 * wp.media.controller.EditAttachmentMetadata 4 * 5 * A state for editing an attachment's metadata. 6 * 7 * @constructor 8 * @augments wp.media.controller.State 9 * @augments Backbone.Model 10 */ 11 var State = require( './state.js' ), 12 l10n = wp.media.view.l10n, 13 EditAttachmentMetadata; 14 15 EditAttachmentMetadata = State.extend({ 16 defaults: { 17 id: 'edit-attachment', 18 // Title string passed to the frame's title region view. 19 title: l10n.attachmentDetails, 20 // Region mode defaults. 21 content: 'edit-metadata', 22 menu: false, 23 toolbar: false, 24 router: false 25 } 26 }); 27 28 module.exports = EditAttachmentMetadata; 29 },{"./state.js":6}],2:[function(require,module,exports){ 30 /** 31 * wp.media.controller.EditImage 32 * 33 * A state for editing (cropping, etc.) an image. 34 * 35 * @class 36 * @augments wp.media.controller.State 37 * @augments Backbone.Model 38 * 39 * @param {object} attributes The attributes hash passed to the state. 40 * @param {wp.media.model.Attachment} attributes.model The attachment. 41 * @param {string} [attributes.id=edit-image] Unique identifier. 42 * @param {string} [attributes.title=Edit Image] Title for the state. Displays in the media menu and the frame's title region. 43 * @param {string} [attributes.content=edit-image] Initial mode for the content region. 44 * @param {string} [attributes.toolbar=edit-image] Initial mode for the toolbar region. 45 * @param {string} [attributes.menu=false] Initial mode for the menu region. 46 * @param {string} [attributes.url] Unused. @todo Consider removal. 47 */ 48 var State = require( './state.js' ), 49 ToolbarView = require( '../views/toolbar.js' ), 50 l10n = wp.media.view.l10n, 51 EditImage; 52 53 EditImage = State.extend({ 54 defaults: { 55 id: 'edit-image', 56 title: l10n.editImage, 57 menu: false, 58 toolbar: 'edit-image', 59 content: 'edit-image', 60 url: '' 61 }, 62 63 /** 64 * @since 3.9.0 65 */ 66 activate: function() { 67 this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar ); 68 }, 69 70 /** 71 * @since 3.9.0 72 */ 73 deactivate: function() { 74 this.stopListening( this.frame ); 75 }, 76 77 /** 78 * @since 3.9.0 79 */ 80 toolbar: function() { 81 var frame = this.frame, 82 lastState = frame.lastState(), 83 previous = lastState && lastState.id; 84 85 frame.toolbar.set( new ToolbarView({ 86 controller: frame, 87 items: { 88 back: { 89 style: 'primary', 90 text: l10n.back, 91 priority: 20, 92 click: function() { 93 if ( previous ) { 94 frame.setState( previous ); 95 } else { 96 frame.close(); 97 } 98 } 99 } 100 } 101 }) ); 102 } 103 }); 104 105 module.exports = EditImage; 106 },{"../views/toolbar.js":50,"./state.js":6}],3:[function(require,module,exports){ 107 /** 108 * wp.media.controller.Library 109 * 110 * A state for choosing an attachment or group of attachments from the media library. 111 * 112 * @class 113 * @augments wp.media.controller.State 114 * @augments Backbone.Model 115 * @mixes media.selectionSync 116 * 117 * @param {object} [attributes] The attributes hash passed to the state. 118 * @param {string} [attributes.id=library] Unique identifier. 119 * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region. 120 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 121 * If one is not supplied, a collection of all attachments will be created. 122 * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state. 123 * If the 'selection' attribute is a plain JS object, 124 * a Selection will be created using its values as the selection instance's `props` model. 125 * Otherwise, it will copy the library's `props` model. 126 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 127 * @param {string} [attributes.content=upload] Initial mode for the content region. 128 * Overridden by persistent user setting if 'contentUserSetting' is true. 129 * @param {string} [attributes.menu=default] Initial mode for the menu region. 130 * @param {string} [attributes.router=browse] Initial mode for the router region. 131 * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region. 132 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 133 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 134 * Accepts 'all', 'uploaded', or 'unattached'. 135 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 136 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 137 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 138 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 139 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 140 */ 141 var selectionSync = require( '../utils/selection-sync.js' ), 142 SelectionModel = require( '../models/selection.js' ), 143 State = require( './state.js' ), 144 l10n = wp.media.view.l10n, 145 Library; 146 147 Library = State.extend({ 148 defaults: { 149 id: 'library', 150 title: l10n.mediaLibraryTitle, 151 multiple: false, 152 content: 'upload', 153 menu: 'default', 154 router: 'browse', 155 toolbar: 'select', 156 searchable: true, 157 filterable: false, 158 sortable: true, 159 autoSelect: true, 160 describe: false, 161 contentUserSetting: true, 162 syncSelection: true 163 }, 164 165 /** 166 * If a library isn't provided, query all media items. 167 * If a selection instance isn't provided, create one. 168 * 169 * @since 3.5.0 170 */ 171 initialize: function() { 172 var selection = this.get('selection'), 173 props; 174 175 if ( ! this.get('library') ) { 176 this.set( 'library', wp.media.query() ); 177 } 178 179 if ( ! (selection instanceof SelectionModel) ) { 180 props = selection; 181 182 if ( ! props ) { 183 props = this.get('library').props.toJSON(); 184 props = _.omit( props, 'orderby', 'query' ); 185 } 186 187 this.set( 'selection', new SelectionModel( null, { 188 multiple: this.get('multiple'), 189 props: props 190 }) ); 191 } 192 193 this.resetDisplays(); 194 }, 195 196 /** 197 * @since 3.5.0 198 */ 199 activate: function() { 200 this.syncSelection(); 201 202 wp.Uploader.queue.on( 'add', this.uploading, this ); 203 204 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 205 206 if ( this.get( 'router' ) && this.get('contentUserSetting') ) { 207 this.frame.on( 'content:activate', this.saveContentMode, this ); 208 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 209 } 210 }, 211 212 /** 213 * @since 3.5.0 214 */ 215 deactivate: function() { 216 this.recordSelection(); 217 218 this.frame.off( 'content:activate', this.saveContentMode, this ); 219 220 // Unbind all event handlers that use this state as the context 221 // from the selection. 222 this.get('selection').off( null, null, this ); 223 224 wp.Uploader.queue.off( null, null, this ); 225 }, 226 227 /** 228 * Reset the library to its initial state. 229 * 230 * @since 3.5.0 231 */ 232 reset: function() { 233 this.get('selection').reset(); 234 this.resetDisplays(); 235 this.refreshContent(); 236 }, 237 238 /** 239 * Reset the attachment display settings defaults to the site options. 240 * 241 * If site options don't define them, fall back to a persistent user setting. 242 * 243 * @since 3.5.0 244 */ 245 resetDisplays: function() { 246 var defaultProps = wp.media.view.settings.defaultProps; 247 this._displays = []; 248 this._defaultDisplaySettings = { 249 align: defaultProps.align || getUserSetting( 'align', 'none' ), 250 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 251 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 252 }; 253 }, 254 255 /** 256 * Create a model to represent display settings (alignment, etc.) for an attachment. 257 * 258 * @since 3.5.0 259 * 260 * @param {wp.media.model.Attachment} attachment 261 * @returns {Backbone.Model} 262 */ 263 display: function( attachment ) { 264 var displays = this._displays; 265 266 if ( ! displays[ attachment.cid ] ) { 267 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 268 } 269 return displays[ attachment.cid ]; 270 }, 271 272 /** 273 * Given an attachment, create attachment display settings properties. 274 * 275 * @since 3.6.0 276 * 277 * @param {wp.media.model.Attachment} attachment 278 * @returns {Object} 279 */ 280 defaultDisplaySettings: function( attachment ) { 281 var settings = this._defaultDisplaySettings; 282 if ( settings.canEmbed = this.canEmbed( attachment ) ) { 283 settings.link = 'embed'; 284 } 285 return settings; 286 }, 287 288 /** 289 * Whether an attachment can be embedded (audio or video). 290 * 291 * @since 3.6.0 292 * 293 * @param {wp.media.model.Attachment} attachment 294 * @returns {Boolean} 295 */ 296 canEmbed: function( attachment ) { 297 // If uploading, we know the filename but not the mime type. 298 if ( ! attachment.get('uploading') ) { 299 var type = attachment.get('type'); 300 if ( type !== 'audio' && type !== 'video' ) { 301 return false; 302 } 303 } 304 305 return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 306 }, 307 308 309 /** 310 * If the state is active, no items are selected, and the current 311 * content mode is not an option in the state's router (provided 312 * the state has a router), reset the content mode to the default. 313 * 314 * @since 3.5.0 315 */ 316 refreshContent: function() { 317 var selection = this.get('selection'), 318 frame = this.frame, 319 router = frame.router.get(), 320 mode = frame.content.mode(); 321 322 if ( this.active && ! selection.length && router && ! router.get( mode ) ) { 323 this.frame.content.render( this.get('content') ); 324 } 325 }, 326 327 /** 328 * Callback handler when an attachment is uploaded. 329 * 330 * Switch to the Media Library if uploaded from the 'Upload Files' tab. 331 * 332 * Adds any uploading attachments to the selection. 333 * 334 * If the state only supports one attachment to be selected and multiple 335 * attachments are uploaded, the last attachment in the upload queue will 336 * be selected. 337 * 338 * @since 3.5.0 339 * 340 * @param {wp.media.model.Attachment} attachment 341 */ 342 uploading: function( attachment ) { 343 var content = this.frame.content; 344 345 if ( 'upload' === content.mode() ) { 346 this.frame.content.mode('browse'); 347 } 348 349 if ( this.get( 'autoSelect' ) ) { 350 this.get('selection').add( attachment ); 351 this.frame.trigger( 'library:selection:add' ); 352 } 353 }, 354 355 /** 356 * Persist the mode of the content region as a user setting. 357 * 358 * @since 3.5.0 359 */ 360 saveContentMode: function() { 361 if ( 'browse' !== this.get('router') ) { 362 return; 363 } 364 365 var mode = this.frame.content.mode(), 366 view = this.frame.router.get(); 367 368 if ( view && view.get( mode ) ) { 369 setUserSetting( 'libraryContent', mode ); 370 } 371 } 372 }); 373 374 // Make selectionSync available on any Media Library state. 375 _.extend( Library.prototype, selectionSync ); 376 377 module.exports = Library; 378 },{"../models/selection.js":11,"../utils/selection-sync.js":13,"./state.js":6}],4:[function(require,module,exports){ 379 /** 380 * wp.media.controller.Region 381 * 382 * A region is a persistent application layout area. 383 * 384 * A region assumes one mode at any time, and can be switched to another. 385 * 386 * When mode changes, events are triggered on the region's parent view. 387 * The parent view will listen to specific events and fill the region with an 388 * appropriate view depending on mode. For example, a frame listens for the 389 * 'browse' mode t be activated on the 'content' view and then fills the region 390 * with an AttachmentsBrowser view. 391 * 392 * @class 393 * 394 * @param {object} options Options hash for the region. 395 * @param {string} options.id Unique identifier for the region. 396 * @param {Backbone.View} options.view A parent view the region exists within. 397 * @param {string} options.selector jQuery selector for the region within the parent view. 398 */ 399 var Region = function( options ) { 400 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); 401 }; 402 403 // Use Backbone's self-propagating `extend` inheritance method. 404 Region.extend = Backbone.Model.extend; 405 406 _.extend( Region.prototype, { 407 /** 408 * Activate a mode. 409 * 410 * @since 3.5.0 411 * 412 * @param {string} mode 413 * 414 * @fires this.view#{this.id}:activate:{this._mode} 415 * @fires this.view#{this.id}:activate 416 * @fires this.view#{this.id}:deactivate:{this._mode} 417 * @fires this.view#{this.id}:deactivate 418 * 419 * @returns {wp.media.controller.Region} Returns itself to allow chaining. 420 */ 421 mode: function( mode ) { 422 if ( ! mode ) { 423 return this._mode; 424 } 425 // Bail if we're trying to change to the current mode. 426 if ( mode === this._mode ) { 427 return this; 428 } 429 430 /** 431 * Region mode deactivation event. 432 * 433 * @event this.view#{this.id}:deactivate:{this._mode} 434 * @event this.view#{this.id}:deactivate 435 */ 436 this.trigger('deactivate'); 437 438 this._mode = mode; 439 this.render( mode ); 440 441 /** 442 * Region mode activation event. 443 * 444 * @event this.view#{this.id}:activate:{this._mode} 445 * @event this.view#{this.id}:activate 446 */ 447 this.trigger('activate'); 448 return this; 449 }, 450 /** 451 * Render a mode. 452 * 453 * @since 3.5.0 454 * 455 * @param {string} mode 456 * 457 * @fires this.view#{this.id}:create:{this._mode} 458 * @fires this.view#{this.id}:create 459 * @fires this.view#{this.id}:render:{this._mode} 460 * @fires this.view#{this.id}:render 461 * 462 * @returns {wp.media.controller.Region} Returns itself to allow chaining 463 */ 464 render: function( mode ) { 465 // If the mode isn't active, activate it. 466 if ( mode && mode !== this._mode ) { 467 return this.mode( mode ); 468 } 469 470 var set = { view: null }, 471 view; 472 473 /** 474 * Create region view event. 475 * 476 * Region view creation takes place in an event callback on the frame. 477 * 478 * @event this.view#{this.id}:create:{this._mode} 479 * @event this.view#{this.id}:create 480 */ 481 this.trigger( 'create', set ); 482 view = set.view; 483 484 /** 485 * Render region view event. 486 * 487 * Region view creation takes place in an event callback on the frame. 488 * 489 * @event this.view#{this.id}:create:{this._mode} 490 * @event this.view#{this.id}:create 491 */ 492 this.trigger( 'render', view ); 493 if ( view ) { 494 this.set( view ); 495 } 496 return this; 497 }, 498 499 /** 500 * Get the region's view. 501 * 502 * @since 3.5.0 503 * 504 * @returns {wp.media.View} 505 */ 506 get: function() { 507 return this.view.views.first( this.selector ); 508 }, 509 510 /** 511 * Set the region's view as a subview of the frame. 512 * 513 * @since 3.5.0 514 * 515 * @param {Array|Object} views 516 * @param {Object} [options={}] 517 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining 518 */ 519 set: function( views, options ) { 520 if ( options ) { 521 options.add = false; 522 } 523 return this.view.views.set( this.selector, views, options ); 524 }, 525 526 /** 527 * Trigger regional view events on the frame. 528 * 529 * @since 3.5.0 530 * 531 * @param {string} event 532 * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining. 533 */ 534 trigger: function( event ) { 535 var base, args; 536 537 if ( ! this._mode ) { 538 return; 539 } 540 541 args = _.toArray( arguments ); 542 base = this.id + ':' + event; 543 544 // Trigger `{this.id}:{event}:{this._mode}` event on the frame. 545 args[0] = base + ':' + this._mode; 546 this.view.trigger.apply( this.view, args ); 547 548 // Trigger `{this.id}:{event}` event on the frame. 549 args[0] = base; 550 this.view.trigger.apply( this.view, args ); 551 return this; 552 } 553 }); 554 555 module.exports = Region; 556 },{}],5:[function(require,module,exports){ 557 /** 558 * wp.media.controller.StateMachine 559 * 560 * A state machine keeps track of state. It is in one state at a time, 561 * and can change from one state to another. 562 * 563 * States are stored as models in a Backbone collection. 564 * 565 * @since 3.5.0 566 * 567 * @class 568 * @augments Backbone.Model 569 * @mixin 570 * @mixes Backbone.Events 571 * 572 * @param {Array} states 573 */ 574 var StateMachine = function( states ) { 575 // @todo This is dead code. The states collection gets created in media.view.Frame._createStates. 576 this.states = new Backbone.Collection( states ); 577 }; 578 579 // Use Backbone's self-propagating `extend` inheritance method. 580 StateMachine.extend = Backbone.Model.extend; 581 582 _.extend( StateMachine.prototype, Backbone.Events, { 583 /** 584 * Fetch a state. 585 * 586 * If no `id` is provided, returns the active state. 587 * 588 * Implicitly creates states. 589 * 590 * Ensure that the `states` collection exists so the `StateMachine` 591 * can be used as a mixin. 592 * 593 * @since 3.5.0 594 * 595 * @param {string} id 596 * @returns {wp.media.controller.State} Returns a State model 597 * from the StateMachine collection 598 */ 599 state: function( id ) { 600 this.states = this.states || new Backbone.Collection(); 601 602 // Default to the active state. 603 id = id || this._state; 604 605 if ( id && ! this.states.get( id ) ) { 606 this.states.add({ id: id }); 607 } 608 return this.states.get( id ); 609 }, 610 611 /** 612 * Sets the active state. 613 * 614 * Bail if we're trying to select the current state, if we haven't 615 * created the `states` collection, or are trying to select a state 616 * that does not exist. 617 * 618 * @since 3.5.0 619 * 620 * @param {string} id 621 * 622 * @fires wp.media.controller.State#deactivate 623 * @fires wp.media.controller.State#activate 624 * 625 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining 626 */ 627 setState: function( id ) { 628 var previous = this.state(); 629 630 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) { 631 return this; 632 } 633 634 if ( previous ) { 635 previous.trigger('deactivate'); 636 this._lastState = previous.id; 637 } 638 639 this._state = id; 640 this.state().trigger('activate'); 641 642 return this; 643 }, 644 645 /** 646 * Returns the previous active state. 647 * 648 * Call the `state()` method with no parameters to retrieve the current 649 * active state. 650 * 651 * @since 3.5.0 652 * 653 * @returns {wp.media.controller.State} Returns a State model 654 * from the StateMachine collection 655 */ 656 lastState: function() { 657 if ( this._lastState ) { 658 return this.state( this._lastState ); 659 } 660 } 661 }); 662 663 // Map all event binding and triggering on a StateMachine to its `states` collection. 664 _.each([ 'on', 'off', 'trigger' ], function( method ) { 665 /** 666 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. 667 */ 668 StateMachine.prototype[ method ] = function() { 669 // Ensure that the `states` collection exists so the `StateMachine` 670 // can be used as a mixin. 671 this.states = this.states || new Backbone.Collection(); 672 // Forward the method to the `states` collection. 673 this.states[ method ].apply( this.states, arguments ); 674 return this; 675 }; 676 }); 677 678 module.exports = StateMachine; 679 },{}],6:[function(require,module,exports){ 680 /** 681 * wp.media.controller.State 682 * 683 * A state is a step in a workflow that when set will trigger the controllers 684 * for the regions to be updated as specified in the frame. 685 * 686 * A state has an event-driven lifecycle: 687 * 688 * 'ready' triggers when a state is added to a state machine's collection. 689 * 'activate' triggers when a state is activated by a state machine. 690 * 'deactivate' triggers when a state is deactivated by a state machine. 691 * 'reset' is not triggered automatically. It should be invoked by the 692 * proper controller to reset the state to its default. 693 * 694 * @class 695 * @augments Backbone.Model 696 */ 697 var State = Backbone.Model.extend({ 698 /** 699 * Constructor. 700 * 701 * @since 3.5.0 702 */ 703 constructor: function() { 704 this.on( 'activate', this._preActivate, this ); 705 this.on( 'activate', this.activate, this ); 706 this.on( 'activate', this._postActivate, this ); 707 this.on( 'deactivate', this._deactivate, this ); 708 this.on( 'deactivate', this.deactivate, this ); 709 this.on( 'reset', this.reset, this ); 710 this.on( 'ready', this._ready, this ); 711 this.on( 'ready', this.ready, this ); 712 /** 713 * Call parent constructor with passed arguments 714 */ 715 Backbone.Model.apply( this, arguments ); 716 this.on( 'change:menu', this._updateMenu, this ); 717 }, 718 /** 719 * Ready event callback. 720 * 721 * @abstract 722 * @since 3.5.0 723 */ 724 ready: function() {}, 725 726 /** 727 * Activate event callback. 728 * 729 * @abstract 730 * @since 3.5.0 731 */ 732 activate: function() {}, 733 734 /** 735 * Deactivate event callback. 736 * 737 * @abstract 738 * @since 3.5.0 739 */ 740 deactivate: function() {}, 741 742 /** 743 * Reset event callback. 744 * 745 * @abstract 746 * @since 3.5.0 747 */ 748 reset: function() {}, 749 750 /** 751 * @access private 752 * @since 3.5.0 753 */ 754 _ready: function() { 755 this._updateMenu(); 756 }, 757 758 /** 759 * @access private 760 * @since 3.5.0 761 */ 762 _preActivate: function() { 763 this.active = true; 764 }, 765 766 /** 767 * @access private 768 * @since 3.5.0 769 */ 770 _postActivate: function() { 771 this.on( 'change:menu', this._menu, this ); 772 this.on( 'change:titleMode', this._title, this ); 773 this.on( 'change:content', this._content, this ); 774 this.on( 'change:toolbar', this._toolbar, this ); 775 776 this.frame.on( 'title:render:default', this._renderTitle, this ); 777 778 this._title(); 779 this._menu(); 780 this._toolbar(); 781 this._content(); 782 this._router(); 783 }, 784 785 /** 786 * @access private 787 * @since 3.5.0 788 */ 789 _deactivate: function() { 790 this.active = false; 791 792 this.frame.off( 'title:render:default', this._renderTitle, this ); 793 794 this.off( 'change:menu', this._menu, this ); 795 this.off( 'change:titleMode', this._title, this ); 796 this.off( 'change:content', this._content, this ); 797 this.off( 'change:toolbar', this._toolbar, this ); 798 }, 799 800 /** 801 * @access private 802 * @since 3.5.0 803 */ 804 _title: function() { 805 this.frame.title.render( this.get('titleMode') || 'default' ); 806 }, 807 808 /** 809 * @access private 810 * @since 3.5.0 811 */ 812 _renderTitle: function( view ) { 813 view.$el.text( this.get('title') || '' ); 814 }, 815 816 /** 817 * @access private 818 * @since 3.5.0 819 */ 820 _router: function() { 821 var router = this.frame.router, 822 mode = this.get('router'), 823 view; 824 825 this.frame.$el.toggleClass( 'hide-router', ! mode ); 826 if ( ! mode ) { 827 return; 828 } 829 830 this.frame.router.render( mode ); 831 832 view = router.get(); 833 if ( view && view.select ) { 834 view.select( this.frame.content.mode() ); 835 } 836 }, 837 838 /** 839 * @access private 840 * @since 3.5.0 841 */ 842 _menu: function() { 843 var menu = this.frame.menu, 844 mode = this.get('menu'), 845 view; 846 847 this.frame.$el.toggleClass( 'hide-menu', ! mode ); 848 if ( ! mode ) { 849 return; 850 } 851 852 menu.mode( mode ); 853 854 view = menu.get(); 855 if ( view && view.select ) { 856 view.select( this.id ); 857 } 858 }, 859 860 /** 861 * @access private 862 * @since 3.5.0 863 */ 864 _updateMenu: function() { 865 var previous = this.previous('menu'), 866 menu = this.get('menu'); 867 868 if ( previous ) { 869 this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); 870 } 871 872 if ( menu ) { 873 this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); 874 } 875 }, 876 877 /** 878 * Create a view in the media menu for the state. 879 * 880 * @access private 881 * @since 3.5.0 882 * 883 * @param {media.view.Menu} view The menu view. 884 */ 885 _renderMenu: function( view ) { 886 var menuItem = this.get('menuItem'), 887 title = this.get('title'), 888 priority = this.get('priority'); 889 890 if ( ! menuItem && title ) { 891 menuItem = { text: title }; 892 893 if ( priority ) { 894 menuItem.priority = priority; 895 } 896 } 897 898 if ( ! menuItem ) { 899 return; 900 } 901 902 view.set( this.id, menuItem ); 903 } 904 }); 905 906 _.each(['toolbar','content'], function( region ) { 907 /** 908 * @access private 909 */ 910 State.prototype[ '_' + region ] = function() { 911 var mode = this.get( region ); 912 if ( mode ) { 913 this.frame[ region ].render( mode ); 914 } 915 }; 916 }); 917 918 module.exports = State; 919 },{}],7:[function(require,module,exports){ 920 /* global _wpMediaViewsL10n, MediaElementPlayer, _wpMediaGridSettings */ 921 (function($, _, Backbone, wp) { 922 var media = wp.media; 923 924 media.controller.EditAttachmentMetadata = require( './controllers/edit-attachment-metadata.js' ); 925 media.view.MediaFrame.Manage = require( './views/frame/manage.js' ); 926 media.view.Attachment.Details.TwoColumn = require( './views/attachment/details-two-column.js' ); 927 media.view.MediaFrame.Manage.Router = require( './router/manage.js' ); 928 media.view.EditImage.Details = require( './views/edit-image-details.js' ); 929 media.view.MediaFrame.EditAttachments = require( './views/frame/edit-attachments.js' ); 930 media.view.SelectModeToggleButton = require( './views/button/select-mode-toggle.js' ); 931 media.view.DeleteSelectedButton = require( './views/button/delete-selected.js' ); 932 media.view.DeleteSelectedPermanentlyButton = require( './views/button/delete-selected-permanently.js' ); 933 934 }(jQuery, _, Backbone, wp)); 935 936 },{"./controllers/edit-attachment-metadata.js":1,"./router/manage.js":12,"./views/attachment/details-two-column.js":20,"./views/button/delete-selected-permanently.js":26,"./views/button/delete-selected.js":27,"./views/button/select-mode-toggle.js":28,"./views/edit-image-details.js":29,"./views/frame/edit-attachments.js":33,"./views/frame/manage.js":34}],8:[function(require,module,exports){ 937 /** 938 * wp.media.model.Attachment 939 * 940 * @class 941 * @augments Backbone.Model 942 */ 943 var $ = jQuery, 944 Attachment; 945 946 Attachment = Backbone.Model.extend({ 947 /** 948 * Triggered when attachment details change 949 * Overrides Backbone.Model.sync 950 * 951 * @param {string} method 952 * @param {wp.media.model.Attachment} model 953 * @param {Object} [options={}] 954 * 955 * @returns {Promise} 956 */ 957 sync: function( method, model, options ) { 958 // If the attachment does not yet have an `id`, return an instantly 959 // rejected promise. Otherwise, all of our requests will fail. 960 if ( _.isUndefined( this.id ) ) { 961 return $.Deferred().rejectWith( this ).promise(); 962 } 963 964 // Overload the `read` request so Attachment.fetch() functions correctly. 965 if ( 'read' === method ) { 966 options = options || {}; 967 options.context = this; 968 options.data = _.extend( options.data || {}, { 969 action: 'get-attachment', 970 id: this.id 971 }); 972 return wp.media.ajax( options ); 973 974 // Overload the `update` request so properties can be saved. 975 } else if ( 'update' === method ) { 976 // If we do not have the necessary nonce, fail immeditately. 977 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 978 return $.Deferred().rejectWith( this ).promise(); 979 } 980 981 options = options || {}; 982 options.context = this; 983 984 // Set the action and ID. 985 options.data = _.extend( options.data || {}, { 986 action: 'save-attachment', 987 id: this.id, 988 nonce: this.get('nonces').update, 989 post_id: wp.media.model.settings.post.id 990 }); 991 992 // Record the values of the changed attributes. 993 if ( model.hasChanged() ) { 994 options.data.changes = {}; 995 996 _.each( model.changed, function( value, key ) { 997 options.data.changes[ key ] = this.get( key ); 998 }, this ); 999 } 1000 1001 return wp.media.ajax( options ); 1002 1003 // Overload the `delete` request so attachments can be removed. 1004 // This will permanently delete an attachment. 1005 } else if ( 'delete' === method ) { 1006 options = options || {}; 1007 1008 if ( ! options.wait ) { 1009 this.destroyed = true; 1010 } 1011 1012 options.context = this; 1013 options.data = _.extend( options.data || {}, { 1014 action: 'delete-post', 1015 id: this.id, 1016 _wpnonce: this.get('nonces')['delete'] 1017 }); 1018 1019 return wp.media.ajax( options ).done( function() { 1020 this.destroyed = true; 1021 }).fail( function() { 1022 this.destroyed = false; 1023 }); 1024 1025 // Otherwise, fall back to `Backbone.sync()`. 1026 } else { 1027 /** 1028 * Call `sync` directly on Backbone.Model 1029 */ 1030 return Backbone.Model.prototype.sync.apply( this, arguments ); 1031 } 1032 }, 1033 /** 1034 * Convert date strings into Date objects. 1035 * 1036 * @param {Object} resp The raw response object, typically returned by fetch() 1037 * @returns {Object} The modified response object, which is the attributes hash 1038 * to be set on the model. 1039 */ 1040 parse: function( resp ) { 1041 if ( ! resp ) { 1042 return resp; 1043 } 1044 1045 resp.date = new Date( resp.date ); 1046 resp.modified = new Date( resp.modified ); 1047 return resp; 1048 }, 1049 /** 1050 * @param {Object} data The properties to be saved. 1051 * @param {Object} options Sync options. e.g. patch, wait, success, error. 1052 * 1053 * @this Backbone.Model 1054 * 1055 * @returns {Promise} 1056 */ 1057 saveCompat: function( data, options ) { 1058 var model = this; 1059 1060 // If we do not have the necessary nonce, fail immeditately. 1061 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 1062 return $.Deferred().rejectWith( this ).promise(); 1063 } 1064 1065 return media.post( 'save-attachment-compat', _.defaults({ 1066 id: this.id, 1067 nonce: this.get('nonces').update, 1068 post_id: wp.media.model.settings.post.id 1069 }, data ) ).done( function( resp, status, xhr ) { 1070 model.set( model.parse( resp, xhr ), options ); 1071 }); 1072 } 1073 }, { 1074 /** 1075 * Create a new model on the static 'all' attachments collection and return it. 1076 * 1077 * @static 1078 * @param {Object} attrs 1079 * @returns {wp.media.model.Attachment} 1080 */ 1081 create: function( attrs ) { 1082 var Attachments = require( './attachments.js' ); 1083 return Attachments.all.push( attrs ); 1084 }, 1085 /** 1086 * Create a new model on the static 'all' attachments collection and return it. 1087 * 1088 * If this function has already been called for the id, 1089 * it returns the specified attachment. 1090 * 1091 * @static 1092 * @param {string} id A string used to identify a model. 1093 * @param {Backbone.Model|undefined} attachment 1094 * @returns {wp.media.model.Attachment} 1095 */ 1096 get: _.memoize( function( id, attachment ) { 1097 var Attachments = require( './attachments.js' ); 1098 return Attachments.all.push( attachment || { id: id } ); 1099 }) 1100 }); 1101 1102 module.exports = Attachment; 1103 },{"./attachments.js":9}],9:[function(require,module,exports){ 1104 /** 1105 * wp.media.model.Attachments 1106 * 1107 * A collection of attachments. 1108 * 1109 * This collection has no persistence with the server without supplying 1110 * 'options.props.query = true', which will mirror the collection 1111 * to an Attachments Query collection - @see wp.media.model.Attachments.mirror(). 1112 * 1113 * @class 1114 * @augments Backbone.Collection 1115 * 1116 * @param {array} [models] Models to initialize with the collection. 1117 * @param {object} [options] Options hash for the collection. 1118 * @param {string} [options.props] Options hash for the initial query properties. 1119 * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection. 1120 * @param {string} [options.props.orderby] Initial attribute key to order the collection by. 1121 * @param {string} [options.props.query] Whether the collection is linked to an attachments query. 1122 * @param {string} [options.observe] 1123 * @param {string} [options.filters] 1124 * 1125 */ 1126 var Attachment = require( './attachment.js' ), 1127 Attachments; 1128 1129 Attachments = Backbone.Collection.extend({ 1130 /** 1131 * @type {wp.media.model.Attachment} 1132 */ 1133 model: Attachment, 1134 /** 1135 * @param {Array} [models=[]] Array of models used to populate the collection. 1136 * @param {Object} [options={}] 1137 */ 1138 initialize: function( models, options ) { 1139 options = options || {}; 1140 1141 this.props = new Backbone.Model(); 1142 this.filters = options.filters || {}; 1143 1144 // Bind default `change` events to the `props` model. 1145 this.props.on( 'change', this._changeFilteredProps, this ); 1146 1147 this.props.on( 'change:order', this._changeOrder, this ); 1148 this.props.on( 'change:orderby', this._changeOrderby, this ); 1149 this.props.on( 'change:query', this._changeQuery, this ); 1150 1151 this.props.set( _.defaults( options.props || {} ) ); 1152 1153 if ( options.observe ) { 1154 this.observe( options.observe ); 1155 } 1156 }, 1157 /** 1158 * Sort the collection when the order attribute changes. 1159 * 1160 * @access private 1161 */ 1162 _changeOrder: function() { 1163 if ( this.comparator ) { 1164 this.sort(); 1165 } 1166 }, 1167 /** 1168 * Set the default comparator only when the `orderby` property is set. 1169 * 1170 * @access private 1171 * 1172 * @param {Backbone.Model} model 1173 * @param {string} orderby 1174 */ 1175 _changeOrderby: function( model, orderby ) { 1176 // If a different comparator is defined, bail. 1177 if ( this.comparator && this.comparator !== Attachments.comparator ) { 1178 return; 1179 } 1180 1181 if ( orderby && 'post__in' !== orderby ) { 1182 this.comparator = Attachments.comparator; 1183 } else { 1184 delete this.comparator; 1185 } 1186 }, 1187 /** 1188 * If the `query` property is set to true, query the server using 1189 * the `props` values, and sync the results to this collection. 1190 * 1191 * @access private 1192 * 1193 * @param {Backbone.Model} model 1194 * @param {Boolean} query 1195 */ 1196 _changeQuery: function( model, query ) { 1197 if ( query ) { 1198 this.props.on( 'change', this._requery, this ); 1199 this._requery(); 1200 } else { 1201 this.props.off( 'change', this._requery, this ); 1202 } 1203 }, 1204 /** 1205 * @access private 1206 * 1207 * @param {Backbone.Model} model 1208 */ 1209 _changeFilteredProps: function( model ) { 1210 // If this is a query, updating the collection will be handled by 1211 // `this._requery()`. 1212 if ( this.props.get('query') ) { 1213 return; 1214 } 1215 1216 var changed = _.chain( model.changed ).map( function( t, prop ) { 1217 var filter = Attachments.filters[ prop ], 1218 term = model.get( prop ); 1219 1220 if ( ! filter ) { 1221 return; 1222 } 1223 1224 if ( term && ! this.filters[ prop ] ) { 1225 this.filters[ prop ] = filter; 1226 } else if ( ! term && this.filters[ prop ] === filter ) { 1227 delete this.filters[ prop ]; 1228 } else { 1229 return; 1230 } 1231 1232 // Record the change. 1233 return true; 1234 }, this ).any().value(); 1235 1236 if ( ! changed ) { 1237 return; 1238 } 1239 1240 // If no `Attachments` model is provided to source the searches 1241 // from, then automatically generate a source from the existing 1242 // models. 1243 if ( ! this._source ) { 1244 this._source = new Attachments( this.models ); 1245 } 1246 1247 this.reset( this._source.filter( this.validator, this ) ); 1248 }, 1249 1250 validateDestroyed: false, 1251 /** 1252 * Checks whether an attachment is valid. 1253 * 1254 * @param {wp.media.model.Attachment} attachment 1255 * @returns {Boolean} 1256 */ 1257 validator: function( attachment ) { 1258 if ( ! this.validateDestroyed && attachment.destroyed ) { 1259 return false; 1260 } 1261 return _.all( this.filters, function( filter ) { 1262 return !! filter.call( this, attachment ); 1263 }, this ); 1264 }, 1265 /** 1266 * Add or remove an attachment to the collection depending on its validity. 1267 * 1268 * @param {wp.media.model.Attachment} attachment 1269 * @param {Object} options 1270 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1271 */ 1272 validate: function( attachment, options ) { 1273 var valid = this.validator( attachment ), 1274 hasAttachment = !! this.get( attachment.cid ); 1275 1276 if ( ! valid && hasAttachment ) { 1277 this.remove( attachment, options ); 1278 } else if ( valid && ! hasAttachment ) { 1279 this.add( attachment, options ); 1280 } 1281 1282 return this; 1283 }, 1284 1285 /** 1286 * Add or remove all attachments from another collection depending on each one's validity. 1287 * 1288 * @param {wp.media.model.Attachments} attachments 1289 * @param {object} [options={}] 1290 * 1291 * @fires wp.media.model.Attachments#reset 1292 * 1293 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1294 */ 1295 validateAll: function( attachments, options ) { 1296 options = options || {}; 1297 1298 _.each( attachments.models, function( attachment ) { 1299 this.validate( attachment, { silent: true }); 1300 }, this ); 1301 1302 if ( ! options.silent ) { 1303 this.trigger( 'reset', this, options ); 1304 } 1305 return this; 1306 }, 1307 /** 1308 * Start observing another attachments collection change events 1309 * and replicate them on this collection. 1310 * 1311 * @param {wp.media.model.Attachments} The attachments collection to observe. 1312 * @returns {wp.media.model.Attachments} Returns itself to allow chaining. 1313 */ 1314 observe: function( attachments ) { 1315 this.observers = this.observers || []; 1316 this.observers.push( attachments ); 1317 1318 attachments.on( 'add change remove', this._validateHandler, this ); 1319 attachments.on( 'reset', this._validateAllHandler, this ); 1320 this.validateAll( attachments ); 1321 return this; 1322 }, 1323 /** 1324 * Stop replicating collection change events from another attachments collection. 1325 * 1326 * @param {wp.media.model.Attachments} The attachments collection to stop observing. 1327 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1328 */ 1329 unobserve: function( attachments ) { 1330 if ( attachments ) { 1331 attachments.off( null, null, this ); 1332 this.observers = _.without( this.observers, attachments ); 1333 1334 } else { 1335 _.each( this.observers, function( attachments ) { 1336 attachments.off( null, null, this ); 1337 }, this ); 1338 delete this.observers; 1339 } 1340 1341 return this; 1342 }, 1343 /** 1344 * @access private 1345 * 1346 * @param {wp.media.model.Attachments} attachment 1347 * @param {wp.media.model.Attachments} attachments 1348 * @param {Object} options 1349 * 1350 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1351 */ 1352 _validateHandler: function( attachment, attachments, options ) { 1353 // If we're not mirroring this `attachments` collection, 1354 // only retain the `silent` option. 1355 options = attachments === this.mirroring ? options : { 1356 silent: options && options.silent 1357 }; 1358 1359 return this.validate( attachment, options ); 1360 }, 1361 /** 1362 * @access private 1363 * 1364 * @param {wp.media.model.Attachments} attachments 1365 * @param {Object} options 1366 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1367 */ 1368 _validateAllHandler: function( attachments, options ) { 1369 return this.validateAll( attachments, options ); 1370 }, 1371 /** 1372 * Start mirroring another attachments collection, clearing out any models already 1373 * in the collection. 1374 * 1375 * @param {wp.media.model.Attachments} The attachments collection to mirror. 1376 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1377 */ 1378 mirror: function( attachments ) { 1379 if ( this.mirroring && this.mirroring === attachments ) { 1380 return this; 1381 } 1382 1383 this.unmirror(); 1384 this.mirroring = attachments; 1385 1386 // Clear the collection silently. A `reset` event will be fired 1387 // when `observe()` calls `validateAll()`. 1388 this.reset( [], { silent: true } ); 1389 this.observe( attachments ); 1390 1391 return this; 1392 }, 1393 /** 1394 * Stop mirroring another attachments collection. 1395 */ 1396 unmirror: function() { 1397 if ( ! this.mirroring ) { 1398 return; 1399 } 1400 1401 this.unobserve( this.mirroring ); 1402 delete this.mirroring; 1403 }, 1404 /** 1405 * Retrive more attachments from the server for the collection. 1406 * 1407 * Only works if the collection is mirroring a Query Attachments collection, 1408 * and forwards to its `more` method. This collection class doesn't have 1409 * server persistence by itself. 1410 * 1411 * @param {object} options 1412 * @returns {Promise} 1413 */ 1414 more: function( options ) { 1415 var deferred = jQuery.Deferred(), 1416 mirroring = this.mirroring, 1417 attachments = this; 1418 1419 if ( ! mirroring || ! mirroring.more ) { 1420 return deferred.resolveWith( this ).promise(); 1421 } 1422 // If we're mirroring another collection, forward `more` to 1423 // the mirrored collection. Account for a race condition by 1424 // checking if we're still mirroring that collection when 1425 // the request resolves. 1426 mirroring.more( options ).done( function() { 1427 if ( this === attachments.mirroring ) 1428 deferred.resolveWith( this ); 1429 }); 1430 1431 return deferred.promise(); 1432 }, 1433 /** 1434 * Whether there are more attachments that haven't been sync'd from the server 1435 * that match the collection's query. 1436 * 1437 * Only works if the collection is mirroring a Query Attachments collection, 1438 * and forwards to its `hasMore` method. This collection class doesn't have 1439 * server persistence by itself. 1440 * 1441 * @returns {boolean} 1442 */ 1443 hasMore: function() { 1444 return this.mirroring ? this.mirroring.hasMore() : false; 1445 }, 1446 /** 1447 * A custom AJAX-response parser. 1448 * 1449 * See trac ticket #24753 1450 * 1451 * @param {Object|Array} resp The raw response Object/Array. 1452 * @param {Object} xhr 1453 * @returns {Array} The array of model attributes to be added to the collection 1454 */ 1455 parse: function( resp, xhr ) { 1456 if ( ! _.isArray( resp ) ) { 1457 resp = [resp]; 1458 } 1459 1460 return _.map( resp, function( attrs ) { 1461 var id, attachment, newAttributes; 1462 1463 if ( attrs instanceof Backbone.Model ) { 1464 id = attrs.get( 'id' ); 1465 attrs = attrs.attributes; 1466 } else { 1467 id = attrs.id; 1468 } 1469 1470 attachment = Attachment.get( id ); 1471 newAttributes = attachment.parse( attrs, xhr ); 1472 1473 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { 1474 attachment.set( newAttributes ); 1475 } 1476 1477 return attachment; 1478 }); 1479 }, 1480 /** 1481 * If the collection is a query, create and mirror an Attachments Query collection. 1482 * 1483 * @access private 1484 */ 1485 _requery: function( refresh ) { 1486 var props, Query; 1487 if ( this.props.get('query') ) { 1488 Query = require( './query.js' ); 1489 props = this.props.toJSON(); 1490 props.cache = ( true !== refresh ); 1491 this.mirror( Query.get( props ) ); 1492 } 1493 }, 1494 /** 1495 * If this collection is sorted by `menuOrder`, recalculates and saves 1496 * the menu order to the database. 1497 * 1498 * @returns {undefined|Promise} 1499 */ 1500 saveMenuOrder: function() { 1501 if ( 'menuOrder' !== this.props.get('orderby') ) { 1502 return; 1503 } 1504 1505 // Removes any uploading attachments, updates each attachment's 1506 // menu order, and returns an object with an { id: menuOrder } 1507 // mapping to pass to the request. 1508 var attachments = this.chain().filter( function( attachment ) { 1509 return ! _.isUndefined( attachment.id ); 1510 }).map( function( attachment, index ) { 1511 // Indices start at 1. 1512 index = index + 1; 1513 attachment.set( 'menuOrder', index ); 1514 return [ attachment.id, index ]; 1515 }).object().value(); 1516 1517 if ( _.isEmpty( attachments ) ) { 1518 return; 1519 } 1520 1521 return wp.media.post( 'save-attachment-order', { 1522 nonce: wp.media.model.settings.post.nonce, 1523 post_id: wp.media.model.settings.post.id, 1524 attachments: attachments 1525 }); 1526 } 1527 }, { 1528 /** 1529 * A function to compare two attachment models in an attachments collection. 1530 * 1531 * Used as the default comparator for instances of wp.media.model.Attachments 1532 * and its subclasses. @see wp.media.model.Attachments._changeOrderby(). 1533 * 1534 * @static 1535 * 1536 * @param {Backbone.Model} a 1537 * @param {Backbone.Model} b 1538 * @param {Object} options 1539 * @returns {Number} -1 if the first model should come before the second, 1540 * 0 if they are of the same rank and 1541 * 1 if the first model should come after. 1542 */ 1543 comparator: function( a, b, options ) { 1544 var key = this.props.get('orderby'), 1545 order = this.props.get('order') || 'DESC', 1546 ac = a.cid, 1547 bc = b.cid; 1548 1549 a = a.get( key ); 1550 b = b.get( key ); 1551 1552 if ( 'date' === key || 'modified' === key ) { 1553 a = a || new Date(); 1554 b = b || new Date(); 1555 } 1556 1557 // If `options.ties` is set, don't enforce the `cid` tiebreaker. 1558 if ( options && options.ties ) { 1559 ac = bc = null; 1560 } 1561 1562 return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac ); 1563 }, 1564 /** 1565 * @namespace 1566 */ 1567 filters: { 1568 /** 1569 * @static 1570 * Note that this client-side searching is *not* equivalent 1571 * to our server-side searching. 1572 * 1573 * @param {wp.media.model.Attachment} attachment 1574 * 1575 * @this wp.media.model.Attachments 1576 * 1577 * @returns {Boolean} 1578 */ 1579 search: function( attachment ) { 1580 if ( ! this.props.get('search') ) { 1581 return true; 1582 } 1583 1584 return _.any(['title','filename','description','caption','name'], function( key ) { 1585 var value = attachment.get( key ); 1586 return value && -1 !== value.search( this.props.get('search') ); 1587 }, this ); 1588 }, 1589 /** 1590 * @static 1591 * @param {wp.media.model.Attachment} attachment 1592 * 1593 * @this wp.media.model.Attachments 1594 * 1595 * @returns {Boolean} 1596 */ 1597 type: function( attachment ) { 1598 var type = this.props.get('type'); 1599 return ! type || -1 !== type.indexOf( attachment.get('type') ); 1600 }, 1601 /** 1602 * @static 1603 * @param {wp.media.model.Attachment} attachment 1604 * 1605 * @this wp.media.model.Attachments 1606 * 1607 * @returns {Boolean} 1608 */ 1609 uploadedTo: function( attachment ) { 1610 var uploadedTo = this.props.get('uploadedTo'); 1611 if ( _.isUndefined( uploadedTo ) ) { 1612 return true; 1613 } 1614 1615 return uploadedTo === attachment.get('uploadedTo'); 1616 }, 1617 /** 1618 * @static 1619 * @param {wp.media.model.Attachment} attachment 1620 * 1621 * @this wp.media.model.Attachments 1622 * 1623 * @returns {Boolean} 1624 */ 1625 status: function( attachment ) { 1626 var status = this.props.get('status'); 1627 if ( _.isUndefined( status ) ) { 1628 return true; 1629 } 1630 1631 return status === attachment.get('status'); 1632 } 1633 } 1634 }); 1635 1636 module.exports = Attachments; 1637 },{"./attachment.js":8,"./query.js":10}],10:[function(require,module,exports){ 1638 /** 1639 * wp.media.model.Query 1640 * 1641 * A collection of attachments that match the supplied query arguments. 1642 * 1643 * Note: Do NOT change this.args after the query has been initialized. 1644 * Things will break. 1645 * 1646 * @class 1647 * @augments wp.media.model.Attachments 1648 * @augments Backbone.Collection 1649 * 1650 * @param {array} [models] Models to initialize with the collection. 1651 * @param {object} [options] Options hash. 1652 * @param {object} [options.args] Attachments query arguments. 1653 * @param {object} [options.args.posts_per_page] 1654 */ 1655 var Attachments = require( './attachments.js' ), 1656 Query; 1657 1658 Query = Attachments.extend({ 1659 /** 1660 * @global wp.Uploader 1661 * 1662 * @param {array} [models=[]] Array of initial models to populate the collection. 1663 * @param {object} [options={}] 1664 */ 1665 initialize: function( models, options ) { 1666 var allowed; 1667 1668 options = options || {}; 1669 Attachments.prototype.initialize.apply( this, arguments ); 1670 1671 this.args = options.args; 1672 this._hasMore = true; 1673 this.created = new Date(); 1674 1675 this.filters.order = function( attachment ) { 1676 var orderby = this.props.get('orderby'), 1677 order = this.props.get('order'); 1678 1679 if ( ! this.comparator ) { 1680 return true; 1681 } 1682 1683 // We want any items that can be placed before the last 1684 // item in the set. If we add any items after the last 1685 // item, then we can't guarantee the set is complete. 1686 if ( this.length ) { 1687 return 1 !== this.comparator( attachment, this.last(), { ties: true }); 1688 1689 // Handle the case where there are no items yet and 1690 // we're sorting for recent items. In that case, we want 1691 // changes that occurred after we created the query. 1692 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { 1693 return attachment.get( orderby ) >= this.created; 1694 1695 // If we're sorting by menu order and we have no items, 1696 // accept any items that have the default menu order (0). 1697 } else if ( 'ASC' === order && 'menuOrder' === orderby ) { 1698 return attachment.get( orderby ) === 0; 1699 } 1700 1701 // Otherwise, we don't want any items yet. 1702 return false; 1703 }; 1704 1705 // Observe the central `wp.Uploader.queue` collection to watch for 1706 // new matches for the query. 1707 // 1708 // Only observe when a limited number of query args are set. There 1709 // are no filters for other properties, so observing will result in 1710 // false positives in those queries. 1711 allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; 1712 if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { 1713 this.observe( wp.Uploader.queue ); 1714 } 1715 }, 1716 /** 1717 * Whether there are more attachments that haven't been sync'd from the server 1718 * that match the collection's query. 1719 * 1720 * @returns {boolean} 1721 */ 1722 hasMore: function() { 1723 return this._hasMore; 1724 }, 1725 /** 1726 * Fetch more attachments from the server for the collection. 1727 * 1728 * @param {object} [options={}] 1729 * @returns {Promise} 1730 */ 1731 more: function( options ) { 1732 var query = this; 1733 1734 // If there is already a request pending, return early with the Deferred object. 1735 if ( this._more && 'pending' === this._more.state() ) { 1736 return this._more; 1737 } 1738 1739 if ( ! this.hasMore() ) { 1740 return jQuery.Deferred().resolveWith( this ).promise(); 1741 } 1742 1743 options = options || {}; 1744 options.remove = false; 1745 1746 return this._more = this.fetch( options ).done( function( resp ) { 1747 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { 1748 query._hasMore = false; 1749 } 1750 }); 1751 }, 1752 /** 1753 * Overrides Backbone.Collection.sync 1754 * Overrides wp.media.model.Attachments.sync 1755 * 1756 * @param {String} method 1757 * @param {Backbone.Model} model 1758 * @param {Object} [options={}] 1759 * @returns {Promise} 1760 */ 1761 sync: function( method, model, options ) { 1762 var args, fallback; 1763 1764 // Overload the read method so Attachment.fetch() functions correctly. 1765 if ( 'read' === method ) { 1766 options = options || {}; 1767 options.context = this; 1768 options.data = _.extend( options.data || {}, { 1769 action: 'query-attachments', 1770 post_id: wp.media.model.settings.post.id 1771 }); 1772 1773 // Clone the args so manipulation is non-destructive. 1774 args = _.clone( this.args ); 1775 1776 // Determine which page to query. 1777 if ( -1 !== args.posts_per_page ) { 1778 args.paged = Math.floor( this.length / args.posts_per_page ) + 1; 1779 } 1780 1781 options.data.query = args; 1782 return wp.media.ajax( options ); 1783 1784 // Otherwise, fall back to Backbone.sync() 1785 } else { 1786 /** 1787 * Call wp.media.model.Attachments.sync or Backbone.sync 1788 */ 1789 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; 1790 return fallback.sync.apply( this, arguments ); 1791 } 1792 } 1793 }, { 1794 /** 1795 * @readonly 1796 */ 1797 defaultProps: { 1798 orderby: 'date', 1799 order: 'DESC' 1800 }, 1801 /** 1802 * @readonly 1803 */ 1804 defaultArgs: { 1805 posts_per_page: 40 1806 }, 1807 /** 1808 * @readonly 1809 */ 1810 orderby: { 1811 allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], 1812 /** 1813 * A map of JavaScript orderby values to their WP_Query equivalents. 1814 * @type {Object} 1815 */ 1816 valuemap: { 1817 'id': 'ID', 1818 'uploadedTo': 'parent', 1819 'menuOrder': 'menu_order ID' 1820 } 1821 }, 1822 /** 1823 * A map of JavaScript query properties to their WP_Query equivalents. 1824 * 1825 * @readonly 1826 */ 1827 propmap: { 1828 'search': 's', 1829 'type': 'post_mime_type', 1830 'perPage': 'posts_per_page', 1831 'menuOrder': 'menu_order', 1832 'uploadedTo': 'post_parent', 1833 'status': 'post_status', 1834 'include': 'post__in', 1835 'exclude': 'post__not_in' 1836 }, 1837 /** 1838 * Creates and returns an Attachments Query collection given the properties. 1839 * 1840 * Caches query objects and reuses where possible. 1841 * 1842 * @static 1843 * @method 1844 * 1845 * @param {object} [props] 1846 * @param {Object} [props.cache=true] Whether to use the query cache or not. 1847 * @param {Object} [props.order] 1848 * @param {Object} [props.orderby] 1849 * @param {Object} [props.include] 1850 * @param {Object} [props.exclude] 1851 * @param {Object} [props.s] 1852 * @param {Object} [props.post_mime_type] 1853 * @param {Object} [props.posts_per_page] 1854 * @param {Object} [props.menu_order] 1855 * @param {Object} [props.post_parent] 1856 * @param {Object} [props.post_status] 1857 * @param {Object} [options] 1858 * 1859 * @returns {wp.media.model.Query} A new Attachments Query collection. 1860 */ 1861 get: (function(){ 1862 /** 1863 * @static 1864 * @type Array 1865 */ 1866 var queries = []; 1867 1868 /** 1869 * @returns {Query} 1870 */ 1871 return function( props, options ) { 1872 var args = {}, 1873 orderby = Query.orderby, 1874 defaults = Query.defaultProps, 1875 query, 1876 cache = !! props.cache || _.isUndefined( props.cache ); 1877 1878 // Remove the `query` property. This isn't linked to a query, 1879 // this *is* the query. 1880 delete props.query; 1881 delete props.cache; 1882 1883 // Fill default args. 1884 _.defaults( props, defaults ); 1885 1886 // Normalize the order. 1887 props.order = props.order.toUpperCase(); 1888 if ( 'DESC' !== props.order && 'ASC' !== props.order ) { 1889 props.order = defaults.order.toUpperCase(); 1890 } 1891 1892 // Ensure we have a valid orderby value. 1893 if ( ! _.contains( orderby.allowed, props.orderby ) ) { 1894 props.orderby = defaults.orderby; 1895 } 1896 1897 _.each( [ 'include', 'exclude' ], function( prop ) { 1898 if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { 1899 props[ prop ] = [ props[ prop ] ]; 1900 } 1901 } ); 1902 1903 // Generate the query `args` object. 1904 // Correct any differing property names. 1905 _.each( props, function( value, prop ) { 1906 if ( _.isNull( value ) ) { 1907 return; 1908 } 1909 1910 args[ Query.propmap[ prop ] || prop ] = value; 1911 }); 1912 1913 // Fill any other default query args. 1914 _.defaults( args, Query.defaultArgs ); 1915 1916 // `props.orderby` does not always map directly to `args.orderby`. 1917 // Substitute exceptions specified in orderby.keymap. 1918 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; 1919 1920 // Search the query cache for a matching query. 1921 if ( cache ) { 1922 query = _.find( queries, function( query ) { 1923 return _.isEqual( query.args, args ); 1924 }); 1925 } else { 1926 queries = []; 1927 } 1928 1929 // Otherwise, create a new query and add it to the cache. 1930 if ( ! query ) { 1931 query = new Query( [], _.extend( options || {}, { 1932 props: props, 1933 args: args 1934 } ) ); 1935 queries.push( query ); 1936 } 1937 1938 return query; 1939 }; 1940 }()) 1941 }); 1942 1943 module.exports = Query; 1944 },{"./attachments.js":9}],11:[function(require,module,exports){ 1945 /** 1946 * wp.media.model.Selection 1947 * 1948 * A selection of attachments. 1949 * 1950 * @class 1951 * @augments wp.media.model.Attachments 1952 * @augments Backbone.Collection 1953 */ 1954 var Attachments = require( './attachments.js' ), 1955 Selection; 1956 1957 Selection = Attachments.extend({ 1958 /** 1959 * Refresh the `single` model whenever the selection changes. 1960 * Binds `single` instead of using the context argument to ensure 1961 * it receives no parameters. 1962 * 1963 * @param {Array} [models=[]] Array of models used to populate the collection. 1964 * @param {Object} [options={}] 1965 */ 1966 initialize: function( models, options ) { 1967 /** 1968 * call 'initialize' directly on the parent class 1969 */ 1970 Attachments.prototype.initialize.apply( this, arguments ); 1971 this.multiple = options && options.multiple; 1972 1973 this.on( 'add remove reset', _.bind( this.single, this, false ) ); 1974 }, 1975 1976 /** 1977 * If the workflow does not support multi-select, clear out the selection 1978 * before adding a new attachment to it. 1979 * 1980 * @param {Array} models 1981 * @param {Object} options 1982 * @returns {wp.media.model.Attachment[]} 1983 */ 1984 add: function( models, options ) { 1985 if ( ! this.multiple ) { 1986 this.remove( this.models ); 1987 } 1988 /** 1989 * call 'add' directly on the parent class 1990 */ 1991 return Attachments.prototype.add.call( this, models, options ); 1992 }, 1993 1994 /** 1995 * Fired when toggling (clicking on) an attachment in the modal. 1996 * 1997 * @param {undefined|boolean|wp.media.model.Attachment} model 1998 * 1999 * @fires wp.media.model.Selection#selection:single 2000 * @fires wp.media.model.Selection#selection:unsingle 2001 * 2002 * @returns {Backbone.Model} 2003 */ 2004 single: function( model ) { 2005 var previous = this._single; 2006 2007 // If a `model` is provided, use it as the single model. 2008 if ( model ) { 2009 this._single = model; 2010 } 2011 // If the single model isn't in the selection, remove it. 2012 if ( this._single && ! this.get( this._single.cid ) ) { 2013 delete this._single; 2014 } 2015 2016 this._single = this._single || this.last(); 2017 2018 // If single has changed, fire an event. 2019 if ( this._single !== previous ) { 2020 if ( previous ) { 2021 previous.trigger( 'selection:unsingle', previous, this ); 2022 2023 // If the model was already removed, trigger the collection 2024 // event manually. 2025 if ( ! this.get( previous.cid ) ) { 2026 this.trigger( 'selection:unsingle', previous, this ); 2027 } 2028 } 2029 if ( this._single ) { 2030 this._single.trigger( 'selection:single', this._single, this ); 2031 } 2032 } 2033 2034 // Return the single model, or the last model as a fallback. 2035 return this._single; 2036 } 2037 }); 2038 2039 module.exports = Selection; 2040 },{"./attachments.js":9}],12:[function(require,module,exports){ 2041 /** 2042 * A router for handling the browser history and application state. 2043 * 2044 * @constructor 2045 * @augments Backbone.Router 2046 */ 2047 var Router = Backbone.Router.extend({ 2048 routes: { 2049 'upload.php?item=:slug': 'showItem', 2050 'upload.php?search=:query': 'search' 2051 }, 2052 2053 // Map routes against the page URL 2054 baseUrl: function( url ) { 2055 return 'upload.php' + url; 2056 }, 2057 2058 // Respond to the search route by filling the search field and trigggering the input event 2059 search: function( query ) { 2060 jQuery( '#media-search-input' ).val( query ).trigger( 'input' ); 2061 }, 2062 2063 // Show the modal with a specific item 2064 showItem: function( query ) { 2065 var media = wp.media, 2066 library = media.frame.state().get('library'), 2067 item; 2068 2069 // Trigger the media frame to open the correct item 2070 item = library.findWhere( { id: parseInt( query, 10 ) } ); 2071 if ( item ) { 2072 media.frame.trigger( 'edit:attachment', item ); 2073 } else { 2074 item = media.attachment( query ); 2075 media.frame.listenTo( item, 'change', function( model ) { 2076 media.frame.stopListening( item ); 2077 media.frame.trigger( 'edit:attachment', model ); 2078 } ); 2079 item.fetch(); 2080 } 2081 } 2082 }); 2083 2084 module.exports = Router; 2085 },{}],13:[function(require,module,exports){ 2086 /** 2087 * wp.media.selectionSync 2088 * 2089 * Sync an attachments selection in a state with another state. 2090 * 2091 * Allows for selecting multiple images in the Insert Media workflow, and then 2092 * switching to the Insert Gallery workflow while preserving the attachments selection. 2093 * 2094 * @mixin 2095 */ 2096 var selectionSync = { 2097 /** 2098 * @since 3.5.0 2099 */ 2100 syncSelection: function() { 2101 var selection = this.get('selection'), 2102 manager = this.frame._selection; 2103 2104 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 2105 return; 2106 } 2107 2108 // If the selection supports multiple items, validate the stored 2109 // attachments based on the new selection's conditions. Record 2110 // the attachments that are not included; we'll maintain a 2111 // reference to those. Other attachments are considered in flux. 2112 if ( selection.multiple ) { 2113 selection.reset( [], { silent: true }); 2114 selection.validateAll( manager.attachments ); 2115 manager.difference = _.difference( manager.attachments.models, selection.models ); 2116 } 2117 2118 // Sync the selection's single item with the master. 2119 selection.single( manager.single ); 2120 }, 2121 2122 /** 2123 * Record the currently active attachments, which is a combination 2124 * of the selection's attachments and the set of selected 2125 * attachments that this specific selection considered invalid. 2126 * Reset the difference and record the single attachment. 2127 * 2128 * @since 3.5.0 2129 */ 2130 recordSelection: function() { 2131 var selection = this.get('selection'), 2132 manager = this.frame._selection; 2133 2134 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 2135 return; 2136 } 2137 2138 if ( selection.multiple ) { 2139 manager.attachments.reset( selection.toArray().concat( manager.difference ) ); 2140 manager.difference = []; 2141 } else { 2142 manager.attachments.add( selection.toArray() ); 2143 } 2144 2145 manager.single = selection._single; 2146 } 2147 }; 2148 2149 module.exports = selectionSync; 2150 },{}],14:[function(require,module,exports){ 2151 /** 2152 * wp.media.view.AttachmentCompat 2153 * 2154 * A view to display fields added via the `attachment_fields_to_edit` filter. 2155 * 2156 * @class 2157 * @augments wp.media.View 2158 * @augments wp.Backbone.View 2159 * @augments Backbone.View 2160 */ 2161 var View = require( './view.js' ), 2162 AttachmentCompat; 2163 2164 AttachmentCompat = View.extend({ 2165 tagName: 'form', 2166 className: 'compat-item', 2167 2168 events: { 2169 'submit': 'preventDefault', 2170 'change input': 'save', 2171 'change select': 'save', 2172 'change textarea': 'save' 2173 }, 2174 2175 initialize: function() { 2176 this.model.on( 'change:compat', this.render, this ); 2177 }, 2178 /** 2179 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 2180 */ 2181 dispose: function() { 2182 if ( this.$(':focus').length ) { 2183 this.save(); 2184 } 2185 /** 2186 * call 'dispose' directly on the parent class 2187 */ 2188 return View.prototype.dispose.apply( this, arguments ); 2189 }, 2190 /** 2191 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 2192 */ 2193 render: function() { 2194 var compat = this.model.get('compat'); 2195 if ( ! compat || ! compat.item ) { 2196 return; 2197 } 2198 2199 this.views.detach(); 2200 this.$el.html( compat.item ); 2201 this.views.render(); 2202 return this; 2203 }, 2204 /** 2205 * @param {Object} event 2206 */ 2207 preventDefault: function( event ) { 2208 event.preventDefault(); 2209 }, 2210 /** 2211 * @param {Object} event 2212 */ 2213 save: function( event ) { 2214 var data = {}; 2215 2216 if ( event ) { 2217 event.preventDefault(); 2218 } 2219 2220 _.each( this.$el.serializeArray(), function( pair ) { 2221 data[ pair.name ] = pair.value; 2222 }); 2223 2224 this.controller.trigger( 'attachment:compat:waiting', ['waiting'] ); 2225 this.model.saveCompat( data ).always( _.bind( this.postSave, this ) ); 2226 }, 2227 2228 postSave: function() { 2229 this.controller.trigger( 'attachment:compat:ready', ['ready'] ); 2230 } 2231 }); 2232 2233 module.exports = AttachmentCompat; 2234 },{"./view.js":55}],15:[function(require,module,exports){ 2235 /** 2236 * wp.media.view.AttachmentFilters 2237 * 2238 * @class 2239 * @augments wp.media.View 2240 * @augments wp.Backbone.View 2241 * @augments Backbone.View 2242 */ 2243 var View = require( './view.js' ), 2244 $ = jQuery, 2245 AttachmentFilters; 2246 2247 AttachmentFilters = View.extend({ 2248 tagName: 'select', 2249 className: 'attachment-filters', 2250 id: 'media-attachment-filters', 2251 2252 events: { 2253 change: 'change' 2254 }, 2255 2256 keys: [], 2257 2258 initialize: function() { 2259 this.createFilters(); 2260 _.extend( this.filters, this.options.filters ); 2261 2262 // Build `<option>` elements. 2263 this.$el.html( _.chain( this.filters ).map( function( filter, value ) { 2264 return { 2265 el: $( '<option></option>' ).val( value ).html( filter.text )[0], 2266 priority: filter.priority || 50 2267 }; 2268 }, this ).sortBy('priority').pluck('el').value() ); 2269 2270 this.model.on( 'change', this.select, this ); 2271 this.select(); 2272 }, 2273 2274 /** 2275 * @abstract 2276 */ 2277 createFilters: function() { 2278 this.filters = {}; 2279 }, 2280 2281 /** 2282 * When the selected filter changes, update the Attachment Query properties to match. 2283 */ 2284 change: function() { 2285 var filter = this.filters[ this.el.value ]; 2286 if ( filter ) { 2287 this.model.set( filter.props ); 2288 } 2289 }, 2290 2291 select: function() { 2292 var model = this.model, 2293 value = 'all', 2294 props = model.toJSON(); 2295 2296 _.find( this.filters, function( filter, id ) { 2297 var equal = _.all( filter.props, function( prop, key ) { 2298 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); 2299 }); 2300 2301 if ( equal ) { 2302 return value = id; 2303 } 2304 }); 2305 2306 this.$el.val( value ); 2307 } 2308 }); 2309 2310 module.exports = AttachmentFilters; 2311 },{"./view.js":55}],16:[function(require,module,exports){ 2312 /** 2313 * wp.media.view.AttachmentFilters.All 2314 * 2315 * @class 2316 * @augments wp.media.view.AttachmentFilters 2317 * @augments wp.media.View 2318 * @augments wp.Backbone.View 2319 * @augments Backbone.View 2320 */ 2321 var AttachmentFilters = require( '../attachment-filters.js' ), 2322 l10n = wp.media.view.l10n, 2323 All; 2324 2325 All = AttachmentFilters.extend({ 2326 createFilters: function() { 2327 var filters = {}; 2328 2329 _.each( wp.media.view.settings.mimeTypes || {}, function( text, key ) { 2330 filters[ key ] = { 2331 text: text, 2332 props: { 2333 status: null, 2334 type: key, 2335 uploadedTo: null, 2336 orderby: 'date', 2337 order: 'DESC' 2338 } 2339 }; 2340 }); 2341 2342 filters.all = { 2343 text: l10n.allMediaItems, 2344 props: { 2345 status: null, 2346 type: null, 2347 uploadedTo: null, 2348 orderby: 'date', 2349 order: 'DESC' 2350 }, 2351 priority: 10 2352 }; 2353 2354 if ( wp.media.view.settings.post.id ) { 2355 filters.uploaded = { 2356 text: l10n.uploadedToThisPost, 2357 props: { 2358 status: null, 2359 type: null, 2360 uploadedTo: wp.media.view.settings.post.id, 2361 orderby: 'menuOrder', 2362 order: 'ASC' 2363 }, 2364 priority: 20 2365 }; 2366 } 2367 2368 filters.unattached = { 2369 text: l10n.unattached, 2370 props: { 2371 status: null, 2372 uploadedTo: 0, 2373 type: null, 2374 orderby: 'menuOrder', 2375 order: 'ASC' 2376 }, 2377 priority: 50 2378 }; 2379 2380 if ( wp.media.view.settings.mediaTrash && 2381 this.controller.isModeActive( 'grid' ) ) { 2382 2383 filters.trash = { 2384 text: l10n.trash, 2385 props: { 2386 uploadedTo: null, 2387 status: 'trash', 2388 type: null, 2389 orderby: 'date', 2390 order: 'DESC' 2391 }, 2392 priority: 50 2393 }; 2394 } 2395 2396 this.filters = filters; 2397 } 2398 }); 2399 2400 module.exports = All; 2401 },{"../attachment-filters.js":15}],17:[function(require,module,exports){ 2402 /** 2403 * A filter dropdown for month/dates. 2404 * 2405 * @class 2406 * @augments wp.media.view.AttachmentFilters 2407 * @augments wp.media.View 2408 * @augments wp.Backbone.View 2409 * @augments Backbone.View 2410 */ 2411 var AttachmentFilters = require( '../attachment-filters.js' ), 2412 l10n = wp.media.view.l10n, 2413 DateFilter; 2414 2415 DateFilter = AttachmentFilters.extend({ 2416 id: 'media-attachment-date-filters', 2417 2418 createFilters: function() { 2419 var filters = {}; 2420 _.each( wp.media.view.settings.months || {}, function( value, index ) { 2421 filters[ index ] = { 2422 text: value.text, 2423 props: { 2424 year: value.year, 2425 monthnum: value.month 2426 } 2427 }; 2428 }); 2429 filters.all = { 2430 text: l10n.allDates, 2431 props: { 2432 monthnum: false, 2433 year: false 2434 }, 2435 priority: 10 2436 }; 2437 this.filters = filters; 2438 } 2439 }); 2440 2441 module.exports = DateFilter; 2442 },{"../attachment-filters.js":15}],18:[function(require,module,exports){ 2443 /** 2444 * wp.media.view.AttachmentFilters.Uploaded 2445 * 2446 * @class 2447 * @augments wp.media.view.AttachmentFilters 2448 * @augments wp.media.View 2449 * @augments wp.Backbone.View 2450 * @augments Backbone.View 2451 */ 2452 var AttachmentFilters = require( '../attachment-filters.js' ), 2453 l10n = wp.media.view.l10n, 2454 Uploaded; 2455 2456 Uploaded = AttachmentFilters.extend({ 2457 createFilters: function() { 2458 var type = this.model.get('type'), 2459 types = wp.media.view.settings.mimeTypes, 2460 text; 2461 2462 if ( types && type ) { 2463 text = types[ type ]; 2464 } 2465 2466 this.filters = { 2467 all: { 2468 text: text || l10n.allMediaItems, 2469 props: { 2470 uploadedTo: null, 2471 orderby: 'date', 2472 order: 'DESC' 2473 }, 2474 priority: 10 2475 }, 2476 2477 uploaded: { 2478 text: l10n.uploadedToThisPost, 2479 props: { 2480 uploadedTo: wp.media.view.settings.post.id, 2481 orderby: 'menuOrder', 2482 order: 'ASC' 2483 }, 2484 priority: 20 2485 }, 2486 2487 unattached: { 2488 text: l10n.unattached, 2489 props: { 2490 uploadedTo: 0, 2491 orderby: 'menuOrder', 2492 order: 'ASC' 2493 }, 2494 priority: 50 2495 } 2496 }; 2497 } 2498 }); 2499 2500 module.exports = Uploaded; 2501 },{"../attachment-filters.js":15}],19:[function(require,module,exports){ 2502 /** 2503 * wp.media.view.Attachment 2504 * 2505 * @class 2506 * @augments wp.media.View 2507 * @augments wp.Backbone.View 2508 * @augments Backbone.View 2509 */ 2510 var View = require( './view.js' ), 2511 $ = jQuery, 2512 Attachment; 2513 2514 Attachment = View.extend({ 2515 tagName: 'li', 2516 className: 'attachment', 2517 template: wp.template('attachment'), 2518 2519 attributes: function() { 2520 return { 2521 'tabIndex': 0, 2522 'role': 'checkbox', 2523 'aria-label': this.model.get( 'title' ), 2524 'aria-checked': false, 2525 'data-id': this.model.get( 'id' ) 2526 }; 2527 }, 2528 2529 events: { 2530 'click .js--select-attachment': 'toggleSelectionHandler', 2531 'change [data-setting]': 'updateSetting', 2532 'change [data-setting] input': 'updateSetting', 2533 'change [data-setting] select': 'updateSetting', 2534 'change [data-setting] textarea': 'updateSetting', 2535 'click .close': 'removeFromLibrary', 2536 'click .check': 'checkClickHandler', 2537 'click a': 'preventDefault', 2538 'keydown .close': 'removeFromLibrary', 2539 'keydown': 'toggleSelectionHandler' 2540 }, 2541 2542 buttons: {}, 2543 2544 initialize: function() { 2545 var selection = this.options.selection, 2546 options = _.defaults( this.options, { 2547 rerenderOnModelChange: true 2548 } ); 2549 2550 if ( options.rerenderOnModelChange ) { 2551 this.model.on( 'change', this.render, this ); 2552 } else { 2553 this.model.on( 'change:percent', this.progress, this ); 2554 } 2555 this.model.on( 'change:title', this._syncTitle, this ); 2556 this.model.on( 'change:caption', this._syncCaption, this ); 2557 this.model.on( 'change:artist', this._syncArtist, this ); 2558 this.model.on( 'change:album', this._syncAlbum, this ); 2559 2560 // Update the selection. 2561 this.model.on( 'add', this.select, this ); 2562 this.model.on( 'remove', this.deselect, this ); 2563 if ( selection ) { 2564 selection.on( 'reset', this.updateSelect, this ); 2565 // Update the model's details view. 2566 this.model.on( 'selection:single selection:unsingle', this.details, this ); 2567 this.details( this.model, this.controller.state().get('selection') ); 2568 } 2569 2570 this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); 2571 }, 2572 /** 2573 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2574 */ 2575 dispose: function() { 2576 var selection = this.options.selection; 2577 2578 // Make sure all settings are saved before removing the view. 2579 this.updateAll(); 2580 2581 if ( selection ) { 2582 selection.off( null, null, this ); 2583 } 2584 /** 2585 * call 'dispose' directly on the parent class 2586 */ 2587 View.prototype.dispose.apply( this, arguments ); 2588 return this; 2589 }, 2590 /** 2591 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2592 */ 2593 render: function() { 2594 var options = _.defaults( this.model.toJSON(), { 2595 orientation: 'landscape', 2596 uploading: false, 2597 type: '', 2598 subtype: '', 2599 icon: '', 2600 filename: '', 2601 caption: '', 2602 title: '', 2603 dateFormatted: '', 2604 width: '', 2605 height: '', 2606 compat: false, 2607 alt: '', 2608 description: '' 2609 }, this.options ); 2610 2611 options.buttons = this.buttons; 2612 options.describe = this.controller.state().get('describe'); 2613 2614 if ( 'image' === options.type ) { 2615 options.size = this.imageSize(); 2616 } 2617 2618 options.can = {}; 2619 if ( options.nonces ) { 2620 options.can.remove = !! options.nonces['delete']; 2621 options.can.save = !! options.nonces.update; 2622 } 2623 2624 if ( this.controller.state().get('allowLocalEdits') ) { 2625 options.allowLocalEdits = true; 2626 } 2627 2628 if ( options.uploading && ! options.percent ) { 2629 options.percent = 0; 2630 } 2631 2632 this.views.detach(); 2633 this.$el.html( this.template( options ) ); 2634 2635 this.$el.toggleClass( 'uploading', options.uploading ); 2636 2637 if ( options.uploading ) { 2638 this.$bar = this.$('.media-progress-bar div'); 2639 } else { 2640 delete this.$bar; 2641 } 2642 2643 // Check if the model is selected. 2644 this.updateSelect(); 2645 2646 // Update the save status. 2647 this.updateSave(); 2648 2649 this.views.render(); 2650 2651 return this; 2652 }, 2653 2654 progress: function() { 2655 if ( this.$bar && this.$bar.length ) { 2656 this.$bar.width( this.model.get('percent') + '%' ); 2657 } 2658 }, 2659 2660 /** 2661 * @param {Object} event 2662 */ 2663 toggleSelectionHandler: function( event ) { 2664 var method; 2665 2666 // Don't do anything inside inputs. 2667 if ( 'INPUT' === event.target.nodeName ) { 2668 return; 2669 } 2670 2671 // Catch arrow events 2672 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 2673 this.controller.trigger( 'attachment:keydown:arrow', event ); 2674 return; 2675 } 2676 2677 // Catch enter and space events 2678 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 2679 return; 2680 } 2681 2682 event.preventDefault(); 2683 2684 // In the grid view, bubble up an edit:attachment event to the controller. 2685 if ( this.controller.isModeActive( 'grid' ) ) { 2686 if ( this.controller.isModeActive( 'edit' ) ) { 2687 // Pass the current target to restore focus when closing 2688 this.controller.trigger( 'edit:attachment', this.model, event.currentTarget ); 2689 return; 2690 } 2691 2692 if ( this.controller.isModeActive( 'select' ) ) { 2693 method = 'toggle'; 2694 } 2695 } 2696 2697 if ( event.shiftKey ) { 2698 method = 'between'; 2699 } else if ( event.ctrlKey || event.metaKey ) { 2700 method = 'toggle'; 2701 } 2702 2703 this.toggleSelection({ 2704 method: method 2705 }); 2706 2707 this.controller.trigger( 'selection:toggle' ); 2708 }, 2709 /** 2710 * @param {Object} options 2711 */ 2712 toggleSelection: function( options ) { 2713 var collection = this.collection, 2714 selection = this.options.selection, 2715 model = this.model, 2716 method = options && options.method, 2717 single, models, singleIndex, modelIndex; 2718 2719 if ( ! selection ) { 2720 return; 2721 } 2722 2723 single = selection.single(); 2724 method = _.isUndefined( method ) ? selection.multiple : method; 2725 2726 // If the `method` is set to `between`, select all models that 2727 // exist between the current and the selected model. 2728 if ( 'between' === method && single && selection.multiple ) { 2729 // If the models are the same, short-circuit. 2730 if ( single === model ) { 2731 return; 2732 } 2733 2734 singleIndex = collection.indexOf( single ); 2735 modelIndex = collection.indexOf( this.model ); 2736 2737 if ( singleIndex < modelIndex ) { 2738 models = collection.models.slice( singleIndex, modelIndex + 1 ); 2739 } else { 2740 models = collection.models.slice( modelIndex, singleIndex + 1 ); 2741 } 2742 2743 selection.add( models ); 2744 selection.single( model ); 2745 return; 2746 2747 // If the `method` is set to `toggle`, just flip the selection 2748 // status, regardless of whether the model is the single model. 2749 } else if ( 'toggle' === method ) { 2750 selection[ this.selected() ? 'remove' : 'add' ]( model ); 2751 selection.single( model ); 2752 return; 2753 } else if ( 'add' === method ) { 2754 selection.add( model ); 2755 selection.single( model ); 2756 return; 2757 } 2758 2759 // Fixes bug that loses focus when selecting a featured image 2760 if ( ! method ) { 2761 method = 'add'; 2762 } 2763 2764 if ( method !== 'add' ) { 2765 method = 'reset'; 2766 } 2767 2768 if ( this.selected() ) { 2769 // If the model is the single model, remove it. 2770 // If it is not the same as the single model, 2771 // it now becomes the single model. 2772 selection[ single === model ? 'remove' : 'single' ]( model ); 2773 } else { 2774 // If the model is not selected, run the `method` on the 2775 // selection. By default, we `reset` the selection, but the 2776 // `method` can be set to `add` the model to the selection. 2777 selection[ method ]( model ); 2778 selection.single( model ); 2779 } 2780 }, 2781 2782 updateSelect: function() { 2783 this[ this.selected() ? 'select' : 'deselect' ](); 2784 }, 2785 /** 2786 * @returns {unresolved|Boolean} 2787 */ 2788 selected: function() { 2789 var selection = this.options.selection; 2790 if ( selection ) { 2791 return !! selection.get( this.model.cid ); 2792 } 2793 }, 2794 /** 2795 * @param {Backbone.Model} model 2796 * @param {Backbone.Collection} collection 2797 */ 2798 select: function( model, collection ) { 2799 var selection = this.options.selection, 2800 controller = this.controller; 2801 2802 // Check if a selection exists and if it's the collection provided. 2803 // If they're not the same collection, bail; we're in another 2804 // selection's event loop. 2805 if ( ! selection || ( collection && collection !== selection ) ) { 2806 return; 2807 } 2808 2809 // Bail if the model is already selected. 2810 if ( this.$el.hasClass( 'selected' ) ) { 2811 return; 2812 } 2813 2814 // Add 'selected' class to model, set aria-checked to true. 2815 this.$el.addClass( 'selected' ).attr( 'aria-checked', true ); 2816 // Make the checkbox tabable, except in media grid (bulk select mode). 2817 if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) { 2818 this.$( '.check' ).attr( 'tabindex', '0' ); 2819 } 2820 }, 2821 /** 2822 * @param {Backbone.Model} model 2823 * @param {Backbone.Collection} collection 2824 */ 2825 deselect: function( model, collection ) { 2826 var selection = this.options.selection; 2827 2828 // Check if a selection exists and if it's the collection provided. 2829 // If they're not the same collection, bail; we're in another 2830 // selection's event loop. 2831 if ( ! selection || ( collection && collection !== selection ) ) { 2832 return; 2833 } 2834 this.$el.removeClass( 'selected' ).attr( 'aria-checked', false ) 2835 .find( '.check' ).attr( 'tabindex', '-1' ); 2836 }, 2837 /** 2838 * @param {Backbone.Model} model 2839 * @param {Backbone.Collection} collection 2840 */ 2841 details: function( model, collection ) { 2842 var selection = this.options.selection, 2843 details; 2844 2845 if ( selection !== collection ) { 2846 return; 2847 } 2848 2849 details = selection.single(); 2850 this.$el.toggleClass( 'details', details === this.model ); 2851 }, 2852 /** 2853 * @param {Object} event 2854 */ 2855 preventDefault: function( event ) { 2856 event.preventDefault(); 2857 }, 2858 /** 2859 * @param {string} size 2860 * @returns {Object} 2861 */ 2862 imageSize: function( size ) { 2863 var sizes = this.model.get('sizes'); 2864 2865 size = size || 'medium'; 2866 2867 // Use the provided image size if possible. 2868 if ( sizes && sizes[ size ] ) { 2869 return _.clone( sizes[ size ] ); 2870 } else { 2871 return { 2872 url: this.model.get('url'), 2873 width: this.model.get('width'), 2874 height: this.model.get('height'), 2875 orientation: this.model.get('orientation') 2876 }; 2877 } 2878 }, 2879 /** 2880 * @param {Object} event 2881 */ 2882 updateSetting: function( event ) { 2883 var $setting = $( event.target ).closest('[data-setting]'), 2884 setting, value; 2885 2886 if ( ! $setting.length ) { 2887 return; 2888 } 2889 2890 setting = $setting.data('setting'); 2891 value = event.target.value; 2892 2893 if ( this.model.get( setting ) !== value ) { 2894 this.save( setting, value ); 2895 } 2896 }, 2897 2898 /** 2899 * Pass all the arguments to the model's save method. 2900 * 2901 * Records the aggregate status of all save requests and updates the 2902 * view's classes accordingly. 2903 */ 2904 save: function() { 2905 var view = this, 2906 save = this._save = this._save || { status: 'ready' }, 2907 request = this.model.save.apply( this.model, arguments ), 2908 requests = save.requests ? $.when( request, save.requests ) : request; 2909 2910 // If we're waiting to remove 'Saved.', stop. 2911 if ( save.savedTimer ) { 2912 clearTimeout( save.savedTimer ); 2913 } 2914 2915 this.updateSave('waiting'); 2916 save.requests = requests; 2917 requests.always( function() { 2918 // If we've performed another request since this one, bail. 2919 if ( save.requests !== requests ) { 2920 return; 2921 } 2922 2923 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' ); 2924 save.savedTimer = setTimeout( function() { 2925 view.updateSave('ready'); 2926 delete save.savedTimer; 2927 }, 2000 ); 2928 }); 2929 }, 2930 /** 2931 * @param {string} status 2932 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2933 */ 2934 updateSave: function( status ) { 2935 var save = this._save = this._save || { status: 'ready' }; 2936 2937 if ( status && status !== save.status ) { 2938 this.$el.removeClass( 'save-' + save.status ); 2939 save.status = status; 2940 } 2941 2942 this.$el.addClass( 'save-' + save.status ); 2943 return this; 2944 }, 2945 2946 updateAll: function() { 2947 var $settings = this.$('[data-setting]'), 2948 model = this.model, 2949 changed; 2950 2951 changed = _.chain( $settings ).map( function( el ) { 2952 var $input = $('input, textarea, select, [value]', el ), 2953 setting, value; 2954 2955 if ( ! $input.length ) { 2956 return; 2957 } 2958 2959 setting = $(el).data('setting'); 2960 value = $input.val(); 2961 2962 // Record the value if it changed. 2963 if ( model.get( setting ) !== value ) { 2964 return [ setting, value ]; 2965 } 2966 }).compact().object().value(); 2967 2968 if ( ! _.isEmpty( changed ) ) { 2969 model.save( changed ); 2970 } 2971 }, 2972 /** 2973 * @param {Object} event 2974 */ 2975 removeFromLibrary: function( event ) { 2976 // Catch enter and space events 2977 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 2978 return; 2979 } 2980 2981 // Stop propagation so the model isn't selected. 2982 event.stopPropagation(); 2983 2984 this.collection.remove( this.model ); 2985 }, 2986 2987 /** 2988 * Add the model if it isn't in the selection, if it is in the selection, 2989 * remove it. 2990 * 2991 * @param {[type]} event [description] 2992 * @return {[type]} [description] 2993 */ 2994 checkClickHandler: function ( event ) { 2995 var selection = this.options.selection; 2996 if ( ! selection ) { 2997 return; 2998 } 2999 event.stopPropagation(); 3000 if ( selection.where( { id: this.model.get( 'id' ) } ).length ) { 3001 selection.remove( this.model ); 3002 // Move focus back to the attachment tile (from the check). 3003 this.$el.focus(); 3004 } else { 3005 selection.add( this.model ); 3006 } 3007 } 3008 }); 3009 3010 // Ensure settings remain in sync between attachment views. 3011 _.each({ 3012 caption: '_syncCaption', 3013 title: '_syncTitle', 3014 artist: '_syncArtist', 3015 album: '_syncAlbum' 3016 }, function( method, setting ) { 3017 /** 3018 * @param {Backbone.Model} model 3019 * @param {string} value 3020 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 3021 */ 3022 Attachment.prototype[ method ] = function( model, value ) { 3023 var $setting = this.$('[data-setting="' + setting + '"]'); 3024 3025 if ( ! $setting.length ) { 3026 return this; 3027 } 3028 3029 // If the updated value is in sync with the value in the DOM, there 3030 // is no need to re-render. If we're currently editing the value, 3031 // it will automatically be in sync, suppressing the re-render for 3032 // the view we're editing, while updating any others. 3033 if ( value === $setting.find('input, textarea, select, [value]').val() ) { 3034 return this; 3035 } 3036 3037 return this.render(); 3038 }; 3039 }); 3040 3041 module.exports = Attachment; 3042 },{"./view.js":55}],20:[function(require,module,exports){ 3043 /** 3044 * A similar view to media.view.Attachment.Details 3045 * for use in the Edit Attachment modal. 3046 * 3047 * @constructor 3048 * @augments wp.media.view.Attachment.Details 3049 * @augments wp.media.view.Attachment 3050 * @augments wp.media.View 3051 * @augments wp.Backbone.View 3052 * @augments Backbone.View 3053 */ 3054 var Details = require( './details.js' ), 3055 MediaDetails = require( '../media-details.js' ), 3056 TwoColumn; 3057 3058 TwoColumn = Details.extend({ 3059 template: wp.template( 'attachment-details-two-column' ), 3060 3061 editAttachment: function( event ) { 3062 event.preventDefault(); 3063 this.controller.content.mode( 'edit-image' ); 3064 }, 3065 3066 /** 3067 * Noop this from parent class, doesn't apply here. 3068 */ 3069 toggleSelectionHandler: function() {}, 3070 3071 render: function() { 3072 Details.prototype.render.apply( this, arguments ); 3073 3074 wp.media.mixin.removeAllPlayers(); 3075 this.$( 'audio, video' ).each( function (i, elem) { 3076 var el = MediaDetails.prepareSrc( elem ); 3077 new MediaElementPlayer( el, wp.media.mixin.mejsSettings ); 3078 } ); 3079 } 3080 }); 3081 3082 module.exports = TwoColumn; 3083 },{"../media-details.js":37,"./details.js":21}],21:[function(require,module,exports){ 3084 /** 3085 * wp.media.view.Attachment.Details 3086 * 3087 * @class 3088 * @augments wp.media.view.Attachment 3089 * @augments wp.media.View 3090 * @augments wp.Backbone.View 3091 * @augments Backbone.View 3092 */ 3093 var Attachment = require( '../attachment.js' ), 3094 l10n = wp.media.view.l10n, 3095 Details; 3096 3097 Details = Attachment.extend({ 3098 tagName: 'div', 3099 className: 'attachment-details', 3100 template: wp.template('attachment-details'), 3101 3102 attributes: function() { 3103 return { 3104 'tabIndex': 0, 3105 'data-id': this.model.get( 'id' ) 3106 }; 3107 }, 3108 3109 events: { 3110 'change [data-setting]': 'updateSetting', 3111 'change [data-setting] input': 'updateSetting', 3112 'change [data-setting] select': 'updateSetting', 3113 'change [data-setting] textarea': 'updateSetting', 3114 'click .delete-attachment': 'deleteAttachment', 3115 'click .trash-attachment': 'trashAttachment', 3116 'click .untrash-attachment': 'untrashAttachment', 3117 'click .edit-attachment': 'editAttachment', 3118 'click .refresh-attachment': 'refreshAttachment', 3119 'keydown': 'toggleSelectionHandler' 3120 }, 3121 3122 initialize: function() { 3123 this.options = _.defaults( this.options, { 3124 rerenderOnModelChange: false 3125 }); 3126 3127 this.on( 'ready', this.initialFocus ); 3128 // Call 'initialize' directly on the parent class. 3129 Attachment.prototype.initialize.apply( this, arguments ); 3130 }, 3131 3132 initialFocus: function() { 3133 if ( ! wp.media.isTouchDevice ) { 3134 this.$( ':input' ).eq( 0 ).focus(); 3135 } 3136 }, 3137 /** 3138 * @param {Object} event 3139 */ 3140 deleteAttachment: function( event ) { 3141 event.preventDefault(); 3142 3143 if ( confirm( l10n.warnDelete ) ) { 3144 this.model.destroy(); 3145 // Keep focus inside media modal 3146 // after image is deleted 3147 this.controller.modal.focusManager.focus(); 3148 } 3149 }, 3150 /** 3151 * @param {Object} event 3152 */ 3153 trashAttachment: function( event ) { 3154 var library = this.controller.library; 3155 event.preventDefault(); 3156 3157 if ( wp.media.view.settings.mediaTrash && 3158 'edit-metadata' === this.controller.content.mode() ) { 3159 3160 this.model.set( 'status', 'trash' ); 3161 this.model.save().done( function() { 3162 library._requery( true ); 3163 } ); 3164 } else { 3165 this.model.destroy(); 3166 } 3167 }, 3168 /** 3169 * @param {Object} event 3170 */ 3171 untrashAttachment: function( event ) { 3172 var library = this.controller.library; 3173 event.preventDefault(); 3174 3175 this.model.set( 'status', 'inherit' ); 3176 this.model.save().done( function() { 3177 library._requery( true ); 3178 } ); 3179 }, 3180 /** 3181 * @param {Object} event 3182 */ 3183 editAttachment: function( event ) { 3184 var editState = this.controller.states.get( 'edit-image' ); 3185 if ( window.imageEdit && editState ) { 3186 event.preventDefault(); 3187 3188 editState.set( 'image', this.model ); 3189 this.controller.setState( 'edit-image' ); 3190 } else { 3191 this.$el.addClass('needs-refresh'); 3192 } 3193 }, 3194 /** 3195 * @param {Object} event 3196 */ 3197 refreshAttachment: function( event ) { 3198 this.$el.removeClass('needs-refresh'); 3199 event.preventDefault(); 3200 this.model.fetch(); 3201 }, 3202 /** 3203 * When reverse tabbing(shift+tab) out of the right details panel, deliver 3204 * the focus to the item in the list that was being edited. 3205 * 3206 * @param {Object} event 3207 */ 3208 toggleSelectionHandler: function( event ) { 3209 if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { 3210 this.controller.trigger( 'attachment:details:shift-tab', event ); 3211 return false; 3212 } 3213 3214 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 3215 this.controller.trigger( 'attachment:keydown:arrow', event ); 3216 return; 3217 } 3218 } 3219 }); 3220 3221 module.exports = Details; 3222 },{"../attachment.js":19}],22:[function(require,module,exports){ 3223 /** 3224 * wp.media.view.Attachment.Library 3225 * 3226 * @class 3227 * @augments wp.media.view.Attachment 3228 * @augments wp.media.View 3229 * @augments wp.Backbone.View 3230 * @augments Backbone.View 3231 */ 3232 var Attachment = require( '../attachment.js' ), 3233 Library; 3234 3235 Library = Attachment.extend({ 3236 buttons: { 3237 check: true 3238 } 3239 }); 3240 3241 module.exports = Library; 3242 },{"../attachment.js":19}],23:[function(require,module,exports){ 3243 /** 3244 * wp.media.view.Attachments 3245 * 3246 * @class 3247 * @augments wp.media.View 3248 * @augments wp.Backbone.View 3249 * @augments Backbone.View 3250 */ 3251 var View = require( './view.js' ), 3252 Attachment = require( './attachment.js' ), 3253 $ = jQuery, 3254 Attachments; 3255 3256 Attachments = View.extend({ 3257 tagName: 'ul', 3258 className: 'attachments', 3259 3260 attributes: { 3261 tabIndex: -1 3262 }, 3263 3264 initialize: function() { 3265 this.el.id = _.uniqueId('__attachments-view-'); 3266 3267 _.defaults( this.options, { 3268 refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, 3269 refreshThreshold: 3, 3270 AttachmentView: Attachment, 3271 sortable: false, 3272 resize: true, 3273 idealColumnWidth: $( window ).width() < 640 ? 135 : 150 3274 }); 3275 3276 this._viewsByCid = {}; 3277 this.$window = $( window ); 3278 this.resizeEvent = 'resize.media-modal-columns'; 3279 3280 this.collection.on( 'add', function( attachment ) { 3281 this.views.add( this.createAttachmentView( attachment ), { 3282 at: this.collection.indexOf( attachment ) 3283 }); 3284 }, this ); 3285 3286 this.collection.on( 'remove', function( attachment ) { 3287 var view = this._viewsByCid[ attachment.cid ]; 3288 delete this._viewsByCid[ attachment.cid ]; 3289 3290 if ( view ) { 3291 view.remove(); 3292 } 3293 }, this ); 3294 3295 this.collection.on( 'reset', this.render, this ); 3296 3297 this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); 3298 3299 // Throttle the scroll handler and bind this. 3300 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 3301 3302 this.options.scrollElement = this.options.scrollElement || this.el; 3303 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 3304 3305 this.initSortable(); 3306 3307 _.bindAll( this, 'setColumns' ); 3308 3309 if ( this.options.resize ) { 3310 this.on( 'ready', this.bindEvents ); 3311 this.controller.on( 'open', this.setColumns ); 3312 3313 // Call this.setColumns() after this view has been rendered in the DOM so 3314 // attachments get proper width applied. 3315 _.defer( this.setColumns, this ); 3316 } 3317 }, 3318 3319 bindEvents: function() { 3320 this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); 3321 }, 3322 3323 attachmentFocus: function() { 3324 this.$( 'li:first' ).focus(); 3325 }, 3326 3327 restoreFocus: function() { 3328 this.$( 'li.selected:first' ).focus(); 3329 }, 3330 3331 arrowEvent: function( event ) { 3332 var attachments = this.$el.children( 'li' ), 3333 perRow = this.columns, 3334 index = attachments.filter( ':focus' ).index(), 3335 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); 3336 3337 if ( index === -1 ) { 3338 return; 3339 } 3340 3341 // Left arrow 3342 if ( 37 === event.keyCode ) { 3343 if ( 0 === index ) { 3344 return; 3345 } 3346 attachments.eq( index - 1 ).focus(); 3347 } 3348 3349 // Up arrow 3350 if ( 38 === event.keyCode ) { 3351 if ( 1 === row ) { 3352 return; 3353 } 3354 attachments.eq( index - perRow ).focus(); 3355 } 3356 3357 // Right arrow 3358 if ( 39 === event.keyCode ) { 3359 if ( attachments.length === index ) { 3360 return; 3361 } 3362 attachments.eq( index + 1 ).focus(); 3363 } 3364 3365 // Down arrow 3366 if ( 40 === event.keyCode ) { 3367 if ( Math.ceil( attachments.length / perRow ) === row ) { 3368 return; 3369 } 3370 attachments.eq( index + perRow ).focus(); 3371 } 3372 }, 3373 3374 dispose: function() { 3375 this.collection.props.off( null, null, this ); 3376 if ( this.options.resize ) { 3377 this.$window.off( this.resizeEvent ); 3378 } 3379 3380 /** 3381 * call 'dispose' directly on the parent class 3382 */ 3383 View.prototype.dispose.apply( this, arguments ); 3384 }, 3385 3386 setColumns: function() { 3387 var prev = this.columns, 3388 width = this.$el.width(); 3389 3390 if ( width ) { 3391 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; 3392 3393 if ( ! prev || prev !== this.columns ) { 3394 this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); 3395 } 3396 } 3397 }, 3398 3399 initSortable: function() { 3400 var collection = this.collection; 3401 3402 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 3403 return; 3404 } 3405 3406 this.$el.sortable( _.extend({ 3407 // If the `collection` has a `comparator`, disable sorting. 3408 disabled: !! collection.comparator, 3409 3410 // Change the position of the attachment as soon as the 3411 // mouse pointer overlaps a thumbnail. 3412 tolerance: 'pointer', 3413 3414 // Record the initial `index` of the dragged model. 3415 start: function( event, ui ) { 3416 ui.item.data('sortableIndexStart', ui.item.index()); 3417 }, 3418 3419 // Update the model's index in the collection. 3420 // Do so silently, as the view is already accurate. 3421 update: function( event, ui ) { 3422 var model = collection.at( ui.item.data('sortableIndexStart') ), 3423 comparator = collection.comparator; 3424 3425 // Temporarily disable the comparator to prevent `add` 3426 // from re-sorting. 3427 delete collection.comparator; 3428 3429 // Silently shift the model to its new index. 3430 collection.remove( model, { 3431 silent: true 3432 }); 3433 collection.add( model, { 3434 silent: true, 3435 at: ui.item.index() 3436 }); 3437 3438 // Restore the comparator. 3439 collection.comparator = comparator; 3440 3441 // Fire the `reset` event to ensure other collections sync. 3442 collection.trigger( 'reset', collection ); 3443 3444 // If the collection is sorted by menu order, 3445 // update the menu order. 3446 collection.saveMenuOrder(); 3447 } 3448 }, this.options.sortable ) ); 3449 3450 // If the `orderby` property is changed on the `collection`, 3451 // check to see if we have a `comparator`. If so, disable sorting. 3452 collection.props.on( 'change:orderby', function() { 3453 this.$el.sortable( 'option', 'disabled', !! collection.comparator ); 3454 }, this ); 3455 3456 this.collection.props.on( 'change:orderby', this.refreshSortable, this ); 3457 this.refreshSortable(); 3458 }, 3459 3460 refreshSortable: function() { 3461 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 3462 return; 3463 } 3464 3465 // If the `collection` has a `comparator`, disable sorting. 3466 var collection = this.collection, 3467 orderby = collection.props.get('orderby'), 3468 enabled = 'menuOrder' === orderby || ! collection.comparator; 3469 3470 this.$el.sortable( 'option', 'disabled', ! enabled ); 3471 }, 3472 3473 /** 3474 * @param {wp.media.model.Attachment} attachment 3475 * @returns {wp.media.View} 3476 */ 3477 createAttachmentView: function( attachment ) { 3478 var view = new this.options.AttachmentView({ 3479 controller: this.controller, 3480 model: attachment, 3481 collection: this.collection, 3482 selection: this.options.selection 3483 }); 3484 3485 return this._viewsByCid[ attachment.cid ] = view; 3486 }, 3487 3488 prepare: function() { 3489 // Create all of the Attachment views, and replace 3490 // the list in a single DOM operation. 3491 if ( this.collection.length ) { 3492 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 3493 3494 // If there are no elements, clear the views and load some. 3495 } else { 3496 this.views.unset(); 3497 this.collection.more().done( this.scroll ); 3498 } 3499 }, 3500 3501 ready: function() { 3502 // Trigger the scroll event to check if we're within the 3503 // threshold to query for additional attachments. 3504 this.scroll(); 3505 }, 3506 3507 scroll: function() { 3508 var view = this, 3509 el = this.options.scrollElement, 3510 scrollTop = el.scrollTop, 3511 toolbar; 3512 3513 // The scroll event occurs on the document, but the element 3514 // that should be checked is the document body. 3515 if ( el == document ) { 3516 el = document.body; 3517 scrollTop = $(document).scrollTop(); 3518 } 3519 3520 if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { 3521 return; 3522 } 3523 3524 toolbar = this.views.parent.toolbar; 3525 3526 // Show the spinner only if we are close to the bottom. 3527 if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { 3528 toolbar.get('spinner').show(); 3529 } 3530 3531 if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { 3532 this.collection.more().done(function() { 3533 view.scroll(); 3534 toolbar.get('spinner').hide(); 3535 }); 3536 } 3537 } 3538 }); 3539 3540 module.exports = Attachments; 3541 },{"./attachment.js":19,"./view.js":55}],24:[function(require,module,exports){ 3542 /** 3543 * wp.media.view.AttachmentsBrowser 3544 * 3545 * @class 3546 * @augments wp.media.View 3547 * @augments wp.Backbone.View 3548 * @augments Backbone.View 3549 * 3550 * @param {object} options 3551 * @param {object} [options.filters=false] Which filters to show in the browser's toolbar. 3552 * Accepts 'uploaded' and 'all'. 3553 * @param {object} [options.search=true] Whether to show the search interface in the 3554 * browser's toolbar. 3555 * @param {object} [options.display=false] Whether to show the attachments display settings 3556 * view in the sidebar. 3557 * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser. 3558 * Accepts true, false, and 'errors'. 3559 */ 3560 var View = require( '../view.js' ), 3561 Library = require( '../attachment/library.js' ), 3562 Toolbar = require( '../toolbar.js' ), 3563 Spinner = require( '../spinner.js' ), 3564 Search = require( '../search.js' ), 3565 Label = require( '../label.js' ), 3566 Uploaded = require( '../attachment-filters/uploaded.js' ), 3567 All = require( '../attachment-filters/all.js' ), 3568 DateFilter = require( '../attachment-filters/date.js' ), 3569 UploaderInline = require( '../uploader/inline.js' ), 3570 Attachments = require( '../attachments.js' ), 3571 Sidebar = require( '../sidebar.js' ), 3572 UploaderStatus = require( '../uploader/status.js' ), 3573 Details = require( '../attachment/details.js' ), 3574 AttachmentCompat = require( '../attachment-compat.js' ), 3575 AttachmentDisplay = require( '../settings/attachment-display.js' ), 3576 mediaTrash = wp.media.view.settings.mediaTrash, 3577 l10n = wp.media.view.l10n, 3578 $ = jQuery, 3579 AttachmentsBrowser; 3580 3581 AttachmentsBrowser = View.extend({ 3582 tagName: 'div', 3583 className: 'attachments-browser', 3584 3585 initialize: function() { 3586 _.defaults( this.options, { 3587 filters: false, 3588 search: true, 3589 display: false, 3590 sidebar: true, 3591 AttachmentView: Library 3592 }); 3593 3594 this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) ); 3595 this.controller.on( 'edit:selection', this.editSelection ); 3596 this.createToolbar(); 3597 if ( this.options.sidebar ) { 3598 this.createSidebar(); 3599 } 3600 this.createUploader(); 3601 this.createAttachments(); 3602 this.updateContent(); 3603 3604 if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) { 3605 this.$el.addClass( 'hide-sidebar' ); 3606 3607 if ( 'errors' === this.options.sidebar ) { 3608 this.$el.addClass( 'sidebar-for-errors' ); 3609 } 3610 } 3611 3612 this.collection.on( 'add remove reset', this.updateContent, this ); 3613 }, 3614 3615 editSelection: function( modal ) { 3616 modal.$( '.media-button-backToLibrary' ).focus(); 3617 }, 3618 3619 /** 3620 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining 3621 */ 3622 dispose: function() { 3623 this.options.selection.off( null, null, this ); 3624 View.prototype.dispose.apply( this, arguments ); 3625 return this; 3626 }, 3627 3628 createToolbar: function() { 3629 var LibraryViewSwitcher, Filters, toolbarOptions; 3630 3631 toolbarOptions = { 3632 controller: this.controller 3633 }; 3634 3635 if ( this.controller.isModeActive( 'grid' ) ) { 3636 toolbarOptions.className = 'media-toolbar wp-filter'; 3637 } 3638 3639 /** 3640 * @member {wp.media.view.Toolbar} 3641 */ 3642 this.toolbar = new Toolbar( toolbarOptions ); 3643 3644 this.views.add( this.toolbar ); 3645 3646 this.toolbar.set( 'spinner', new Spinner({ 3647 priority: -60 3648 }) ); 3649 3650 if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { 3651 // "Filters" will return a <select>, need to render 3652 // screen reader text before 3653 this.toolbar.set( 'filtersLabel', new Label({ 3654 value: l10n.filterByType, 3655 attributes: { 3656 'for': 'media-attachment-filters' 3657 }, 3658 priority: -80 3659 }).render() ); 3660 3661 if ( 'uploaded' === this.options.filters ) { 3662 this.toolbar.set( 'filters', new Uploaded({ 3663 controller: this.controller, 3664 model: this.collection.props, 3665 priority: -80 3666 }).render() ); 3667 } else { 3668 Filters = new All({ 3669 controller: this.controller, 3670 model: this.collection.props, 3671 priority: -80 3672 }); 3673 3674 this.toolbar.set( 'filters', Filters.render() ); 3675 } 3676 } 3677 3678 // Feels odd to bring the global media library switcher into the Attachment 3679 // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); 3680 // which the controller can tap into and add this view? 3681 if ( this.controller.isModeActive( 'grid' ) ) { 3682 LibraryViewSwitcher = View.extend({ 3683 className: 'view-switch media-grid-view-switch', 3684 template: wp.template( 'media-library-view-switcher') 3685 }); 3686 3687 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({ 3688 controller: this.controller, 3689 priority: -90 3690 }).render() ); 3691 3692 // DateFilter is a <select>, screen reader text needs to be rendered before 3693 this.toolbar.set( 'dateFilterLabel', new Label({ 3694 value: l10n.filterByDate, 3695 attributes: { 3696 'for': 'media-attachment-date-filters' 3697 }, 3698 priority: -75 3699 }).render() ); 3700 this.toolbar.set( 'dateFilter', new DateFilter({ 3701 controller: this.controller, 3702 model: this.collection.props, 3703 priority: -75 3704 }).render() ); 3705 3706 // BulkSelection is a <div> with subviews, including screen reader text 3707 this.toolbar.set( 'selectModeToggleButton', new wp.media.view.SelectModeToggleButton({ 3708 text: l10n.bulkSelect, 3709 controller: this.controller, 3710 priority: -70 3711 }).render() ); 3712 3713 this.toolbar.set( 'deleteSelectedButton', new wp.media.view.DeleteSelectedButton({ 3714 filters: Filters, 3715 style: 'primary', 3716 disabled: true, 3717 text: mediaTrash ? l10n.trashSelected : l10n.deleteSelected, 3718 controller: this.controller, 3719 priority: -60, 3720 click: function() { 3721 var changed = [], removed = [], self = this, 3722 selection = this.controller.state().get( 'selection' ), 3723 library = this.controller.state().get( 'library' ); 3724 3725 if ( ! selection.length ) { 3726 return; 3727 } 3728 3729 if ( ! mediaTrash && ! confirm( l10n.warnBulkDelete ) ) { 3730 return; 3731 } 3732 3733 if ( mediaTrash && 3734 'trash' !== selection.at( 0 ).get( 'status' ) && 3735 ! confirm( l10n.warnBulkTrash ) ) { 3736 3737 return; 3738 } 3739 3740 selection.each( function( model ) { 3741 if ( ! model.get( 'nonces' )['delete'] ) { 3742 removed.push( model ); 3743 return; 3744 } 3745 3746 if ( mediaTrash && 'trash' === model.get( 'status' ) ) { 3747 model.set( 'status', 'inherit' ); 3748 changed.push( model.save() ); 3749 removed.push( model ); 3750 } else if ( mediaTrash ) { 3751 model.set( 'status', 'trash' ); 3752 changed.push( model.save() ); 3753 removed.push( model ); 3754 } else { 3755 model.destroy({wait: true}); 3756 } 3757 } ); 3758 3759 if ( changed.length ) { 3760 selection.remove( removed ); 3761 3762 $.when.apply( null, changed ).then( function() { 3763 library._requery( true ); 3764 self.controller.trigger( 'selection:action:done' ); 3765 } ); 3766 } else { 3767 this.controller.trigger( 'selection:action:done' ); 3768 } 3769 } 3770 }).render() ); 3771 3772 if ( mediaTrash ) { 3773 this.toolbar.set( 'deleteSelectedPermanentlyButton', new wp.media.view.DeleteSelectedPermanentlyButton({ 3774 filters: Filters, 3775 style: 'primary', 3776 disabled: true, 3777 text: l10n.deleteSelected, 3778 controller: this.controller, 3779 priority: -55, 3780 click: function() { 3781 var removed = [], selection = this.controller.state().get( 'selection' ); 3782 3783 if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) { 3784 return; 3785 } 3786 3787 selection.each( function( model ) { 3788 if ( ! model.get( 'nonces' )['delete'] ) { 3789 removed.push( model ); 3790 return; 3791 } 3792 3793 model.destroy(); 3794 } ); 3795 3796 selection.remove( removed ); 3797 this.controller.trigger( 'selection:action:done' ); 3798 } 3799 }).render() ); 3800 } 3801 3802 } else { 3803 // DateFilter is a <select>, screen reader text needs to be rendered before 3804 this.toolbar.set( 'dateFilterLabel', new Label({ 3805 value: l10n.filterByDate, 3806 attributes: { 3807 'for': 'media-attachment-date-filters' 3808 }, 3809 priority: -75 3810 }).render() ); 3811 this.toolbar.set( 'dateFilter', new DateFilter({ 3812 controller: this.controller, 3813 model: this.collection.props, 3814 priority: -75 3815 }).render() ); 3816 } 3817 3818 if ( this.options.search ) { 3819 // Search is an input, screen reader text needs to be rendered before 3820 this.toolbar.set( 'searchLabel', new Label({ 3821 value: l10n.searchMediaLabel, 3822 attributes: { 3823 'for': 'media-search-input' 3824 }, 3825 priority: 60 3826 }).render() ); 3827 this.toolbar.set( 'search', new Search({ 3828 controller: this.controller, 3829 model: this.collection.props, 3830 priority: 60 3831 }).render() ); 3832 } 3833 3834 if ( this.options.dragInfo ) { 3835 this.toolbar.set( 'dragInfo', new View({ 3836 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0], 3837 priority: -40 3838 }) ); 3839 } 3840 3841 if ( this.options.suggestedWidth && this.options.suggestedHeight ) { 3842 this.toolbar.set( 'suggestedDimensions', new View({ 3843 el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '</div>' )[0], 3844 priority: -40 3845 }) ); 3846 } 3847 }, 3848 3849 updateContent: function() { 3850 var view = this, 3851 noItemsView; 3852 3853 if ( this.controller.isModeActive( 'grid' ) ) { 3854 noItemsView = view.attachmentsNoResults; 3855 } else { 3856 noItemsView = view.uploader; 3857 } 3858 3859 if ( ! this.collection.length ) { 3860 this.toolbar.get( 'spinner' ).show(); 3861 this.dfd = this.collection.more().done( function() { 3862 if ( ! view.collection.length ) { 3863 noItemsView.$el.removeClass( 'hidden' ); 3864 } else { 3865 noItemsView.$el.addClass( 'hidden' ); 3866 } 3867 view.toolbar.get( 'spinner' ).hide(); 3868 } ); 3869 } else { 3870 noItemsView.$el.addClass( 'hidden' ); 3871 view.toolbar.get( 'spinner' ).hide(); 3872 } 3873 }, 3874 3875 createUploader: function() { 3876 this.uploader = new UploaderInline({ 3877 controller: this.controller, 3878 status: false, 3879 message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound, 3880 canClose: this.controller.isModeActive( 'grid' ) 3881 }); 3882 3883 this.uploader.hide(); 3884 this.views.add( this.uploader ); 3885 }, 3886 3887 toggleUploader: function() { 3888 if ( this.uploader.$el.hasClass( 'hidden' ) ) { 3889 this.uploader.show(); 3890 } else { 3891 this.uploader.hide(); 3892 } 3893 }, 3894 3895 createAttachments: function() { 3896 this.attachments = new Attachments({ 3897 controller: this.controller, 3898 collection: this.collection, 3899 selection: this.options.selection, 3900 model: this.model, 3901 sortable: this.options.sortable, 3902 scrollElement: this.options.scrollElement, 3903 idealColumnWidth: this.options.idealColumnWidth, 3904 3905 // The single `Attachment` view to be used in the `Attachments` view. 3906 AttachmentView: this.options.AttachmentView 3907 }); 3908 3909 // Add keydown listener to the instance of the Attachments view 3910 this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent ); 3911 this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus ); 3912 3913 this.views.add( this.attachments ); 3914 3915 3916 if ( this.controller.isModeActive( 'grid' ) ) { 3917 this.attachmentsNoResults = new View({ 3918 controller: this.controller, 3919 tagName: 'p' 3920 }); 3921 3922 this.attachmentsNoResults.$el.addClass( 'hidden no-media' ); 3923 this.attachmentsNoResults.$el.html( l10n.noMedia ); 3924 3925 this.views.add( this.attachmentsNoResults ); 3926 } 3927 }, 3928 3929 createSidebar: function() { 3930 var options = this.options, 3931 selection = options.selection, 3932 sidebar = this.sidebar = new Sidebar({ 3933 controller: this.controller 3934 }); 3935 3936 this.views.add( sidebar ); 3937 3938 if ( this.controller.uploader ) { 3939 sidebar.set( 'uploads', new UploaderStatus({ 3940 controller: this.controller, 3941 priority: 40 3942 }) ); 3943 } 3944 3945 selection.on( 'selection:single', this.createSingle, this ); 3946 selection.on( 'selection:unsingle', this.disposeSingle, this ); 3947 3948 if ( selection.single() ) { 3949 this.createSingle(); 3950 } 3951 }, 3952 3953 createSingle: function() { 3954 var sidebar = this.sidebar, 3955 single = this.options.selection.single(); 3956 3957 sidebar.set( 'details', new Details({ 3958 controller: this.controller, 3959 model: single, 3960 priority: 80 3961 }) ); 3962 3963 sidebar.set( 'compat', new AttachmentCompat({ 3964 controller: this.controller, 3965 model: single, 3966 priority: 120 3967 }) ); 3968 3969 if ( this.options.display ) { 3970 sidebar.set( 'display', new AttachmentDisplay({ 3971 controller: this.controller, 3972 model: this.model.display( single ), 3973 attachment: single, 3974 priority: 160, 3975 userSettings: this.model.get('displayUserSettings') 3976 }) ); 3977 } 3978 3979 // Show the sidebar on mobile 3980 if ( this.model.id === 'insert' ) { 3981 sidebar.$el.addClass( 'visible' ); 3982 } 3983 }, 3984 3985 disposeSingle: function() { 3986 var sidebar = this.sidebar; 3987 sidebar.unset('details'); 3988 sidebar.unset('compat'); 3989 sidebar.unset('display'); 3990 // Hide the sidebar on mobile 3991 sidebar.$el.removeClass( 'visible' ); 3992 } 3993 }); 3994 3995 module.exports = AttachmentsBrowser; 3996 },{"../attachment-compat.js":14,"../attachment-filters/all.js":16,"../attachment-filters/date.js":17,"../attachment-filters/uploaded.js":18,"../attachment/details.js":21,"../attachment/library.js":22,"../attachments.js":23,"../label.js":36,"../search.js":45,"../settings/attachment-display.js":47,"../sidebar.js":48,"../spinner.js":49,"../toolbar.js":50,"../uploader/inline.js":51,"../uploader/status.js":53,"../view.js":55}],25:[function(require,module,exports){ 3997 /** 3998 * wp.media.view.Button 3999 * 4000 * @class 4001 * @augments wp.media.View 4002 * @augments wp.Backbone.View 4003 * @augments Backbone.View 4004 */ 4005 var View = require( './view.js' ), 4006 Button; 4007 4008 Button = View.extend({ 4009 tagName: 'a', 4010 className: 'media-button', 4011 attributes: { href: '#' }, 4012 4013 events: { 4014 'click': 'click' 4015 }, 4016 4017 defaults: { 4018 text: '', 4019 style: '', 4020 size: 'large', 4021 disabled: false 4022 }, 4023 4024 initialize: function() { 4025 /** 4026 * Create a model with the provided `defaults`. 4027 * 4028 * @member {Backbone.Model} 4029 */ 4030 this.model = new Backbone.Model( this.defaults ); 4031 4032 // If any of the `options` have a key from `defaults`, apply its 4033 // value to the `model` and remove it from the `options object. 4034 _.each( this.defaults, function( def, key ) { 4035 var value = this.options[ key ]; 4036 if ( _.isUndefined( value ) ) { 4037 return; 4038 } 4039 4040 this.model.set( key, value ); 4041 delete this.options[ key ]; 4042 }, this ); 4043 4044 this.model.on( 'change', this.render, this ); 4045 }, 4046 /** 4047 * @returns {wp.media.view.Button} Returns itself to allow chaining 4048 */ 4049 render: function() { 4050 var classes = [ 'button', this.className ], 4051 model = this.model.toJSON(); 4052 4053 if ( model.style ) { 4054 classes.push( 'button-' + model.style ); 4055 } 4056 4057 if ( model.size ) { 4058 classes.push( 'button-' + model.size ); 4059 } 4060 4061 classes = _.uniq( classes.concat( this.options.classes ) ); 4062 this.el.className = classes.join(' '); 4063 4064 this.$el.attr( 'disabled', model.disabled ); 4065 this.$el.text( this.model.get('text') ); 4066 4067 return this; 4068 }, 4069 /** 4070 * @param {Object} event 4071 */ 4072 click: function( event ) { 4073 if ( '#' === this.attributes.href ) { 4074 event.preventDefault(); 4075 } 4076 4077 if ( this.options.click && ! this.model.get('disabled') ) { 4078 this.options.click.apply( this, arguments ); 4079 } 4080 } 4081 }); 4082 4083 module.exports = Button; 4084 },{"./view.js":55}],26:[function(require,module,exports){ 4085 /** 4086 * When MEDIA_TRASH is true, a button that handles bulk Delete Permanently logic 4087 * 4088 * @constructor 4089 * @augments wp.media.view.DeleteSelectedButton 4090 * @augments wp.media.view.Button 4091 * @augments wp.media.View 4092 * @augments wp.Backbone.View 4093 * @augments Backbone.View 4094 */ 4095 var Button = require( '../button.js' ), 4096 DeleteSelected = require( './delete-selected.js' ), 4097 DeleteSelectedPermanently; 4098 4099 DeleteSelectedPermanently = DeleteSelected.extend({ 4100 initialize: function() { 4101 DeleteSelected.prototype.initialize.apply( this, arguments ); 4102 this.listenTo( this.controller, 'select:activate', this.selectActivate ); 4103 this.listenTo( this.controller, 'select:deactivate', this.selectDeactivate ); 4104 }, 4105 4106 filterChange: function( model ) { 4107 this.canShow = ( 'trash' === model.get( 'status' ) ); 4108 }, 4109 4110 selectActivate: function() { 4111 this.toggleDisabled(); 4112 this.$el.toggleClass( 'hidden', ! this.canShow ); 4113 }, 4114 4115 selectDeactivate: function() { 4116 this.toggleDisabled(); 4117 this.$el.addClass( 'hidden' ); 4118 }, 4119 4120 render: function() { 4121 Button.prototype.render.apply( this, arguments ); 4122 this.selectActivate(); 4123 return this; 4124 } 4125 }); 4126 4127 module.exports = DeleteSelectedPermanently; 4128 },{"../button.js":25,"./delete-selected.js":27}],27:[function(require,module,exports){ 4129 /** 4130 * A button that handles bulk Delete/Trash logic 4131 * 4132 * @constructor 4133 * @augments wp.media.view.Button 4134 * @augments wp.media.View 4135 * @augments wp.Backbone.View 4136 * @augments Backbone.View 4137 */ 4138 var Button = require( '../button.js' ), 4139 l10n = wp.media.view.l10n, 4140 DeleteSelected; 4141 4142 DeleteSelected = Button.extend({ 4143 initialize: function() { 4144 Button.prototype.initialize.apply( this, arguments ); 4145 if ( this.options.filters ) { 4146 this.listenTo( this.options.filters.model, 'change', this.filterChange ); 4147 } 4148 this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled ); 4149 }, 4150 4151 filterChange: function( model ) { 4152 if ( 'trash' === model.get( 'status' ) ) { 4153 this.model.set( 'text', l10n.untrashSelected ); 4154 } else if ( wp.media.view.settings.mediaTrash ) { 4155 this.model.set( 'text', l10n.trashSelected ); 4156 } else { 4157 this.model.set( 'text', l10n.deleteSelected ); 4158 } 4159 }, 4160 4161 toggleDisabled: function() { 4162 this.model.set( 'disabled', ! this.controller.state().get( 'selection' ).length ); 4163 }, 4164 4165 render: function() { 4166 Button.prototype.render.apply( this, arguments ); 4167 if ( this.controller.isModeActive( 'select' ) ) { 4168 this.$el.addClass( 'delete-selected-button' ); 4169 } else { 4170 this.$el.addClass( 'delete-selected-button hidden' ); 4171 } 4172 this.toggleDisabled(); 4173 return this; 4174 } 4175 }); 4176 4177 module.exports = DeleteSelected; 4178 },{"../button.js":25}],28:[function(require,module,exports){ 4179 var Button = require( '../button.js' ), 4180 l10n = wp.media.view.l10n, 4181 SelectModeToggle; 4182 4183 SelectModeToggle = Button.extend({ 4184 initialize: function() { 4185 Button.prototype.initialize.apply( this, arguments ); 4186 this.listenTo( this.controller, 'select:activate select:deactivate', this.toggleBulkEditHandler ); 4187 this.listenTo( this.controller, 'selection:action:done', this.back ); 4188 }, 4189 4190 back: function () { 4191 this.controller.deactivateMode( 'select' ).activateMode( 'edit' ); 4192 }, 4193 4194 click: function() { 4195 Button.prototype.click.apply( this, arguments ); 4196 if ( this.controller.isModeActive( 'select' ) ) { 4197 this.back(); 4198 } else { 4199 this.controller.deactivateMode( 'edit' ).activateMode( 'select' ); 4200 } 4201 }, 4202 4203 render: function() { 4204 Button.prototype.render.apply( this, arguments ); 4205 this.$el.addClass( 'select-mode-toggle-button' ); 4206 return this; 4207 }, 4208 4209 toggleBulkEditHandler: function() { 4210 var toolbar = this.controller.content.get().toolbar, children; 4211 4212 children = toolbar.$( '.media-toolbar-secondary > *, .media-toolbar-primary > *' ); 4213 4214 // TODO: the Frame should be doing all of this. 4215 if ( this.controller.isModeActive( 'select' ) ) { 4216 this.model.set( 'text', l10n.cancelSelection ); 4217 children.not( '.media-button' ).hide(); 4218 this.$el.show(); 4219 toolbar.$( '.delete-selected-button' ).removeClass( 'hidden' ); 4220 } else { 4221 this.model.set( 'text', l10n.bulkSelect ); 4222 this.controller.content.get().$el.removeClass( 'fixed' ); 4223 toolbar.$el.css( 'width', '' ); 4224 toolbar.$( '.delete-selected-button' ).addClass( 'hidden' ); 4225 children.not( '.spinner, .media-button' ).show(); 4226 this.controller.state().get( 'selection' ).reset(); 4227 } 4228 } 4229 }); 4230 4231 module.exports = SelectModeToggle; 4232 },{"../button.js":25}],29:[function(require,module,exports){ 4233 var View = require( './view.js' ), 4234 EditImage = require( './edit-image.js' ), 4235 Details; 4236 4237 Details = EditImage.extend({ 4238 initialize: function( options ) { 4239 this.editor = window.imageEdit; 4240 this.frame = options.frame; 4241 this.controller = options.controller; 4242 View.prototype.initialize.apply( this, arguments ); 4243 }, 4244 4245 back: function() { 4246 this.frame.content.mode( 'edit-metadata' ); 4247 }, 4248 4249 save: function() { 4250 var self = this; 4251 4252 this.model.fetch().done( function() { 4253 self.frame.content.mode( 'edit-metadata' ); 4254 }); 4255 } 4256 }); 4257 4258 module.exports = Details; 4259 },{"./edit-image.js":30,"./view.js":55}],30:[function(require,module,exports){ 4260 var View = require( './view.js' ), 4261 EditImage; 4262 4263 EditImage = View.extend({ 4264 className: 'image-editor', 4265 template: wp.template('image-editor'), 4266 4267 initialize: function( options ) { 4268 this.editor = window.imageEdit; 4269 this.controller = options.controller; 4270 View.prototype.initialize.apply( this, arguments ); 4271 }, 4272 4273 prepare: function() { 4274 return this.model.toJSON(); 4275 }, 4276 4277 render: function() { 4278 View.prototype.render.apply( this, arguments ); 4279 return this; 4280 }, 4281 4282 loadEditor: function() { 4283 var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this ); 4284 dfd.done( _.bind( this.focus, this ) ); 4285 }, 4286 4287 focus: function() { 4288 this.$( '.imgedit-submit .button' ).eq( 0 ).focus(); 4289 }, 4290 4291 back: function() { 4292 var lastState = this.controller.lastState(); 4293 this.controller.setState( lastState ); 4294 }, 4295 4296 refresh: function() { 4297 this.model.fetch(); 4298 }, 4299 4300 save: function() { 4301 var self = this, 4302 lastState = this.controller.lastState(); 4303 4304 this.model.fetch().done( function() { 4305 self.controller.setState( lastState ); 4306 }); 4307 } 4308 4309 }); 4310 4311 module.exports = EditImage; 4312 },{"./view.js":55}],31:[function(require,module,exports){ 4313 /** 4314 * wp.media.view.FocusManager 4315 * 4316 * @class 4317 * @augments wp.media.View 4318 * @augments wp.Backbone.View 4319 * @augments Backbone.View 4320 */ 4321 var View = require( './view.js' ), 4322 FocusManager; 4323 4324 FocusManager = View.extend({ 4325 4326 events: { 4327 'keydown': 'constrainTabbing' 4328 }, 4329 4330 focus: function() { // Reset focus on first left menu item 4331 this.$('.media-menu-item').first().focus(); 4332 }, 4333 /** 4334 * @param {Object} event 4335 */ 4336 constrainTabbing: function( event ) { 4337 var tabbables; 4338 4339 // Look for the tab key. 4340 if ( 9 !== event.keyCode ) { 4341 return; 4342 } 4343 4344 tabbables = this.$( ':tabbable' ); 4345 4346 // Keep tab focus within media modal while it's open 4347 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { 4348 tabbables.first().focus(); 4349 return false; 4350 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { 4351 tabbables.last().focus(); 4352 return false; 4353 } 4354 } 4355 4356 }); 4357 4358 module.exports = FocusManager; 4359 },{"./view.js":55}],32:[function(require,module,exports){ 4360 /** 4361 * wp.media.view.Frame 4362 * 4363 * A frame is a composite view consisting of one or more regions and one or more 4364 * states. 4365 * 4366 * @see wp.media.controller.State 4367 * @see wp.media.controller.Region 4368 * 4369 * @class 4370 * @augments wp.media.View 4371 * @augments wp.Backbone.View 4372 * @augments Backbone.View 4373 * @mixes wp.media.controller.StateMachine 4374 */ 4375 var StateMachine = require( '../controllers/state-machine.js' ), 4376 State = require( '../controllers/state.js' ), 4377 Region = require( '../controllers/region.js' ), 4378 View = require( './view.js' ), 4379 Frame; 4380 4381 Frame = View.extend({ 4382 initialize: function() { 4383 _.defaults( this.options, { 4384 mode: [ 'select' ] 4385 }); 4386 this._createRegions(); 4387 this._createStates(); 4388 this._createModes(); 4389 }, 4390 4391 _createRegions: function() { 4392 // Clone the regions array. 4393 this.regions = this.regions ? this.regions.slice() : []; 4394 4395 // Initialize regions. 4396 _.each( this.regions, function( region ) { 4397 this[ region ] = new Region({ 4398 view: this, 4399 id: region, 4400 selector: '.media-frame-' + region 4401 }); 4402 }, this ); 4403 }, 4404 /** 4405 * Create the frame's states. 4406 * 4407 * @see wp.media.controller.State 4408 * @see wp.media.controller.StateMachine 4409 * 4410 * @fires wp.media.controller.State#ready 4411 */ 4412 _createStates: function() { 4413 // Create the default `states` collection. 4414 this.states = new Backbone.Collection( null, { 4415 model: State 4416 }); 4417 4418 // Ensure states have a reference to the frame. 4419 this.states.on( 'add', function( model ) { 4420 model.frame = this; 4421 model.trigger('ready'); 4422 }, this ); 4423 4424 if ( this.options.states ) { 4425 this.states.add( this.options.states ); 4426 } 4427 }, 4428 4429 /** 4430 * A frame can be in a mode or multiple modes at one time. 4431 * 4432 * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode. 4433 */ 4434 _createModes: function() { 4435 // Store active "modes" that the frame is in. Unrelated to region modes. 4436 this.activeModes = new Backbone.Collection(); 4437 this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) ); 4438 4439 _.each( this.options.mode, function( mode ) { 4440 this.activateMode( mode ); 4441 }, this ); 4442 }, 4443 /** 4444 * Reset all states on the frame to their defaults. 4445 * 4446 * @returns {wp.media.view.Frame} Returns itself to allow chaining 4447 */ 4448 reset: function() { 4449 this.states.invoke( 'trigger', 'reset' ); 4450 return this; 4451 }, 4452 /** 4453 * Map activeMode collection events to the frame. 4454 */ 4455 triggerModeEvents: function( model, collection, options ) { 4456 var collectionEvent, 4457 modeEventMap = { 4458 add: 'activate', 4459 remove: 'deactivate' 4460 }, 4461 eventToTrigger; 4462 // Probably a better way to do this. 4463 _.each( options, function( value, key ) { 4464 if ( value ) { 4465 collectionEvent = key; 4466 } 4467 } ); 4468 4469 if ( ! _.has( modeEventMap, collectionEvent ) ) { 4470 return; 4471 } 4472 4473 eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent]; 4474 this.trigger( eventToTrigger ); 4475 }, 4476 /** 4477 * Activate a mode on the frame. 4478 * 4479 * @param string mode Mode ID. 4480 * @returns {this} Returns itself to allow chaining. 4481 */ 4482 activateMode: function( mode ) { 4483 // Bail if the mode is already active. 4484 if ( this.isModeActive( mode ) ) { 4485 return; 4486 } 4487 this.activeModes.add( [ { id: mode } ] ); 4488 // Add a CSS class to the frame so elements can be styled for the mode. 4489 this.$el.addClass( 'mode-' + mode ); 4490 4491 return this; 4492 }, 4493 /** 4494 * Deactivate a mode on the frame. 4495 * 4496 * @param string mode Mode ID. 4497 * @returns {this} Returns itself to allow chaining. 4498 */ 4499 deactivateMode: function( mode ) { 4500 // Bail if the mode isn't active. 4501 if ( ! this.isModeActive( mode ) ) { 4502 return this; 4503 } 4504 this.activeModes.remove( this.activeModes.where( { id: mode } ) ); 4505 this.$el.removeClass( 'mode-' + mode ); 4506 /** 4507 * Frame mode deactivation event. 4508 * 4509 * @event this#{mode}:deactivate 4510 */ 4511 this.trigger( mode + ':deactivate' ); 4512 4513 return this; 4514 }, 4515 /** 4516 * Check if a mode is enabled on the frame. 4517 * 4518 * @param string mode Mode ID. 4519 * @return bool 4520 */ 4521 isModeActive: function( mode ) { 4522 return Boolean( this.activeModes.where( { id: mode } ).length ); 4523 } 4524 }); 4525 4526 // Make the `Frame` a `StateMachine`. 4527 _.extend( Frame.prototype, StateMachine.prototype ); 4528 4529 module.exports = Frame; 4530 },{"../controllers/region.js":4,"../controllers/state-machine.js":5,"../controllers/state.js":6,"./view.js":55}],33:[function(require,module,exports){ 4531 /** 4532 * A frame for editing the details of a specific media item. 4533 * 4534 * Opens in a modal by default. 4535 * 4536 * Requires an attachment model to be passed in the options hash under `model`. 4537 * 4538 * @constructor 4539 * @augments wp.media.view.Frame 4540 * @augments wp.media.View 4541 * @augments wp.Backbone.View 4542 * @augments Backbone.View 4543 * @mixes wp.media.controller.StateMachine 4544 */ 4545 var Frame = require( '../frame.js' ), 4546 MediaFrame = require( '../media-frame.js' ), 4547 Modal = require( '../modal.js' ), 4548 EditAttachmentMetadata = require( '../../controllers/edit-attachment-metadata.js' ), 4549 TwoColumn = require( '../attachment/details-two-column.js' ), 4550 AttachmentCompat = require( '../attachment-compat.js' ), 4551 EditImageController = require( '../../controllers/edit-image.js' ), 4552 DetailsView = require( '../edit-image-details.js' ), 4553 $ = jQuery, 4554 EditAttachments; 4555 4556 EditAttachments = MediaFrame.extend({ 4557 4558 className: 'edit-attachment-frame', 4559 template: wp.template( 'edit-attachment-frame' ), 4560 regions: [ 'title', 'content' ], 4561 4562 events: { 4563 'click .left': 'previousMediaItem', 4564 'click .right': 'nextMediaItem' 4565 }, 4566 4567 initialize: function() { 4568 Frame.prototype.initialize.apply( this, arguments ); 4569 4570 _.defaults( this.options, { 4571 modal: true, 4572 state: 'edit-attachment' 4573 }); 4574 4575 this.controller = this.options.controller; 4576 this.gridRouter = this.controller.gridRouter; 4577 this.library = this.options.library; 4578 4579 if ( this.options.model ) { 4580 this.model = this.options.model; 4581 } 4582 4583 this.bindHandlers(); 4584 this.createStates(); 4585 this.createModal(); 4586 4587 this.title.mode( 'default' ); 4588 this.toggleNav(); 4589 }, 4590 4591 bindHandlers: function() { 4592 // Bind default title creation. 4593 this.on( 'title:create:default', this.createTitle, this ); 4594 4595 // Close the modal if the attachment is deleted. 4596 this.listenTo( this.model, 'change:status destroy', this.close, this ); 4597 4598 this.on( 'content:create:edit-metadata', this.editMetadataMode, this ); 4599 this.on( 'content:create:edit-image', this.editImageMode, this ); 4600 this.on( 'content:render:edit-image', this.editImageModeRender, this ); 4601 this.on( 'close', this.detach ); 4602 }, 4603 4604 createModal: function() { 4605 var self = this; 4606 4607 // Initialize modal container view. 4608 if ( this.options.modal ) { 4609 this.modal = new Modal({ 4610 controller: this, 4611 title: this.options.title 4612 }); 4613 4614 this.modal.on( 'open', function () { 4615 $( 'body' ).on( 'keydown.media-modal', _.bind( self.keyEvent, self ) ); 4616 } ); 4617 4618 // Completely destroy the modal DOM element when closing it. 4619 this.modal.on( 'close', function() { 4620 self.modal.remove(); 4621 $( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */ 4622 // Restore the original focus item if possible 4623 $( 'li.attachment[data-id="' + self.model.get( 'id' ) +'"]' ).focus(); 4624 self.resetRoute(); 4625 } ); 4626 4627 // Set this frame as the modal's content. 4628 this.modal.content( this ); 4629 this.modal.open(); 4630 } 4631 }, 4632 4633 /** 4634 * Add the default states to the frame. 4635 */ 4636 createStates: function() { 4637 this.states.add([ 4638 new EditAttachmentMetadata( { model: this.model } ) 4639 ]); 4640 }, 4641 4642 /** 4643 * Content region rendering callback for the `edit-metadata` mode. 4644 * 4645 * @param {Object} contentRegion Basic object with a `view` property, which 4646 * should be set with the proper region view. 4647 */ 4648 editMetadataMode: function( contentRegion ) { 4649 contentRegion.view = new TwoColumn({ 4650 controller: this, 4651 model: this.model 4652 }); 4653 4654 /** 4655 * Attach a subview to display fields added via the 4656 * `attachment_fields_to_edit` filter. 4657 */ 4658 contentRegion.view.views.set( '.attachment-compat', new AttachmentCompat({ 4659 controller: this, 4660 model: this.model 4661 }) ); 4662 4663 // Update browser url when navigating media details 4664 if ( this.model ) { 4665 this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id ) ); 4666 } 4667 }, 4668 4669 /** 4670 * Render the EditImage view into the frame's content region. 4671 * 4672 * @param {Object} contentRegion Basic object with a `view` property, which 4673 * should be set with the proper region view. 4674 */ 4675 editImageMode: function( contentRegion ) { 4676 var editImageController = new EditImageController( { 4677 model: this.model, 4678 frame: this 4679 } ); 4680 // Noop some methods. 4681 editImageController._toolbar = function() {}; 4682 editImageController._router = function() {}; 4683 editImageController._menu = function() {}; 4684 4685 contentRegion.view = new DetailsView( { 4686 model: this.model, 4687 frame: this, 4688 controller: editImageController 4689 } ); 4690 }, 4691 4692 editImageModeRender: function( view ) { 4693 view.on( 'ready', view.loadEditor ); 4694 }, 4695 4696 toggleNav: function() { 4697 this.$('.left').toggleClass( 'disabled', ! this.hasPrevious() ); 4698 this.$('.right').toggleClass( 'disabled', ! this.hasNext() ); 4699 }, 4700 4701 /** 4702 * Rerender the view. 4703 */ 4704 rerender: function() { 4705 // Only rerender the `content` region. 4706 if ( this.content.mode() !== 'edit-metadata' ) { 4707 this.content.mode( 'edit-metadata' ); 4708 } else { 4709 this.content.render(); 4710 } 4711 4712 this.toggleNav(); 4713 }, 4714 4715 /** 4716 * Click handler to switch to the previous media item. 4717 */ 4718 previousMediaItem: function() { 4719 if ( ! this.hasPrevious() ) { 4720 this.$( '.left' ).blur(); 4721 return; 4722 } 4723 this.model = this.library.at( this.getCurrentIndex() - 1 ); 4724 this.rerender(); 4725 this.$( '.left' ).focus(); 4726 }, 4727 4728 /** 4729 * Click handler to switch to the next media item. 4730 */ 4731 nextMediaItem: function() { 4732 if ( ! this.hasNext() ) { 4733 this.$( '.right' ).blur(); 4734 return; 4735 } 4736 this.model = this.library.at( this.getCurrentIndex() + 1 ); 4737 this.rerender(); 4738 this.$( '.right' ).focus(); 4739 }, 4740 4741 getCurrentIndex: function() { 4742 return this.library.indexOf( this.model ); 4743 }, 4744 4745 hasNext: function() { 4746 return ( this.getCurrentIndex() + 1 ) < this.library.length; 4747 }, 4748 4749 hasPrevious: function() { 4750 return ( this.getCurrentIndex() - 1 ) > -1; 4751 }, 4752 /** 4753 * Respond to the keyboard events: right arrow, left arrow, except when 4754 * focus is in a textarea or input field. 4755 */ 4756 keyEvent: function( event ) { 4757 if ( ( 'INPUT' === event.target.nodeName || 'TEXTAREA' === event.target.nodeName ) && ! ( event.target.readOnly || event.target.disabled ) ) { 4758 return; 4759 } 4760 4761 // The right arrow key 4762 if ( 39 === event.keyCode ) { 4763 this.nextMediaItem(); 4764 } 4765 // The left arrow key 4766 if ( 37 === event.keyCode ) { 4767 this.previousMediaItem(); 4768 } 4769 }, 4770 4771 resetRoute: function() { 4772 this.gridRouter.navigate( this.gridRouter.baseUrl( '' ) ); 4773 } 4774 }); 4775 4776 module.exports = EditAttachments; 4777 },{"../../controllers/edit-attachment-metadata.js":1,"../../controllers/edit-image.js":2,"../attachment-compat.js":14,"../attachment/details-two-column.js":20,"../edit-image-details.js":29,"../frame.js":32,"../media-frame.js":38,"../modal.js":41}],34:[function(require,module,exports){ 4778 /** 4779 * wp.media.view.MediaFrame.Manage 4780 * 4781 * A generic management frame workflow. 4782 * 4783 * Used in the media grid view. 4784 * 4785 * @constructor 4786 * @augments wp.media.view.MediaFrame 4787 * @augments wp.media.view.Frame 4788 * @augments wp.media.View 4789 * @augments wp.Backbone.View 4790 * @augments Backbone.View 4791 * @mixes wp.media.controller.StateMachine 4792 */ 4793 var MediaFrame = require( '../media-frame.js' ), 4794 UploaderWindow = require( '../uploader/window.js' ), 4795 AttachmentsBrowser = require( '../attachments/browser.js' ), 4796 Router = require( '../../router/manage.js' ), 4797 Library = require( '../../controllers/library.js' ), 4798 $ = jQuery, 4799 Manage; 4800 4801 Manage = MediaFrame.extend({ 4802 /** 4803 * @global wp.Uploader 4804 */ 4805 initialize: function() { 4806 var self = this; 4807 _.defaults( this.options, { 4808 title: '', 4809 modal: false, 4810 selection: [], 4811 library: {}, // Options hash for the query to the media library. 4812 multiple: 'add', 4813 state: 'library', 4814 uploader: true, 4815 mode: [ 'grid', 'edit' ] 4816 }); 4817 4818 this.$body = $( document.body ); 4819 this.$window = $( window ); 4820 this.$adminBar = $( '#wpadminbar' ); 4821 this.$window.on( 'scroll resize', _.debounce( _.bind( this.fixPosition, this ), 15 ) ); 4822 $( document ).on( 'click', '.add-new-h2', _.bind( this.addNewClickHandler, this ) ); 4823 4824 // Ensure core and media grid view UI is enabled. 4825 this.$el.addClass('wp-core-ui'); 4826 4827 // Force the uploader off if the upload limit has been exceeded or 4828 // if the browser isn't supported. 4829 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 4830 this.options.uploader = false; 4831 } 4832 4833 // Initialize a window-wide uploader. 4834 if ( this.options.uploader ) { 4835 this.uploader = new UploaderWindow({ 4836 controller: this, 4837 uploader: { 4838 dropzone: document.body, 4839 container: document.body 4840 } 4841 }).render(); 4842 this.uploader.ready(); 4843 $('body').append( this.uploader.el ); 4844 4845 this.options.uploader = false; 4846 } 4847 4848 this.gridRouter = new Router(); 4849 4850 // Call 'initialize' directly on the parent class. 4851 MediaFrame.prototype.initialize.apply( this, arguments ); 4852 4853 // Append the frame view directly the supplied container. 4854 this.$el.appendTo( this.options.container ); 4855 4856 this.createStates(); 4857 this.bindRegionModeHandlers(); 4858 this.render(); 4859 4860 // Update the URL when entering search string (at most once per second) 4861 $( '#media-search-input' ).on( 'input', _.debounce( function(e) { 4862 var val = $( e.currentTarget ).val(), url = ''; 4863 if ( val ) { 4864 url += '?search=' + val; 4865 } 4866 self.gridRouter.navigate( self.gridRouter.baseUrl( url ) ); 4867 }, 1000 ) ); 4868 }, 4869 4870 /** 4871 * Create the default states for the frame. 4872 */ 4873 createStates: function() { 4874 var options = this.options; 4875 4876 if ( this.options.states ) { 4877 return; 4878 } 4879 4880 // Add the default states. 4881 this.states.add([ 4882 new Library({ 4883 library: wp.media.query( options.library ), 4884 multiple: options.multiple, 4885 title: options.title, 4886 content: 'browse', 4887 toolbar: 'select', 4888 contentUserSetting: false, 4889 filterable: 'all', 4890 autoSelect: false 4891 }) 4892 ]); 4893 }, 4894 4895 /** 4896 * Bind region mode activation events to proper handlers. 4897 */ 4898 bindRegionModeHandlers: function() { 4899 this.on( 'content:create:browse', this.browseContent, this ); 4900 4901 // Handle a frame-level event for editing an attachment. 4902 this.on( 'edit:attachment', this.openEditAttachmentModal, this ); 4903 4904 this.on( 'select:activate', this.bindKeydown, this ); 4905 this.on( 'select:deactivate', this.unbindKeydown, this ); 4906 }, 4907 4908 handleKeydown: function( e ) { 4909 if ( 27 === e.which ) { 4910 e.preventDefault(); 4911 this.deactivateMode( 'select' ).activateMode( 'edit' ); 4912 } 4913 }, 4914 4915 bindKeydown: function() { 4916 this.$body.on( 'keydown.select', _.bind( this.handleKeydown, this ) ); 4917 }, 4918 4919 unbindKeydown: function() { 4920 this.$body.off( 'keydown.select' ); 4921 }, 4922 4923 fixPosition: function() { 4924 var $browser, $toolbar; 4925 if ( ! this.isModeActive( 'select' ) ) { 4926 return; 4927 } 4928 4929 $browser = this.$('.attachments-browser'); 4930 $toolbar = $browser.find('.media-toolbar'); 4931 4932 // Offset doesn't appear to take top margin into account, hence +16 4933 if ( ( $browser.offset().top + 16 ) < this.$window.scrollTop() + this.$adminBar.height() ) { 4934 $browser.addClass( 'fixed' ); 4935 $toolbar.css('width', $browser.width() + 'px'); 4936 } else { 4937 $browser.removeClass( 'fixed' ); 4938 $toolbar.css('width', ''); 4939 } 4940 }, 4941 4942 /** 4943 * Click handler for the `Add New` button. 4944 */ 4945 addNewClickHandler: function( event ) { 4946 event.preventDefault(); 4947 this.trigger( 'toggle:upload:attachment' ); 4948 }, 4949 4950 /** 4951 * Open the Edit Attachment modal. 4952 */ 4953 openEditAttachmentModal: function( model ) { 4954 // Create a new EditAttachment frame, passing along the library and the attachment model. 4955 wp.media( { 4956 frame: 'edit-attachments', 4957 controller: this, 4958 library: this.state().get('library'), 4959 model: model 4960 } ); 4961 }, 4962 4963 /** 4964 * Create an attachments browser view within the content region. 4965 * 4966 * @param {Object} contentRegion Basic object with a `view` property, which 4967 * should be set with the proper region view. 4968 * @this wp.media.controller.Region 4969 */ 4970 browseContent: function( contentRegion ) { 4971 var state = this.state(); 4972 4973 // Browse our library of attachments. 4974 this.browserView = contentRegion.view = new AttachmentsBrowser({ 4975 controller: this, 4976 collection: state.get('library'), 4977 selection: state.get('selection'), 4978 model: state, 4979 sortable: state.get('sortable'), 4980 search: state.get('searchable'), 4981 filters: state.get('filterable'), 4982 display: state.get('displaySettings'), 4983 dragInfo: state.get('dragInfo'), 4984 sidebar: 'errors', 4985 4986 suggestedWidth: state.get('suggestedWidth'), 4987 suggestedHeight: state.get('suggestedHeight'), 4988 4989 AttachmentView: state.get('AttachmentView'), 4990 4991 scrollElement: document 4992 }); 4993 this.browserView.on( 'ready', _.bind( this.bindDeferred, this ) ); 4994 4995 this.errors = wp.Uploader.errors; 4996 this.errors.on( 'add remove reset', this.sidebarVisibility, this ); 4997 }, 4998 4999 sidebarVisibility: function() { 5000 this.browserView.$( '.media-sidebar' ).toggle( !! this.errors.length ); 5001 }, 5002 5003 bindDeferred: function() { 5004 if ( ! this.browserView.dfd ) { 5005 return; 5006 } 5007 this.browserView.dfd.done( _.bind( this.startHistory, this ) ); 5008 }, 5009 5010 startHistory: function() { 5011 // Verify pushState support and activate 5012 if ( window.history && window.history.pushState ) { 5013 Backbone.history.start( { 5014 root: _wpMediaGridSettings.adminUrl, 5015 pushState: true 5016 } ); 5017 } 5018 } 5019 }); 5020 5021 module.exports = Manage; 5022 },{"../../controllers/library.js":3,"../../router/manage.js":12,"../attachments/browser.js":24,"../media-frame.js":38,"../uploader/window.js":54}],35:[function(require,module,exports){ 5023 /** 5024 * wp.media.view.Iframe 5025 * 5026 * @class 5027 * @augments wp.media.View 5028 * @augments wp.Backbone.View 5029 * @augments Backbone.View 5030 */ 5031 var View = require( './view.js' ), 5032 Iframe; 5033 5034 Iframe = View.extend({ 5035 className: 'media-iframe', 5036 /** 5037 * @returns {wp.media.view.Iframe} Returns itself to allow chaining 5038 */ 5039 render: function() { 5040 this.views.detach(); 5041 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' ); 5042 this.views.render(); 5043 return this; 5044 } 5045 }); 5046 5047 module.exports = Iframe; 5048 },{"./view.js":55}],36:[function(require,module,exports){ 5049 /** 5050 * @class 5051 * @augments wp.media.View 5052 * @augments wp.Backbone.View 5053 * @augments Backbone.View 5054 */ 5055 var View = require( './view.js' ), 5056 Label; 5057 5058 Label = View.extend({ 5059 tagName: 'label', 5060 className: 'screen-reader-text', 5061 5062 initialize: function() { 5063 this.value = this.options.value; 5064 }, 5065 5066 render: function() { 5067 this.$el.html( this.value ); 5068 5069 return this; 5070 } 5071 }); 5072 5073 module.exports = Label; 5074 },{"./view.js":55}],37:[function(require,module,exports){ 5075 /** 5076 * wp.media.view.MediaDetails 5077 * 5078 * @constructor 5079 * @augments wp.media.view.Settings.AttachmentDisplay 5080 * @augments wp.media.view.Settings 5081 * @augments wp.media.View 5082 * @augments wp.Backbone.View 5083 * @augments Backbone.View 5084 */ 5085 var AttachmentDisplay = require( './settings/attachment-display.js' ), 5086 $ = jQuery, 5087 MediaDetails; 5088 5089 MediaDetails = AttachmentDisplay.extend({ 5090 initialize: function() { 5091 _.bindAll(this, 'success'); 5092 this.players = []; 5093 this.listenTo( this.controller, 'close', wp.media.mixin.unsetPlayers ); 5094 this.on( 'ready', this.setPlayer ); 5095 this.on( 'media:setting:remove', wp.media.mixin.unsetPlayers, this ); 5096 this.on( 'media:setting:remove', this.render ); 5097 this.on( 'media:setting:remove', this.setPlayer ); 5098 this.events = _.extend( this.events, { 5099 'click .remove-setting' : 'removeSetting', 5100 'change .content-track' : 'setTracks', 5101 'click .remove-track' : 'setTracks', 5102 'click .add-media-source' : 'addSource' 5103 } ); 5104 5105 AttachmentDisplay.prototype.initialize.apply( this, arguments ); 5106 }, 5107 5108 prepare: function() { 5109 return _.defaults({ 5110 model: this.model.toJSON() 5111 }, this.options ); 5112 }, 5113 5114 /** 5115 * Remove a setting's UI when the model unsets it 5116 * 5117 * @fires wp.media.view.MediaDetails#media:setting:remove 5118 * 5119 * @param {Event} e 5120 */ 5121 removeSetting : function(e) { 5122 var wrap = $( e.currentTarget ).parent(), setting; 5123 setting = wrap.find( 'input' ).data( 'setting' ); 5124 5125 if ( setting ) { 5126 this.model.unset( setting ); 5127 this.trigger( 'media:setting:remove', this ); 5128 } 5129 5130 wrap.remove(); 5131 }, 5132 5133 /** 5134 * 5135 * @fires wp.media.view.MediaDetails#media:setting:remove 5136 */ 5137 setTracks : function() { 5138 var tracks = ''; 5139 5140 _.each( this.$('.content-track'), function(track) { 5141 tracks += $( track ).val(); 5142 } ); 5143 5144 this.model.set( 'content', tracks ); 5145 this.trigger( 'media:setting:remove', this ); 5146 }, 5147 5148 addSource : function( e ) { 5149 this.controller.lastMime = $( e.currentTarget ).data( 'mime' ); 5150 this.controller.setState( 'add-' + this.controller.defaults.id + '-source' ); 5151 }, 5152 5153 /** 5154 * @global MediaElementPlayer 5155 */ 5156 setPlayer : function() { 5157 if ( ! this.players.length && this.media ) { 5158 this.players.push( new MediaElementPlayer( this.media, this.settings ) ); 5159 } 5160 }, 5161 5162 /** 5163 * @abstract 5164 */ 5165 setMedia : function() { 5166 return this; 5167 }, 5168 5169 success : function(mejs) { 5170 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; 5171 5172 if ( 'flash' === mejs.pluginType && autoplay ) { 5173 mejs.addEventListener( 'canplay', function() { 5174 mejs.play(); 5175 }, false ); 5176 } 5177 5178 this.mejs = mejs; 5179 }, 5180 5181 /** 5182 * @returns {media.view.MediaDetails} Returns itself to allow chaining 5183 */ 5184 render: function() { 5185 var self = this; 5186 5187 AttachmentDisplay.prototype.render.apply( this, arguments ); 5188 setTimeout( function() { self.resetFocus(); }, 10 ); 5189 5190 this.settings = _.defaults( { 5191 success : this.success 5192 }, wp.media.mixin.mejsSettings ); 5193 5194 return this.setMedia(); 5195 }, 5196 5197 resetFocus: function() { 5198 this.$( '.embed-media-settings' ).scrollTop( 0 ); 5199 } 5200 }, { 5201 instances : 0, 5202 /** 5203 * When multiple players in the DOM contain the same src, things get weird. 5204 * 5205 * @param {HTMLElement} elem 5206 * @returns {HTMLElement} 5207 */ 5208 prepareSrc : function( elem ) { 5209 var i = MediaDetails.instances++; 5210 _.each( $( elem ).find( 'source' ), function( source ) { 5211 source.src = [ 5212 source.src, 5213 source.src.indexOf('?') > -1 ? '&' : '?', 5214 '_=', 5215 i 5216 ].join(''); 5217 } ); 5218 5219 return elem; 5220 } 5221 }); 5222 5223 module.exports = MediaDetails; 5224 },{"./settings/attachment-display.js":47}],38:[function(require,module,exports){ 5225 /** 5226 * wp.media.view.MediaFrame 5227 * 5228 * The frame used to create the media modal. 5229 * 5230 * @class 5231 * @augments wp.media.view.Frame 5232 * @augments wp.media.View 5233 * @augments wp.Backbone.View 5234 * @augments Backbone.View 5235 * @mixes wp.media.controller.StateMachine 5236 */ 5237 var View = require( './view.js' ), 5238 Frame = require( './frame.js' ), 5239 Modal = require( './modal.js' ), 5240 UploaderWindow = require( './uploader/window.js' ), 5241 Menu = require( './menu.js' ), 5242 Toolbar = require( './toolbar.js' ), 5243 Router = require( './router.js' ), 5244 Iframe = require( './iframe.js' ), 5245 $ = jQuery, 5246 MediaFrame; 5247 5248 MediaFrame = Frame.extend({ 5249 className: 'media-frame', 5250 template: wp.template('media-frame'), 5251 regions: ['menu','title','content','toolbar','router'], 5252 5253 events: { 5254 'click div.media-frame-title h1': 'toggleMenu' 5255 }, 5256 5257 /** 5258 * @global wp.Uploader 5259 */ 5260 initialize: function() { 5261 Frame.prototype.initialize.apply( this, arguments ); 5262 5263 _.defaults( this.options, { 5264 title: '', 5265 modal: true, 5266 uploader: true 5267 }); 5268 5269 // Ensure core UI is enabled. 5270 this.$el.addClass('wp-core-ui'); 5271 5272 // Initialize modal container view. 5273 if ( this.options.modal ) { 5274 this.modal = new Modal({ 5275 controller: this, 5276 title: this.options.title 5277 }); 5278 5279 this.modal.content( this ); 5280 } 5281 5282 // Force the uploader off if the upload limit has been exceeded or 5283 // if the browser isn't supported. 5284 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 5285 this.options.uploader = false; 5286 } 5287 5288 // Initialize window-wide uploader. 5289 if ( this.options.uploader ) { 5290 this.uploader = new UploaderWindow({ 5291 controller: this, 5292 uploader: { 5293 dropzone: this.modal ? this.modal.$el : this.$el, 5294 container: this.$el 5295 } 5296 }); 5297 this.views.set( '.media-frame-uploader', this.uploader ); 5298 } 5299 5300 this.on( 'attach', _.bind( this.views.ready, this.views ), this ); 5301 5302 // Bind default title creation. 5303 this.on( 'title:create:default', this.createTitle, this ); 5304 this.title.mode('default'); 5305 5306 this.on( 'title:render', function( view ) { 5307 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' ); 5308 }); 5309 5310 // Bind default menu. 5311 this.on( 'menu:create:default', this.createMenu, this ); 5312 }, 5313 /** 5314 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 5315 */ 5316 render: function() { 5317 // Activate the default state if no active state exists. 5318 if ( ! this.state() && this.options.state ) { 5319 this.setState( this.options.state ); 5320 } 5321 /** 5322 * call 'render' directly on the parent class 5323 */ 5324 return Frame.prototype.render.apply( this, arguments ); 5325 }, 5326 /** 5327 * @param {Object} title 5328 * @this wp.media.controller.Region 5329 */ 5330 createTitle: function( title ) { 5331 title.view = new View({ 5332 controller: this, 5333 tagName: 'h1' 5334 }); 5335 }, 5336 /** 5337 * @param {Object} menu 5338 * @this wp.media.controller.Region 5339 */ 5340 createMenu: function( menu ) { 5341 menu.view = new Menu({ 5342 controller: this 5343 }); 5344 }, 5345 5346 toggleMenu: function() { 5347 this.$el.find( '.media-menu' ).toggleClass( 'visible' ); 5348 }, 5349 5350 /** 5351 * @param {Object} toolbar 5352 * @this wp.media.controller.Region 5353 */ 5354 createToolbar: function( toolbar ) { 5355 toolbar.view = new Toolbar({ 5356 controller: this 5357 }); 5358 }, 5359 /** 5360 * @param {Object} router 5361 * @this wp.media.controller.Region 5362 */ 5363 createRouter: function( router ) { 5364 router.view = new Router({ 5365 controller: this 5366 }); 5367 }, 5368 /** 5369 * @param {Object} options 5370 */ 5371 createIframeStates: function( options ) { 5372 var settings = wp.media.view.settings, 5373 tabs = settings.tabs, 5374 tabUrl = settings.tabUrl, 5375 $postId; 5376 5377 if ( ! tabs || ! tabUrl ) { 5378 return; 5379 } 5380 5381 // Add the post ID to the tab URL if it exists. 5382 $postId = $('#post_ID'); 5383 if ( $postId.length ) { 5384 tabUrl += '&post_id=' + $postId.val(); 5385 } 5386 5387 // Generate the tab states. 5388 _.each( tabs, function( title, id ) { 5389 this.state( 'iframe:' + id ).set( _.defaults({ 5390 tab: id, 5391 src: tabUrl + '&tab=' + id, 5392 title: title, 5393 content: 'iframe', 5394 menu: 'default' 5395 }, options ) ); 5396 }, this ); 5397 5398 this.on( 'content:create:iframe', this.iframeContent, this ); 5399 this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this ); 5400 this.on( 'menu:render:default', this.iframeMenu, this ); 5401 this.on( 'open', this.hijackThickbox, this ); 5402 this.on( 'close', this.restoreThickbox, this ); 5403 }, 5404 5405 /** 5406 * @param {Object} content 5407 * @this wp.media.controller.Region 5408 */ 5409 iframeContent: function( content ) { 5410 this.$el.addClass('hide-toolbar'); 5411 content.view = new Iframe({ 5412 controller: this 5413 }); 5414 }, 5415 5416 iframeContentCleanup: function() { 5417 this.$el.removeClass('hide-toolbar'); 5418 }, 5419 5420 iframeMenu: function( view ) { 5421 var views = {}; 5422 5423 if ( ! view ) { 5424 return; 5425 } 5426 5427 _.each( wp.media.view.settings.tabs, function( title, id ) { 5428 views[ 'iframe:' + id ] = { 5429 text: this.state( 'iframe:' + id ).get('title'), 5430 priority: 200 5431 }; 5432 }, this ); 5433 5434 view.set( views ); 5435 }, 5436 5437 hijackThickbox: function() { 5438 var frame = this; 5439 5440 if ( ! window.tb_remove || this._tb_remove ) { 5441 return; 5442 } 5443 5444 this._tb_remove = window.tb_remove; 5445 window.tb_remove = function() { 5446 frame.close(); 5447 frame.reset(); 5448 frame.setState( frame.options.state ); 5449 frame._tb_remove.call( window ); 5450 }; 5451 }, 5452 5453 restoreThickbox: function() { 5454 if ( ! this._tb_remove ) { 5455 return; 5456 } 5457 5458 window.tb_remove = this._tb_remove; 5459 delete this._tb_remove; 5460 } 5461 }); 5462 5463 // Map some of the modal's methods to the frame. 5464 _.each(['open','close','attach','detach','escape'], function( method ) { 5465 /** 5466 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 5467 */ 5468 MediaFrame.prototype[ method ] = function() { 5469 if ( this.modal ) { 5470 this.modal[ method ].apply( this.modal, arguments ); 5471 } 5472 return this; 5473 }; 5474 }); 5475 5476 module.exports = MediaFrame; 5477 },{"./frame.js":32,"./iframe.js":35,"./menu.js":40,"./modal.js":41,"./router.js":44,"./toolbar.js":50,"./uploader/window.js":54,"./view.js":55}],39:[function(require,module,exports){ 5478 /** 5479 * wp.media.view.MenuItem 5480 * 5481 * @class 5482 * @augments wp.media.View 5483 * @augments wp.Backbone.View 5484 * @augments Backbone.View 5485 */ 5486 var View = require( './view.js' ), 5487 $ = jQuery, 5488 MenuItem; 5489 5490 MenuItem = View.extend({ 5491 tagName: 'a', 5492 className: 'media-menu-item', 5493 5494 attributes: { 5495 href: '#' 5496 }, 5497 5498 events: { 5499 'click': '_click' 5500 }, 5501 /** 5502 * @param {Object} event 5503 */ 5504 _click: function( event ) { 5505 var clickOverride = this.options.click; 5506 5507 if ( event ) { 5508 event.preventDefault(); 5509 } 5510 5511 if ( clickOverride ) { 5512 clickOverride.call( this ); 5513 } else { 5514 this.click(); 5515 } 5516 5517 // When selecting a tab along the left side, 5518 // focus should be transferred into the main panel 5519 if ( ! wp.media.isTouchDevice ) { 5520 $('.media-frame-content input').first().focus(); 5521 } 5522 }, 5523 5524 click: function() { 5525 var state = this.options.state; 5526 5527 if ( state ) { 5528 this.controller.setState( state ); 5529 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below 5530 } 5531 }, 5532 /** 5533 * @returns {wp.media.view.MenuItem} returns itself to allow chaining 5534 */ 5535 render: function() { 5536 var options = this.options; 5537 5538 if ( options.text ) { 5539 this.$el.text( options.text ); 5540 } else if ( options.html ) { 5541 this.$el.html( options.html ); 5542 } 5543 5544 return this; 5545 } 5546 }); 5547 5548 module.exports = MenuItem; 5549 },{"./view.js":55}],40:[function(require,module,exports){ 5550 /** 5551 * wp.media.view.Menu 5552 * 5553 * @class 5554 * @augments wp.media.view.PriorityList 5555 * @augments wp.media.View 5556 * @augments wp.Backbone.View 5557 * @augments Backbone.View 5558 */ 5559 var MenuItem = require( './menu-item.js' ), 5560 PriorityList = require( './priority-list.js' ), 5561 Menu; 5562 5563 Menu = PriorityList.extend({ 5564 tagName: 'div', 5565 className: 'media-menu', 5566 property: 'state', 5567 ItemView: MenuItem, 5568 region: 'menu', 5569 5570 /* TODO: alternatively hide on any click anywhere 5571 events: { 5572 'click': 'click' 5573 }, 5574 5575 click: function() { 5576 this.$el.removeClass( 'visible' ); 5577 }, 5578 */ 5579 5580 /** 5581 * @param {Object} options 5582 * @param {string} id 5583 * @returns {wp.media.View} 5584 */ 5585 toView: function( options, id ) { 5586 options = options || {}; 5587 options[ this.property ] = options[ this.property ] || id; 5588 return new this.ItemView( options ).render(); 5589 }, 5590 5591 ready: function() { 5592 /** 5593 * call 'ready' directly on the parent class 5594 */ 5595 PriorityList.prototype.ready.apply( this, arguments ); 5596 this.visibility(); 5597 }, 5598 5599 set: function() { 5600 /** 5601 * call 'set' directly on the parent class 5602 */ 5603 PriorityList.prototype.set.apply( this, arguments ); 5604 this.visibility(); 5605 }, 5606 5607 unset: function() { 5608 /** 5609 * call 'unset' directly on the parent class 5610 */ 5611 PriorityList.prototype.unset.apply( this, arguments ); 5612 this.visibility(); 5613 }, 5614 5615 visibility: function() { 5616 var region = this.region, 5617 view = this.controller[ region ].get(), 5618 views = this.views.get(), 5619 hide = ! views || views.length < 2; 5620 5621 if ( this === view ) { 5622 this.controller.$el.toggleClass( 'hide-' + region, hide ); 5623 } 5624 }, 5625 /** 5626 * @param {string} id 5627 */ 5628 select: function( id ) { 5629 var view = this.get( id ); 5630 5631 if ( ! view ) { 5632 return; 5633 } 5634 5635 this.deselect(); 5636 view.$el.addClass('active'); 5637 }, 5638 5639 deselect: function() { 5640 this.$el.children().removeClass('active'); 5641 }, 5642 5643 hide: function( id ) { 5644 var view = this.get( id ); 5645 5646 if ( ! view ) { 5647 return; 5648 } 5649 5650 view.$el.addClass('hidden'); 5651 }, 5652 5653 show: function( id ) { 5654 var view = this.get( id ); 5655 5656 if ( ! view ) { 5657 return; 5658 } 5659 5660 view.$el.removeClass('hidden'); 5661 } 5662 }); 5663 5664 module.exports = Menu; 5665 },{"./menu-item.js":39,"./priority-list.js":42}],41:[function(require,module,exports){ 5666 /** 5667 * wp.media.view.Modal 5668 * 5669 * A modal view, which the media modal uses as its default container. 5670 * 5671 * @class 5672 * @augments wp.media.View 5673 * @augments wp.Backbone.View 5674 * @augments Backbone.View 5675 */ 5676 var View = require( './view.js' ), 5677 FocusManager = require( './focus-manager.js' ), 5678 $ = jQuery, 5679 Modal; 5680 5681 Modal = View.extend({ 5682 tagName: 'div', 5683 template: wp.template('media-modal'), 5684 5685 attributes: { 5686 tabindex: 0 5687 }, 5688 5689 events: { 5690 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 5691 'keydown': 'keydown' 5692 }, 5693 5694 initialize: function() { 5695 _.defaults( this.options, { 5696 container: document.body, 5697 title: '', 5698 propagate: true, 5699 freeze: true 5700 }); 5701 5702 this.focusManager = new FocusManager({ 5703 el: this.el 5704 }); 5705 }, 5706 /** 5707 * @returns {Object} 5708 */ 5709 prepare: function() { 5710 return { 5711 title: this.options.title 5712 }; 5713 }, 5714 5715 /** 5716 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5717 */ 5718 attach: function() { 5719 if ( this.views.attached ) { 5720 return this; 5721 } 5722 5723 if ( ! this.views.rendered ) { 5724 this.render(); 5725 } 5726 5727 this.$el.appendTo( this.options.container ); 5728 5729 // Manually mark the view as attached and trigger ready. 5730 this.views.attached = true; 5731 this.views.ready(); 5732 5733 return this.propagate('attach'); 5734 }, 5735 5736 /** 5737 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5738 */ 5739 detach: function() { 5740 if ( this.$el.is(':visible') ) { 5741 this.close(); 5742 } 5743 5744 this.$el.detach(); 5745 this.views.attached = false; 5746 return this.propagate('detach'); 5747 }, 5748 5749 /** 5750 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5751 */ 5752 open: function() { 5753 var $el = this.$el, 5754 options = this.options, 5755 mceEditor; 5756 5757 if ( $el.is(':visible') ) { 5758 return this; 5759 } 5760 5761 if ( ! this.views.attached ) { 5762 this.attach(); 5763 } 5764 5765 // If the `freeze` option is set, record the window's scroll position. 5766 if ( options.freeze ) { 5767 this._freeze = { 5768 scrollTop: $( window ).scrollTop() 5769 }; 5770 } 5771 5772 // Disable page scrolling. 5773 $( 'body' ).addClass( 'modal-open' ); 5774 5775 $el.show(); 5776 5777 // Try to close the onscreen keyboard 5778 if ( 'ontouchend' in document ) { 5779 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) { 5780 mceEditor.iframeElement.focus(); 5781 mceEditor.iframeElement.blur(); 5782 5783 setTimeout( function() { 5784 mceEditor.iframeElement.blur(); 5785 }, 100 ); 5786 } 5787 } 5788 5789 this.$el.focus(); 5790 5791 return this.propagate('open'); 5792 }, 5793 5794 /** 5795 * @param {Object} options 5796 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5797 */ 5798 close: function( options ) { 5799 var freeze = this._freeze; 5800 5801 if ( ! this.views.attached || ! this.$el.is(':visible') ) { 5802 return this; 5803 } 5804 5805 // Enable page scrolling. 5806 $( 'body' ).removeClass( 'modal-open' ); 5807 5808 // Hide modal and remove restricted media modal tab focus once it's closed 5809 this.$el.hide().undelegate( 'keydown' ); 5810 5811 // Put focus back in useful location once modal is closed 5812 $('#wpbody-content').focus(); 5813 5814 this.propagate('close'); 5815 5816 // If the `freeze` option is set, restore the container's scroll position. 5817 if ( freeze ) { 5818 $( window ).scrollTop( freeze.scrollTop ); 5819 } 5820 5821 if ( options && options.escape ) { 5822 this.propagate('escape'); 5823 } 5824 5825 return this; 5826 }, 5827 /** 5828 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5829 */ 5830 escape: function() { 5831 return this.close({ escape: true }); 5832 }, 5833 /** 5834 * @param {Object} event 5835 */ 5836 escapeHandler: function( event ) { 5837 event.preventDefault(); 5838 this.escape(); 5839 }, 5840 5841 /** 5842 * @param {Array|Object} content Views to register to '.media-modal-content' 5843 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5844 */ 5845 content: function( content ) { 5846 this.views.set( '.media-modal-content', content ); 5847 return this; 5848 }, 5849 5850 /** 5851 * Triggers a modal event and if the `propagate` option is set, 5852 * forwards events to the modal's controller. 5853 * 5854 * @param {string} id 5855 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5856 */ 5857 propagate: function( id ) { 5858 this.trigger( id ); 5859 5860 if ( this.options.propagate ) { 5861 this.controller.trigger( id ); 5862 } 5863 5864 return this; 5865 }, 5866 /** 5867 * @param {Object} event 5868 */ 5869 keydown: function( event ) { 5870 // Close the modal when escape is pressed. 5871 if ( 27 === event.which && this.$el.is(':visible') ) { 5872 this.escape(); 5873 event.stopImmediatePropagation(); 5874 } 5875 } 5876 }); 5877 5878 module.exports = Modal; 5879 },{"./focus-manager.js":31,"./view.js":55}],42:[function(require,module,exports){ 5880 /** 5881 * wp.media.view.PriorityList 5882 * 5883 * @class 5884 * @augments wp.media.View 5885 * @augments wp.Backbone.View 5886 * @augments Backbone.View 5887 */ 5888 var View = require( './view.js' ), 5889 PriorityList; 5890 5891 PriorityList = View.extend({ 5892 tagName: 'div', 5893 5894 initialize: function() { 5895 this._views = {}; 5896 5897 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 5898 delete this.options.views; 5899 5900 if ( ! this.options.silent ) { 5901 this.render(); 5902 } 5903 }, 5904 /** 5905 * @param {string} id 5906 * @param {wp.media.View|Object} view 5907 * @param {Object} options 5908 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining 5909 */ 5910 set: function( id, view, options ) { 5911 var priority, views, index; 5912 5913 options = options || {}; 5914 5915 // Accept an object with an `id` : `view` mapping. 5916 if ( _.isObject( id ) ) { 5917 _.each( id, function( view, id ) { 5918 this.set( id, view ); 5919 }, this ); 5920 return this; 5921 } 5922 5923 if ( ! (view instanceof Backbone.View) ) { 5924 view = this.toView( view, id, options ); 5925 } 5926 view.controller = view.controller || this.controller; 5927 5928 this.unset( id ); 5929 5930 priority = view.options.priority || 10; 5931 views = this.views.get() || []; 5932 5933 _.find( views, function( existing, i ) { 5934 if ( existing.options.priority > priority ) { 5935 index = i; 5936 return true; 5937 } 5938 }); 5939 5940 this._views[ id ] = view; 5941 this.views.add( view, { 5942 at: _.isNumber( index ) ? index : views.length || 0 5943 }); 5944 5945 return this; 5946 }, 5947 /** 5948 * @param {string} id 5949 * @returns {wp.media.View} 5950 */ 5951 get: function( id ) { 5952 return this._views[ id ]; 5953 }, 5954 /** 5955 * @param {string} id 5956 * @returns {wp.media.view.PriorityList} 5957 */ 5958 unset: function( id ) { 5959 var view = this.get( id ); 5960 5961 if ( view ) { 5962 view.remove(); 5963 } 5964 5965 delete this._views[ id ]; 5966 return this; 5967 }, 5968 /** 5969 * @param {Object} options 5970 * @returns {wp.media.View} 5971 */ 5972 toView: function( options ) { 5973 return new View( options ); 5974 } 5975 }); 5976 5977 module.exports = PriorityList; 5978 },{"./view.js":55}],43:[function(require,module,exports){ 5979 /** 5980 * wp.media.view.RouterItem 5981 * 5982 * @class 5983 * @augments wp.media.view.MenuItem 5984 * @augments wp.media.View 5985 * @augments wp.Backbone.View 5986 * @augments Backbone.View 5987 */ 5988 var MenuItem = require( './menu-item.js' ), 5989 RouterItem; 5990 5991 RouterItem = MenuItem.extend({ 5992 /** 5993 * On click handler to activate the content region's corresponding mode. 5994 */ 5995 click: function() { 5996 var contentMode = this.options.contentMode; 5997 if ( contentMode ) { 5998 this.controller.content.mode( contentMode ); 5999 } 6000 } 6001 }); 6002 6003 module.exports = RouterItem; 6004 },{"./menu-item.js":39}],44:[function(require,module,exports){ 6005 /** 6006 * wp.media.view.Router 6007 * 6008 * @class 6009 * @augments wp.media.view.Menu 6010 * @augments wp.media.view.PriorityList 6011 * @augments wp.media.View 6012 * @augments wp.Backbone.View 6013 * @augments Backbone.View 6014 */ 6015 var Menu = require( './menu.js' ), 6016 RouterItem = require( './router-item.js' ), 6017 Router; 6018 6019 Router = Menu.extend({ 6020 tagName: 'div', 6021 className: 'media-router', 6022 property: 'contentMode', 6023 ItemView: RouterItem, 6024 region: 'router', 6025 6026 initialize: function() { 6027 this.controller.on( 'content:render', this.update, this ); 6028 // Call 'initialize' directly on the parent class. 6029 Menu.prototype.initialize.apply( this, arguments ); 6030 }, 6031 6032 update: function() { 6033 var mode = this.controller.content.mode(); 6034 if ( mode ) { 6035 this.select( mode ); 6036 } 6037 } 6038 }); 6039 6040 module.exports = Router; 6041 },{"./menu.js":40,"./router-item.js":43}],45:[function(require,module,exports){ 6042 /** 6043 * wp.media.view.Search 6044 * 6045 * @class 6046 * @augments wp.media.View 6047 * @augments wp.Backbone.View 6048 * @augments Backbone.View 6049 */ 6050 var View = require( './view.js' ), 6051 l10n = wp.media.view.l10n, 6052 Search; 6053 6054 Search = View.extend({ 6055 tagName: 'input', 6056 className: 'search', 6057 id: 'media-search-input', 6058 6059 attributes: { 6060 type: 'search', 6061 placeholder: l10n.search 6062 }, 6063 6064 events: { 6065 'input': 'search', 6066 'keyup': 'search', 6067 'change': 'search', 6068 'search': 'search' 6069 }, 6070 6071 /** 6072 * @returns {wp.media.view.Search} Returns itself to allow chaining 6073 */ 6074 render: function() { 6075 this.el.value = this.model.escape('search'); 6076 return this; 6077 }, 6078 6079 search: function( event ) { 6080 if ( event.target.value ) { 6081 this.model.set( 'search', event.target.value ); 6082 } else { 6083 this.model.unset('search'); 6084 } 6085 } 6086 }); 6087 6088 module.exports = Search; 6089 },{"./view.js":55}],46:[function(require,module,exports){ 6090 /** 6091 * wp.media.view.Settings 6092 * 6093 * @class 6094 * @augments wp.media.View 6095 * @augments wp.Backbone.View 6096 * @augments Backbone.View 6097 */ 6098 var View = require( './view.js' ), 6099 $ = jQuery, 6100 Settings; 6101 6102 Settings = View.extend({ 6103 events: { 6104 'click button': 'updateHandler', 6105 'change input': 'updateHandler', 6106 'change select': 'updateHandler', 6107 'change textarea': 'updateHandler' 6108 }, 6109 6110 initialize: function() { 6111 this.model = this.model || new Backbone.Model(); 6112 this.model.on( 'change', this.updateChanges, this ); 6113 }, 6114 6115 prepare: function() { 6116 return _.defaults({ 6117 model: this.model.toJSON() 6118 }, this.options ); 6119 }, 6120 /** 6121 * @returns {wp.media.view.Settings} Returns itself to allow chaining 6122 */ 6123 render: function() { 6124 View.prototype.render.apply( this, arguments ); 6125 // Select the correct values. 6126 _( this.model.attributes ).chain().keys().each( this.update, this ); 6127 return this; 6128 }, 6129 /** 6130 * @param {string} key 6131 */ 6132 update: function( key ) { 6133 var value = this.model.get( key ), 6134 $setting = this.$('[data-setting="' + key + '"]'), 6135 $buttons, $value; 6136 6137 // Bail if we didn't find a matching setting. 6138 if ( ! $setting.length ) { 6139 return; 6140 } 6141 6142 // Attempt to determine how the setting is rendered and update 6143 // the selected value. 6144 6145 // Handle dropdowns. 6146 if ( $setting.is('select') ) { 6147 $value = $setting.find('[value="' + value + '"]'); 6148 6149 if ( $value.length ) { 6150 $setting.find('option').prop( 'selected', false ); 6151 $value.prop( 'selected', true ); 6152 } else { 6153 // If we can't find the desired value, record what *is* selected. 6154 this.model.set( key, $setting.find(':selected').val() ); 6155 } 6156 6157 // Handle button groups. 6158 } else if ( $setting.hasClass('button-group') ) { 6159 $buttons = $setting.find('button').removeClass('active'); 6160 $buttons.filter( '[value="' + value + '"]' ).addClass('active'); 6161 6162 // Handle text inputs and textareas. 6163 } else if ( $setting.is('input[type="text"], textarea') ) { 6164 if ( ! $setting.is(':focus') ) { 6165 $setting.val( value ); 6166 } 6167 // Handle checkboxes. 6168 } else if ( $setting.is('input[type="checkbox"]') ) { 6169 $setting.prop( 'checked', !! value && 'false' !== value ); 6170 } 6171 }, 6172 /** 6173 * @param {Object} event 6174 */ 6175 updateHandler: function( event ) { 6176 var $setting = $( event.target ).closest('[data-setting]'), 6177 value = event.target.value, 6178 userSetting; 6179 6180 event.preventDefault(); 6181 6182 if ( ! $setting.length ) { 6183 return; 6184 } 6185 6186 // Use the correct value for checkboxes. 6187 if ( $setting.is('input[type="checkbox"]') ) { 6188 value = $setting[0].checked; 6189 } 6190 6191 // Update the corresponding setting. 6192 this.model.set( $setting.data('setting'), value ); 6193 6194 // If the setting has a corresponding user setting, 6195 // update that as well. 6196 if ( userSetting = $setting.data('userSetting') ) { 6197 setUserSetting( userSetting, value ); 6198 } 6199 }, 6200 6201 updateChanges: function( model ) { 6202 if ( model.hasChanged() ) { 6203 _( model.changed ).chain().keys().each( this.update, this ); 6204 } 6205 } 6206 }); 6207 6208 module.exports = Settings; 6209 },{"./view.js":55}],47:[function(require,module,exports){ 6210 /** 6211 * wp.media.view.Settings.AttachmentDisplay 6212 * 6213 * @class 6214 * @augments wp.media.view.Settings 6215 * @augments wp.media.View 6216 * @augments wp.Backbone.View 6217 * @augments Backbone.View 6218 */ 6219 var Settings = require( '../settings.js' ), 6220 AttachmentDisplay; 6221 6222 AttachmentDisplay = Settings.extend({ 6223 className: 'attachment-display-settings', 6224 template: wp.template('attachment-display-settings'), 6225 6226 initialize: function() { 6227 var attachment = this.options.attachment; 6228 6229 _.defaults( this.options, { 6230 userSettings: false 6231 }); 6232 // Call 'initialize' directly on the parent class. 6233 Settings.prototype.initialize.apply( this, arguments ); 6234 this.model.on( 'change:link', this.updateLinkTo, this ); 6235 6236 if ( attachment ) { 6237 attachment.on( 'change:uploading', this.render, this ); 6238 } 6239 }, 6240 6241 dispose: function() { 6242 var attachment = this.options.attachment; 6243 if ( attachment ) { 6244 attachment.off( null, null, this ); 6245 } 6246 /** 6247 * call 'dispose' directly on the parent class 6248 */ 6249 Settings.prototype.dispose.apply( this, arguments ); 6250 }, 6251 /** 6252 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining 6253 */ 6254 render: function() { 6255 var attachment = this.options.attachment; 6256 if ( attachment ) { 6257 _.extend( this.options, { 6258 sizes: attachment.get('sizes'), 6259 type: attachment.get('type') 6260 }); 6261 } 6262 /** 6263 * call 'render' directly on the parent class 6264 */ 6265 Settings.prototype.render.call( this ); 6266 this.updateLinkTo(); 6267 return this; 6268 }, 6269 6270 updateLinkTo: function() { 6271 var linkTo = this.model.get('link'), 6272 $input = this.$('.link-to-custom'), 6273 attachment = this.options.attachment; 6274 6275 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { 6276 $input.addClass( 'hidden' ); 6277 return; 6278 } 6279 6280 if ( attachment ) { 6281 if ( 'post' === linkTo ) { 6282 $input.val( attachment.get('link') ); 6283 } else if ( 'file' === linkTo ) { 6284 $input.val( attachment.get('url') ); 6285 } else if ( ! this.model.get('linkUrl') ) { 6286 $input.val('http://'); 6287 } 6288 6289 $input.prop( 'readonly', 'custom' !== linkTo ); 6290 } 6291 6292 $input.removeClass( 'hidden' ); 6293 6294 // If the input is visible, focus and select its contents. 6295 if ( ! wp.media.isTouchDevice && $input.is(':visible') ) { 6296 $input.focus()[0].select(); 6297 } 6298 } 6299 }); 6300 6301 module.exports = AttachmentDisplay; 6302 },{"../settings.js":46}],48:[function(require,module,exports){ 6303 /** 6304 * wp.media.view.Sidebar 6305 * 6306 * @class 6307 * @augments wp.media.view.PriorityList 6308 * @augments wp.media.View 6309 * @augments wp.Backbone.View 6310 * @augments Backbone.View 6311 */ 6312 var PriorityList = require( './priority-list.js' ), 6313 Sidebar; 6314 6315 Sidebar = PriorityList.extend({ 6316 className: 'media-sidebar' 6317 }); 6318 6319 module.exports = Sidebar; 6320 },{"./priority-list.js":42}],49:[function(require,module,exports){ 6321 /** 6322 * wp.media.view.Spinner 6323 * 6324 * @class 6325 * @augments wp.media.View 6326 * @augments wp.Backbone.View 6327 * @augments Backbone.View 6328 */ 6329 var View = require( './view.js' ), 6330 Spinner; 6331 6332 Spinner = View.extend({ 6333 tagName: 'span', 6334 className: 'spinner', 6335 spinnerTimeout: false, 6336 delay: 400, 6337 6338 show: function() { 6339 if ( ! this.spinnerTimeout ) { 6340 this.spinnerTimeout = _.delay(function( $el ) { 6341 $el.show(); 6342 }, this.delay, this.$el ); 6343 } 6344 6345 return this; 6346 }, 6347 6348 hide: function() { 6349 this.$el.hide(); 6350 this.spinnerTimeout = clearTimeout( this.spinnerTimeout ); 6351 6352 return this; 6353 } 6354 }); 6355 6356 module.exports = Spinner; 6357 },{"./view.js":55}],50:[function(require,module,exports){ 6358 /** 6359 * wp.media.view.Toolbar 6360 * 6361 * A toolbar which consists of a primary and a secondary section. Each sections 6362 * can be filled with views. 6363 * 6364 * @class 6365 * @augments wp.media.View 6366 * @augments wp.Backbone.View 6367 * @augments Backbone.View 6368 */ 6369 var View = require( './view.js' ), 6370 Button = require( './button.js' ), 6371 PriorityList = require( './priority-list.js' ), 6372 Toolbar; 6373 6374 Toolbar = View.extend({ 6375 tagName: 'div', 6376 className: 'media-toolbar', 6377 6378 initialize: function() { 6379 var state = this.controller.state(), 6380 selection = this.selection = state.get('selection'), 6381 library = this.library = state.get('library'); 6382 6383 this._views = {}; 6384 6385 // The toolbar is composed of two `PriorityList` views. 6386 this.primary = new PriorityList(); 6387 this.secondary = new PriorityList(); 6388 this.primary.$el.addClass('media-toolbar-primary search-form'); 6389 this.secondary.$el.addClass('media-toolbar-secondary'); 6390 6391 this.views.set([ this.secondary, this.primary ]); 6392 6393 if ( this.options.items ) { 6394 this.set( this.options.items, { silent: true }); 6395 } 6396 6397 if ( ! this.options.silent ) { 6398 this.render(); 6399 } 6400 6401 if ( selection ) { 6402 selection.on( 'add remove reset', this.refresh, this ); 6403 } 6404 6405 if ( library ) { 6406 library.on( 'add remove reset', this.refresh, this ); 6407 } 6408 }, 6409 /** 6410 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining 6411 */ 6412 dispose: function() { 6413 if ( this.selection ) { 6414 this.selection.off( null, null, this ); 6415 } 6416 6417 if ( this.library ) { 6418 this.library.off( null, null, this ); 6419 } 6420 /** 6421 * call 'dispose' directly on the parent class 6422 */ 6423 return View.prototype.dispose.apply( this, arguments ); 6424 }, 6425 6426 ready: function() { 6427 this.refresh(); 6428 }, 6429 6430 /** 6431 * @param {string} id 6432 * @param {Backbone.View|Object} view 6433 * @param {Object} [options={}] 6434 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 6435 */ 6436 set: function( id, view, options ) { 6437 var list; 6438 options = options || {}; 6439 6440 // Accept an object with an `id` : `view` mapping. 6441 if ( _.isObject( id ) ) { 6442 _.each( id, function( view, id ) { 6443 this.set( id, view, { silent: true }); 6444 }, this ); 6445 6446 } else { 6447 if ( ! ( view instanceof Backbone.View ) ) { 6448 view.classes = [ 'media-button-' + id ].concat( view.classes || [] ); 6449 view = new Button( view ).render(); 6450 } 6451 6452 view.controller = view.controller || this.controller; 6453 6454 this._views[ id ] = view; 6455 6456 list = view.options.priority < 0 ? 'secondary' : 'primary'; 6457 this[ list ].set( id, view, options ); 6458 } 6459 6460 if ( ! options.silent ) { 6461 this.refresh(); 6462 } 6463 6464 return this; 6465 }, 6466 /** 6467 * @param {string} id 6468 * @returns {wp.media.view.Button} 6469 */ 6470 get: function( id ) { 6471 return this._views[ id ]; 6472 }, 6473 /** 6474 * @param {string} id 6475 * @param {Object} options 6476 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 6477 */ 6478 unset: function( id, options ) { 6479 delete this._views[ id ]; 6480 this.primary.unset( id, options ); 6481 this.secondary.unset( id, options ); 6482 6483 if ( ! options || ! options.silent ) { 6484 this.refresh(); 6485 } 6486 return this; 6487 }, 6488 6489 refresh: function() { 6490 var state = this.controller.state(), 6491 library = state.get('library'), 6492 selection = state.get('selection'); 6493 6494 _.each( this._views, function( button ) { 6495 if ( ! button.model || ! button.options || ! button.options.requires ) { 6496 return; 6497 } 6498 6499 var requires = button.options.requires, 6500 disabled = false; 6501 6502 // Prevent insertion of attachments if any of them are still uploading 6503 disabled = _.some( selection.models, function( attachment ) { 6504 return attachment.get('uploading') === true; 6505 }); 6506 6507 if ( requires.selection && selection && ! selection.length ) { 6508 disabled = true; 6509 } else if ( requires.library && library && ! library.length ) { 6510 disabled = true; 6511 } 6512 button.model.set( 'disabled', disabled ); 6513 }); 6514 } 6515 }); 6516 6517 module.exports = Toolbar; 6518 },{"./button.js":25,"./priority-list.js":42,"./view.js":55}],51:[function(require,module,exports){ 6519 /** 6520 * wp.media.view.UploaderInline 6521 * 6522 * The inline uploader that shows up in the 'Upload Files' tab. 6523 * 6524 * @class 6525 * @augments wp.media.View 6526 * @augments wp.Backbone.View 6527 * @augments Backbone.View 6528 */ 6529 var View = require( '../view.js' ), 6530 UploaderStatus = require( './status.js' ), 6531 UploaderInline; 6532 6533 UploaderInline = View.extend({ 6534 tagName: 'div', 6535 className: 'uploader-inline', 6536 template: wp.template('uploader-inline'), 6537 6538 events: { 6539 'click .close': 'hide' 6540 }, 6541 6542 initialize: function() { 6543 _.defaults( this.options, { 6544 message: '', 6545 status: true, 6546 canClose: false 6547 }); 6548 6549 if ( ! this.options.$browser && this.controller.uploader ) { 6550 this.options.$browser = this.controller.uploader.$browser; 6551 } 6552 6553 if ( _.isUndefined( this.options.postId ) ) { 6554 this.options.postId = wp.media.view.settings.post.id; 6555 } 6556 6557 if ( this.options.status ) { 6558 this.views.set( '.upload-inline-status', new UploaderStatus({ 6559 controller: this.controller 6560 }) ); 6561 } 6562 }, 6563 6564 prepare: function() { 6565 var suggestedWidth = this.controller.state().get('suggestedWidth'), 6566 suggestedHeight = this.controller.state().get('suggestedHeight'), 6567 data = {}; 6568 6569 data.message = this.options.message; 6570 data.canClose = this.options.canClose; 6571 6572 if ( suggestedWidth && suggestedHeight ) { 6573 data.suggestedWidth = suggestedWidth; 6574 data.suggestedHeight = suggestedHeight; 6575 } 6576 6577 return data; 6578 }, 6579 /** 6580 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 6581 */ 6582 dispose: function() { 6583 if ( this.disposing ) { 6584 /** 6585 * call 'dispose' directly on the parent class 6586 */ 6587 return View.prototype.dispose.apply( this, arguments ); 6588 } 6589 6590 // Run remove on `dispose`, so we can be sure to refresh the 6591 // uploader with a view-less DOM. Track whether we're disposing 6592 // so we don't trigger an infinite loop. 6593 this.disposing = true; 6594 return this.remove(); 6595 }, 6596 /** 6597 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 6598 */ 6599 remove: function() { 6600 /** 6601 * call 'remove' directly on the parent class 6602 */ 6603 var result = View.prototype.remove.apply( this, arguments ); 6604 6605 _.defer( _.bind( this.refresh, this ) ); 6606 return result; 6607 }, 6608 6609 refresh: function() { 6610 var uploader = this.controller.uploader; 6611 6612 if ( uploader ) { 6613 uploader.refresh(); 6614 } 6615 }, 6616 /** 6617 * @returns {wp.media.view.UploaderInline} 6618 */ 6619 ready: function() { 6620 var $browser = this.options.$browser, 6621 $placeholder; 6622 6623 if ( this.controller.uploader ) { 6624 $placeholder = this.$('.browser'); 6625 6626 // Check if we've already replaced the placeholder. 6627 if ( $placeholder[0] === $browser[0] ) { 6628 return; 6629 } 6630 6631 $browser.detach().text( $placeholder.text() ); 6632 $browser[0].className = $placeholder[0].className; 6633 $placeholder.replaceWith( $browser.show() ); 6634 } 6635 6636 this.refresh(); 6637 return this; 6638 }, 6639 show: function() { 6640 this.$el.removeClass( 'hidden' ); 6641 }, 6642 hide: function() { 6643 this.$el.addClass( 'hidden' ); 6644 } 6645 6646 }); 6647 6648 module.exports = UploaderInline; 6649 },{"../view.js":55,"./status.js":53}],52:[function(require,module,exports){ 6650 /** 6651 * wp.media.view.UploaderStatusError 6652 * 6653 * @class 6654 * @augments wp.media.View 6655 * @augments wp.Backbone.View 6656 * @augments Backbone.View 6657 */ 6658 var View = require( '../view.js' ), 6659 UploaderStatusError; 6660 6661 UploaderStatusError = View.extend({ 6662 className: 'upload-error', 6663 template: wp.template('uploader-status-error') 6664 }); 6665 6666 module.exports = UploaderStatusError; 6667 },{"../view.js":55}],53:[function(require,module,exports){ 6668 /** 6669 * wp.media.view.UploaderStatus 6670 * 6671 * An uploader status for on-going uploads. 6672 * 6673 * @class 6674 * @augments wp.media.View 6675 * @augments wp.Backbone.View 6676 * @augments Backbone.View 6677 */ 6678 var View = require( '../view.js' ), 6679 UploaderStatusError = require( './status-error.js' ), 6680 UploaderStatus; 6681 6682 UploaderStatus = View.extend({ 6683 className: 'media-uploader-status', 6684 template: wp.template('uploader-status'), 6685 6686 events: { 6687 'click .upload-dismiss-errors': 'dismiss' 6688 }, 6689 6690 initialize: function() { 6691 this.queue = wp.Uploader.queue; 6692 this.queue.on( 'add remove reset', this.visibility, this ); 6693 this.queue.on( 'add remove reset change:percent', this.progress, this ); 6694 this.queue.on( 'add remove reset change:uploading', this.info, this ); 6695 6696 this.errors = wp.Uploader.errors; 6697 this.errors.reset(); 6698 this.errors.on( 'add remove reset', this.visibility, this ); 6699 this.errors.on( 'add', this.error, this ); 6700 }, 6701 /** 6702 * @global wp.Uploader 6703 * @returns {wp.media.view.UploaderStatus} 6704 */ 6705 dispose: function() { 6706 wp.Uploader.queue.off( null, null, this ); 6707 /** 6708 * call 'dispose' directly on the parent class 6709 */ 6710 View.prototype.dispose.apply( this, arguments ); 6711 return this; 6712 }, 6713 6714 visibility: function() { 6715 this.$el.toggleClass( 'uploading', !! this.queue.length ); 6716 this.$el.toggleClass( 'errors', !! this.errors.length ); 6717 this.$el.toggle( !! this.queue.length || !! this.errors.length ); 6718 }, 6719 6720 ready: function() { 6721 _.each({ 6722 '$bar': '.media-progress-bar div', 6723 '$index': '.upload-index', 6724 '$total': '.upload-total', 6725 '$filename': '.upload-filename' 6726 }, function( selector, key ) { 6727 this[ key ] = this.$( selector ); 6728 }, this ); 6729 6730 this.visibility(); 6731 this.progress(); 6732 this.info(); 6733 }, 6734 6735 progress: function() { 6736 var queue = this.queue, 6737 $bar = this.$bar; 6738 6739 if ( ! $bar || ! queue.length ) { 6740 return; 6741 } 6742 6743 $bar.width( ( queue.reduce( function( memo, attachment ) { 6744 if ( ! attachment.get('uploading') ) { 6745 return memo + 100; 6746 } 6747 6748 var percent = attachment.get('percent'); 6749 return memo + ( _.isNumber( percent ) ? percent : 100 ); 6750 }, 0 ) / queue.length ) + '%' ); 6751 }, 6752 6753 info: function() { 6754 var queue = this.queue, 6755 index = 0, active; 6756 6757 if ( ! queue.length ) { 6758 return; 6759 } 6760 6761 active = this.queue.find( function( attachment, i ) { 6762 index = i; 6763 return attachment.get('uploading'); 6764 }); 6765 6766 this.$index.text( index + 1 ); 6767 this.$total.text( queue.length ); 6768 this.$filename.html( active ? this.filename( active.get('filename') ) : '' ); 6769 }, 6770 /** 6771 * @param {string} filename 6772 * @returns {string} 6773 */ 6774 filename: function( filename ) { 6775 return wp.media.truncate( _.escape( filename ), 24 ); 6776 }, 6777 /** 6778 * @param {Backbone.Model} error 6779 */ 6780 error: function( error ) { 6781 this.views.add( '.upload-errors', new UploaderStatusError({ 6782 filename: this.filename( error.get('file').name ), 6783 message: error.get('message') 6784 }), { at: 0 }); 6785 }, 6786 6787 /** 6788 * @global wp.Uploader 6789 * 6790 * @param {Object} event 6791 */ 6792 dismiss: function( event ) { 6793 var errors = this.views.get('.upload-errors'); 6794 6795 event.preventDefault(); 6796 6797 if ( errors ) { 6798 _.invoke( errors, 'remove' ); 6799 } 6800 wp.Uploader.errors.reset(); 6801 } 6802 }); 6803 6804 module.exports = UploaderStatus; 6805 },{"../view.js":55,"./status-error.js":52}],54:[function(require,module,exports){ 6806 /** 6807 * wp.media.view.UploaderWindow 6808 * 6809 * An uploader window that allows for dragging and dropping media. 6810 * 6811 * @class 6812 * @augments wp.media.View 6813 * @augments wp.Backbone.View 6814 * @augments Backbone.View 6815 * 6816 * @param {object} [options] Options hash passed to the view. 6817 * @param {object} [options.uploader] Uploader properties. 6818 * @param {jQuery} [options.uploader.browser] 6819 * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone. 6820 * @param {object} [options.uploader.params] 6821 */ 6822 var View = require( '../view.js' ), 6823 $ = jQuery, 6824 UploaderWindow; 6825 6826 UploaderWindow = View.extend({ 6827 tagName: 'div', 6828 className: 'uploader-window', 6829 template: wp.template('uploader-window'), 6830 6831 initialize: function() { 6832 var uploader; 6833 6834 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body'); 6835 6836 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { 6837 dropzone: this.$el, 6838 browser: this.$browser, 6839 params: {} 6840 }); 6841 6842 // Ensure the dropzone is a jQuery collection. 6843 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) { 6844 uploader.dropzone = $( uploader.dropzone ); 6845 } 6846 6847 this.controller.on( 'activate', this.refresh, this ); 6848 6849 this.controller.on( 'detach', function() { 6850 this.$browser.remove(); 6851 }, this ); 6852 }, 6853 6854 refresh: function() { 6855 if ( this.uploader ) { 6856 this.uploader.refresh(); 6857 } 6858 }, 6859 6860 ready: function() { 6861 var postId = wp.media.view.settings.post.id, 6862 dropzone; 6863 6864 // If the uploader already exists, bail. 6865 if ( this.uploader ) { 6866 return; 6867 } 6868 6869 if ( postId ) { 6870 this.options.uploader.params.post_id = postId; 6871 } 6872 this.uploader = new wp.Uploader( this.options.uploader ); 6873 6874 dropzone = this.uploader.dropzone; 6875 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) ); 6876 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) ); 6877 6878 $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) ); 6879 }, 6880 6881 _ready: function() { 6882 this.controller.trigger( 'uploader:ready' ); 6883 }, 6884 6885 show: function() { 6886 var $el = this.$el.show(); 6887 6888 // Ensure that the animation is triggered by waiting until 6889 // the transparent element is painted into the DOM. 6890 _.defer( function() { 6891 $el.css({ opacity: 1 }); 6892 }); 6893 }, 6894 6895 hide: function() { 6896 var $el = this.$el.css({ opacity: 0 }); 6897 6898 wp.media.transition( $el ).done( function() { 6899 // Transition end events are subject to race conditions. 6900 // Make sure that the value is set as intended. 6901 if ( '0' === $el.css('opacity') ) { 6902 $el.hide(); 6903 } 6904 }); 6905 6906 // https://core.trac.wordpress.org/ticket/27341 6907 _.delay( function() { 6908 if ( '0' === $el.css('opacity') && $el.is(':visible') ) { 6909 $el.hide(); 6910 } 6911 }, 500 ); 6912 } 6913 }); 6914 6915 module.exports = UploaderWindow; 6916 },{"../view.js":55}],55:[function(require,module,exports){ 6917 /** 6918 * wp.media.View 6919 * 6920 * The base view class for media. 6921 * 6922 * Undelegating events, removing events from the model, and 6923 * removing events from the controller mirror the code for 6924 * `Backbone.View.dispose` in Backbone 0.9.8 development. 6925 * 6926 * This behavior has since been removed, and should not be used 6927 * outside of the media manager. 6928 * 6929 * @class 6930 * @augments wp.Backbone.View 6931 * @augments Backbone.View 6932 */ 6933 var View = wp.Backbone.View.extend({ 6934 constructor: function( options ) { 6935 if ( options && options.controller ) { 6936 this.controller = options.controller; 6937 } 6938 wp.Backbone.View.apply( this, arguments ); 6939 }, 6940 /** 6941 * @todo The internal comment mentions this might have been a stop-gap 6942 * before Backbone 0.9.8 came out. Figure out if Backbone core takes 6943 * care of this in Backbone.View now. 6944 * 6945 * @returns {wp.media.View} Returns itself to allow chaining 6946 */ 6947 dispose: function() { 6948 // Undelegating events, removing events from the model, and 6949 // removing events from the controller mirror the code for 6950 // `Backbone.View.dispose` in Backbone 0.9.8 development. 6951 this.undelegateEvents(); 6952 6953 if ( this.model && this.model.off ) { 6954 this.model.off( null, null, this ); 6955 } 6956 6957 if ( this.collection && this.collection.off ) { 6958 this.collection.off( null, null, this ); 6959 } 6960 6961 // Unbind controller events. 6962 if ( this.controller && this.controller.off ) { 6963 this.controller.off( null, null, this ); 6964 } 6965 6966 return this; 6967 }, 6968 /** 6969 * @returns {wp.media.View} Returns itself to allow chaining 6970 */ 6971 remove: function() { 6972 this.dispose(); 6973 /** 6974 * call 'remove' directly on the parent class 6975 */ 6976 return wp.Backbone.View.prototype.remove.apply( this, arguments ); 6977 } 6978 }); 6979 6980 module.exports = View; 6981 },{}]},{},[7]); -
src/wp-includes/js/media/grid.manifest.js
1 /* global _wpMediaViewsL10n, MediaElementPlayer, _wpMediaGridSettings */ 2 (function($, _, Backbone, wp) { 3 var media = wp.media; 4 5 media.controller.EditAttachmentMetadata = require( './controllers/edit-attachment-metadata.js' ); 6 media.view.MediaFrame.Manage = require( './views/frame/manage.js' ); 7 media.view.Attachment.Details.TwoColumn = require( './views/attachment/details-two-column.js' ); 8 media.view.MediaFrame.Manage.Router = require( './router/manage.js' ); 9 media.view.EditImage.Details = require( './views/edit-image-details.js' ); 10 media.view.MediaFrame.EditAttachments = require( './views/frame/edit-attachments.js' ); 11 media.view.SelectModeToggleButton = require( './views/button/select-mode-toggle.js' ); 12 media.view.DeleteSelectedButton = require( './views/button/delete-selected.js' ); 13 media.view.DeleteSelectedPermanentlyButton = require( './views/button/delete-selected-permanently.js' ); 14 15 }(jQuery, _, Backbone, wp)); -
src/wp-includes/js/media/models/attachment.js
1 /** 2 * wp.media.model.Attachment 3 * 4 * @class 5 * @augments Backbone.Model 6 */ 7 var $ = jQuery, 8 Attachment; 9 10 Attachment = Backbone.Model.extend({ 11 /** 12 * Triggered when attachment details change 13 * Overrides Backbone.Model.sync 14 * 15 * @param {string} method 16 * @param {wp.media.model.Attachment} model 17 * @param {Object} [options={}] 18 * 19 * @returns {Promise} 20 */ 21 sync: function( method, model, options ) { 22 // If the attachment does not yet have an `id`, return an instantly 23 // rejected promise. Otherwise, all of our requests will fail. 24 if ( _.isUndefined( this.id ) ) { 25 return $.Deferred().rejectWith( this ).promise(); 26 } 27 28 // Overload the `read` request so Attachment.fetch() functions correctly. 29 if ( 'read' === method ) { 30 options = options || {}; 31 options.context = this; 32 options.data = _.extend( options.data || {}, { 33 action: 'get-attachment', 34 id: this.id 35 }); 36 return wp.media.ajax( options ); 37 38 // Overload the `update` request so properties can be saved. 39 } else if ( 'update' === method ) { 40 // If we do not have the necessary nonce, fail immeditately. 41 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 42 return $.Deferred().rejectWith( this ).promise(); 43 } 44 45 options = options || {}; 46 options.context = this; 47 48 // Set the action and ID. 49 options.data = _.extend( options.data || {}, { 50 action: 'save-attachment', 51 id: this.id, 52 nonce: this.get('nonces').update, 53 post_id: wp.media.model.settings.post.id 54 }); 55 56 // Record the values of the changed attributes. 57 if ( model.hasChanged() ) { 58 options.data.changes = {}; 59 60 _.each( model.changed, function( value, key ) { 61 options.data.changes[ key ] = this.get( key ); 62 }, this ); 63 } 64 65 return wp.media.ajax( options ); 66 67 // Overload the `delete` request so attachments can be removed. 68 // This will permanently delete an attachment. 69 } else if ( 'delete' === method ) { 70 options = options || {}; 71 72 if ( ! options.wait ) { 73 this.destroyed = true; 74 } 75 76 options.context = this; 77 options.data = _.extend( options.data || {}, { 78 action: 'delete-post', 79 id: this.id, 80 _wpnonce: this.get('nonces')['delete'] 81 }); 82 83 return wp.media.ajax( options ).done( function() { 84 this.destroyed = true; 85 }).fail( function() { 86 this.destroyed = false; 87 }); 88 89 // Otherwise, fall back to `Backbone.sync()`. 90 } else { 91 /** 92 * Call `sync` directly on Backbone.Model 93 */ 94 return Backbone.Model.prototype.sync.apply( this, arguments ); 95 } 96 }, 97 /** 98 * Convert date strings into Date objects. 99 * 100 * @param {Object} resp The raw response object, typically returned by fetch() 101 * @returns {Object} The modified response object, which is the attributes hash 102 * to be set on the model. 103 */ 104 parse: function( resp ) { 105 if ( ! resp ) { 106 return resp; 107 } 108 109 resp.date = new Date( resp.date ); 110 resp.modified = new Date( resp.modified ); 111 return resp; 112 }, 113 /** 114 * @param {Object} data The properties to be saved. 115 * @param {Object} options Sync options. e.g. patch, wait, success, error. 116 * 117 * @this Backbone.Model 118 * 119 * @returns {Promise} 120 */ 121 saveCompat: function( data, options ) { 122 var model = this; 123 124 // If we do not have the necessary nonce, fail immeditately. 125 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 126 return $.Deferred().rejectWith( this ).promise(); 127 } 128 129 return media.post( 'save-attachment-compat', _.defaults({ 130 id: this.id, 131 nonce: this.get('nonces').update, 132 post_id: wp.media.model.settings.post.id 133 }, data ) ).done( function( resp, status, xhr ) { 134 model.set( model.parse( resp, xhr ), options ); 135 }); 136 } 137 }, { 138 /** 139 * Create a new model on the static 'all' attachments collection and return it. 140 * 141 * @static 142 * @param {Object} attrs 143 * @returns {wp.media.model.Attachment} 144 */ 145 create: function( attrs ) { 146 var Attachments = require( './attachments.js' ); 147 return Attachments.all.push( attrs ); 148 }, 149 /** 150 * Create a new model on the static 'all' attachments collection and return it. 151 * 152 * If this function has already been called for the id, 153 * it returns the specified attachment. 154 * 155 * @static 156 * @param {string} id A string used to identify a model. 157 * @param {Backbone.Model|undefined} attachment 158 * @returns {wp.media.model.Attachment} 159 */ 160 get: _.memoize( function( id, attachment ) { 161 var Attachments = require( './attachments.js' ); 162 return Attachments.all.push( attachment || { id: id } ); 163 }) 164 }); 165 166 module.exports = Attachment; 167 No newline at end of file -
src/wp-includes/js/media/models/attachments.js
1 /** 2 * wp.media.model.Attachments 3 * 4 * A collection of attachments. 5 * 6 * This collection has no persistence with the server without supplying 7 * 'options.props.query = true', which will mirror the collection 8 * to an Attachments Query collection - @see wp.media.model.Attachments.mirror(). 9 * 10 * @class 11 * @augments Backbone.Collection 12 * 13 * @param {array} [models] Models to initialize with the collection. 14 * @param {object} [options] Options hash for the collection. 15 * @param {string} [options.props] Options hash for the initial query properties. 16 * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection. 17 * @param {string} [options.props.orderby] Initial attribute key to order the collection by. 18 * @param {string} [options.props.query] Whether the collection is linked to an attachments query. 19 * @param {string} [options.observe] 20 * @param {string} [options.filters] 21 * 22 */ 23 var Attachment = require( './attachment.js' ), 24 Attachments; 25 26 Attachments = Backbone.Collection.extend({ 27 /** 28 * @type {wp.media.model.Attachment} 29 */ 30 model: Attachment, 31 /** 32 * @param {Array} [models=[]] Array of models used to populate the collection. 33 * @param {Object} [options={}] 34 */ 35 initialize: function( models, options ) { 36 options = options || {}; 37 38 this.props = new Backbone.Model(); 39 this.filters = options.filters || {}; 40 41 // Bind default `change` events to the `props` model. 42 this.props.on( 'change', this._changeFilteredProps, this ); 43 44 this.props.on( 'change:order', this._changeOrder, this ); 45 this.props.on( 'change:orderby', this._changeOrderby, this ); 46 this.props.on( 'change:query', this._changeQuery, this ); 47 48 this.props.set( _.defaults( options.props || {} ) ); 49 50 if ( options.observe ) { 51 this.observe( options.observe ); 52 } 53 }, 54 /** 55 * Sort the collection when the order attribute changes. 56 * 57 * @access private 58 */ 59 _changeOrder: function() { 60 if ( this.comparator ) { 61 this.sort(); 62 } 63 }, 64 /** 65 * Set the default comparator only when the `orderby` property is set. 66 * 67 * @access private 68 * 69 * @param {Backbone.Model} model 70 * @param {string} orderby 71 */ 72 _changeOrderby: function( model, orderby ) { 73 // If a different comparator is defined, bail. 74 if ( this.comparator && this.comparator !== Attachments.comparator ) { 75 return; 76 } 77 78 if ( orderby && 'post__in' !== orderby ) { 79 this.comparator = Attachments.comparator; 80 } else { 81 delete this.comparator; 82 } 83 }, 84 /** 85 * If the `query` property is set to true, query the server using 86 * the `props` values, and sync the results to this collection. 87 * 88 * @access private 89 * 90 * @param {Backbone.Model} model 91 * @param {Boolean} query 92 */ 93 _changeQuery: function( model, query ) { 94 if ( query ) { 95 this.props.on( 'change', this._requery, this ); 96 this._requery(); 97 } else { 98 this.props.off( 'change', this._requery, this ); 99 } 100 }, 101 /** 102 * @access private 103 * 104 * @param {Backbone.Model} model 105 */ 106 _changeFilteredProps: function( model ) { 107 // If this is a query, updating the collection will be handled by 108 // `this._requery()`. 109 if ( this.props.get('query') ) { 110 return; 111 } 112 113 var changed = _.chain( model.changed ).map( function( t, prop ) { 114 var filter = Attachments.filters[ prop ], 115 term = model.get( prop ); 116 117 if ( ! filter ) { 118 return; 119 } 120 121 if ( term && ! this.filters[ prop ] ) { 122 this.filters[ prop ] = filter; 123 } else if ( ! term && this.filters[ prop ] === filter ) { 124 delete this.filters[ prop ]; 125 } else { 126 return; 127 } 128 129 // Record the change. 130 return true; 131 }, this ).any().value(); 132 133 if ( ! changed ) { 134 return; 135 } 136 137 // If no `Attachments` model is provided to source the searches 138 // from, then automatically generate a source from the existing 139 // models. 140 if ( ! this._source ) { 141 this._source = new Attachments( this.models ); 142 } 143 144 this.reset( this._source.filter( this.validator, this ) ); 145 }, 146 147 validateDestroyed: false, 148 /** 149 * Checks whether an attachment is valid. 150 * 151 * @param {wp.media.model.Attachment} attachment 152 * @returns {Boolean} 153 */ 154 validator: function( attachment ) { 155 if ( ! this.validateDestroyed && attachment.destroyed ) { 156 return false; 157 } 158 return _.all( this.filters, function( filter ) { 159 return !! filter.call( this, attachment ); 160 }, this ); 161 }, 162 /** 163 * Add or remove an attachment to the collection depending on its validity. 164 * 165 * @param {wp.media.model.Attachment} attachment 166 * @param {Object} options 167 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 168 */ 169 validate: function( attachment, options ) { 170 var valid = this.validator( attachment ), 171 hasAttachment = !! this.get( attachment.cid ); 172 173 if ( ! valid && hasAttachment ) { 174 this.remove( attachment, options ); 175 } else if ( valid && ! hasAttachment ) { 176 this.add( attachment, options ); 177 } 178 179 return this; 180 }, 181 182 /** 183 * Add or remove all attachments from another collection depending on each one's validity. 184 * 185 * @param {wp.media.model.Attachments} attachments 186 * @param {object} [options={}] 187 * 188 * @fires wp.media.model.Attachments#reset 189 * 190 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 191 */ 192 validateAll: function( attachments, options ) { 193 options = options || {}; 194 195 _.each( attachments.models, function( attachment ) { 196 this.validate( attachment, { silent: true }); 197 }, this ); 198 199 if ( ! options.silent ) { 200 this.trigger( 'reset', this, options ); 201 } 202 return this; 203 }, 204 /** 205 * Start observing another attachments collection change events 206 * and replicate them on this collection. 207 * 208 * @param {wp.media.model.Attachments} The attachments collection to observe. 209 * @returns {wp.media.model.Attachments} Returns itself to allow chaining. 210 */ 211 observe: function( attachments ) { 212 this.observers = this.observers || []; 213 this.observers.push( attachments ); 214 215 attachments.on( 'add change remove', this._validateHandler, this ); 216 attachments.on( 'reset', this._validateAllHandler, this ); 217 this.validateAll( attachments ); 218 return this; 219 }, 220 /** 221 * Stop replicating collection change events from another attachments collection. 222 * 223 * @param {wp.media.model.Attachments} The attachments collection to stop observing. 224 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 225 */ 226 unobserve: function( attachments ) { 227 if ( attachments ) { 228 attachments.off( null, null, this ); 229 this.observers = _.without( this.observers, attachments ); 230 231 } else { 232 _.each( this.observers, function( attachments ) { 233 attachments.off( null, null, this ); 234 }, this ); 235 delete this.observers; 236 } 237 238 return this; 239 }, 240 /** 241 * @access private 242 * 243 * @param {wp.media.model.Attachments} attachment 244 * @param {wp.media.model.Attachments} attachments 245 * @param {Object} options 246 * 247 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 248 */ 249 _validateHandler: function( attachment, attachments, options ) { 250 // If we're not mirroring this `attachments` collection, 251 // only retain the `silent` option. 252 options = attachments === this.mirroring ? options : { 253 silent: options && options.silent 254 }; 255 256 return this.validate( attachment, options ); 257 }, 258 /** 259 * @access private 260 * 261 * @param {wp.media.model.Attachments} attachments 262 * @param {Object} options 263 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 264 */ 265 _validateAllHandler: function( attachments, options ) { 266 return this.validateAll( attachments, options ); 267 }, 268 /** 269 * Start mirroring another attachments collection, clearing out any models already 270 * in the collection. 271 * 272 * @param {wp.media.model.Attachments} The attachments collection to mirror. 273 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 274 */ 275 mirror: function( attachments ) { 276 if ( this.mirroring && this.mirroring === attachments ) { 277 return this; 278 } 279 280 this.unmirror(); 281 this.mirroring = attachments; 282 283 // Clear the collection silently. A `reset` event will be fired 284 // when `observe()` calls `validateAll()`. 285 this.reset( [], { silent: true } ); 286 this.observe( attachments ); 287 288 return this; 289 }, 290 /** 291 * Stop mirroring another attachments collection. 292 */ 293 unmirror: function() { 294 if ( ! this.mirroring ) { 295 return; 296 } 297 298 this.unobserve( this.mirroring ); 299 delete this.mirroring; 300 }, 301 /** 302 * Retrive more attachments from the server for the collection. 303 * 304 * Only works if the collection is mirroring a Query Attachments collection, 305 * and forwards to its `more` method. This collection class doesn't have 306 * server persistence by itself. 307 * 308 * @param {object} options 309 * @returns {Promise} 310 */ 311 more: function( options ) { 312 var deferred = jQuery.Deferred(), 313 mirroring = this.mirroring, 314 attachments = this; 315 316 if ( ! mirroring || ! mirroring.more ) { 317 return deferred.resolveWith( this ).promise(); 318 } 319 // If we're mirroring another collection, forward `more` to 320 // the mirrored collection. Account for a race condition by 321 // checking if we're still mirroring that collection when 322 // the request resolves. 323 mirroring.more( options ).done( function() { 324 if ( this === attachments.mirroring ) 325 deferred.resolveWith( this ); 326 }); 327 328 return deferred.promise(); 329 }, 330 /** 331 * Whether there are more attachments that haven't been sync'd from the server 332 * that match the collection's query. 333 * 334 * Only works if the collection is mirroring a Query Attachments collection, 335 * and forwards to its `hasMore` method. This collection class doesn't have 336 * server persistence by itself. 337 * 338 * @returns {boolean} 339 */ 340 hasMore: function() { 341 return this.mirroring ? this.mirroring.hasMore() : false; 342 }, 343 /** 344 * A custom AJAX-response parser. 345 * 346 * See trac ticket #24753 347 * 348 * @param {Object|Array} resp The raw response Object/Array. 349 * @param {Object} xhr 350 * @returns {Array} The array of model attributes to be added to the collection 351 */ 352 parse: function( resp, xhr ) { 353 if ( ! _.isArray( resp ) ) { 354 resp = [resp]; 355 } 356 357 return _.map( resp, function( attrs ) { 358 var id, attachment, newAttributes; 359 360 if ( attrs instanceof Backbone.Model ) { 361 id = attrs.get( 'id' ); 362 attrs = attrs.attributes; 363 } else { 364 id = attrs.id; 365 } 366 367 attachment = Attachment.get( id ); 368 newAttributes = attachment.parse( attrs, xhr ); 369 370 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { 371 attachment.set( newAttributes ); 372 } 373 374 return attachment; 375 }); 376 }, 377 /** 378 * If the collection is a query, create and mirror an Attachments Query collection. 379 * 380 * @access private 381 */ 382 _requery: function( refresh ) { 383 var props, Query; 384 if ( this.props.get('query') ) { 385 Query = require( './query.js' ); 386 props = this.props.toJSON(); 387 props.cache = ( true !== refresh ); 388 this.mirror( Query.get( props ) ); 389 } 390 }, 391 /** 392 * If this collection is sorted by `menuOrder`, recalculates and saves 393 * the menu order to the database. 394 * 395 * @returns {undefined|Promise} 396 */ 397 saveMenuOrder: function() { 398 if ( 'menuOrder' !== this.props.get('orderby') ) { 399 return; 400 } 401 402 // Removes any uploading attachments, updates each attachment's 403 // menu order, and returns an object with an { id: menuOrder } 404 // mapping to pass to the request. 405 var attachments = this.chain().filter( function( attachment ) { 406 return ! _.isUndefined( attachment.id ); 407 }).map( function( attachment, index ) { 408 // Indices start at 1. 409 index = index + 1; 410 attachment.set( 'menuOrder', index ); 411 return [ attachment.id, index ]; 412 }).object().value(); 413 414 if ( _.isEmpty( attachments ) ) { 415 return; 416 } 417 418 return wp.media.post( 'save-attachment-order', { 419 nonce: wp.media.model.settings.post.nonce, 420 post_id: wp.media.model.settings.post.id, 421 attachments: attachments 422 }); 423 } 424 }, { 425 /** 426 * A function to compare two attachment models in an attachments collection. 427 * 428 * Used as the default comparator for instances of wp.media.model.Attachments 429 * and its subclasses. @see wp.media.model.Attachments._changeOrderby(). 430 * 431 * @static 432 * 433 * @param {Backbone.Model} a 434 * @param {Backbone.Model} b 435 * @param {Object} options 436 * @returns {Number} -1 if the first model should come before the second, 437 * 0 if they are of the same rank and 438 * 1 if the first model should come after. 439 */ 440 comparator: function( a, b, options ) { 441 var key = this.props.get('orderby'), 442 order = this.props.get('order') || 'DESC', 443 ac = a.cid, 444 bc = b.cid; 445 446 a = a.get( key ); 447 b = b.get( key ); 448 449 if ( 'date' === key || 'modified' === key ) { 450 a = a || new Date(); 451 b = b || new Date(); 452 } 453 454 // If `options.ties` is set, don't enforce the `cid` tiebreaker. 455 if ( options && options.ties ) { 456 ac = bc = null; 457 } 458 459 return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac ); 460 }, 461 /** 462 * @namespace 463 */ 464 filters: { 465 /** 466 * @static 467 * Note that this client-side searching is *not* equivalent 468 * to our server-side searching. 469 * 470 * @param {wp.media.model.Attachment} attachment 471 * 472 * @this wp.media.model.Attachments 473 * 474 * @returns {Boolean} 475 */ 476 search: function( attachment ) { 477 if ( ! this.props.get('search') ) { 478 return true; 479 } 480 481 return _.any(['title','filename','description','caption','name'], function( key ) { 482 var value = attachment.get( key ); 483 return value && -1 !== value.search( this.props.get('search') ); 484 }, this ); 485 }, 486 /** 487 * @static 488 * @param {wp.media.model.Attachment} attachment 489 * 490 * @this wp.media.model.Attachments 491 * 492 * @returns {Boolean} 493 */ 494 type: function( attachment ) { 495 var type = this.props.get('type'); 496 return ! type || -1 !== type.indexOf( attachment.get('type') ); 497 }, 498 /** 499 * @static 500 * @param {wp.media.model.Attachment} attachment 501 * 502 * @this wp.media.model.Attachments 503 * 504 * @returns {Boolean} 505 */ 506 uploadedTo: function( attachment ) { 507 var uploadedTo = this.props.get('uploadedTo'); 508 if ( _.isUndefined( uploadedTo ) ) { 509 return true; 510 } 511 512 return uploadedTo === attachment.get('uploadedTo'); 513 }, 514 /** 515 * @static 516 * @param {wp.media.model.Attachment} attachment 517 * 518 * @this wp.media.model.Attachments 519 * 520 * @returns {Boolean} 521 */ 522 status: function( attachment ) { 523 var status = this.props.get('status'); 524 if ( _.isUndefined( status ) ) { 525 return true; 526 } 527 528 return status === attachment.get('status'); 529 } 530 } 531 }); 532 533 module.exports = Attachments; 534 No newline at end of file -
src/wp-includes/js/media/models/post-image.js
1 /** 2 * wp.media.model.PostImage 3 * 4 * An instance of an image that's been embedded into a post. 5 * 6 * Used in the embedded image attachment display settings modal - @see wp.media.view.MediaFrame.ImageDetails. 7 * 8 * @class 9 * @augments Backbone.Model 10 * 11 * @param {int} [attributes] Initial model attributes. 12 * @param {int} [attributes.attachment_id] ID of the attachment. 13 **/ 14 var Attachment = require( './attachment' ), 15 PostImage; 16 17 PostImage = Backbone.Model.extend({ 18 19 initialize: function( attributes ) { 20 this.attachment = false; 21 22 if ( attributes.attachment_id ) { 23 this.attachment = Attachment.get( attributes.attachment_id ); 24 if ( this.attachment.get( 'url' ) ) { 25 this.dfd = jQuery.Deferred(); 26 this.dfd.resolve(); 27 } else { 28 this.dfd = this.attachment.fetch(); 29 } 30 this.bindAttachmentListeners(); 31 } 32 33 // keep url in sync with changes to the type of link 34 this.on( 'change:link', this.updateLinkUrl, this ); 35 this.on( 'change:size', this.updateSize, this ); 36 37 this.setLinkTypeFromUrl(); 38 this.setAspectRatio(); 39 40 this.set( 'originalUrl', attributes.url ); 41 }, 42 43 bindAttachmentListeners: function() { 44 this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl ); 45 this.listenTo( this.attachment, 'sync', this.setAspectRatio ); 46 this.listenTo( this.attachment, 'change', this.updateSize ); 47 }, 48 49 changeAttachment: function( attachment, props ) { 50 this.stopListening( this.attachment ); 51 this.attachment = attachment; 52 this.bindAttachmentListeners(); 53 54 this.set( 'attachment_id', this.attachment.get( 'id' ) ); 55 this.set( 'caption', this.attachment.get( 'caption' ) ); 56 this.set( 'alt', this.attachment.get( 'alt' ) ); 57 this.set( 'size', props.get( 'size' ) ); 58 this.set( 'align', props.get( 'align' ) ); 59 this.set( 'link', props.get( 'link' ) ); 60 this.updateLinkUrl(); 61 this.updateSize(); 62 }, 63 64 setLinkTypeFromUrl: function() { 65 var linkUrl = this.get( 'linkUrl' ), 66 type; 67 68 if ( ! linkUrl ) { 69 this.set( 'link', 'none' ); 70 return; 71 } 72 73 // default to custom if there is a linkUrl 74 type = 'custom'; 75 76 if ( this.attachment ) { 77 if ( this.attachment.get( 'url' ) === linkUrl ) { 78 type = 'file'; 79 } else if ( this.attachment.get( 'link' ) === linkUrl ) { 80 type = 'post'; 81 } 82 } else { 83 if ( this.get( 'url' ) === linkUrl ) { 84 type = 'file'; 85 } 86 } 87 88 this.set( 'link', type ); 89 }, 90 91 updateLinkUrl: function() { 92 var link = this.get( 'link' ), 93 url; 94 95 switch( link ) { 96 case 'file': 97 if ( this.attachment ) { 98 url = this.attachment.get( 'url' ); 99 } else { 100 url = this.get( 'url' ); 101 } 102 this.set( 'linkUrl', url ); 103 break; 104 case 'post': 105 this.set( 'linkUrl', this.attachment.get( 'link' ) ); 106 break; 107 case 'none': 108 this.set( 'linkUrl', '' ); 109 break; 110 } 111 }, 112 113 updateSize: function() { 114 var size; 115 116 if ( ! this.attachment ) { 117 return; 118 } 119 120 if ( this.get( 'size' ) === 'custom' ) { 121 this.set( 'width', this.get( 'customWidth' ) ); 122 this.set( 'height', this.get( 'customHeight' ) ); 123 this.set( 'url', this.get( 'originalUrl' ) ); 124 return; 125 } 126 127 size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ]; 128 129 if ( ! size ) { 130 return; 131 } 132 133 this.set( 'url', size.url ); 134 this.set( 'width', size.width ); 135 this.set( 'height', size.height ); 136 }, 137 138 setAspectRatio: function() { 139 var full; 140 141 if ( this.attachment && this.attachment.get( 'sizes' ) ) { 142 full = this.attachment.get( 'sizes' ).full; 143 144 if ( full ) { 145 this.set( 'aspectRatio', full.width / full.height ); 146 return; 147 } 148 } 149 150 this.set( 'aspectRatio', this.get( 'customWidth' ) / this.get( 'customHeight' ) ); 151 } 152 }); 153 154 module.exports = PostImage; 155 No newline at end of file -
src/wp-includes/js/media/models/post-media.js
1 /** 2 * Shared model class for audio and video. Updates the model after 3 * "Add Audio|Video Source" and "Replace Audio|Video" states return 4 * 5 * @constructor 6 * @augments Backbone.Model 7 */ 8 var PostMedia = Backbone.Model.extend({ 9 initialize: function() { 10 this.attachment = false; 11 }, 12 13 setSource: function( attachment ) { 14 this.attachment = attachment; 15 this.extension = attachment.get( 'filename' ).split('.').pop(); 16 17 if ( this.get( 'src' ) && this.extension === this.get( 'src' ).split('.').pop() ) { 18 this.unset( 'src' ); 19 } 20 21 if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) { 22 this.set( this.extension, this.attachment.get( 'url' ) ); 23 } else { 24 this.unset( this.extension ); 25 } 26 }, 27 28 changeAttachment: function( attachment ) { 29 var self = this; 30 31 this.setSource( attachment ); 32 33 this.unset( 'src' ); 34 _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function( ext ) { 35 self.unset( ext ); 36 } ); 37 } 38 }); 39 40 module.exports = PostMedia; 41 No newline at end of file -
src/wp-includes/js/media/models/query.js
1 /** 2 * wp.media.model.Query 3 * 4 * A collection of attachments that match the supplied query arguments. 5 * 6 * Note: Do NOT change this.args after the query has been initialized. 7 * Things will break. 8 * 9 * @class 10 * @augments wp.media.model.Attachments 11 * @augments Backbone.Collection 12 * 13 * @param {array} [models] Models to initialize with the collection. 14 * @param {object} [options] Options hash. 15 * @param {object} [options.args] Attachments query arguments. 16 * @param {object} [options.args.posts_per_page] 17 */ 18 var Attachments = require( './attachments.js' ), 19 Query; 20 21 Query = Attachments.extend({ 22 /** 23 * @global wp.Uploader 24 * 25 * @param {array} [models=[]] Array of initial models to populate the collection. 26 * @param {object} [options={}] 27 */ 28 initialize: function( models, options ) { 29 var allowed; 30 31 options = options || {}; 32 Attachments.prototype.initialize.apply( this, arguments ); 33 34 this.args = options.args; 35 this._hasMore = true; 36 this.created = new Date(); 37 38 this.filters.order = function( attachment ) { 39 var orderby = this.props.get('orderby'), 40 order = this.props.get('order'); 41 42 if ( ! this.comparator ) { 43 return true; 44 } 45 46 // We want any items that can be placed before the last 47 // item in the set. If we add any items after the last 48 // item, then we can't guarantee the set is complete. 49 if ( this.length ) { 50 return 1 !== this.comparator( attachment, this.last(), { ties: true }); 51 52 // Handle the case where there are no items yet and 53 // we're sorting for recent items. In that case, we want 54 // changes that occurred after we created the query. 55 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { 56 return attachment.get( orderby ) >= this.created; 57 58 // If we're sorting by menu order and we have no items, 59 // accept any items that have the default menu order (0). 60 } else if ( 'ASC' === order && 'menuOrder' === orderby ) { 61 return attachment.get( orderby ) === 0; 62 } 63 64 // Otherwise, we don't want any items yet. 65 return false; 66 }; 67 68 // Observe the central `wp.Uploader.queue` collection to watch for 69 // new matches for the query. 70 // 71 // Only observe when a limited number of query args are set. There 72 // are no filters for other properties, so observing will result in 73 // false positives in those queries. 74 allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; 75 if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { 76 this.observe( wp.Uploader.queue ); 77 } 78 }, 79 /** 80 * Whether there are more attachments that haven't been sync'd from the server 81 * that match the collection's query. 82 * 83 * @returns {boolean} 84 */ 85 hasMore: function() { 86 return this._hasMore; 87 }, 88 /** 89 * Fetch more attachments from the server for the collection. 90 * 91 * @param {object} [options={}] 92 * @returns {Promise} 93 */ 94 more: function( options ) { 95 var query = this; 96 97 // If there is already a request pending, return early with the Deferred object. 98 if ( this._more && 'pending' === this._more.state() ) { 99 return this._more; 100 } 101 102 if ( ! this.hasMore() ) { 103 return jQuery.Deferred().resolveWith( this ).promise(); 104 } 105 106 options = options || {}; 107 options.remove = false; 108 109 return this._more = this.fetch( options ).done( function( resp ) { 110 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { 111 query._hasMore = false; 112 } 113 }); 114 }, 115 /** 116 * Overrides Backbone.Collection.sync 117 * Overrides wp.media.model.Attachments.sync 118 * 119 * @param {String} method 120 * @param {Backbone.Model} model 121 * @param {Object} [options={}] 122 * @returns {Promise} 123 */ 124 sync: function( method, model, options ) { 125 var args, fallback; 126 127 // Overload the read method so Attachment.fetch() functions correctly. 128 if ( 'read' === method ) { 129 options = options || {}; 130 options.context = this; 131 options.data = _.extend( options.data || {}, { 132 action: 'query-attachments', 133 post_id: wp.media.model.settings.post.id 134 }); 135 136 // Clone the args so manipulation is non-destructive. 137 args = _.clone( this.args ); 138 139 // Determine which page to query. 140 if ( -1 !== args.posts_per_page ) { 141 args.paged = Math.floor( this.length / args.posts_per_page ) + 1; 142 } 143 144 options.data.query = args; 145 return wp.media.ajax( options ); 146 147 // Otherwise, fall back to Backbone.sync() 148 } else { 149 /** 150 * Call wp.media.model.Attachments.sync or Backbone.sync 151 */ 152 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; 153 return fallback.sync.apply( this, arguments ); 154 } 155 } 156 }, { 157 /** 158 * @readonly 159 */ 160 defaultProps: { 161 orderby: 'date', 162 order: 'DESC' 163 }, 164 /** 165 * @readonly 166 */ 167 defaultArgs: { 168 posts_per_page: 40 169 }, 170 /** 171 * @readonly 172 */ 173 orderby: { 174 allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], 175 /** 176 * A map of JavaScript orderby values to their WP_Query equivalents. 177 * @type {Object} 178 */ 179 valuemap: { 180 'id': 'ID', 181 'uploadedTo': 'parent', 182 'menuOrder': 'menu_order ID' 183 } 184 }, 185 /** 186 * A map of JavaScript query properties to their WP_Query equivalents. 187 * 188 * @readonly 189 */ 190 propmap: { 191 'search': 's', 192 'type': 'post_mime_type', 193 'perPage': 'posts_per_page', 194 'menuOrder': 'menu_order', 195 'uploadedTo': 'post_parent', 196 'status': 'post_status', 197 'include': 'post__in', 198 'exclude': 'post__not_in' 199 }, 200 /** 201 * Creates and returns an Attachments Query collection given the properties. 202 * 203 * Caches query objects and reuses where possible. 204 * 205 * @static 206 * @method 207 * 208 * @param {object} [props] 209 * @param {Object} [props.cache=true] Whether to use the query cache or not. 210 * @param {Object} [props.order] 211 * @param {Object} [props.orderby] 212 * @param {Object} [props.include] 213 * @param {Object} [props.exclude] 214 * @param {Object} [props.s] 215 * @param {Object} [props.post_mime_type] 216 * @param {Object} [props.posts_per_page] 217 * @param {Object} [props.menu_order] 218 * @param {Object} [props.post_parent] 219 * @param {Object} [props.post_status] 220 * @param {Object} [options] 221 * 222 * @returns {wp.media.model.Query} A new Attachments Query collection. 223 */ 224 get: (function(){ 225 /** 226 * @static 227 * @type Array 228 */ 229 var queries = []; 230 231 /** 232 * @returns {Query} 233 */ 234 return function( props, options ) { 235 var args = {}, 236 orderby = Query.orderby, 237 defaults = Query.defaultProps, 238 query, 239 cache = !! props.cache || _.isUndefined( props.cache ); 240 241 // Remove the `query` property. This isn't linked to a query, 242 // this *is* the query. 243 delete props.query; 244 delete props.cache; 245 246 // Fill default args. 247 _.defaults( props, defaults ); 248 249 // Normalize the order. 250 props.order = props.order.toUpperCase(); 251 if ( 'DESC' !== props.order && 'ASC' !== props.order ) { 252 props.order = defaults.order.toUpperCase(); 253 } 254 255 // Ensure we have a valid orderby value. 256 if ( ! _.contains( orderby.allowed, props.orderby ) ) { 257 props.orderby = defaults.orderby; 258 } 259 260 _.each( [ 'include', 'exclude' ], function( prop ) { 261 if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { 262 props[ prop ] = [ props[ prop ] ]; 263 } 264 } ); 265 266 // Generate the query `args` object. 267 // Correct any differing property names. 268 _.each( props, function( value, prop ) { 269 if ( _.isNull( value ) ) { 270 return; 271 } 272 273 args[ Query.propmap[ prop ] || prop ] = value; 274 }); 275 276 // Fill any other default query args. 277 _.defaults( args, Query.defaultArgs ); 278 279 // `props.orderby` does not always map directly to `args.orderby`. 280 // Substitute exceptions specified in orderby.keymap. 281 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; 282 283 // Search the query cache for a matching query. 284 if ( cache ) { 285 query = _.find( queries, function( query ) { 286 return _.isEqual( query.args, args ); 287 }); 288 } else { 289 queries = []; 290 } 291 292 // Otherwise, create a new query and add it to the cache. 293 if ( ! query ) { 294 query = new Query( [], _.extend( options || {}, { 295 props: props, 296 args: args 297 } ) ); 298 queries.push( query ); 299 } 300 301 return query; 302 }; 303 }()) 304 }); 305 306 module.exports = Query; 307 No newline at end of file -
src/wp-includes/js/media/models/selection.js
1 /** 2 * wp.media.model.Selection 3 * 4 * A selection of attachments. 5 * 6 * @class 7 * @augments wp.media.model.Attachments 8 * @augments Backbone.Collection 9 */ 10 var Attachments = require( './attachments.js' ), 11 Selection; 12 13 Selection = Attachments.extend({ 14 /** 15 * Refresh the `single` model whenever the selection changes. 16 * Binds `single` instead of using the context argument to ensure 17 * it receives no parameters. 18 * 19 * @param {Array} [models=[]] Array of models used to populate the collection. 20 * @param {Object} [options={}] 21 */ 22 initialize: function( models, options ) { 23 /** 24 * call 'initialize' directly on the parent class 25 */ 26 Attachments.prototype.initialize.apply( this, arguments ); 27 this.multiple = options && options.multiple; 28 29 this.on( 'add remove reset', _.bind( this.single, this, false ) ); 30 }, 31 32 /** 33 * If the workflow does not support multi-select, clear out the selection 34 * before adding a new attachment to it. 35 * 36 * @param {Array} models 37 * @param {Object} options 38 * @returns {wp.media.model.Attachment[]} 39 */ 40 add: function( models, options ) { 41 if ( ! this.multiple ) { 42 this.remove( this.models ); 43 } 44 /** 45 * call 'add' directly on the parent class 46 */ 47 return Attachments.prototype.add.call( this, models, options ); 48 }, 49 50 /** 51 * Fired when toggling (clicking on) an attachment in the modal. 52 * 53 * @param {undefined|boolean|wp.media.model.Attachment} model 54 * 55 * @fires wp.media.model.Selection#selection:single 56 * @fires wp.media.model.Selection#selection:unsingle 57 * 58 * @returns {Backbone.Model} 59 */ 60 single: function( model ) { 61 var previous = this._single; 62 63 // If a `model` is provided, use it as the single model. 64 if ( model ) { 65 this._single = model; 66 } 67 // If the single model isn't in the selection, remove it. 68 if ( this._single && ! this.get( this._single.cid ) ) { 69 delete this._single; 70 } 71 72 this._single = this._single || this.last(); 73 74 // If single has changed, fire an event. 75 if ( this._single !== previous ) { 76 if ( previous ) { 77 previous.trigger( 'selection:unsingle', previous, this ); 78 79 // If the model was already removed, trigger the collection 80 // event manually. 81 if ( ! this.get( previous.cid ) ) { 82 this.trigger( 'selection:unsingle', previous, this ); 83 } 84 } 85 if ( this._single ) { 86 this._single.trigger( 'selection:single', this._single, this ); 87 } 88 } 89 90 // Return the single model, or the last model as a fallback. 91 return this._single; 92 } 93 }); 94 95 module.exports = Selection; 96 No newline at end of file -
src/wp-includes/js/media/models.js
1 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 2 /* global _wpMediaModelsL10n:false */ 3 window.wp = window.wp || {}; 4 5 (function($){ 6 var Attachment, Attachments, l10n, media; 7 8 /** 9 * Create and return a media frame. 10 * 11 * Handles the default media experience. 12 * 13 * @param {object} attributes The properties passed to the main media controller. 14 * @return {wp.media.view.MediaFrame} A media workflow. 15 */ 16 media = wp.media = function( attributes ) { 17 var MediaFrame = media.view.MediaFrame, 18 frame; 19 20 if ( ! MediaFrame ) { 21 return; 22 } 23 24 attributes = _.defaults( attributes || {}, { 25 frame: 'select' 26 }); 27 28 if ( 'select' === attributes.frame && MediaFrame.Select ) { 29 frame = new MediaFrame.Select( attributes ); 30 } else if ( 'post' === attributes.frame && MediaFrame.Post ) { 31 frame = new MediaFrame.Post( attributes ); 32 } else if ( 'manage' === attributes.frame && MediaFrame.Manage ) { 33 frame = new MediaFrame.Manage( attributes ); 34 } else if ( 'image' === attributes.frame && MediaFrame.ImageDetails ) { 35 frame = new MediaFrame.ImageDetails( attributes ); 36 } else if ( 'audio' === attributes.frame && MediaFrame.AudioDetails ) { 37 frame = new MediaFrame.AudioDetails( attributes ); 38 } else if ( 'video' === attributes.frame && MediaFrame.VideoDetails ) { 39 frame = new MediaFrame.VideoDetails( attributes ); 40 } else if ( 'edit-attachments' === attributes.frame && MediaFrame.EditAttachments ) { 41 frame = new MediaFrame.EditAttachments( attributes ); 42 } 43 44 delete attributes.frame; 45 46 media.frame = frame; 47 48 return frame; 49 }; 50 51 _.extend( media, { model: {}, view: {}, controller: {}, frames: {} }); 52 53 // Link any localized strings. 54 l10n = media.model.l10n = typeof _wpMediaModelsL10n === 'undefined' ? {} : _wpMediaModelsL10n; 55 56 // Link any settings. 57 media.model.settings = l10n.settings || {}; 58 delete l10n.settings; 59 60 Attachments = media.model.Attachments = require( './models/attachments.js' ); 61 Attachment = media.model.Attachment = require( './models/attachment.js' ); 62 63 media.model.Query = require( './models/query.js' ); 64 media.model.PostImage = require( './models/post-image.js' ); 65 media.model.Selection = require( './models/selection.js' ); 66 67 /** 68 * ======================================================================== 69 * UTILITIES 70 * ======================================================================== 71 */ 72 73 /** 74 * A basic equality comparator for Backbone models. 75 * 76 * Used to order models within a collection - @see wp.media.model.Attachments.comparator(). 77 * 78 * @param {mixed} a The primary parameter to compare. 79 * @param {mixed} b The primary parameter to compare. 80 * @param {string} ac The fallback parameter to compare, a's cid. 81 * @param {string} bc The fallback parameter to compare, b's cid. 82 * @return {number} -1: a should come before b. 83 * 0: a and b are of the same rank. 84 * 1: b should come before a. 85 */ 86 media.compare = function( a, b, ac, bc ) { 87 if ( _.isEqual( a, b ) ) { 88 return ac === bc ? 0 : (ac > bc ? -1 : 1); 89 } else { 90 return a > b ? -1 : 1; 91 } 92 }; 93 94 _.extend( media, { 95 /** 96 * media.template( id ) 97 * 98 * Fetch a JavaScript template for an id, and return a templating function for it. 99 * 100 * See wp.template() in `wp-includes/js/wp-util.js`. 101 * 102 * @borrows wp.template as template 103 */ 104 template: wp.template, 105 106 /** 107 * media.post( [action], [data] ) 108 * 109 * Sends a POST request to WordPress. 110 * See wp.ajax.post() in `wp-includes/js/wp-util.js`. 111 * 112 * @borrows wp.ajax.post as post 113 */ 114 post: wp.ajax.post, 115 116 /** 117 * media.ajax( [action], [options] ) 118 * 119 * Sends an XHR request to WordPress. 120 * See wp.ajax.send() in `wp-includes/js/wp-util.js`. 121 * 122 * @borrows wp.ajax.send as ajax 123 */ 124 ajax: wp.ajax.send, 125 126 /** 127 * Scales a set of dimensions to fit within bounding dimensions. 128 * 129 * @param {Object} dimensions 130 * @returns {Object} 131 */ 132 fit: function( dimensions ) { 133 var width = dimensions.width, 134 height = dimensions.height, 135 maxWidth = dimensions.maxWidth, 136 maxHeight = dimensions.maxHeight, 137 constraint; 138 139 // Compare ratios between the two values to determine which 140 // max to constrain by. If a max value doesn't exist, then the 141 // opposite side is the constraint. 142 if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) { 143 constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height'; 144 } else if ( _.isUndefined( maxHeight ) ) { 145 constraint = 'width'; 146 } else if ( _.isUndefined( maxWidth ) && height > maxHeight ) { 147 constraint = 'height'; 148 } 149 150 // If the value of the constrained side is larger than the max, 151 // then scale the values. Otherwise return the originals; they fit. 152 if ( 'width' === constraint && width > maxWidth ) { 153 return { 154 width : maxWidth, 155 height: Math.round( maxWidth * height / width ) 156 }; 157 } else if ( 'height' === constraint && height > maxHeight ) { 158 return { 159 width : Math.round( maxHeight * width / height ), 160 height: maxHeight 161 }; 162 } else { 163 return { 164 width : width, 165 height: height 166 }; 167 } 168 }, 169 /** 170 * Truncates a string by injecting an ellipsis into the middle. 171 * Useful for filenames. 172 * 173 * @param {String} string 174 * @param {Number} [length=30] 175 * @param {String} [replacement=…] 176 * @returns {String} The string, unless length is greater than string.length. 177 */ 178 truncate: function( string, length, replacement ) { 179 length = length || 30; 180 replacement = replacement || '…'; 181 182 if ( string.length <= length ) { 183 return string; 184 } 185 186 return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 ); 187 } 188 }); 189 190 /** 191 * ======================================================================== 192 * MODELS 193 * ======================================================================== 194 */ 195 /** 196 * wp.media.attachment 197 * 198 * @static 199 * @param {String} id A string used to identify a model. 200 * @returns {wp.media.model.Attachment} 201 */ 202 media.attachment = function( id ) { 203 return Attachment.get( id ); 204 }; 205 206 /** 207 * A collection of all attachments that have been fetched from the server. 208 * 209 * @static 210 * @member {wp.media.model.Attachments} 211 */ 212 Attachments.all = new Attachments(); 213 214 /** 215 * wp.media.query 216 * 217 * Shorthand for creating a new Attachments Query. 218 * 219 * @param {object} [props] 220 * @returns {wp.media.model.Attachments} 221 */ 222 media.query = function( props ) { 223 return new Attachments( null, { 224 props: _.extend( _.defaults( props || {}, { orderby: 'date' } ), { query: true } ) 225 }); 226 }; 227 228 // Clean up. Prevents mobile browsers caching 229 $(window).on('unload', function(){ 230 window.wp = null; 231 }); 232 233 }(jQuery)); 234 235 },{"./models/attachment.js":2,"./models/attachments.js":3,"./models/post-image.js":4,"./models/query.js":5,"./models/selection.js":6}],2:[function(require,module,exports){ 236 /** 237 * wp.media.model.Attachment 238 * 239 * @class 240 * @augments Backbone.Model 241 */ 242 var $ = jQuery, 243 Attachment; 244 245 Attachment = Backbone.Model.extend({ 246 /** 247 * Triggered when attachment details change 248 * Overrides Backbone.Model.sync 249 * 250 * @param {string} method 251 * @param {wp.media.model.Attachment} model 252 * @param {Object} [options={}] 253 * 254 * @returns {Promise} 255 */ 256 sync: function( method, model, options ) { 257 // If the attachment does not yet have an `id`, return an instantly 258 // rejected promise. Otherwise, all of our requests will fail. 259 if ( _.isUndefined( this.id ) ) { 260 return $.Deferred().rejectWith( this ).promise(); 261 } 262 263 // Overload the `read` request so Attachment.fetch() functions correctly. 264 if ( 'read' === method ) { 265 options = options || {}; 266 options.context = this; 267 options.data = _.extend( options.data || {}, { 268 action: 'get-attachment', 269 id: this.id 270 }); 271 return wp.media.ajax( options ); 272 273 // Overload the `update` request so properties can be saved. 274 } else if ( 'update' === method ) { 275 // If we do not have the necessary nonce, fail immeditately. 276 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 277 return $.Deferred().rejectWith( this ).promise(); 278 } 279 280 options = options || {}; 281 options.context = this; 282 283 // Set the action and ID. 284 options.data = _.extend( options.data || {}, { 285 action: 'save-attachment', 286 id: this.id, 287 nonce: this.get('nonces').update, 288 post_id: wp.media.model.settings.post.id 289 }); 290 291 // Record the values of the changed attributes. 292 if ( model.hasChanged() ) { 293 options.data.changes = {}; 294 295 _.each( model.changed, function( value, key ) { 296 options.data.changes[ key ] = this.get( key ); 297 }, this ); 298 } 299 300 return wp.media.ajax( options ); 301 302 // Overload the `delete` request so attachments can be removed. 303 // This will permanently delete an attachment. 304 } else if ( 'delete' === method ) { 305 options = options || {}; 306 307 if ( ! options.wait ) { 308 this.destroyed = true; 309 } 310 311 options.context = this; 312 options.data = _.extend( options.data || {}, { 313 action: 'delete-post', 314 id: this.id, 315 _wpnonce: this.get('nonces')['delete'] 316 }); 317 318 return wp.media.ajax( options ).done( function() { 319 this.destroyed = true; 320 }).fail( function() { 321 this.destroyed = false; 322 }); 323 324 // Otherwise, fall back to `Backbone.sync()`. 325 } else { 326 /** 327 * Call `sync` directly on Backbone.Model 328 */ 329 return Backbone.Model.prototype.sync.apply( this, arguments ); 330 } 331 }, 332 /** 333 * Convert date strings into Date objects. 334 * 335 * @param {Object} resp The raw response object, typically returned by fetch() 336 * @returns {Object} The modified response object, which is the attributes hash 337 * to be set on the model. 338 */ 339 parse: function( resp ) { 340 if ( ! resp ) { 341 return resp; 342 } 343 344 resp.date = new Date( resp.date ); 345 resp.modified = new Date( resp.modified ); 346 return resp; 347 }, 348 /** 349 * @param {Object} data The properties to be saved. 350 * @param {Object} options Sync options. e.g. patch, wait, success, error. 351 * 352 * @this Backbone.Model 353 * 354 * @returns {Promise} 355 */ 356 saveCompat: function( data, options ) { 357 var model = this; 358 359 // If we do not have the necessary nonce, fail immeditately. 360 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 361 return $.Deferred().rejectWith( this ).promise(); 362 } 363 364 return media.post( 'save-attachment-compat', _.defaults({ 365 id: this.id, 366 nonce: this.get('nonces').update, 367 post_id: wp.media.model.settings.post.id 368 }, data ) ).done( function( resp, status, xhr ) { 369 model.set( model.parse( resp, xhr ), options ); 370 }); 371 } 372 }, { 373 /** 374 * Create a new model on the static 'all' attachments collection and return it. 375 * 376 * @static 377 * @param {Object} attrs 378 * @returns {wp.media.model.Attachment} 379 */ 380 create: function( attrs ) { 381 var Attachments = require( './attachments.js' ); 382 return Attachments.all.push( attrs ); 383 }, 384 /** 385 * Create a new model on the static 'all' attachments collection and return it. 386 * 387 * If this function has already been called for the id, 388 * it returns the specified attachment. 389 * 390 * @static 391 * @param {string} id A string used to identify a model. 392 * @param {Backbone.Model|undefined} attachment 393 * @returns {wp.media.model.Attachment} 394 */ 395 get: _.memoize( function( id, attachment ) { 396 var Attachments = require( './attachments.js' ); 397 return Attachments.all.push( attachment || { id: id } ); 398 }) 399 }); 400 401 module.exports = Attachment; 402 },{"./attachments.js":3}],3:[function(require,module,exports){ 403 /** 404 * wp.media.model.Attachments 405 * 406 * A collection of attachments. 407 * 408 * This collection has no persistence with the server without supplying 409 * 'options.props.query = true', which will mirror the collection 410 * to an Attachments Query collection - @see wp.media.model.Attachments.mirror(). 411 * 412 * @class 413 * @augments Backbone.Collection 414 * 415 * @param {array} [models] Models to initialize with the collection. 416 * @param {object} [options] Options hash for the collection. 417 * @param {string} [options.props] Options hash for the initial query properties. 418 * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection. 419 * @param {string} [options.props.orderby] Initial attribute key to order the collection by. 420 * @param {string} [options.props.query] Whether the collection is linked to an attachments query. 421 * @param {string} [options.observe] 422 * @param {string} [options.filters] 423 * 424 */ 425 var Attachment = require( './attachment.js' ), 426 Attachments; 427 428 Attachments = Backbone.Collection.extend({ 429 /** 430 * @type {wp.media.model.Attachment} 431 */ 432 model: Attachment, 433 /** 434 * @param {Array} [models=[]] Array of models used to populate the collection. 435 * @param {Object} [options={}] 436 */ 437 initialize: function( models, options ) { 438 options = options || {}; 439 440 this.props = new Backbone.Model(); 441 this.filters = options.filters || {}; 442 443 // Bind default `change` events to the `props` model. 444 this.props.on( 'change', this._changeFilteredProps, this ); 445 446 this.props.on( 'change:order', this._changeOrder, this ); 447 this.props.on( 'change:orderby', this._changeOrderby, this ); 448 this.props.on( 'change:query', this._changeQuery, this ); 449 450 this.props.set( _.defaults( options.props || {} ) ); 451 452 if ( options.observe ) { 453 this.observe( options.observe ); 454 } 455 }, 456 /** 457 * Sort the collection when the order attribute changes. 458 * 459 * @access private 460 */ 461 _changeOrder: function() { 462 if ( this.comparator ) { 463 this.sort(); 464 } 465 }, 466 /** 467 * Set the default comparator only when the `orderby` property is set. 468 * 469 * @access private 470 * 471 * @param {Backbone.Model} model 472 * @param {string} orderby 473 */ 474 _changeOrderby: function( model, orderby ) { 475 // If a different comparator is defined, bail. 476 if ( this.comparator && this.comparator !== Attachments.comparator ) { 477 return; 478 } 479 480 if ( orderby && 'post__in' !== orderby ) { 481 this.comparator = Attachments.comparator; 482 } else { 483 delete this.comparator; 484 } 485 }, 486 /** 487 * If the `query` property is set to true, query the server using 488 * the `props` values, and sync the results to this collection. 489 * 490 * @access private 491 * 492 * @param {Backbone.Model} model 493 * @param {Boolean} query 494 */ 495 _changeQuery: function( model, query ) { 496 if ( query ) { 497 this.props.on( 'change', this._requery, this ); 498 this._requery(); 499 } else { 500 this.props.off( 'change', this._requery, this ); 501 } 502 }, 503 /** 504 * @access private 505 * 506 * @param {Backbone.Model} model 507 */ 508 _changeFilteredProps: function( model ) { 509 // If this is a query, updating the collection will be handled by 510 // `this._requery()`. 511 if ( this.props.get('query') ) { 512 return; 513 } 514 515 var changed = _.chain( model.changed ).map( function( t, prop ) { 516 var filter = Attachments.filters[ prop ], 517 term = model.get( prop ); 518 519 if ( ! filter ) { 520 return; 521 } 522 523 if ( term && ! this.filters[ prop ] ) { 524 this.filters[ prop ] = filter; 525 } else if ( ! term && this.filters[ prop ] === filter ) { 526 delete this.filters[ prop ]; 527 } else { 528 return; 529 } 530 531 // Record the change. 532 return true; 533 }, this ).any().value(); 534 535 if ( ! changed ) { 536 return; 537 } 538 539 // If no `Attachments` model is provided to source the searches 540 // from, then automatically generate a source from the existing 541 // models. 542 if ( ! this._source ) { 543 this._source = new Attachments( this.models ); 544 } 545 546 this.reset( this._source.filter( this.validator, this ) ); 547 }, 548 549 validateDestroyed: false, 550 /** 551 * Checks whether an attachment is valid. 552 * 553 * @param {wp.media.model.Attachment} attachment 554 * @returns {Boolean} 555 */ 556 validator: function( attachment ) { 557 if ( ! this.validateDestroyed && attachment.destroyed ) { 558 return false; 559 } 560 return _.all( this.filters, function( filter ) { 561 return !! filter.call( this, attachment ); 562 }, this ); 563 }, 564 /** 565 * Add or remove an attachment to the collection depending on its validity. 566 * 567 * @param {wp.media.model.Attachment} attachment 568 * @param {Object} options 569 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 570 */ 571 validate: function( attachment, options ) { 572 var valid = this.validator( attachment ), 573 hasAttachment = !! this.get( attachment.cid ); 574 575 if ( ! valid && hasAttachment ) { 576 this.remove( attachment, options ); 577 } else if ( valid && ! hasAttachment ) { 578 this.add( attachment, options ); 579 } 580 581 return this; 582 }, 583 584 /** 585 * Add or remove all attachments from another collection depending on each one's validity. 586 * 587 * @param {wp.media.model.Attachments} attachments 588 * @param {object} [options={}] 589 * 590 * @fires wp.media.model.Attachments#reset 591 * 592 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 593 */ 594 validateAll: function( attachments, options ) { 595 options = options || {}; 596 597 _.each( attachments.models, function( attachment ) { 598 this.validate( attachment, { silent: true }); 599 }, this ); 600 601 if ( ! options.silent ) { 602 this.trigger( 'reset', this, options ); 603 } 604 return this; 605 }, 606 /** 607 * Start observing another attachments collection change events 608 * and replicate them on this collection. 609 * 610 * @param {wp.media.model.Attachments} The attachments collection to observe. 611 * @returns {wp.media.model.Attachments} Returns itself to allow chaining. 612 */ 613 observe: function( attachments ) { 614 this.observers = this.observers || []; 615 this.observers.push( attachments ); 616 617 attachments.on( 'add change remove', this._validateHandler, this ); 618 attachments.on( 'reset', this._validateAllHandler, this ); 619 this.validateAll( attachments ); 620 return this; 621 }, 622 /** 623 * Stop replicating collection change events from another attachments collection. 624 * 625 * @param {wp.media.model.Attachments} The attachments collection to stop observing. 626 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 627 */ 628 unobserve: function( attachments ) { 629 if ( attachments ) { 630 attachments.off( null, null, this ); 631 this.observers = _.without( this.observers, attachments ); 632 633 } else { 634 _.each( this.observers, function( attachments ) { 635 attachments.off( null, null, this ); 636 }, this ); 637 delete this.observers; 638 } 639 640 return this; 641 }, 642 /** 643 * @access private 644 * 645 * @param {wp.media.model.Attachments} attachment 646 * @param {wp.media.model.Attachments} attachments 647 * @param {Object} options 648 * 649 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 650 */ 651 _validateHandler: function( attachment, attachments, options ) { 652 // If we're not mirroring this `attachments` collection, 653 // only retain the `silent` option. 654 options = attachments === this.mirroring ? options : { 655 silent: options && options.silent 656 }; 657 658 return this.validate( attachment, options ); 659 }, 660 /** 661 * @access private 662 * 663 * @param {wp.media.model.Attachments} attachments 664 * @param {Object} options 665 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 666 */ 667 _validateAllHandler: function( attachments, options ) { 668 return this.validateAll( attachments, options ); 669 }, 670 /** 671 * Start mirroring another attachments collection, clearing out any models already 672 * in the collection. 673 * 674 * @param {wp.media.model.Attachments} The attachments collection to mirror. 675 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 676 */ 677 mirror: function( attachments ) { 678 if ( this.mirroring && this.mirroring === attachments ) { 679 return this; 680 } 681 682 this.unmirror(); 683 this.mirroring = attachments; 684 685 // Clear the collection silently. A `reset` event will be fired 686 // when `observe()` calls `validateAll()`. 687 this.reset( [], { silent: true } ); 688 this.observe( attachments ); 689 690 return this; 691 }, 692 /** 693 * Stop mirroring another attachments collection. 694 */ 695 unmirror: function() { 696 if ( ! this.mirroring ) { 697 return; 698 } 699 700 this.unobserve( this.mirroring ); 701 delete this.mirroring; 702 }, 703 /** 704 * Retrive more attachments from the server for the collection. 705 * 706 * Only works if the collection is mirroring a Query Attachments collection, 707 * and forwards to its `more` method. This collection class doesn't have 708 * server persistence by itself. 709 * 710 * @param {object} options 711 * @returns {Promise} 712 */ 713 more: function( options ) { 714 var deferred = jQuery.Deferred(), 715 mirroring = this.mirroring, 716 attachments = this; 717 718 if ( ! mirroring || ! mirroring.more ) { 719 return deferred.resolveWith( this ).promise(); 720 } 721 // If we're mirroring another collection, forward `more` to 722 // the mirrored collection. Account for a race condition by 723 // checking if we're still mirroring that collection when 724 // the request resolves. 725 mirroring.more( options ).done( function() { 726 if ( this === attachments.mirroring ) 727 deferred.resolveWith( this ); 728 }); 729 730 return deferred.promise(); 731 }, 732 /** 733 * Whether there are more attachments that haven't been sync'd from the server 734 * that match the collection's query. 735 * 736 * Only works if the collection is mirroring a Query Attachments collection, 737 * and forwards to its `hasMore` method. This collection class doesn't have 738 * server persistence by itself. 739 * 740 * @returns {boolean} 741 */ 742 hasMore: function() { 743 return this.mirroring ? this.mirroring.hasMore() : false; 744 }, 745 /** 746 * A custom AJAX-response parser. 747 * 748 * See trac ticket #24753 749 * 750 * @param {Object|Array} resp The raw response Object/Array. 751 * @param {Object} xhr 752 * @returns {Array} The array of model attributes to be added to the collection 753 */ 754 parse: function( resp, xhr ) { 755 if ( ! _.isArray( resp ) ) { 756 resp = [resp]; 757 } 758 759 return _.map( resp, function( attrs ) { 760 var id, attachment, newAttributes; 761 762 if ( attrs instanceof Backbone.Model ) { 763 id = attrs.get( 'id' ); 764 attrs = attrs.attributes; 765 } else { 766 id = attrs.id; 767 } 768 769 attachment = Attachment.get( id ); 770 newAttributes = attachment.parse( attrs, xhr ); 771 772 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { 773 attachment.set( newAttributes ); 774 } 775 776 return attachment; 777 }); 778 }, 779 /** 780 * If the collection is a query, create and mirror an Attachments Query collection. 781 * 782 * @access private 783 */ 784 _requery: function( refresh ) { 785 var props, Query; 786 if ( this.props.get('query') ) { 787 Query = require( './query.js' ); 788 props = this.props.toJSON(); 789 props.cache = ( true !== refresh ); 790 this.mirror( Query.get( props ) ); 791 } 792 }, 793 /** 794 * If this collection is sorted by `menuOrder`, recalculates and saves 795 * the menu order to the database. 796 * 797 * @returns {undefined|Promise} 798 */ 799 saveMenuOrder: function() { 800 if ( 'menuOrder' !== this.props.get('orderby') ) { 801 return; 802 } 803 804 // Removes any uploading attachments, updates each attachment's 805 // menu order, and returns an object with an { id: menuOrder } 806 // mapping to pass to the request. 807 var attachments = this.chain().filter( function( attachment ) { 808 return ! _.isUndefined( attachment.id ); 809 }).map( function( attachment, index ) { 810 // Indices start at 1. 811 index = index + 1; 812 attachment.set( 'menuOrder', index ); 813 return [ attachment.id, index ]; 814 }).object().value(); 815 816 if ( _.isEmpty( attachments ) ) { 817 return; 818 } 819 820 return wp.media.post( 'save-attachment-order', { 821 nonce: wp.media.model.settings.post.nonce, 822 post_id: wp.media.model.settings.post.id, 823 attachments: attachments 824 }); 825 } 826 }, { 827 /** 828 * A function to compare two attachment models in an attachments collection. 829 * 830 * Used as the default comparator for instances of wp.media.model.Attachments 831 * and its subclasses. @see wp.media.model.Attachments._changeOrderby(). 832 * 833 * @static 834 * 835 * @param {Backbone.Model} a 836 * @param {Backbone.Model} b 837 * @param {Object} options 838 * @returns {Number} -1 if the first model should come before the second, 839 * 0 if they are of the same rank and 840 * 1 if the first model should come after. 841 */ 842 comparator: function( a, b, options ) { 843 var key = this.props.get('orderby'), 844 order = this.props.get('order') || 'DESC', 845 ac = a.cid, 846 bc = b.cid; 847 848 a = a.get( key ); 849 b = b.get( key ); 850 851 if ( 'date' === key || 'modified' === key ) { 852 a = a || new Date(); 853 b = b || new Date(); 854 } 855 856 // If `options.ties` is set, don't enforce the `cid` tiebreaker. 857 if ( options && options.ties ) { 858 ac = bc = null; 859 } 860 861 return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac ); 862 }, 863 /** 864 * @namespace 865 */ 866 filters: { 867 /** 868 * @static 869 * Note that this client-side searching is *not* equivalent 870 * to our server-side searching. 871 * 872 * @param {wp.media.model.Attachment} attachment 873 * 874 * @this wp.media.model.Attachments 875 * 876 * @returns {Boolean} 877 */ 878 search: function( attachment ) { 879 if ( ! this.props.get('search') ) { 880 return true; 881 } 882 883 return _.any(['title','filename','description','caption','name'], function( key ) { 884 var value = attachment.get( key ); 885 return value && -1 !== value.search( this.props.get('search') ); 886 }, this ); 887 }, 888 /** 889 * @static 890 * @param {wp.media.model.Attachment} attachment 891 * 892 * @this wp.media.model.Attachments 893 * 894 * @returns {Boolean} 895 */ 896 type: function( attachment ) { 897 var type = this.props.get('type'); 898 return ! type || -1 !== type.indexOf( attachment.get('type') ); 899 }, 900 /** 901 * @static 902 * @param {wp.media.model.Attachment} attachment 903 * 904 * @this wp.media.model.Attachments 905 * 906 * @returns {Boolean} 907 */ 908 uploadedTo: function( attachment ) { 909 var uploadedTo = this.props.get('uploadedTo'); 910 if ( _.isUndefined( uploadedTo ) ) { 911 return true; 912 } 913 914 return uploadedTo === attachment.get('uploadedTo'); 915 }, 916 /** 917 * @static 918 * @param {wp.media.model.Attachment} attachment 919 * 920 * @this wp.media.model.Attachments 921 * 922 * @returns {Boolean} 923 */ 924 status: function( attachment ) { 925 var status = this.props.get('status'); 926 if ( _.isUndefined( status ) ) { 927 return true; 928 } 929 930 return status === attachment.get('status'); 931 } 932 } 933 }); 934 935 module.exports = Attachments; 936 },{"./attachment.js":2,"./query.js":5}],4:[function(require,module,exports){ 937 /** 938 * wp.media.model.PostImage 939 * 940 * An instance of an image that's been embedded into a post. 941 * 942 * Used in the embedded image attachment display settings modal - @see wp.media.view.MediaFrame.ImageDetails. 943 * 944 * @class 945 * @augments Backbone.Model 946 * 947 * @param {int} [attributes] Initial model attributes. 948 * @param {int} [attributes.attachment_id] ID of the attachment. 949 **/ 950 var Attachment = require( './attachment' ), 951 PostImage; 952 953 PostImage = Backbone.Model.extend({ 954 955 initialize: function( attributes ) { 956 this.attachment = false; 957 958 if ( attributes.attachment_id ) { 959 this.attachment = Attachment.get( attributes.attachment_id ); 960 if ( this.attachment.get( 'url' ) ) { 961 this.dfd = jQuery.Deferred(); 962 this.dfd.resolve(); 963 } else { 964 this.dfd = this.attachment.fetch(); 965 } 966 this.bindAttachmentListeners(); 967 } 968 969 // keep url in sync with changes to the type of link 970 this.on( 'change:link', this.updateLinkUrl, this ); 971 this.on( 'change:size', this.updateSize, this ); 972 973 this.setLinkTypeFromUrl(); 974 this.setAspectRatio(); 975 976 this.set( 'originalUrl', attributes.url ); 977 }, 978 979 bindAttachmentListeners: function() { 980 this.listenTo( this.attachment, 'sync', this.setLinkTypeFromUrl ); 981 this.listenTo( this.attachment, 'sync', this.setAspectRatio ); 982 this.listenTo( this.attachment, 'change', this.updateSize ); 983 }, 984 985 changeAttachment: function( attachment, props ) { 986 this.stopListening( this.attachment ); 987 this.attachment = attachment; 988 this.bindAttachmentListeners(); 989 990 this.set( 'attachment_id', this.attachment.get( 'id' ) ); 991 this.set( 'caption', this.attachment.get( 'caption' ) ); 992 this.set( 'alt', this.attachment.get( 'alt' ) ); 993 this.set( 'size', props.get( 'size' ) ); 994 this.set( 'align', props.get( 'align' ) ); 995 this.set( 'link', props.get( 'link' ) ); 996 this.updateLinkUrl(); 997 this.updateSize(); 998 }, 999 1000 setLinkTypeFromUrl: function() { 1001 var linkUrl = this.get( 'linkUrl' ), 1002 type; 1003 1004 if ( ! linkUrl ) { 1005 this.set( 'link', 'none' ); 1006 return; 1007 } 1008 1009 // default to custom if there is a linkUrl 1010 type = 'custom'; 1011 1012 if ( this.attachment ) { 1013 if ( this.attachment.get( 'url' ) === linkUrl ) { 1014 type = 'file'; 1015 } else if ( this.attachment.get( 'link' ) === linkUrl ) { 1016 type = 'post'; 1017 } 1018 } else { 1019 if ( this.get( 'url' ) === linkUrl ) { 1020 type = 'file'; 1021 } 1022 } 1023 1024 this.set( 'link', type ); 1025 }, 1026 1027 updateLinkUrl: function() { 1028 var link = this.get( 'link' ), 1029 url; 1030 1031 switch( link ) { 1032 case 'file': 1033 if ( this.attachment ) { 1034 url = this.attachment.get( 'url' ); 1035 } else { 1036 url = this.get( 'url' ); 1037 } 1038 this.set( 'linkUrl', url ); 1039 break; 1040 case 'post': 1041 this.set( 'linkUrl', this.attachment.get( 'link' ) ); 1042 break; 1043 case 'none': 1044 this.set( 'linkUrl', '' ); 1045 break; 1046 } 1047 }, 1048 1049 updateSize: function() { 1050 var size; 1051 1052 if ( ! this.attachment ) { 1053 return; 1054 } 1055 1056 if ( this.get( 'size' ) === 'custom' ) { 1057 this.set( 'width', this.get( 'customWidth' ) ); 1058 this.set( 'height', this.get( 'customHeight' ) ); 1059 this.set( 'url', this.get( 'originalUrl' ) ); 1060 return; 1061 } 1062 1063 size = this.attachment.get( 'sizes' )[ this.get( 'size' ) ]; 1064 1065 if ( ! size ) { 1066 return; 1067 } 1068 1069 this.set( 'url', size.url ); 1070 this.set( 'width', size.width ); 1071 this.set( 'height', size.height ); 1072 }, 1073 1074 setAspectRatio: function() { 1075 var full; 1076 1077 if ( this.attachment && this.attachment.get( 'sizes' ) ) { 1078 full = this.attachment.get( 'sizes' ).full; 1079 1080 if ( full ) { 1081 this.set( 'aspectRatio', full.width / full.height ); 1082 return; 1083 } 1084 } 1085 1086 this.set( 'aspectRatio', this.get( 'customWidth' ) / this.get( 'customHeight' ) ); 1087 } 1088 }); 1089 1090 module.exports = PostImage; 1091 },{"./attachment":2}],5:[function(require,module,exports){ 1092 /** 1093 * wp.media.model.Query 1094 * 1095 * A collection of attachments that match the supplied query arguments. 1096 * 1097 * Note: Do NOT change this.args after the query has been initialized. 1098 * Things will break. 1099 * 1100 * @class 1101 * @augments wp.media.model.Attachments 1102 * @augments Backbone.Collection 1103 * 1104 * @param {array} [models] Models to initialize with the collection. 1105 * @param {object} [options] Options hash. 1106 * @param {object} [options.args] Attachments query arguments. 1107 * @param {object} [options.args.posts_per_page] 1108 */ 1109 var Attachments = require( './attachments.js' ), 1110 Query; 1111 1112 Query = Attachments.extend({ 1113 /** 1114 * @global wp.Uploader 1115 * 1116 * @param {array} [models=[]] Array of initial models to populate the collection. 1117 * @param {object} [options={}] 1118 */ 1119 initialize: function( models, options ) { 1120 var allowed; 1121 1122 options = options || {}; 1123 Attachments.prototype.initialize.apply( this, arguments ); 1124 1125 this.args = options.args; 1126 this._hasMore = true; 1127 this.created = new Date(); 1128 1129 this.filters.order = function( attachment ) { 1130 var orderby = this.props.get('orderby'), 1131 order = this.props.get('order'); 1132 1133 if ( ! this.comparator ) { 1134 return true; 1135 } 1136 1137 // We want any items that can be placed before the last 1138 // item in the set. If we add any items after the last 1139 // item, then we can't guarantee the set is complete. 1140 if ( this.length ) { 1141 return 1 !== this.comparator( attachment, this.last(), { ties: true }); 1142 1143 // Handle the case where there are no items yet and 1144 // we're sorting for recent items. In that case, we want 1145 // changes that occurred after we created the query. 1146 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { 1147 return attachment.get( orderby ) >= this.created; 1148 1149 // If we're sorting by menu order and we have no items, 1150 // accept any items that have the default menu order (0). 1151 } else if ( 'ASC' === order && 'menuOrder' === orderby ) { 1152 return attachment.get( orderby ) === 0; 1153 } 1154 1155 // Otherwise, we don't want any items yet. 1156 return false; 1157 }; 1158 1159 // Observe the central `wp.Uploader.queue` collection to watch for 1160 // new matches for the query. 1161 // 1162 // Only observe when a limited number of query args are set. There 1163 // are no filters for other properties, so observing will result in 1164 // false positives in those queries. 1165 allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; 1166 if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { 1167 this.observe( wp.Uploader.queue ); 1168 } 1169 }, 1170 /** 1171 * Whether there are more attachments that haven't been sync'd from the server 1172 * that match the collection's query. 1173 * 1174 * @returns {boolean} 1175 */ 1176 hasMore: function() { 1177 return this._hasMore; 1178 }, 1179 /** 1180 * Fetch more attachments from the server for the collection. 1181 * 1182 * @param {object} [options={}] 1183 * @returns {Promise} 1184 */ 1185 more: function( options ) { 1186 var query = this; 1187 1188 // If there is already a request pending, return early with the Deferred object. 1189 if ( this._more && 'pending' === this._more.state() ) { 1190 return this._more; 1191 } 1192 1193 if ( ! this.hasMore() ) { 1194 return jQuery.Deferred().resolveWith( this ).promise(); 1195 } 1196 1197 options = options || {}; 1198 options.remove = false; 1199 1200 return this._more = this.fetch( options ).done( function( resp ) { 1201 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { 1202 query._hasMore = false; 1203 } 1204 }); 1205 }, 1206 /** 1207 * Overrides Backbone.Collection.sync 1208 * Overrides wp.media.model.Attachments.sync 1209 * 1210 * @param {String} method 1211 * @param {Backbone.Model} model 1212 * @param {Object} [options={}] 1213 * @returns {Promise} 1214 */ 1215 sync: function( method, model, options ) { 1216 var args, fallback; 1217 1218 // Overload the read method so Attachment.fetch() functions correctly. 1219 if ( 'read' === method ) { 1220 options = options || {}; 1221 options.context = this; 1222 options.data = _.extend( options.data || {}, { 1223 action: 'query-attachments', 1224 post_id: wp.media.model.settings.post.id 1225 }); 1226 1227 // Clone the args so manipulation is non-destructive. 1228 args = _.clone( this.args ); 1229 1230 // Determine which page to query. 1231 if ( -1 !== args.posts_per_page ) { 1232 args.paged = Math.floor( this.length / args.posts_per_page ) + 1; 1233 } 1234 1235 options.data.query = args; 1236 return wp.media.ajax( options ); 1237 1238 // Otherwise, fall back to Backbone.sync() 1239 } else { 1240 /** 1241 * Call wp.media.model.Attachments.sync or Backbone.sync 1242 */ 1243 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; 1244 return fallback.sync.apply( this, arguments ); 1245 } 1246 } 1247 }, { 1248 /** 1249 * @readonly 1250 */ 1251 defaultProps: { 1252 orderby: 'date', 1253 order: 'DESC' 1254 }, 1255 /** 1256 * @readonly 1257 */ 1258 defaultArgs: { 1259 posts_per_page: 40 1260 }, 1261 /** 1262 * @readonly 1263 */ 1264 orderby: { 1265 allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], 1266 /** 1267 * A map of JavaScript orderby values to their WP_Query equivalents. 1268 * @type {Object} 1269 */ 1270 valuemap: { 1271 'id': 'ID', 1272 'uploadedTo': 'parent', 1273 'menuOrder': 'menu_order ID' 1274 } 1275 }, 1276 /** 1277 * A map of JavaScript query properties to their WP_Query equivalents. 1278 * 1279 * @readonly 1280 */ 1281 propmap: { 1282 'search': 's', 1283 'type': 'post_mime_type', 1284 'perPage': 'posts_per_page', 1285 'menuOrder': 'menu_order', 1286 'uploadedTo': 'post_parent', 1287 'status': 'post_status', 1288 'include': 'post__in', 1289 'exclude': 'post__not_in' 1290 }, 1291 /** 1292 * Creates and returns an Attachments Query collection given the properties. 1293 * 1294 * Caches query objects and reuses where possible. 1295 * 1296 * @static 1297 * @method 1298 * 1299 * @param {object} [props] 1300 * @param {Object} [props.cache=true] Whether to use the query cache or not. 1301 * @param {Object} [props.order] 1302 * @param {Object} [props.orderby] 1303 * @param {Object} [props.include] 1304 * @param {Object} [props.exclude] 1305 * @param {Object} [props.s] 1306 * @param {Object} [props.post_mime_type] 1307 * @param {Object} [props.posts_per_page] 1308 * @param {Object} [props.menu_order] 1309 * @param {Object} [props.post_parent] 1310 * @param {Object} [props.post_status] 1311 * @param {Object} [options] 1312 * 1313 * @returns {wp.media.model.Query} A new Attachments Query collection. 1314 */ 1315 get: (function(){ 1316 /** 1317 * @static 1318 * @type Array 1319 */ 1320 var queries = []; 1321 1322 /** 1323 * @returns {Query} 1324 */ 1325 return function( props, options ) { 1326 var args = {}, 1327 orderby = Query.orderby, 1328 defaults = Query.defaultProps, 1329 query, 1330 cache = !! props.cache || _.isUndefined( props.cache ); 1331 1332 // Remove the `query` property. This isn't linked to a query, 1333 // this *is* the query. 1334 delete props.query; 1335 delete props.cache; 1336 1337 // Fill default args. 1338 _.defaults( props, defaults ); 1339 1340 // Normalize the order. 1341 props.order = props.order.toUpperCase(); 1342 if ( 'DESC' !== props.order && 'ASC' !== props.order ) { 1343 props.order = defaults.order.toUpperCase(); 1344 } 1345 1346 // Ensure we have a valid orderby value. 1347 if ( ! _.contains( orderby.allowed, props.orderby ) ) { 1348 props.orderby = defaults.orderby; 1349 } 1350 1351 _.each( [ 'include', 'exclude' ], function( prop ) { 1352 if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { 1353 props[ prop ] = [ props[ prop ] ]; 1354 } 1355 } ); 1356 1357 // Generate the query `args` object. 1358 // Correct any differing property names. 1359 _.each( props, function( value, prop ) { 1360 if ( _.isNull( value ) ) { 1361 return; 1362 } 1363 1364 args[ Query.propmap[ prop ] || prop ] = value; 1365 }); 1366 1367 // Fill any other default query args. 1368 _.defaults( args, Query.defaultArgs ); 1369 1370 // `props.orderby` does not always map directly to `args.orderby`. 1371 // Substitute exceptions specified in orderby.keymap. 1372 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; 1373 1374 // Search the query cache for a matching query. 1375 if ( cache ) { 1376 query = _.find( queries, function( query ) { 1377 return _.isEqual( query.args, args ); 1378 }); 1379 } else { 1380 queries = []; 1381 } 1382 1383 // Otherwise, create a new query and add it to the cache. 1384 if ( ! query ) { 1385 query = new Query( [], _.extend( options || {}, { 1386 props: props, 1387 args: args 1388 } ) ); 1389 queries.push( query ); 1390 } 1391 1392 return query; 1393 }; 1394 }()) 1395 }); 1396 1397 module.exports = Query; 1398 },{"./attachments.js":3}],6:[function(require,module,exports){ 1399 /** 1400 * wp.media.model.Selection 1401 * 1402 * A selection of attachments. 1403 * 1404 * @class 1405 * @augments wp.media.model.Attachments 1406 * @augments Backbone.Collection 1407 */ 1408 var Attachments = require( './attachments.js' ), 1409 Selection; 1410 1411 Selection = Attachments.extend({ 1412 /** 1413 * Refresh the `single` model whenever the selection changes. 1414 * Binds `single` instead of using the context argument to ensure 1415 * it receives no parameters. 1416 * 1417 * @param {Array} [models=[]] Array of models used to populate the collection. 1418 * @param {Object} [options={}] 1419 */ 1420 initialize: function( models, options ) { 1421 /** 1422 * call 'initialize' directly on the parent class 1423 */ 1424 Attachments.prototype.initialize.apply( this, arguments ); 1425 this.multiple = options && options.multiple; 1426 1427 this.on( 'add remove reset', _.bind( this.single, this, false ) ); 1428 }, 1429 1430 /** 1431 * If the workflow does not support multi-select, clear out the selection 1432 * before adding a new attachment to it. 1433 * 1434 * @param {Array} models 1435 * @param {Object} options 1436 * @returns {wp.media.model.Attachment[]} 1437 */ 1438 add: function( models, options ) { 1439 if ( ! this.multiple ) { 1440 this.remove( this.models ); 1441 } 1442 /** 1443 * call 'add' directly on the parent class 1444 */ 1445 return Attachments.prototype.add.call( this, models, options ); 1446 }, 1447 1448 /** 1449 * Fired when toggling (clicking on) an attachment in the modal. 1450 * 1451 * @param {undefined|boolean|wp.media.model.Attachment} model 1452 * 1453 * @fires wp.media.model.Selection#selection:single 1454 * @fires wp.media.model.Selection#selection:unsingle 1455 * 1456 * @returns {Backbone.Model} 1457 */ 1458 single: function( model ) { 1459 var previous = this._single; 1460 1461 // If a `model` is provided, use it as the single model. 1462 if ( model ) { 1463 this._single = model; 1464 } 1465 // If the single model isn't in the selection, remove it. 1466 if ( this._single && ! this.get( this._single.cid ) ) { 1467 delete this._single; 1468 } 1469 1470 this._single = this._single || this.last(); 1471 1472 // If single has changed, fire an event. 1473 if ( this._single !== previous ) { 1474 if ( previous ) { 1475 previous.trigger( 'selection:unsingle', previous, this ); 1476 1477 // If the model was already removed, trigger the collection 1478 // event manually. 1479 if ( ! this.get( previous.cid ) ) { 1480 this.trigger( 'selection:unsingle', previous, this ); 1481 } 1482 } 1483 if ( this._single ) { 1484 this._single.trigger( 'selection:single', this._single, this ); 1485 } 1486 } 1487 1488 // Return the single model, or the last model as a fallback. 1489 return this._single; 1490 } 1491 }); 1492 1493 module.exports = Selection; 1494 },{"./attachments.js":3}]},{},[1]); -
src/wp-includes/js/media/models.manifest.js
1 /* global _wpMediaModelsL10n:false */ 2 window.wp = window.wp || {}; 3 4 (function($){ 5 var Attachment, Attachments, l10n, media; 6 7 /** 8 * Create and return a media frame. 9 * 10 * Handles the default media experience. 11 * 12 * @param {object} attributes The properties passed to the main media controller. 13 * @return {wp.media.view.MediaFrame} A media workflow. 14 */ 15 media = wp.media = function( attributes ) { 16 var MediaFrame = media.view.MediaFrame, 17 frame; 18 19 if ( ! MediaFrame ) { 20 return; 21 } 22 23 attributes = _.defaults( attributes || {}, { 24 frame: 'select' 25 }); 26 27 if ( 'select' === attributes.frame && MediaFrame.Select ) { 28 frame = new MediaFrame.Select( attributes ); 29 } else if ( 'post' === attributes.frame && MediaFrame.Post ) { 30 frame = new MediaFrame.Post( attributes ); 31 } else if ( 'manage' === attributes.frame && MediaFrame.Manage ) { 32 frame = new MediaFrame.Manage( attributes ); 33 } else if ( 'image' === attributes.frame && MediaFrame.ImageDetails ) { 34 frame = new MediaFrame.ImageDetails( attributes ); 35 } else if ( 'audio' === attributes.frame && MediaFrame.AudioDetails ) { 36 frame = new MediaFrame.AudioDetails( attributes ); 37 } else if ( 'video' === attributes.frame && MediaFrame.VideoDetails ) { 38 frame = new MediaFrame.VideoDetails( attributes ); 39 } else if ( 'edit-attachments' === attributes.frame && MediaFrame.EditAttachments ) { 40 frame = new MediaFrame.EditAttachments( attributes ); 41 } 42 43 delete attributes.frame; 44 45 media.frame = frame; 46 47 return frame; 48 }; 49 50 _.extend( media, { model: {}, view: {}, controller: {}, frames: {} }); 51 52 // Link any localized strings. 53 l10n = media.model.l10n = typeof _wpMediaModelsL10n === 'undefined' ? {} : _wpMediaModelsL10n; 54 55 // Link any settings. 56 media.model.settings = l10n.settings || {}; 57 delete l10n.settings; 58 59 Attachments = media.model.Attachments = require( './models/attachments.js' ); 60 Attachment = media.model.Attachment = require( './models/attachment.js' ); 61 62 media.model.Query = require( './models/query.js' ); 63 media.model.PostImage = require( './models/post-image.js' ); 64 media.model.Selection = require( './models/selection.js' ); 65 66 /** 67 * ======================================================================== 68 * UTILITIES 69 * ======================================================================== 70 */ 71 72 /** 73 * A basic equality comparator for Backbone models. 74 * 75 * Used to order models within a collection - @see wp.media.model.Attachments.comparator(). 76 * 77 * @param {mixed} a The primary parameter to compare. 78 * @param {mixed} b The primary parameter to compare. 79 * @param {string} ac The fallback parameter to compare, a's cid. 80 * @param {string} bc The fallback parameter to compare, b's cid. 81 * @return {number} -1: a should come before b. 82 * 0: a and b are of the same rank. 83 * 1: b should come before a. 84 */ 85 media.compare = function( a, b, ac, bc ) { 86 if ( _.isEqual( a, b ) ) { 87 return ac === bc ? 0 : (ac > bc ? -1 : 1); 88 } else { 89 return a > b ? -1 : 1; 90 } 91 }; 92 93 _.extend( media, { 94 /** 95 * media.template( id ) 96 * 97 * Fetch a JavaScript template for an id, and return a templating function for it. 98 * 99 * See wp.template() in `wp-includes/js/wp-util.js`. 100 * 101 * @borrows wp.template as template 102 */ 103 template: wp.template, 104 105 /** 106 * media.post( [action], [data] ) 107 * 108 * Sends a POST request to WordPress. 109 * See wp.ajax.post() in `wp-includes/js/wp-util.js`. 110 * 111 * @borrows wp.ajax.post as post 112 */ 113 post: wp.ajax.post, 114 115 /** 116 * media.ajax( [action], [options] ) 117 * 118 * Sends an XHR request to WordPress. 119 * See wp.ajax.send() in `wp-includes/js/wp-util.js`. 120 * 121 * @borrows wp.ajax.send as ajax 122 */ 123 ajax: wp.ajax.send, 124 125 /** 126 * Scales a set of dimensions to fit within bounding dimensions. 127 * 128 * @param {Object} dimensions 129 * @returns {Object} 130 */ 131 fit: function( dimensions ) { 132 var width = dimensions.width, 133 height = dimensions.height, 134 maxWidth = dimensions.maxWidth, 135 maxHeight = dimensions.maxHeight, 136 constraint; 137 138 // Compare ratios between the two values to determine which 139 // max to constrain by. If a max value doesn't exist, then the 140 // opposite side is the constraint. 141 if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) { 142 constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height'; 143 } else if ( _.isUndefined( maxHeight ) ) { 144 constraint = 'width'; 145 } else if ( _.isUndefined( maxWidth ) && height > maxHeight ) { 146 constraint = 'height'; 147 } 148 149 // If the value of the constrained side is larger than the max, 150 // then scale the values. Otherwise return the originals; they fit. 151 if ( 'width' === constraint && width > maxWidth ) { 152 return { 153 width : maxWidth, 154 height: Math.round( maxWidth * height / width ) 155 }; 156 } else if ( 'height' === constraint && height > maxHeight ) { 157 return { 158 width : Math.round( maxHeight * width / height ), 159 height: maxHeight 160 }; 161 } else { 162 return { 163 width : width, 164 height: height 165 }; 166 } 167 }, 168 /** 169 * Truncates a string by injecting an ellipsis into the middle. 170 * Useful for filenames. 171 * 172 * @param {String} string 173 * @param {Number} [length=30] 174 * @param {String} [replacement=…] 175 * @returns {String} The string, unless length is greater than string.length. 176 */ 177 truncate: function( string, length, replacement ) { 178 length = length || 30; 179 replacement = replacement || '…'; 180 181 if ( string.length <= length ) { 182 return string; 183 } 184 185 return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 ); 186 } 187 }); 188 189 /** 190 * ======================================================================== 191 * MODELS 192 * ======================================================================== 193 */ 194 /** 195 * wp.media.attachment 196 * 197 * @static 198 * @param {String} id A string used to identify a model. 199 * @returns {wp.media.model.Attachment} 200 */ 201 media.attachment = function( id ) { 202 return Attachment.get( id ); 203 }; 204 205 /** 206 * A collection of all attachments that have been fetched from the server. 207 * 208 * @static 209 * @member {wp.media.model.Attachments} 210 */ 211 Attachments.all = new Attachments(); 212 213 /** 214 * wp.media.query 215 * 216 * Shorthand for creating a new Attachments Query. 217 * 218 * @param {object} [props] 219 * @returns {wp.media.model.Attachments} 220 */ 221 media.query = function( props ) { 222 return new Attachments( null, { 223 props: _.extend( _.defaults( props || {}, { orderby: 'date' } ), { query: true } ) 224 }); 225 }; 226 227 // Clean up. Prevents mobile browsers caching 228 $(window).on('unload', function(){ 229 window.wp = null; 230 }); 231 232 }(jQuery)); -
src/wp-includes/js/media/router/manage.js
1 /** 2 * A router for handling the browser history and application state. 3 * 4 * @constructor 5 * @augments Backbone.Router 6 */ 7 var Router = Backbone.Router.extend({ 8 routes: { 9 'upload.php?item=:slug': 'showItem', 10 'upload.php?search=:query': 'search' 11 }, 12 13 // Map routes against the page URL 14 baseUrl: function( url ) { 15 return 'upload.php' + url; 16 }, 17 18 // Respond to the search route by filling the search field and trigggering the input event 19 search: function( query ) { 20 jQuery( '#media-search-input' ).val( query ).trigger( 'input' ); 21 }, 22 23 // Show the modal with a specific item 24 showItem: function( query ) { 25 var media = wp.media, 26 library = media.frame.state().get('library'), 27 item; 28 29 // Trigger the media frame to open the correct item 30 item = library.findWhere( { id: parseInt( query, 10 ) } ); 31 if ( item ) { 32 media.frame.trigger( 'edit:attachment', item ); 33 } else { 34 item = media.attachment( query ); 35 media.frame.listenTo( item, 'change', function( model ) { 36 media.frame.stopListening( item ); 37 media.frame.trigger( 'edit:attachment', model ); 38 } ); 39 item.fetch(); 40 } 41 } 42 }); 43 44 module.exports = Router; 45 No newline at end of file -
src/wp-includes/js/media/utils/selection-sync.js
1 /** 2 * wp.media.selectionSync 3 * 4 * Sync an attachments selection in a state with another state. 5 * 6 * Allows for selecting multiple images in the Insert Media workflow, and then 7 * switching to the Insert Gallery workflow while preserving the attachments selection. 8 * 9 * @mixin 10 */ 11 var selectionSync = { 12 /** 13 * @since 3.5.0 14 */ 15 syncSelection: function() { 16 var selection = this.get('selection'), 17 manager = this.frame._selection; 18 19 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 20 return; 21 } 22 23 // If the selection supports multiple items, validate the stored 24 // attachments based on the new selection's conditions. Record 25 // the attachments that are not included; we'll maintain a 26 // reference to those. Other attachments are considered in flux. 27 if ( selection.multiple ) { 28 selection.reset( [], { silent: true }); 29 selection.validateAll( manager.attachments ); 30 manager.difference = _.difference( manager.attachments.models, selection.models ); 31 } 32 33 // Sync the selection's single item with the master. 34 selection.single( manager.single ); 35 }, 36 37 /** 38 * Record the currently active attachments, which is a combination 39 * of the selection's attachments and the set of selected 40 * attachments that this specific selection considered invalid. 41 * Reset the difference and record the single attachment. 42 * 43 * @since 3.5.0 44 */ 45 recordSelection: function() { 46 var selection = this.get('selection'), 47 manager = this.frame._selection; 48 49 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 50 return; 51 } 52 53 if ( selection.multiple ) { 54 manager.attachments.reset( selection.toArray().concat( manager.difference ) ); 55 manager.difference = []; 56 } else { 57 manager.attachments.add( selection.toArray() ); 58 } 59 60 manager.single = selection._single; 61 } 62 }; 63 64 module.exports = selectionSync; 65 No newline at end of file -
src/wp-includes/js/media/views/attachment/details-two-column.js
1 /** 2 * A similar view to media.view.Attachment.Details 3 * for use in the Edit Attachment modal. 4 * 5 * @constructor 6 * @augments wp.media.view.Attachment.Details 7 * @augments wp.media.view.Attachment 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 */ 12 var Details = require( './details.js' ), 13 MediaDetails = require( '../media-details.js' ), 14 TwoColumn; 15 16 TwoColumn = Details.extend({ 17 template: wp.template( 'attachment-details-two-column' ), 18 19 editAttachment: function( event ) { 20 event.preventDefault(); 21 this.controller.content.mode( 'edit-image' ); 22 }, 23 24 /** 25 * Noop this from parent class, doesn't apply here. 26 */ 27 toggleSelectionHandler: function() {}, 28 29 render: function() { 30 Details.prototype.render.apply( this, arguments ); 31 32 wp.media.mixin.removeAllPlayers(); 33 this.$( 'audio, video' ).each( function (i, elem) { 34 var el = MediaDetails.prepareSrc( elem ); 35 new MediaElementPlayer( el, wp.media.mixin.mejsSettings ); 36 } ); 37 } 38 }); 39 40 module.exports = TwoColumn; 41 No newline at end of file -
src/wp-includes/js/media/views/attachment/details.js
1 /** 2 * wp.media.view.Attachment.Details 3 * 4 * @class 5 * @augments wp.media.view.Attachment 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Attachment = require( '../attachment.js' ), 11 l10n = wp.media.view.l10n, 12 Details; 13 14 Details = Attachment.extend({ 15 tagName: 'div', 16 className: 'attachment-details', 17 template: wp.template('attachment-details'), 18 19 attributes: function() { 20 return { 21 'tabIndex': 0, 22 'data-id': this.model.get( 'id' ) 23 }; 24 }, 25 26 events: { 27 'change [data-setting]': 'updateSetting', 28 'change [data-setting] input': 'updateSetting', 29 'change [data-setting] select': 'updateSetting', 30 'change [data-setting] textarea': 'updateSetting', 31 'click .delete-attachment': 'deleteAttachment', 32 'click .trash-attachment': 'trashAttachment', 33 'click .untrash-attachment': 'untrashAttachment', 34 'click .edit-attachment': 'editAttachment', 35 'click .refresh-attachment': 'refreshAttachment', 36 'keydown': 'toggleSelectionHandler' 37 }, 38 39 initialize: function() { 40 this.options = _.defaults( this.options, { 41 rerenderOnModelChange: false 42 }); 43 44 this.on( 'ready', this.initialFocus ); 45 // Call 'initialize' directly on the parent class. 46 Attachment.prototype.initialize.apply( this, arguments ); 47 }, 48 49 initialFocus: function() { 50 if ( ! wp.media.isTouchDevice ) { 51 this.$( ':input' ).eq( 0 ).focus(); 52 } 53 }, 54 /** 55 * @param {Object} event 56 */ 57 deleteAttachment: function( event ) { 58 event.preventDefault(); 59 60 if ( confirm( l10n.warnDelete ) ) { 61 this.model.destroy(); 62 // Keep focus inside media modal 63 // after image is deleted 64 this.controller.modal.focusManager.focus(); 65 } 66 }, 67 /** 68 * @param {Object} event 69 */ 70 trashAttachment: function( event ) { 71 var library = this.controller.library; 72 event.preventDefault(); 73 74 if ( wp.media.view.settings.mediaTrash && 75 'edit-metadata' === this.controller.content.mode() ) { 76 77 this.model.set( 'status', 'trash' ); 78 this.model.save().done( function() { 79 library._requery( true ); 80 } ); 81 } else { 82 this.model.destroy(); 83 } 84 }, 85 /** 86 * @param {Object} event 87 */ 88 untrashAttachment: function( event ) { 89 var library = this.controller.library; 90 event.preventDefault(); 91 92 this.model.set( 'status', 'inherit' ); 93 this.model.save().done( function() { 94 library._requery( true ); 95 } ); 96 }, 97 /** 98 * @param {Object} event 99 */ 100 editAttachment: function( event ) { 101 var editState = this.controller.states.get( 'edit-image' ); 102 if ( window.imageEdit && editState ) { 103 event.preventDefault(); 104 105 editState.set( 'image', this.model ); 106 this.controller.setState( 'edit-image' ); 107 } else { 108 this.$el.addClass('needs-refresh'); 109 } 110 }, 111 /** 112 * @param {Object} event 113 */ 114 refreshAttachment: function( event ) { 115 this.$el.removeClass('needs-refresh'); 116 event.preventDefault(); 117 this.model.fetch(); 118 }, 119 /** 120 * When reverse tabbing(shift+tab) out of the right details panel, deliver 121 * the focus to the item in the list that was being edited. 122 * 123 * @param {Object} event 124 */ 125 toggleSelectionHandler: function( event ) { 126 if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { 127 this.controller.trigger( 'attachment:details:shift-tab', event ); 128 return false; 129 } 130 131 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 132 this.controller.trigger( 'attachment:keydown:arrow', event ); 133 return; 134 } 135 } 136 }); 137 138 module.exports = Details; 139 No newline at end of file -
src/wp-includes/js/media/views/attachment/edit-library.js
1 /** 2 * wp.media.view.Attachment.EditLibrary 3 * 4 * @class 5 * @augments wp.media.view.Attachment 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Attachment = require( '../attachment.js' ), 11 EditLibrary; 12 13 EditLibrary = Attachment.extend({ 14 buttons: { 15 close: true 16 } 17 }); 18 19 module.exports = EditLibrary; 20 No newline at end of file -
src/wp-includes/js/media/views/attachment/edit-selection.js
1 /** 2 * wp.media.view.Attachments.EditSelection 3 * 4 * @class 5 * @augments wp.media.view.Attachment.Selection 6 * @augments wp.media.view.Attachment 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var Selection = require( './selection.js' ), 12 EditSelection; 13 14 EditSelection = Selection.extend({ 15 buttons: { 16 close: true 17 } 18 }); 19 20 module.exports = EditSelection; 21 No newline at end of file -
src/wp-includes/js/media/views/attachment/library.js
1 /** 2 * wp.media.view.Attachment.Library 3 * 4 * @class 5 * @augments wp.media.view.Attachment 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Attachment = require( '../attachment.js' ), 11 Library; 12 13 Library = Attachment.extend({ 14 buttons: { 15 check: true 16 } 17 }); 18 19 module.exports = Library; 20 No newline at end of file -
src/wp-includes/js/media/views/attachment/selection.js
1 /** 2 * wp.media.view.Attachment.Selection 3 * 4 * @class 5 * @augments wp.media.view.Attachment 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Attachment = require( '../attachment.js' ), 11 Selection; 12 13 Selection = Attachment.extend({ 14 className: 'attachment selection', 15 16 // On click, just select the model, instead of removing the model from 17 // the selection. 18 toggleSelection: function() { 19 this.options.selection.single( this.model ); 20 } 21 }); 22 23 module.exports = Selection; 24 No newline at end of file -
src/wp-includes/js/media/views/attachment-compat.js
1 /** 2 * wp.media.view.AttachmentCompat 3 * 4 * A view to display fields added via the `attachment_fields_to_edit` filter. 5 * 6 * @class 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var View = require( './view.js' ), 12 AttachmentCompat; 13 14 AttachmentCompat = View.extend({ 15 tagName: 'form', 16 className: 'compat-item', 17 18 events: { 19 'submit': 'preventDefault', 20 'change input': 'save', 21 'change select': 'save', 22 'change textarea': 'save' 23 }, 24 25 initialize: function() { 26 this.model.on( 'change:compat', this.render, this ); 27 }, 28 /** 29 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 30 */ 31 dispose: function() { 32 if ( this.$(':focus').length ) { 33 this.save(); 34 } 35 /** 36 * call 'dispose' directly on the parent class 37 */ 38 return View.prototype.dispose.apply( this, arguments ); 39 }, 40 /** 41 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 42 */ 43 render: function() { 44 var compat = this.model.get('compat'); 45 if ( ! compat || ! compat.item ) { 46 return; 47 } 48 49 this.views.detach(); 50 this.$el.html( compat.item ); 51 this.views.render(); 52 return this; 53 }, 54 /** 55 * @param {Object} event 56 */ 57 preventDefault: function( event ) { 58 event.preventDefault(); 59 }, 60 /** 61 * @param {Object} event 62 */ 63 save: function( event ) { 64 var data = {}; 65 66 if ( event ) { 67 event.preventDefault(); 68 } 69 70 _.each( this.$el.serializeArray(), function( pair ) { 71 data[ pair.name ] = pair.value; 72 }); 73 74 this.controller.trigger( 'attachment:compat:waiting', ['waiting'] ); 75 this.model.saveCompat( data ).always( _.bind( this.postSave, this ) ); 76 }, 77 78 postSave: function() { 79 this.controller.trigger( 'attachment:compat:ready', ['ready'] ); 80 } 81 }); 82 83 module.exports = AttachmentCompat; 84 No newline at end of file -
src/wp-includes/js/media/views/attachment-filters/all.js
1 /** 2 * wp.media.view.AttachmentFilters.All 3 * 4 * @class 5 * @augments wp.media.view.AttachmentFilters 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var AttachmentFilters = require( '../attachment-filters.js' ), 11 l10n = wp.media.view.l10n, 12 All; 13 14 All = AttachmentFilters.extend({ 15 createFilters: function() { 16 var filters = {}; 17 18 _.each( wp.media.view.settings.mimeTypes || {}, function( text, key ) { 19 filters[ key ] = { 20 text: text, 21 props: { 22 status: null, 23 type: key, 24 uploadedTo: null, 25 orderby: 'date', 26 order: 'DESC' 27 } 28 }; 29 }); 30 31 filters.all = { 32 text: l10n.allMediaItems, 33 props: { 34 status: null, 35 type: null, 36 uploadedTo: null, 37 orderby: 'date', 38 order: 'DESC' 39 }, 40 priority: 10 41 }; 42 43 if ( wp.media.view.settings.post.id ) { 44 filters.uploaded = { 45 text: l10n.uploadedToThisPost, 46 props: { 47 status: null, 48 type: null, 49 uploadedTo: wp.media.view.settings.post.id, 50 orderby: 'menuOrder', 51 order: 'ASC' 52 }, 53 priority: 20 54 }; 55 } 56 57 filters.unattached = { 58 text: l10n.unattached, 59 props: { 60 status: null, 61 uploadedTo: 0, 62 type: null, 63 orderby: 'menuOrder', 64 order: 'ASC' 65 }, 66 priority: 50 67 }; 68 69 if ( wp.media.view.settings.mediaTrash && 70 this.controller.isModeActive( 'grid' ) ) { 71 72 filters.trash = { 73 text: l10n.trash, 74 props: { 75 uploadedTo: null, 76 status: 'trash', 77 type: null, 78 orderby: 'date', 79 order: 'DESC' 80 }, 81 priority: 50 82 }; 83 } 84 85 this.filters = filters; 86 } 87 }); 88 89 module.exports = All; 90 No newline at end of file -
src/wp-includes/js/media/views/attachment-filters/date.js
1 /** 2 * A filter dropdown for month/dates. 3 * 4 * @class 5 * @augments wp.media.view.AttachmentFilters 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var AttachmentFilters = require( '../attachment-filters.js' ), 11 l10n = wp.media.view.l10n, 12 DateFilter; 13 14 DateFilter = AttachmentFilters.extend({ 15 id: 'media-attachment-date-filters', 16 17 createFilters: function() { 18 var filters = {}; 19 _.each( wp.media.view.settings.months || {}, function( value, index ) { 20 filters[ index ] = { 21 text: value.text, 22 props: { 23 year: value.year, 24 monthnum: value.month 25 } 26 }; 27 }); 28 filters.all = { 29 text: l10n.allDates, 30 props: { 31 monthnum: false, 32 year: false 33 }, 34 priority: 10 35 }; 36 this.filters = filters; 37 } 38 }); 39 40 module.exports = DateFilter; 41 No newline at end of file -
src/wp-includes/js/media/views/attachment-filters/uploaded.js
1 /** 2 * wp.media.view.AttachmentFilters.Uploaded 3 * 4 * @class 5 * @augments wp.media.view.AttachmentFilters 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var AttachmentFilters = require( '../attachment-filters.js' ), 11 l10n = wp.media.view.l10n, 12 Uploaded; 13 14 Uploaded = AttachmentFilters.extend({ 15 createFilters: function() { 16 var type = this.model.get('type'), 17 types = wp.media.view.settings.mimeTypes, 18 text; 19 20 if ( types && type ) { 21 text = types[ type ]; 22 } 23 24 this.filters = { 25 all: { 26 text: text || l10n.allMediaItems, 27 props: { 28 uploadedTo: null, 29 orderby: 'date', 30 order: 'DESC' 31 }, 32 priority: 10 33 }, 34 35 uploaded: { 36 text: l10n.uploadedToThisPost, 37 props: { 38 uploadedTo: wp.media.view.settings.post.id, 39 orderby: 'menuOrder', 40 order: 'ASC' 41 }, 42 priority: 20 43 }, 44 45 unattached: { 46 text: l10n.unattached, 47 props: { 48 uploadedTo: 0, 49 orderby: 'menuOrder', 50 order: 'ASC' 51 }, 52 priority: 50 53 } 54 }; 55 } 56 }); 57 58 module.exports = Uploaded; 59 No newline at end of file -
src/wp-includes/js/media/views/attachment-filters.js
1 /** 2 * wp.media.view.AttachmentFilters 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 $ = jQuery, 11 AttachmentFilters; 12 13 AttachmentFilters = View.extend({ 14 tagName: 'select', 15 className: 'attachment-filters', 16 id: 'media-attachment-filters', 17 18 events: { 19 change: 'change' 20 }, 21 22 keys: [], 23 24 initialize: function() { 25 this.createFilters(); 26 _.extend( this.filters, this.options.filters ); 27 28 // Build `<option>` elements. 29 this.$el.html( _.chain( this.filters ).map( function( filter, value ) { 30 return { 31 el: $( '<option></option>' ).val( value ).html( filter.text )[0], 32 priority: filter.priority || 50 33 }; 34 }, this ).sortBy('priority').pluck('el').value() ); 35 36 this.model.on( 'change', this.select, this ); 37 this.select(); 38 }, 39 40 /** 41 * @abstract 42 */ 43 createFilters: function() { 44 this.filters = {}; 45 }, 46 47 /** 48 * When the selected filter changes, update the Attachment Query properties to match. 49 */ 50 change: function() { 51 var filter = this.filters[ this.el.value ]; 52 if ( filter ) { 53 this.model.set( filter.props ); 54 } 55 }, 56 57 select: function() { 58 var model = this.model, 59 value = 'all', 60 props = model.toJSON(); 61 62 _.find( this.filters, function( filter, id ) { 63 var equal = _.all( filter.props, function( prop, key ) { 64 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); 65 }); 66 67 if ( equal ) { 68 return value = id; 69 } 70 }); 71 72 this.$el.val( value ); 73 } 74 }); 75 76 module.exports = AttachmentFilters; 77 No newline at end of file -
src/wp-includes/js/media/views/attachment.js
1 /** 2 * wp.media.view.Attachment 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 $ = jQuery, 11 Attachment; 12 13 Attachment = View.extend({ 14 tagName: 'li', 15 className: 'attachment', 16 template: wp.template('attachment'), 17 18 attributes: function() { 19 return { 20 'tabIndex': 0, 21 'role': 'checkbox', 22 'aria-label': this.model.get( 'title' ), 23 'aria-checked': false, 24 'data-id': this.model.get( 'id' ) 25 }; 26 }, 27 28 events: { 29 'click .js--select-attachment': 'toggleSelectionHandler', 30 'change [data-setting]': 'updateSetting', 31 'change [data-setting] input': 'updateSetting', 32 'change [data-setting] select': 'updateSetting', 33 'change [data-setting] textarea': 'updateSetting', 34 'click .close': 'removeFromLibrary', 35 'click .check': 'checkClickHandler', 36 'click a': 'preventDefault', 37 'keydown .close': 'removeFromLibrary', 38 'keydown': 'toggleSelectionHandler' 39 }, 40 41 buttons: {}, 42 43 initialize: function() { 44 var selection = this.options.selection, 45 options = _.defaults( this.options, { 46 rerenderOnModelChange: true 47 } ); 48 49 if ( options.rerenderOnModelChange ) { 50 this.model.on( 'change', this.render, this ); 51 } else { 52 this.model.on( 'change:percent', this.progress, this ); 53 } 54 this.model.on( 'change:title', this._syncTitle, this ); 55 this.model.on( 'change:caption', this._syncCaption, this ); 56 this.model.on( 'change:artist', this._syncArtist, this ); 57 this.model.on( 'change:album', this._syncAlbum, this ); 58 59 // Update the selection. 60 this.model.on( 'add', this.select, this ); 61 this.model.on( 'remove', this.deselect, this ); 62 if ( selection ) { 63 selection.on( 'reset', this.updateSelect, this ); 64 // Update the model's details view. 65 this.model.on( 'selection:single selection:unsingle', this.details, this ); 66 this.details( this.model, this.controller.state().get('selection') ); 67 } 68 69 this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); 70 }, 71 /** 72 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 73 */ 74 dispose: function() { 75 var selection = this.options.selection; 76 77 // Make sure all settings are saved before removing the view. 78 this.updateAll(); 79 80 if ( selection ) { 81 selection.off( null, null, this ); 82 } 83 /** 84 * call 'dispose' directly on the parent class 85 */ 86 View.prototype.dispose.apply( this, arguments ); 87 return this; 88 }, 89 /** 90 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 91 */ 92 render: function() { 93 var options = _.defaults( this.model.toJSON(), { 94 orientation: 'landscape', 95 uploading: false, 96 type: '', 97 subtype: '', 98 icon: '', 99 filename: '', 100 caption: '', 101 title: '', 102 dateFormatted: '', 103 width: '', 104 height: '', 105 compat: false, 106 alt: '', 107 description: '' 108 }, this.options ); 109 110 options.buttons = this.buttons; 111 options.describe = this.controller.state().get('describe'); 112 113 if ( 'image' === options.type ) { 114 options.size = this.imageSize(); 115 } 116 117 options.can = {}; 118 if ( options.nonces ) { 119 options.can.remove = !! options.nonces['delete']; 120 options.can.save = !! options.nonces.update; 121 } 122 123 if ( this.controller.state().get('allowLocalEdits') ) { 124 options.allowLocalEdits = true; 125 } 126 127 if ( options.uploading && ! options.percent ) { 128 options.percent = 0; 129 } 130 131 this.views.detach(); 132 this.$el.html( this.template( options ) ); 133 134 this.$el.toggleClass( 'uploading', options.uploading ); 135 136 if ( options.uploading ) { 137 this.$bar = this.$('.media-progress-bar div'); 138 } else { 139 delete this.$bar; 140 } 141 142 // Check if the model is selected. 143 this.updateSelect(); 144 145 // Update the save status. 146 this.updateSave(); 147 148 this.views.render(); 149 150 return this; 151 }, 152 153 progress: function() { 154 if ( this.$bar && this.$bar.length ) { 155 this.$bar.width( this.model.get('percent') + '%' ); 156 } 157 }, 158 159 /** 160 * @param {Object} event 161 */ 162 toggleSelectionHandler: function( event ) { 163 var method; 164 165 // Don't do anything inside inputs. 166 if ( 'INPUT' === event.target.nodeName ) { 167 return; 168 } 169 170 // Catch arrow events 171 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 172 this.controller.trigger( 'attachment:keydown:arrow', event ); 173 return; 174 } 175 176 // Catch enter and space events 177 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 178 return; 179 } 180 181 event.preventDefault(); 182 183 // In the grid view, bubble up an edit:attachment event to the controller. 184 if ( this.controller.isModeActive( 'grid' ) ) { 185 if ( this.controller.isModeActive( 'edit' ) ) { 186 // Pass the current target to restore focus when closing 187 this.controller.trigger( 'edit:attachment', this.model, event.currentTarget ); 188 return; 189 } 190 191 if ( this.controller.isModeActive( 'select' ) ) { 192 method = 'toggle'; 193 } 194 } 195 196 if ( event.shiftKey ) { 197 method = 'between'; 198 } else if ( event.ctrlKey || event.metaKey ) { 199 method = 'toggle'; 200 } 201 202 this.toggleSelection({ 203 method: method 204 }); 205 206 this.controller.trigger( 'selection:toggle' ); 207 }, 208 /** 209 * @param {Object} options 210 */ 211 toggleSelection: function( options ) { 212 var collection = this.collection, 213 selection = this.options.selection, 214 model = this.model, 215 method = options && options.method, 216 single, models, singleIndex, modelIndex; 217 218 if ( ! selection ) { 219 return; 220 } 221 222 single = selection.single(); 223 method = _.isUndefined( method ) ? selection.multiple : method; 224 225 // If the `method` is set to `between`, select all models that 226 // exist between the current and the selected model. 227 if ( 'between' === method && single && selection.multiple ) { 228 // If the models are the same, short-circuit. 229 if ( single === model ) { 230 return; 231 } 232 233 singleIndex = collection.indexOf( single ); 234 modelIndex = collection.indexOf( this.model ); 235 236 if ( singleIndex < modelIndex ) { 237 models = collection.models.slice( singleIndex, modelIndex + 1 ); 238 } else { 239 models = collection.models.slice( modelIndex, singleIndex + 1 ); 240 } 241 242 selection.add( models ); 243 selection.single( model ); 244 return; 245 246 // If the `method` is set to `toggle`, just flip the selection 247 // status, regardless of whether the model is the single model. 248 } else if ( 'toggle' === method ) { 249 selection[ this.selected() ? 'remove' : 'add' ]( model ); 250 selection.single( model ); 251 return; 252 } else if ( 'add' === method ) { 253 selection.add( model ); 254 selection.single( model ); 255 return; 256 } 257 258 // Fixes bug that loses focus when selecting a featured image 259 if ( ! method ) { 260 method = 'add'; 261 } 262 263 if ( method !== 'add' ) { 264 method = 'reset'; 265 } 266 267 if ( this.selected() ) { 268 // If the model is the single model, remove it. 269 // If it is not the same as the single model, 270 // it now becomes the single model. 271 selection[ single === model ? 'remove' : 'single' ]( model ); 272 } else { 273 // If the model is not selected, run the `method` on the 274 // selection. By default, we `reset` the selection, but the 275 // `method` can be set to `add` the model to the selection. 276 selection[ method ]( model ); 277 selection.single( model ); 278 } 279 }, 280 281 updateSelect: function() { 282 this[ this.selected() ? 'select' : 'deselect' ](); 283 }, 284 /** 285 * @returns {unresolved|Boolean} 286 */ 287 selected: function() { 288 var selection = this.options.selection; 289 if ( selection ) { 290 return !! selection.get( this.model.cid ); 291 } 292 }, 293 /** 294 * @param {Backbone.Model} model 295 * @param {Backbone.Collection} collection 296 */ 297 select: function( model, collection ) { 298 var selection = this.options.selection, 299 controller = this.controller; 300 301 // Check if a selection exists and if it's the collection provided. 302 // If they're not the same collection, bail; we're in another 303 // selection's event loop. 304 if ( ! selection || ( collection && collection !== selection ) ) { 305 return; 306 } 307 308 // Bail if the model is already selected. 309 if ( this.$el.hasClass( 'selected' ) ) { 310 return; 311 } 312 313 // Add 'selected' class to model, set aria-checked to true. 314 this.$el.addClass( 'selected' ).attr( 'aria-checked', true ); 315 // Make the checkbox tabable, except in media grid (bulk select mode). 316 if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) { 317 this.$( '.check' ).attr( 'tabindex', '0' ); 318 } 319 }, 320 /** 321 * @param {Backbone.Model} model 322 * @param {Backbone.Collection} collection 323 */ 324 deselect: function( model, collection ) { 325 var selection = this.options.selection; 326 327 // Check if a selection exists and if it's the collection provided. 328 // If they're not the same collection, bail; we're in another 329 // selection's event loop. 330 if ( ! selection || ( collection && collection !== selection ) ) { 331 return; 332 } 333 this.$el.removeClass( 'selected' ).attr( 'aria-checked', false ) 334 .find( '.check' ).attr( 'tabindex', '-1' ); 335 }, 336 /** 337 * @param {Backbone.Model} model 338 * @param {Backbone.Collection} collection 339 */ 340 details: function( model, collection ) { 341 var selection = this.options.selection, 342 details; 343 344 if ( selection !== collection ) { 345 return; 346 } 347 348 details = selection.single(); 349 this.$el.toggleClass( 'details', details === this.model ); 350 }, 351 /** 352 * @param {Object} event 353 */ 354 preventDefault: function( event ) { 355 event.preventDefault(); 356 }, 357 /** 358 * @param {string} size 359 * @returns {Object} 360 */ 361 imageSize: function( size ) { 362 var sizes = this.model.get('sizes'); 363 364 size = size || 'medium'; 365 366 // Use the provided image size if possible. 367 if ( sizes && sizes[ size ] ) { 368 return _.clone( sizes[ size ] ); 369 } else { 370 return { 371 url: this.model.get('url'), 372 width: this.model.get('width'), 373 height: this.model.get('height'), 374 orientation: this.model.get('orientation') 375 }; 376 } 377 }, 378 /** 379 * @param {Object} event 380 */ 381 updateSetting: function( event ) { 382 var $setting = $( event.target ).closest('[data-setting]'), 383 setting, value; 384 385 if ( ! $setting.length ) { 386 return; 387 } 388 389 setting = $setting.data('setting'); 390 value = event.target.value; 391 392 if ( this.model.get( setting ) !== value ) { 393 this.save( setting, value ); 394 } 395 }, 396 397 /** 398 * Pass all the arguments to the model's save method. 399 * 400 * Records the aggregate status of all save requests and updates the 401 * view's classes accordingly. 402 */ 403 save: function() { 404 var view = this, 405 save = this._save = this._save || { status: 'ready' }, 406 request = this.model.save.apply( this.model, arguments ), 407 requests = save.requests ? $.when( request, save.requests ) : request; 408 409 // If we're waiting to remove 'Saved.', stop. 410 if ( save.savedTimer ) { 411 clearTimeout( save.savedTimer ); 412 } 413 414 this.updateSave('waiting'); 415 save.requests = requests; 416 requests.always( function() { 417 // If we've performed another request since this one, bail. 418 if ( save.requests !== requests ) { 419 return; 420 } 421 422 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' ); 423 save.savedTimer = setTimeout( function() { 424 view.updateSave('ready'); 425 delete save.savedTimer; 426 }, 2000 ); 427 }); 428 }, 429 /** 430 * @param {string} status 431 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 432 */ 433 updateSave: function( status ) { 434 var save = this._save = this._save || { status: 'ready' }; 435 436 if ( status && status !== save.status ) { 437 this.$el.removeClass( 'save-' + save.status ); 438 save.status = status; 439 } 440 441 this.$el.addClass( 'save-' + save.status ); 442 return this; 443 }, 444 445 updateAll: function() { 446 var $settings = this.$('[data-setting]'), 447 model = this.model, 448 changed; 449 450 changed = _.chain( $settings ).map( function( el ) { 451 var $input = $('input, textarea, select, [value]', el ), 452 setting, value; 453 454 if ( ! $input.length ) { 455 return; 456 } 457 458 setting = $(el).data('setting'); 459 value = $input.val(); 460 461 // Record the value if it changed. 462 if ( model.get( setting ) !== value ) { 463 return [ setting, value ]; 464 } 465 }).compact().object().value(); 466 467 if ( ! _.isEmpty( changed ) ) { 468 model.save( changed ); 469 } 470 }, 471 /** 472 * @param {Object} event 473 */ 474 removeFromLibrary: function( event ) { 475 // Catch enter and space events 476 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 477 return; 478 } 479 480 // Stop propagation so the model isn't selected. 481 event.stopPropagation(); 482 483 this.collection.remove( this.model ); 484 }, 485 486 /** 487 * Add the model if it isn't in the selection, if it is in the selection, 488 * remove it. 489 * 490 * @param {[type]} event [description] 491 * @return {[type]} [description] 492 */ 493 checkClickHandler: function ( event ) { 494 var selection = this.options.selection; 495 if ( ! selection ) { 496 return; 497 } 498 event.stopPropagation(); 499 if ( selection.where( { id: this.model.get( 'id' ) } ).length ) { 500 selection.remove( this.model ); 501 // Move focus back to the attachment tile (from the check). 502 this.$el.focus(); 503 } else { 504 selection.add( this.model ); 505 } 506 } 507 }); 508 509 // Ensure settings remain in sync between attachment views. 510 _.each({ 511 caption: '_syncCaption', 512 title: '_syncTitle', 513 artist: '_syncArtist', 514 album: '_syncAlbum' 515 }, function( method, setting ) { 516 /** 517 * @param {Backbone.Model} model 518 * @param {string} value 519 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 520 */ 521 Attachment.prototype[ method ] = function( model, value ) { 522 var $setting = this.$('[data-setting="' + setting + '"]'); 523 524 if ( ! $setting.length ) { 525 return this; 526 } 527 528 // If the updated value is in sync with the value in the DOM, there 529 // is no need to re-render. If we're currently editing the value, 530 // it will automatically be in sync, suppressing the re-render for 531 // the view we're editing, while updating any others. 532 if ( value === $setting.find('input, textarea, select, [value]').val() ) { 533 return this; 534 } 535 536 return this.render(); 537 }; 538 }); 539 540 module.exports = Attachment; 541 No newline at end of file -
src/wp-includes/js/media/views/attachments/browser.js
1 /** 2 * wp.media.view.AttachmentsBrowser 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 * 9 * @param {object} options 10 * @param {object} [options.filters=false] Which filters to show in the browser's toolbar. 11 * Accepts 'uploaded' and 'all'. 12 * @param {object} [options.search=true] Whether to show the search interface in the 13 * browser's toolbar. 14 * @param {object} [options.display=false] Whether to show the attachments display settings 15 * view in the sidebar. 16 * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser. 17 * Accepts true, false, and 'errors'. 18 */ 19 var View = require( '../view.js' ), 20 Library = require( '../attachment/library.js' ), 21 Toolbar = require( '../toolbar.js' ), 22 Spinner = require( '../spinner.js' ), 23 Search = require( '../search.js' ), 24 Label = require( '../label.js' ), 25 Uploaded = require( '../attachment-filters/uploaded.js' ), 26 All = require( '../attachment-filters/all.js' ), 27 DateFilter = require( '../attachment-filters/date.js' ), 28 UploaderInline = require( '../uploader/inline.js' ), 29 Attachments = require( '../attachments.js' ), 30 Sidebar = require( '../sidebar.js' ), 31 UploaderStatus = require( '../uploader/status.js' ), 32 Details = require( '../attachment/details.js' ), 33 AttachmentCompat = require( '../attachment-compat.js' ), 34 AttachmentDisplay = require( '../settings/attachment-display.js' ), 35 mediaTrash = wp.media.view.settings.mediaTrash, 36 l10n = wp.media.view.l10n, 37 $ = jQuery, 38 AttachmentsBrowser; 39 40 AttachmentsBrowser = View.extend({ 41 tagName: 'div', 42 className: 'attachments-browser', 43 44 initialize: function() { 45 _.defaults( this.options, { 46 filters: false, 47 search: true, 48 display: false, 49 sidebar: true, 50 AttachmentView: Library 51 }); 52 53 this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) ); 54 this.controller.on( 'edit:selection', this.editSelection ); 55 this.createToolbar(); 56 if ( this.options.sidebar ) { 57 this.createSidebar(); 58 } 59 this.createUploader(); 60 this.createAttachments(); 61 this.updateContent(); 62 63 if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) { 64 this.$el.addClass( 'hide-sidebar' ); 65 66 if ( 'errors' === this.options.sidebar ) { 67 this.$el.addClass( 'sidebar-for-errors' ); 68 } 69 } 70 71 this.collection.on( 'add remove reset', this.updateContent, this ); 72 }, 73 74 editSelection: function( modal ) { 75 modal.$( '.media-button-backToLibrary' ).focus(); 76 }, 77 78 /** 79 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining 80 */ 81 dispose: function() { 82 this.options.selection.off( null, null, this ); 83 View.prototype.dispose.apply( this, arguments ); 84 return this; 85 }, 86 87 createToolbar: function() { 88 var LibraryViewSwitcher, Filters, toolbarOptions; 89 90 toolbarOptions = { 91 controller: this.controller 92 }; 93 94 if ( this.controller.isModeActive( 'grid' ) ) { 95 toolbarOptions.className = 'media-toolbar wp-filter'; 96 } 97 98 /** 99 * @member {wp.media.view.Toolbar} 100 */ 101 this.toolbar = new Toolbar( toolbarOptions ); 102 103 this.views.add( this.toolbar ); 104 105 this.toolbar.set( 'spinner', new Spinner({ 106 priority: -60 107 }) ); 108 109 if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { 110 // "Filters" will return a <select>, need to render 111 // screen reader text before 112 this.toolbar.set( 'filtersLabel', new Label({ 113 value: l10n.filterByType, 114 attributes: { 115 'for': 'media-attachment-filters' 116 }, 117 priority: -80 118 }).render() ); 119 120 if ( 'uploaded' === this.options.filters ) { 121 this.toolbar.set( 'filters', new Uploaded({ 122 controller: this.controller, 123 model: this.collection.props, 124 priority: -80 125 }).render() ); 126 } else { 127 Filters = new All({ 128 controller: this.controller, 129 model: this.collection.props, 130 priority: -80 131 }); 132 133 this.toolbar.set( 'filters', Filters.render() ); 134 } 135 } 136 137 // Feels odd to bring the global media library switcher into the Attachment 138 // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); 139 // which the controller can tap into and add this view? 140 if ( this.controller.isModeActive( 'grid' ) ) { 141 LibraryViewSwitcher = View.extend({ 142 className: 'view-switch media-grid-view-switch', 143 template: wp.template( 'media-library-view-switcher') 144 }); 145 146 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({ 147 controller: this.controller, 148 priority: -90 149 }).render() ); 150 151 // DateFilter is a <select>, screen reader text needs to be rendered before 152 this.toolbar.set( 'dateFilterLabel', new Label({ 153 value: l10n.filterByDate, 154 attributes: { 155 'for': 'media-attachment-date-filters' 156 }, 157 priority: -75 158 }).render() ); 159 this.toolbar.set( 'dateFilter', new DateFilter({ 160 controller: this.controller, 161 model: this.collection.props, 162 priority: -75 163 }).render() ); 164 165 // BulkSelection is a <div> with subviews, including screen reader text 166 this.toolbar.set( 'selectModeToggleButton', new wp.media.view.SelectModeToggleButton({ 167 text: l10n.bulkSelect, 168 controller: this.controller, 169 priority: -70 170 }).render() ); 171 172 this.toolbar.set( 'deleteSelectedButton', new wp.media.view.DeleteSelectedButton({ 173 filters: Filters, 174 style: 'primary', 175 disabled: true, 176 text: mediaTrash ? l10n.trashSelected : l10n.deleteSelected, 177 controller: this.controller, 178 priority: -60, 179 click: function() { 180 var changed = [], removed = [], self = this, 181 selection = this.controller.state().get( 'selection' ), 182 library = this.controller.state().get( 'library' ); 183 184 if ( ! selection.length ) { 185 return; 186 } 187 188 if ( ! mediaTrash && ! confirm( l10n.warnBulkDelete ) ) { 189 return; 190 } 191 192 if ( mediaTrash && 193 'trash' !== selection.at( 0 ).get( 'status' ) && 194 ! confirm( l10n.warnBulkTrash ) ) { 195 196 return; 197 } 198 199 selection.each( function( model ) { 200 if ( ! model.get( 'nonces' )['delete'] ) { 201 removed.push( model ); 202 return; 203 } 204 205 if ( mediaTrash && 'trash' === model.get( 'status' ) ) { 206 model.set( 'status', 'inherit' ); 207 changed.push( model.save() ); 208 removed.push( model ); 209 } else if ( mediaTrash ) { 210 model.set( 'status', 'trash' ); 211 changed.push( model.save() ); 212 removed.push( model ); 213 } else { 214 model.destroy({wait: true}); 215 } 216 } ); 217 218 if ( changed.length ) { 219 selection.remove( removed ); 220 221 $.when.apply( null, changed ).then( function() { 222 library._requery( true ); 223 self.controller.trigger( 'selection:action:done' ); 224 } ); 225 } else { 226 this.controller.trigger( 'selection:action:done' ); 227 } 228 } 229 }).render() ); 230 231 if ( mediaTrash ) { 232 this.toolbar.set( 'deleteSelectedPermanentlyButton', new wp.media.view.DeleteSelectedPermanentlyButton({ 233 filters: Filters, 234 style: 'primary', 235 disabled: true, 236 text: l10n.deleteSelected, 237 controller: this.controller, 238 priority: -55, 239 click: function() { 240 var removed = [], selection = this.controller.state().get( 'selection' ); 241 242 if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) { 243 return; 244 } 245 246 selection.each( function( model ) { 247 if ( ! model.get( 'nonces' )['delete'] ) { 248 removed.push( model ); 249 return; 250 } 251 252 model.destroy(); 253 } ); 254 255 selection.remove( removed ); 256 this.controller.trigger( 'selection:action:done' ); 257 } 258 }).render() ); 259 } 260 261 } else { 262 // DateFilter is a <select>, screen reader text needs to be rendered before 263 this.toolbar.set( 'dateFilterLabel', new Label({ 264 value: l10n.filterByDate, 265 attributes: { 266 'for': 'media-attachment-date-filters' 267 }, 268 priority: -75 269 }).render() ); 270 this.toolbar.set( 'dateFilter', new DateFilter({ 271 controller: this.controller, 272 model: this.collection.props, 273 priority: -75 274 }).render() ); 275 } 276 277 if ( this.options.search ) { 278 // Search is an input, screen reader text needs to be rendered before 279 this.toolbar.set( 'searchLabel', new Label({ 280 value: l10n.searchMediaLabel, 281 attributes: { 282 'for': 'media-search-input' 283 }, 284 priority: 60 285 }).render() ); 286 this.toolbar.set( 'search', new Search({ 287 controller: this.controller, 288 model: this.collection.props, 289 priority: 60 290 }).render() ); 291 } 292 293 if ( this.options.dragInfo ) { 294 this.toolbar.set( 'dragInfo', new View({ 295 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0], 296 priority: -40 297 }) ); 298 } 299 300 if ( this.options.suggestedWidth && this.options.suggestedHeight ) { 301 this.toolbar.set( 'suggestedDimensions', new View({ 302 el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '</div>' )[0], 303 priority: -40 304 }) ); 305 } 306 }, 307 308 updateContent: function() { 309 var view = this, 310 noItemsView; 311 312 if ( this.controller.isModeActive( 'grid' ) ) { 313 noItemsView = view.attachmentsNoResults; 314 } else { 315 noItemsView = view.uploader; 316 } 317 318 if ( ! this.collection.length ) { 319 this.toolbar.get( 'spinner' ).show(); 320 this.dfd = this.collection.more().done( function() { 321 if ( ! view.collection.length ) { 322 noItemsView.$el.removeClass( 'hidden' ); 323 } else { 324 noItemsView.$el.addClass( 'hidden' ); 325 } 326 view.toolbar.get( 'spinner' ).hide(); 327 } ); 328 } else { 329 noItemsView.$el.addClass( 'hidden' ); 330 view.toolbar.get( 'spinner' ).hide(); 331 } 332 }, 333 334 createUploader: function() { 335 this.uploader = new UploaderInline({ 336 controller: this.controller, 337 status: false, 338 message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound, 339 canClose: this.controller.isModeActive( 'grid' ) 340 }); 341 342 this.uploader.hide(); 343 this.views.add( this.uploader ); 344 }, 345 346 toggleUploader: function() { 347 if ( this.uploader.$el.hasClass( 'hidden' ) ) { 348 this.uploader.show(); 349 } else { 350 this.uploader.hide(); 351 } 352 }, 353 354 createAttachments: function() { 355 this.attachments = new Attachments({ 356 controller: this.controller, 357 collection: this.collection, 358 selection: this.options.selection, 359 model: this.model, 360 sortable: this.options.sortable, 361 scrollElement: this.options.scrollElement, 362 idealColumnWidth: this.options.idealColumnWidth, 363 364 // The single `Attachment` view to be used in the `Attachments` view. 365 AttachmentView: this.options.AttachmentView 366 }); 367 368 // Add keydown listener to the instance of the Attachments view 369 this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent ); 370 this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus ); 371 372 this.views.add( this.attachments ); 373 374 375 if ( this.controller.isModeActive( 'grid' ) ) { 376 this.attachmentsNoResults = new View({ 377 controller: this.controller, 378 tagName: 'p' 379 }); 380 381 this.attachmentsNoResults.$el.addClass( 'hidden no-media' ); 382 this.attachmentsNoResults.$el.html( l10n.noMedia ); 383 384 this.views.add( this.attachmentsNoResults ); 385 } 386 }, 387 388 createSidebar: function() { 389 var options = this.options, 390 selection = options.selection, 391 sidebar = this.sidebar = new Sidebar({ 392 controller: this.controller 393 }); 394 395 this.views.add( sidebar ); 396 397 if ( this.controller.uploader ) { 398 sidebar.set( 'uploads', new UploaderStatus({ 399 controller: this.controller, 400 priority: 40 401 }) ); 402 } 403 404 selection.on( 'selection:single', this.createSingle, this ); 405 selection.on( 'selection:unsingle', this.disposeSingle, this ); 406 407 if ( selection.single() ) { 408 this.createSingle(); 409 } 410 }, 411 412 createSingle: function() { 413 var sidebar = this.sidebar, 414 single = this.options.selection.single(); 415 416 sidebar.set( 'details', new Details({ 417 controller: this.controller, 418 model: single, 419 priority: 80 420 }) ); 421 422 sidebar.set( 'compat', new AttachmentCompat({ 423 controller: this.controller, 424 model: single, 425 priority: 120 426 }) ); 427 428 if ( this.options.display ) { 429 sidebar.set( 'display', new AttachmentDisplay({ 430 controller: this.controller, 431 model: this.model.display( single ), 432 attachment: single, 433 priority: 160, 434 userSettings: this.model.get('displayUserSettings') 435 }) ); 436 } 437 438 // Show the sidebar on mobile 439 if ( this.model.id === 'insert' ) { 440 sidebar.$el.addClass( 'visible' ); 441 } 442 }, 443 444 disposeSingle: function() { 445 var sidebar = this.sidebar; 446 sidebar.unset('details'); 447 sidebar.unset('compat'); 448 sidebar.unset('display'); 449 // Hide the sidebar on mobile 450 sidebar.$el.removeClass( 'visible' ); 451 } 452 }); 453 454 module.exports = AttachmentsBrowser; 455 No newline at end of file -
src/wp-includes/js/media/views/attachments/selection.js
1 /** 2 * wp.media.view.Attachments.Selection 3 * 4 * @class 5 * @augments wp.media.view.Attachments 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Attachments = require( '../attachments.js' ), 11 AttachmentSelection = require( '../attachment/selection.js' ), 12 Selection; 13 14 Selection = Attachments.extend({ 15 events: {}, 16 initialize: function() { 17 _.defaults( this.options, { 18 sortable: false, 19 resize: false, 20 21 // The single `Attachment` view to be used in the `Attachments` view. 22 AttachmentView: AttachmentSelection 23 }); 24 // Call 'initialize' directly on the parent class. 25 return Attachments.prototype.initialize.apply( this, arguments ); 26 } 27 }); 28 29 module.exports = Selection; 30 No newline at end of file -
src/wp-includes/js/media/views/attachments.js
1 /** 2 * wp.media.view.Attachments 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 Attachment = require( './attachment.js' ), 11 $ = jQuery, 12 Attachments; 13 14 Attachments = View.extend({ 15 tagName: 'ul', 16 className: 'attachments', 17 18 attributes: { 19 tabIndex: -1 20 }, 21 22 initialize: function() { 23 this.el.id = _.uniqueId('__attachments-view-'); 24 25 _.defaults( this.options, { 26 refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, 27 refreshThreshold: 3, 28 AttachmentView: Attachment, 29 sortable: false, 30 resize: true, 31 idealColumnWidth: $( window ).width() < 640 ? 135 : 150 32 }); 33 34 this._viewsByCid = {}; 35 this.$window = $( window ); 36 this.resizeEvent = 'resize.media-modal-columns'; 37 38 this.collection.on( 'add', function( attachment ) { 39 this.views.add( this.createAttachmentView( attachment ), { 40 at: this.collection.indexOf( attachment ) 41 }); 42 }, this ); 43 44 this.collection.on( 'remove', function( attachment ) { 45 var view = this._viewsByCid[ attachment.cid ]; 46 delete this._viewsByCid[ attachment.cid ]; 47 48 if ( view ) { 49 view.remove(); 50 } 51 }, this ); 52 53 this.collection.on( 'reset', this.render, this ); 54 55 this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); 56 57 // Throttle the scroll handler and bind this. 58 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 59 60 this.options.scrollElement = this.options.scrollElement || this.el; 61 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 62 63 this.initSortable(); 64 65 _.bindAll( this, 'setColumns' ); 66 67 if ( this.options.resize ) { 68 this.on( 'ready', this.bindEvents ); 69 this.controller.on( 'open', this.setColumns ); 70 71 // Call this.setColumns() after this view has been rendered in the DOM so 72 // attachments get proper width applied. 73 _.defer( this.setColumns, this ); 74 } 75 }, 76 77 bindEvents: function() { 78 this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); 79 }, 80 81 attachmentFocus: function() { 82 this.$( 'li:first' ).focus(); 83 }, 84 85 restoreFocus: function() { 86 this.$( 'li.selected:first' ).focus(); 87 }, 88 89 arrowEvent: function( event ) { 90 var attachments = this.$el.children( 'li' ), 91 perRow = this.columns, 92 index = attachments.filter( ':focus' ).index(), 93 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); 94 95 if ( index === -1 ) { 96 return; 97 } 98 99 // Left arrow 100 if ( 37 === event.keyCode ) { 101 if ( 0 === index ) { 102 return; 103 } 104 attachments.eq( index - 1 ).focus(); 105 } 106 107 // Up arrow 108 if ( 38 === event.keyCode ) { 109 if ( 1 === row ) { 110 return; 111 } 112 attachments.eq( index - perRow ).focus(); 113 } 114 115 // Right arrow 116 if ( 39 === event.keyCode ) { 117 if ( attachments.length === index ) { 118 return; 119 } 120 attachments.eq( index + 1 ).focus(); 121 } 122 123 // Down arrow 124 if ( 40 === event.keyCode ) { 125 if ( Math.ceil( attachments.length / perRow ) === row ) { 126 return; 127 } 128 attachments.eq( index + perRow ).focus(); 129 } 130 }, 131 132 dispose: function() { 133 this.collection.props.off( null, null, this ); 134 if ( this.options.resize ) { 135 this.$window.off( this.resizeEvent ); 136 } 137 138 /** 139 * call 'dispose' directly on the parent class 140 */ 141 View.prototype.dispose.apply( this, arguments ); 142 }, 143 144 setColumns: function() { 145 var prev = this.columns, 146 width = this.$el.width(); 147 148 if ( width ) { 149 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; 150 151 if ( ! prev || prev !== this.columns ) { 152 this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); 153 } 154 } 155 }, 156 157 initSortable: function() { 158 var collection = this.collection; 159 160 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 161 return; 162 } 163 164 this.$el.sortable( _.extend({ 165 // If the `collection` has a `comparator`, disable sorting. 166 disabled: !! collection.comparator, 167 168 // Change the position of the attachment as soon as the 169 // mouse pointer overlaps a thumbnail. 170 tolerance: 'pointer', 171 172 // Record the initial `index` of the dragged model. 173 start: function( event, ui ) { 174 ui.item.data('sortableIndexStart', ui.item.index()); 175 }, 176 177 // Update the model's index in the collection. 178 // Do so silently, as the view is already accurate. 179 update: function( event, ui ) { 180 var model = collection.at( ui.item.data('sortableIndexStart') ), 181 comparator = collection.comparator; 182 183 // Temporarily disable the comparator to prevent `add` 184 // from re-sorting. 185 delete collection.comparator; 186 187 // Silently shift the model to its new index. 188 collection.remove( model, { 189 silent: true 190 }); 191 collection.add( model, { 192 silent: true, 193 at: ui.item.index() 194 }); 195 196 // Restore the comparator. 197 collection.comparator = comparator; 198 199 // Fire the `reset` event to ensure other collections sync. 200 collection.trigger( 'reset', collection ); 201 202 // If the collection is sorted by menu order, 203 // update the menu order. 204 collection.saveMenuOrder(); 205 } 206 }, this.options.sortable ) ); 207 208 // If the `orderby` property is changed on the `collection`, 209 // check to see if we have a `comparator`. If so, disable sorting. 210 collection.props.on( 'change:orderby', function() { 211 this.$el.sortable( 'option', 'disabled', !! collection.comparator ); 212 }, this ); 213 214 this.collection.props.on( 'change:orderby', this.refreshSortable, this ); 215 this.refreshSortable(); 216 }, 217 218 refreshSortable: function() { 219 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 220 return; 221 } 222 223 // If the `collection` has a `comparator`, disable sorting. 224 var collection = this.collection, 225 orderby = collection.props.get('orderby'), 226 enabled = 'menuOrder' === orderby || ! collection.comparator; 227 228 this.$el.sortable( 'option', 'disabled', ! enabled ); 229 }, 230 231 /** 232 * @param {wp.media.model.Attachment} attachment 233 * @returns {wp.media.View} 234 */ 235 createAttachmentView: function( attachment ) { 236 var view = new this.options.AttachmentView({ 237 controller: this.controller, 238 model: attachment, 239 collection: this.collection, 240 selection: this.options.selection 241 }); 242 243 return this._viewsByCid[ attachment.cid ] = view; 244 }, 245 246 prepare: function() { 247 // Create all of the Attachment views, and replace 248 // the list in a single DOM operation. 249 if ( this.collection.length ) { 250 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 251 252 // If there are no elements, clear the views and load some. 253 } else { 254 this.views.unset(); 255 this.collection.more().done( this.scroll ); 256 } 257 }, 258 259 ready: function() { 260 // Trigger the scroll event to check if we're within the 261 // threshold to query for additional attachments. 262 this.scroll(); 263 }, 264 265 scroll: function() { 266 var view = this, 267 el = this.options.scrollElement, 268 scrollTop = el.scrollTop, 269 toolbar; 270 271 // The scroll event occurs on the document, but the element 272 // that should be checked is the document body. 273 if ( el == document ) { 274 el = document.body; 275 scrollTop = $(document).scrollTop(); 276 } 277 278 if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { 279 return; 280 } 281 282 toolbar = this.views.parent.toolbar; 283 284 // Show the spinner only if we are close to the bottom. 285 if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { 286 toolbar.get('spinner').show(); 287 } 288 289 if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { 290 this.collection.more().done(function() { 291 view.scroll(); 292 toolbar.get('spinner').hide(); 293 }); 294 } 295 } 296 }); 297 298 module.exports = Attachments; 299 No newline at end of file -
src/wp-includes/js/media/views/audio-details.js
1 /** 2 * wp.media.view.AudioDetails 3 * 4 * @constructor 5 * @augments wp.media.view.MediaDetails 6 * @augments wp.media.view.Settings.AttachmentDisplay 7 * @augments wp.media.view.Settings 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 */ 12 var MediaDetails = require( './media-details' ), 13 AudioDetails; 14 15 AudioDetails = MediaDetails.extend({ 16 className: 'audio-details', 17 template: wp.template('audio-details'), 18 19 setMedia: function() { 20 var audio = this.$('.wp-audio-shortcode'); 21 22 if ( audio.find( 'source' ).length ) { 23 if ( audio.is(':hidden') ) { 24 audio.show(); 25 } 26 this.media = MediaDetails.prepareSrc( audio.get(0) ); 27 } else { 28 audio.hide(); 29 this.media = false; 30 } 31 32 return this; 33 } 34 }); 35 36 module.exports = AudioDetails; 37 No newline at end of file -
src/wp-includes/js/media/views/button/delete-selected-permanently.js
1 /** 2 * When MEDIA_TRASH is true, a button that handles bulk Delete Permanently logic 3 * 4 * @constructor 5 * @augments wp.media.view.DeleteSelectedButton 6 * @augments wp.media.view.Button 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var Button = require( '../button.js' ), 12 DeleteSelected = require( './delete-selected.js' ), 13 DeleteSelectedPermanently; 14 15 DeleteSelectedPermanently = DeleteSelected.extend({ 16 initialize: function() { 17 DeleteSelected.prototype.initialize.apply( this, arguments ); 18 this.listenTo( this.controller, 'select:activate', this.selectActivate ); 19 this.listenTo( this.controller, 'select:deactivate', this.selectDeactivate ); 20 }, 21 22 filterChange: function( model ) { 23 this.canShow = ( 'trash' === model.get( 'status' ) ); 24 }, 25 26 selectActivate: function() { 27 this.toggleDisabled(); 28 this.$el.toggleClass( 'hidden', ! this.canShow ); 29 }, 30 31 selectDeactivate: function() { 32 this.toggleDisabled(); 33 this.$el.addClass( 'hidden' ); 34 }, 35 36 render: function() { 37 Button.prototype.render.apply( this, arguments ); 38 this.selectActivate(); 39 return this; 40 } 41 }); 42 43 module.exports = DeleteSelectedPermanently; 44 No newline at end of file -
src/wp-includes/js/media/views/button/delete-selected.js
1 /** 2 * A button that handles bulk Delete/Trash logic 3 * 4 * @constructor 5 * @augments wp.media.view.Button 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Button = require( '../button.js' ), 11 l10n = wp.media.view.l10n, 12 DeleteSelected; 13 14 DeleteSelected = Button.extend({ 15 initialize: function() { 16 Button.prototype.initialize.apply( this, arguments ); 17 if ( this.options.filters ) { 18 this.listenTo( this.options.filters.model, 'change', this.filterChange ); 19 } 20 this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled ); 21 }, 22 23 filterChange: function( model ) { 24 if ( 'trash' === model.get( 'status' ) ) { 25 this.model.set( 'text', l10n.untrashSelected ); 26 } else if ( wp.media.view.settings.mediaTrash ) { 27 this.model.set( 'text', l10n.trashSelected ); 28 } else { 29 this.model.set( 'text', l10n.deleteSelected ); 30 } 31 }, 32 33 toggleDisabled: function() { 34 this.model.set( 'disabled', ! this.controller.state().get( 'selection' ).length ); 35 }, 36 37 render: function() { 38 Button.prototype.render.apply( this, arguments ); 39 if ( this.controller.isModeActive( 'select' ) ) { 40 this.$el.addClass( 'delete-selected-button' ); 41 } else { 42 this.$el.addClass( 'delete-selected-button hidden' ); 43 } 44 this.toggleDisabled(); 45 return this; 46 } 47 }); 48 49 module.exports = DeleteSelected; 50 No newline at end of file -
src/wp-includes/js/media/views/button/select-mode-toggle.js
1 var Button = require( '../button.js' ), 2 l10n = wp.media.view.l10n, 3 SelectModeToggle; 4 5 SelectModeToggle = Button.extend({ 6 initialize: function() { 7 Button.prototype.initialize.apply( this, arguments ); 8 this.listenTo( this.controller, 'select:activate select:deactivate', this.toggleBulkEditHandler ); 9 this.listenTo( this.controller, 'selection:action:done', this.back ); 10 }, 11 12 back: function () { 13 this.controller.deactivateMode( 'select' ).activateMode( 'edit' ); 14 }, 15 16 click: function() { 17 Button.prototype.click.apply( this, arguments ); 18 if ( this.controller.isModeActive( 'select' ) ) { 19 this.back(); 20 } else { 21 this.controller.deactivateMode( 'edit' ).activateMode( 'select' ); 22 } 23 }, 24 25 render: function() { 26 Button.prototype.render.apply( this, arguments ); 27 this.$el.addClass( 'select-mode-toggle-button' ); 28 return this; 29 }, 30 31 toggleBulkEditHandler: function() { 32 var toolbar = this.controller.content.get().toolbar, children; 33 34 children = toolbar.$( '.media-toolbar-secondary > *, .media-toolbar-primary > *' ); 35 36 // TODO: the Frame should be doing all of this. 37 if ( this.controller.isModeActive( 'select' ) ) { 38 this.model.set( 'text', l10n.cancelSelection ); 39 children.not( '.media-button' ).hide(); 40 this.$el.show(); 41 toolbar.$( '.delete-selected-button' ).removeClass( 'hidden' ); 42 } else { 43 this.model.set( 'text', l10n.bulkSelect ); 44 this.controller.content.get().$el.removeClass( 'fixed' ); 45 toolbar.$el.css( 'width', '' ); 46 toolbar.$( '.delete-selected-button' ).addClass( 'hidden' ); 47 children.not( '.spinner, .media-button' ).show(); 48 this.controller.state().get( 'selection' ).reset(); 49 } 50 } 51 }); 52 53 module.exports = SelectModeToggle; 54 No newline at end of file -
src/wp-includes/js/media/views/button-group.js
1 /** 2 * wp.media.view.ButtonGroup 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 Button = require( './button.js' ), 11 $ = jQuery, 12 ButtonGroup; 13 14 ButtonGroup = View.extend({ 15 tagName: 'div', 16 className: 'button-group button-large media-button-group', 17 18 initialize: function() { 19 /** 20 * @member {wp.media.view.Button[]} 21 */ 22 this.buttons = _.map( this.options.buttons || [], function( button ) { 23 if ( button instanceof Backbone.View ) { 24 return button; 25 } else { 26 return new Button( button ).render(); 27 } 28 }); 29 30 delete this.options.buttons; 31 32 if ( this.options.classes ) { 33 this.$el.addClass( this.options.classes ); 34 } 35 }, 36 37 /** 38 * @returns {wp.media.view.ButtonGroup} 39 */ 40 render: function() { 41 this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() ); 42 return this; 43 } 44 }); 45 46 module.exports = ButtonGroup; 47 No newline at end of file -
src/wp-includes/js/media/views/button.js
1 /** 2 * wp.media.view.Button 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 Button; 11 12 Button = View.extend({ 13 tagName: 'a', 14 className: 'media-button', 15 attributes: { href: '#' }, 16 17 events: { 18 'click': 'click' 19 }, 20 21 defaults: { 22 text: '', 23 style: '', 24 size: 'large', 25 disabled: false 26 }, 27 28 initialize: function() { 29 /** 30 * Create a model with the provided `defaults`. 31 * 32 * @member {Backbone.Model} 33 */ 34 this.model = new Backbone.Model( this.defaults ); 35 36 // If any of the `options` have a key from `defaults`, apply its 37 // value to the `model` and remove it from the `options object. 38 _.each( this.defaults, function( def, key ) { 39 var value = this.options[ key ]; 40 if ( _.isUndefined( value ) ) { 41 return; 42 } 43 44 this.model.set( key, value ); 45 delete this.options[ key ]; 46 }, this ); 47 48 this.model.on( 'change', this.render, this ); 49 }, 50 /** 51 * @returns {wp.media.view.Button} Returns itself to allow chaining 52 */ 53 render: function() { 54 var classes = [ 'button', this.className ], 55 model = this.model.toJSON(); 56 57 if ( model.style ) { 58 classes.push( 'button-' + model.style ); 59 } 60 61 if ( model.size ) { 62 classes.push( 'button-' + model.size ); 63 } 64 65 classes = _.uniq( classes.concat( this.options.classes ) ); 66 this.el.className = classes.join(' '); 67 68 this.$el.attr( 'disabled', model.disabled ); 69 this.$el.text( this.model.get('text') ); 70 71 return this; 72 }, 73 /** 74 * @param {Object} event 75 */ 76 click: function( event ) { 77 if ( '#' === this.attributes.href ) { 78 event.preventDefault(); 79 } 80 81 if ( this.options.click && ! this.model.get('disabled') ) { 82 this.options.click.apply( this, arguments ); 83 } 84 } 85 }); 86 87 module.exports = Button; 88 No newline at end of file -
src/wp-includes/js/media/views/cropper.js
1 /** 2 * wp.media.view.Cropper 3 * 4 * Uses the imgAreaSelect plugin to allow a user to crop an image. 5 * 6 * Takes imgAreaSelect options from 7 * wp.customize.HeaderControl.calculateImageSelectOptions via 8 * wp.customize.HeaderControl.openMM. 9 * 10 * @class 11 * @augments wp.media.View 12 * @augments wp.Backbone.View 13 * @augments Backbone.View 14 */ 15 var View = require( './view.js' ), 16 UploaderStatusError = require( './uploader/status-error.js' ), 17 UploaderStatus = require( './uploader/status.js' ), 18 l10n = wp.media.view.l10n, 19 $ = jQuery, 20 Cropper; 21 22 Cropper = View.extend({ 23 className: 'crop-content', 24 template: wp.template('crop-content'), 25 initialize: function() { 26 _.bindAll(this, 'onImageLoad'); 27 }, 28 ready: function() { 29 this.controller.frame.on('content:error:crop', this.onError, this); 30 this.$image = this.$el.find('.crop-image'); 31 this.$image.on('load', this.onImageLoad); 32 $(window).on('resize.cropper', _.debounce(this.onImageLoad, 250)); 33 }, 34 remove: function() { 35 $(window).off('resize.cropper'); 36 this.$el.remove(); 37 this.$el.off(); 38 View.prototype.remove.apply(this, arguments); 39 }, 40 prepare: function() { 41 return { 42 title: l10n.cropYourImage, 43 url: this.options.attachment.get('url') 44 }; 45 }, 46 onImageLoad: function() { 47 var imgOptions = this.controller.get('imgSelectOptions'); 48 if (typeof imgOptions === 'function') { 49 imgOptions = imgOptions(this.options.attachment, this.controller); 50 } 51 52 imgOptions = _.extend(imgOptions, {parent: this.$el}); 53 this.trigger('image-loaded'); 54 this.controller.imgSelect = this.$image.imgAreaSelect(imgOptions); 55 }, 56 onError: function() { 57 var filename = this.options.attachment.get('filename'); 58 59 this.views.add( '.upload-errors', new UploaderStatusError({ 60 filename: UploaderStatus.prototype.filename(filename), 61 message: _wpMediaViewsL10n.cropError 62 }), { at: 0 }); 63 } 64 }); 65 66 module.exports = Cropper; 67 No newline at end of file -
src/wp-includes/js/media/views/edit-image-details.js
1 var View = require( './view.js' ), 2 EditImage = require( './edit-image.js' ), 3 Details; 4 5 Details = EditImage.extend({ 6 initialize: function( options ) { 7 this.editor = window.imageEdit; 8 this.frame = options.frame; 9 this.controller = options.controller; 10 View.prototype.initialize.apply( this, arguments ); 11 }, 12 13 back: function() { 14 this.frame.content.mode( 'edit-metadata' ); 15 }, 16 17 save: function() { 18 var self = this; 19 20 this.model.fetch().done( function() { 21 self.frame.content.mode( 'edit-metadata' ); 22 }); 23 } 24 }); 25 26 module.exports = Details; 27 No newline at end of file -
src/wp-includes/js/media/views/edit-image.js
1 var View = require( './view.js' ), 2 EditImage; 3 4 EditImage = View.extend({ 5 className: 'image-editor', 6 template: wp.template('image-editor'), 7 8 initialize: function( options ) { 9 this.editor = window.imageEdit; 10 this.controller = options.controller; 11 View.prototype.initialize.apply( this, arguments ); 12 }, 13 14 prepare: function() { 15 return this.model.toJSON(); 16 }, 17 18 render: function() { 19 View.prototype.render.apply( this, arguments ); 20 return this; 21 }, 22 23 loadEditor: function() { 24 var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this ); 25 dfd.done( _.bind( this.focus, this ) ); 26 }, 27 28 focus: function() { 29 this.$( '.imgedit-submit .button' ).eq( 0 ).focus(); 30 }, 31 32 back: function() { 33 var lastState = this.controller.lastState(); 34 this.controller.setState( lastState ); 35 }, 36 37 refresh: function() { 38 this.model.fetch(); 39 }, 40 41 save: function() { 42 var self = this, 43 lastState = this.controller.lastState(); 44 45 this.model.fetch().done( function() { 46 self.controller.setState( lastState ); 47 }); 48 } 49 50 }); 51 52 module.exports = EditImage; 53 No newline at end of file -
src/wp-includes/js/media/views/embed/image.js
1 /** 2 * wp.media.view.EmbedImage 3 * 4 * @class 5 * @augments wp.media.view.Settings.AttachmentDisplay 6 * @augments wp.media.view.Settings 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var AttachmentDisplay = require( '../settings/attachment-display.js' ), 12 EmbedImage; 13 14 EmbedImage = AttachmentDisplay.extend({ 15 className: 'embed-media-settings', 16 template: wp.template('embed-image-settings'), 17 18 initialize: function() { 19 /** 20 * Call `initialize` directly on parent class with passed arguments 21 */ 22 AttachmentDisplay.prototype.initialize.apply( this, arguments ); 23 this.model.on( 'change:url', this.updateImage, this ); 24 }, 25 26 updateImage: function() { 27 this.$('img').attr( 'src', this.model.get('url') ); 28 } 29 }); 30 31 module.exports = EmbedImage; 32 No newline at end of file -
src/wp-includes/js/media/views/embed/link.js
1 /** 2 * wp.media.view.EmbedLink 3 * 4 * @class 5 * @augments wp.media.view.Settings 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Settings = require( '../settings.js' ), 11 $ = jQuery, 12 EmbedLink; 13 14 EmbedLink = Settings.extend({ 15 className: 'embed-link-settings', 16 template: wp.template('embed-link-settings'), 17 18 initialize: function() { 19 this.spinner = $('<span class="spinner" />'); 20 this.$el.append( this.spinner[0] ); 21 this.listenTo( this.model, 'change:url', this.updateoEmbed ); 22 }, 23 24 updateoEmbed: function() { 25 var url = this.model.get( 'url' ); 26 27 this.$('.setting.title').show(); 28 // clear out previous results 29 this.$('.embed-container').hide().find('.embed-preview').html(''); 30 31 // only proceed with embed if the field contains more than 6 characters 32 if ( url && url.length < 6 ) { 33 return; 34 } 35 36 this.spinner.show(); 37 38 setTimeout( _.bind( this.fetch, this ), 500 ); 39 }, 40 41 fetch: function() { 42 // check if they haven't typed in 500 ms 43 if ( $('#embed-url-field').val() !== this.model.get('url') ) { 44 return; 45 } 46 47 wp.ajax.send( 'parse-embed', { 48 data : { 49 post_ID: wp.media.view.settings.post.id, 50 shortcode: '[embed]' + this.model.get('url') + '[/embed]' 51 } 52 } ).done( _.bind( this.renderoEmbed, this ) ); 53 }, 54 55 renderoEmbed: function( response ) { 56 var html = ( response && response.body ) || ''; 57 58 this.spinner.hide(); 59 60 this.$('.setting.title').hide(); 61 this.$('.embed-container').show().find('.embed-preview').html( html ); 62 } 63 }); 64 65 module.exports = EmbedLink; 66 No newline at end of file -
src/wp-includes/js/media/views/embed/url.js
1 /** 2 * wp.media.view.EmbedUrl 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( '../view.js' ), 10 $ = jQuery, 11 EmbedUrl; 12 13 EmbedUrl = View.extend({ 14 tagName: 'label', 15 className: 'embed-url', 16 17 events: { 18 'input': 'url', 19 'keyup': 'url', 20 'change': 'url' 21 }, 22 23 initialize: function() { 24 var self = this; 25 26 this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') ); 27 this.input = this.$input[0]; 28 29 this.spinner = $('<span class="spinner" />')[0]; 30 this.$el.append([ this.input, this.spinner ]); 31 32 this.model.on( 'change:url', this.render, this ); 33 34 if ( this.model.get( 'url' ) ) { 35 _.delay( function () { 36 self.model.trigger( 'change:url' ); 37 }, 500 ); 38 } 39 }, 40 /** 41 * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining 42 */ 43 render: function() { 44 var $input = this.$input; 45 46 if ( $input.is(':focus') ) { 47 return; 48 } 49 50 this.input.value = this.model.get('url') || 'http://'; 51 /** 52 * Call `render` directly on parent class with passed arguments 53 */ 54 View.prototype.render.apply( this, arguments ); 55 return this; 56 }, 57 58 ready: function() { 59 if ( ! wp.media.isTouchDevice ) { 60 this.focus(); 61 } 62 }, 63 64 url: function( event ) { 65 this.model.set( 'url', event.target.value ); 66 }, 67 68 /** 69 * If the input is visible, focus and select its contents. 70 */ 71 focus: function() { 72 var $input = this.$input; 73 if ( $input.is(':visible') ) { 74 $input.focus()[0].select(); 75 } 76 } 77 }); 78 79 module.exports = EmbedUrl; 80 No newline at end of file -
src/wp-includes/js/media/views/embed.js
1 /** 2 * wp.media.view.Embed 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 EmbedImage = require( './embed/image.js' ), 11 EmbedLink = require( './embed/link.js' ), 12 EmbedUrl = require( './embed/url.js' ), 13 Embed; 14 15 Embed = View.extend({ 16 className: 'media-embed', 17 18 initialize: function() { 19 /** 20 * @member {wp.media.view.EmbedUrl} 21 */ 22 this.url = new EmbedUrl({ 23 controller: this.controller, 24 model: this.model.props 25 }).render(); 26 27 this.views.set([ this.url ]); 28 this.refresh(); 29 this.model.on( 'change:type', this.refresh, this ); 30 this.model.on( 'change:loading', this.loading, this ); 31 }, 32 33 /** 34 * @param {Object} view 35 */ 36 settings: function( view ) { 37 if ( this._settings ) { 38 this._settings.remove(); 39 } 40 this._settings = view; 41 this.views.add( view ); 42 }, 43 44 refresh: function() { 45 var type = this.model.get('type'), 46 constructor; 47 48 if ( 'image' === type ) { 49 constructor = EmbedImage; 50 } else if ( 'link' === type ) { 51 constructor = EmbedLink; 52 } else { 53 return; 54 } 55 56 this.settings( new constructor({ 57 controller: this.controller, 58 model: this.model.props, 59 priority: 40 60 }) ); 61 }, 62 63 loading: function() { 64 this.$el.toggleClass( 'embed-loading', this.model.get('loading') ); 65 } 66 }); 67 68 module.exports = Embed; 69 No newline at end of file -
src/wp-includes/js/media/views/focus-manager.js
1 /** 2 * wp.media.view.FocusManager 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 FocusManager; 11 12 FocusManager = View.extend({ 13 14 events: { 15 'keydown': 'constrainTabbing' 16 }, 17 18 focus: function() { // Reset focus on first left menu item 19 this.$('.media-menu-item').first().focus(); 20 }, 21 /** 22 * @param {Object} event 23 */ 24 constrainTabbing: function( event ) { 25 var tabbables; 26 27 // Look for the tab key. 28 if ( 9 !== event.keyCode ) { 29 return; 30 } 31 32 tabbables = this.$( ':tabbable' ); 33 34 // Keep tab focus within media modal while it's open 35 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { 36 tabbables.first().focus(); 37 return false; 38 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { 39 tabbables.last().focus(); 40 return false; 41 } 42 } 43 44 }); 45 46 module.exports = FocusManager; 47 No newline at end of file -
src/wp-includes/js/media/views/frame/audio-details.js
1 /** 2 * wp.media.view.MediaFrame.AudioDetails 3 * 4 * @constructor 5 * @augments wp.media.view.MediaFrame.MediaDetails 6 * @augments wp.media.view.MediaFrame.Select 7 * @augments wp.media.view.MediaFrame 8 * @augments wp.media.view.Frame 9 * @augments wp.media.View 10 * @augments wp.Backbone.View 11 * @augments Backbone.View 12 * @mixes wp.media.controller.StateMachine 13 */ 14 var MediaDetails = require( './media-details' ), 15 MediaLibrary = require( '../../controllers/media-library.js' ), 16 AudioDetailsView = require( '../audio-details.js' ), 17 AudioDetailsController = require( '../../controllers/audio-details.js' ), 18 l10n = wp.media.view.l10n, 19 AudioDetails; 20 21 AudioDetails = MediaDetails.extend({ 22 defaults: { 23 id: 'audio', 24 url: '', 25 menu: 'audio-details', 26 content: 'audio-details', 27 toolbar: 'audio-details', 28 type: 'link', 29 title: l10n.audioDetailsTitle, 30 priority: 120 31 }, 32 33 initialize: function( options ) { 34 options.DetailsView = AudioDetailsView; 35 options.cancelText = l10n.audioDetailsCancel; 36 options.addText = l10n.audioAddSourceTitle; 37 38 MediaDetails.prototype.initialize.call( this, options ); 39 }, 40 41 bindHandlers: function() { 42 MediaDetails.prototype.bindHandlers.apply( this, arguments ); 43 44 this.on( 'toolbar:render:replace-audio', this.renderReplaceToolbar, this ); 45 this.on( 'toolbar:render:add-audio-source', this.renderAddSourceToolbar, this ); 46 }, 47 48 createStates: function() { 49 this.states.add([ 50 new AudioDetailsController( { 51 media: this.media 52 } ), 53 54 new MediaLibrary( { 55 type: 'audio', 56 id: 'replace-audio', 57 title: l10n.audioReplaceTitle, 58 toolbar: 'replace-audio', 59 media: this.media, 60 menu: 'audio-details' 61 } ), 62 63 new MediaLibrary( { 64 type: 'audio', 65 id: 'add-audio-source', 66 title: l10n.audioAddSourceTitle, 67 toolbar: 'add-audio-source', 68 media: this.media, 69 menu: false 70 } ) 71 ]); 72 } 73 }); 74 75 module.exports = AudioDetails; 76 No newline at end of file -
src/wp-includes/js/media/views/frame/edit-attachments.js
1 /** 2 * A frame for editing the details of a specific media item. 3 * 4 * Opens in a modal by default. 5 * 6 * Requires an attachment model to be passed in the options hash under `model`. 7 * 8 * @constructor 9 * @augments wp.media.view.Frame 10 * @augments wp.media.View 11 * @augments wp.Backbone.View 12 * @augments Backbone.View 13 * @mixes wp.media.controller.StateMachine 14 */ 15 var Frame = require( '../frame.js' ), 16 MediaFrame = require( '../media-frame.js' ), 17 Modal = require( '../modal.js' ), 18 EditAttachmentMetadata = require( '../../controllers/edit-attachment-metadata.js' ), 19 TwoColumn = require( '../attachment/details-two-column.js' ), 20 AttachmentCompat = require( '../attachment-compat.js' ), 21 EditImageController = require( '../../controllers/edit-image.js' ), 22 DetailsView = require( '../edit-image-details.js' ), 23 $ = jQuery, 24 EditAttachments; 25 26 EditAttachments = MediaFrame.extend({ 27 28 className: 'edit-attachment-frame', 29 template: wp.template( 'edit-attachment-frame' ), 30 regions: [ 'title', 'content' ], 31 32 events: { 33 'click .left': 'previousMediaItem', 34 'click .right': 'nextMediaItem' 35 }, 36 37 initialize: function() { 38 Frame.prototype.initialize.apply( this, arguments ); 39 40 _.defaults( this.options, { 41 modal: true, 42 state: 'edit-attachment' 43 }); 44 45 this.controller = this.options.controller; 46 this.gridRouter = this.controller.gridRouter; 47 this.library = this.options.library; 48 49 if ( this.options.model ) { 50 this.model = this.options.model; 51 } 52 53 this.bindHandlers(); 54 this.createStates(); 55 this.createModal(); 56 57 this.title.mode( 'default' ); 58 this.toggleNav(); 59 }, 60 61 bindHandlers: function() { 62 // Bind default title creation. 63 this.on( 'title:create:default', this.createTitle, this ); 64 65 // Close the modal if the attachment is deleted. 66 this.listenTo( this.model, 'change:status destroy', this.close, this ); 67 68 this.on( 'content:create:edit-metadata', this.editMetadataMode, this ); 69 this.on( 'content:create:edit-image', this.editImageMode, this ); 70 this.on( 'content:render:edit-image', this.editImageModeRender, this ); 71 this.on( 'close', this.detach ); 72 }, 73 74 createModal: function() { 75 var self = this; 76 77 // Initialize modal container view. 78 if ( this.options.modal ) { 79 this.modal = new Modal({ 80 controller: this, 81 title: this.options.title 82 }); 83 84 this.modal.on( 'open', function () { 85 $( 'body' ).on( 'keydown.media-modal', _.bind( self.keyEvent, self ) ); 86 } ); 87 88 // Completely destroy the modal DOM element when closing it. 89 this.modal.on( 'close', function() { 90 self.modal.remove(); 91 $( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */ 92 // Restore the original focus item if possible 93 $( 'li.attachment[data-id="' + self.model.get( 'id' ) +'"]' ).focus(); 94 self.resetRoute(); 95 } ); 96 97 // Set this frame as the modal's content. 98 this.modal.content( this ); 99 this.modal.open(); 100 } 101 }, 102 103 /** 104 * Add the default states to the frame. 105 */ 106 createStates: function() { 107 this.states.add([ 108 new EditAttachmentMetadata( { model: this.model } ) 109 ]); 110 }, 111 112 /** 113 * Content region rendering callback for the `edit-metadata` mode. 114 * 115 * @param {Object} contentRegion Basic object with a `view` property, which 116 * should be set with the proper region view. 117 */ 118 editMetadataMode: function( contentRegion ) { 119 contentRegion.view = new TwoColumn({ 120 controller: this, 121 model: this.model 122 }); 123 124 /** 125 * Attach a subview to display fields added via the 126 * `attachment_fields_to_edit` filter. 127 */ 128 contentRegion.view.views.set( '.attachment-compat', new AttachmentCompat({ 129 controller: this, 130 model: this.model 131 }) ); 132 133 // Update browser url when navigating media details 134 if ( this.model ) { 135 this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id ) ); 136 } 137 }, 138 139 /** 140 * Render the EditImage view into the frame's content region. 141 * 142 * @param {Object} contentRegion Basic object with a `view` property, which 143 * should be set with the proper region view. 144 */ 145 editImageMode: function( contentRegion ) { 146 var editImageController = new EditImageController( { 147 model: this.model, 148 frame: this 149 } ); 150 // Noop some methods. 151 editImageController._toolbar = function() {}; 152 editImageController._router = function() {}; 153 editImageController._menu = function() {}; 154 155 contentRegion.view = new DetailsView( { 156 model: this.model, 157 frame: this, 158 controller: editImageController 159 } ); 160 }, 161 162 editImageModeRender: function( view ) { 163 view.on( 'ready', view.loadEditor ); 164 }, 165 166 toggleNav: function() { 167 this.$('.left').toggleClass( 'disabled', ! this.hasPrevious() ); 168 this.$('.right').toggleClass( 'disabled', ! this.hasNext() ); 169 }, 170 171 /** 172 * Rerender the view. 173 */ 174 rerender: function() { 175 // Only rerender the `content` region. 176 if ( this.content.mode() !== 'edit-metadata' ) { 177 this.content.mode( 'edit-metadata' ); 178 } else { 179 this.content.render(); 180 } 181 182 this.toggleNav(); 183 }, 184 185 /** 186 * Click handler to switch to the previous media item. 187 */ 188 previousMediaItem: function() { 189 if ( ! this.hasPrevious() ) { 190 this.$( '.left' ).blur(); 191 return; 192 } 193 this.model = this.library.at( this.getCurrentIndex() - 1 ); 194 this.rerender(); 195 this.$( '.left' ).focus(); 196 }, 197 198 /** 199 * Click handler to switch to the next media item. 200 */ 201 nextMediaItem: function() { 202 if ( ! this.hasNext() ) { 203 this.$( '.right' ).blur(); 204 return; 205 } 206 this.model = this.library.at( this.getCurrentIndex() + 1 ); 207 this.rerender(); 208 this.$( '.right' ).focus(); 209 }, 210 211 getCurrentIndex: function() { 212 return this.library.indexOf( this.model ); 213 }, 214 215 hasNext: function() { 216 return ( this.getCurrentIndex() + 1 ) < this.library.length; 217 }, 218 219 hasPrevious: function() { 220 return ( this.getCurrentIndex() - 1 ) > -1; 221 }, 222 /** 223 * Respond to the keyboard events: right arrow, left arrow, except when 224 * focus is in a textarea or input field. 225 */ 226 keyEvent: function( event ) { 227 if ( ( 'INPUT' === event.target.nodeName || 'TEXTAREA' === event.target.nodeName ) && ! ( event.target.readOnly || event.target.disabled ) ) { 228 return; 229 } 230 231 // The right arrow key 232 if ( 39 === event.keyCode ) { 233 this.nextMediaItem(); 234 } 235 // The left arrow key 236 if ( 37 === event.keyCode ) { 237 this.previousMediaItem(); 238 } 239 }, 240 241 resetRoute: function() { 242 this.gridRouter.navigate( this.gridRouter.baseUrl( '' ) ); 243 } 244 }); 245 246 module.exports = EditAttachments; 247 No newline at end of file -
src/wp-includes/js/media/views/frame/image-details.js
1 /** 2 * wp.media.view.MediaFrame.ImageDetails 3 * 4 * A media frame for manipulating an image that's already been inserted 5 * into a post. 6 * 7 * @class 8 * @augments wp.media.view.MediaFrame.Select 9 * @augments wp.media.view.MediaFrame 10 * @augments wp.media.view.Frame 11 * @augments wp.media.View 12 * @augments wp.Backbone.View 13 * @augments Backbone.View 14 * @mixes wp.media.controller.StateMachine 15 */ 16 var Select = require( './select.js' ), 17 Toolbar = require( '../toolbar.js' ), 18 PostImage = require( '../../models/post-image.js' ), 19 Selection = require( '../../models/selection.js' ), 20 ImageDetailsController = require( '../../controllers/image-details.js' ), 21 ReplaceImageController = require( '../../controllers/replace-image.js' ), 22 EditImageController = require( '../../controllers/edit-image.js' ), 23 ImageDetailsView = require( '../image-details.js' ), 24 EditImageView = require( '../edit-image.js' ), 25 l10n = wp.media.view.l10n, 26 ImageDetails; 27 28 ImageDetails = Select.extend({ 29 defaults: { 30 id: 'image', 31 url: '', 32 menu: 'image-details', 33 content: 'image-details', 34 toolbar: 'image-details', 35 type: 'link', 36 title: l10n.imageDetailsTitle, 37 priority: 120 38 }, 39 40 initialize: function( options ) { 41 this.image = new PostImage( options.metadata ); 42 this.options.selection = new Selection( this.image.attachment, { multiple: false } ); 43 Select.prototype.initialize.apply( this, arguments ); 44 }, 45 46 bindHandlers: function() { 47 Select.prototype.bindHandlers.apply( this, arguments ); 48 this.on( 'menu:create:image-details', this.createMenu, this ); 49 this.on( 'content:create:image-details', this.imageDetailsContent, this ); 50 this.on( 'content:render:edit-image', this.editImageContent, this ); 51 this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this ); 52 // override the select toolbar 53 this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this ); 54 }, 55 56 createStates: function() { 57 this.states.add([ 58 new ImageDetailsController({ 59 image: this.image, 60 editable: false 61 }), 62 new ReplaceImageController({ 63 id: 'replace-image', 64 library: wp.media.query( { type: 'image' } ), 65 image: this.image, 66 multiple: false, 67 title: l10n.imageReplaceTitle, 68 toolbar: 'replace', 69 priority: 80, 70 displaySettings: true 71 }), 72 new EditImageController( { 73 image: this.image, 74 selection: this.options.selection 75 } ) 76 ]); 77 }, 78 79 imageDetailsContent: function( options ) { 80 options.view = new ImageDetailsView({ 81 controller: this, 82 model: this.state().image, 83 attachment: this.state().image.attachment 84 }); 85 }, 86 87 editImageContent: function() { 88 var state = this.state(), 89 model = state.get('image'), 90 view; 91 92 if ( ! model ) { 93 return; 94 } 95 96 view = new EditImageView( { model: model, controller: this } ).render(); 97 98 this.content.set( view ); 99 100 // after bringing in the frame, load the actual editor via an ajax call 101 view.loadEditor(); 102 103 }, 104 105 renderImageDetailsToolbar: function() { 106 this.toolbar.set( new Toolbar({ 107 controller: this, 108 items: { 109 select: { 110 style: 'primary', 111 text: l10n.update, 112 priority: 80, 113 114 click: function() { 115 var controller = this.controller, 116 state = controller.state(); 117 118 controller.close(); 119 120 // not sure if we want to use wp.media.string.image which will create a shortcode or 121 // perhaps wp.html.string to at least to build the <img /> 122 state.trigger( 'update', controller.image.toJSON() ); 123 124 // Restore and reset the default state. 125 controller.setState( controller.options.state ); 126 controller.reset(); 127 } 128 } 129 } 130 }) ); 131 }, 132 133 renderReplaceImageToolbar: function() { 134 var frame = this, 135 lastState = frame.lastState(), 136 previous = lastState && lastState.id; 137 138 this.toolbar.set( new Toolbar({ 139 controller: this, 140 items: { 141 back: { 142 text: l10n.back, 143 priority: 20, 144 click: function() { 145 if ( previous ) { 146 frame.setState( previous ); 147 } else { 148 frame.close(); 149 } 150 } 151 }, 152 153 replace: { 154 style: 'primary', 155 text: l10n.replace, 156 priority: 80, 157 158 click: function() { 159 var controller = this.controller, 160 state = controller.state(), 161 selection = state.get( 'selection' ), 162 attachment = selection.single(); 163 164 controller.close(); 165 166 controller.image.changeAttachment( attachment, state.display( attachment ) ); 167 168 // not sure if we want to use wp.media.string.image which will create a shortcode or 169 // perhaps wp.html.string to at least to build the <img /> 170 state.trigger( 'replace', controller.image.toJSON() ); 171 172 // Restore and reset the default state. 173 controller.setState( controller.options.state ); 174 controller.reset(); 175 } 176 } 177 } 178 }) ); 179 } 180 181 }); 182 183 module.exports = ImageDetails; 184 No newline at end of file -
src/wp-includes/js/media/views/frame/manage.js
1 /** 2 * wp.media.view.MediaFrame.Manage 3 * 4 * A generic management frame workflow. 5 * 6 * Used in the media grid view. 7 * 8 * @constructor 9 * @augments wp.media.view.MediaFrame 10 * @augments wp.media.view.Frame 11 * @augments wp.media.View 12 * @augments wp.Backbone.View 13 * @augments Backbone.View 14 * @mixes wp.media.controller.StateMachine 15 */ 16 var MediaFrame = require( '../media-frame.js' ), 17 UploaderWindow = require( '../uploader/window.js' ), 18 AttachmentsBrowser = require( '../attachments/browser.js' ), 19 Router = require( '../../router/manage.js' ), 20 Library = require( '../../controllers/library.js' ), 21 $ = jQuery, 22 Manage; 23 24 Manage = MediaFrame.extend({ 25 /** 26 * @global wp.Uploader 27 */ 28 initialize: function() { 29 var self = this; 30 _.defaults( this.options, { 31 title: '', 32 modal: false, 33 selection: [], 34 library: {}, // Options hash for the query to the media library. 35 multiple: 'add', 36 state: 'library', 37 uploader: true, 38 mode: [ 'grid', 'edit' ] 39 }); 40 41 this.$body = $( document.body ); 42 this.$window = $( window ); 43 this.$adminBar = $( '#wpadminbar' ); 44 this.$window.on( 'scroll resize', _.debounce( _.bind( this.fixPosition, this ), 15 ) ); 45 $( document ).on( 'click', '.add-new-h2', _.bind( this.addNewClickHandler, this ) ); 46 47 // Ensure core and media grid view UI is enabled. 48 this.$el.addClass('wp-core-ui'); 49 50 // Force the uploader off if the upload limit has been exceeded or 51 // if the browser isn't supported. 52 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 53 this.options.uploader = false; 54 } 55 56 // Initialize a window-wide uploader. 57 if ( this.options.uploader ) { 58 this.uploader = new UploaderWindow({ 59 controller: this, 60 uploader: { 61 dropzone: document.body, 62 container: document.body 63 } 64 }).render(); 65 this.uploader.ready(); 66 $('body').append( this.uploader.el ); 67 68 this.options.uploader = false; 69 } 70 71 this.gridRouter = new Router(); 72 73 // Call 'initialize' directly on the parent class. 74 MediaFrame.prototype.initialize.apply( this, arguments ); 75 76 // Append the frame view directly the supplied container. 77 this.$el.appendTo( this.options.container ); 78 79 this.createStates(); 80 this.bindRegionModeHandlers(); 81 this.render(); 82 83 // Update the URL when entering search string (at most once per second) 84 $( '#media-search-input' ).on( 'input', _.debounce( function(e) { 85 var val = $( e.currentTarget ).val(), url = ''; 86 if ( val ) { 87 url += '?search=' + val; 88 } 89 self.gridRouter.navigate( self.gridRouter.baseUrl( url ) ); 90 }, 1000 ) ); 91 }, 92 93 /** 94 * Create the default states for the frame. 95 */ 96 createStates: function() { 97 var options = this.options; 98 99 if ( this.options.states ) { 100 return; 101 } 102 103 // Add the default states. 104 this.states.add([ 105 new Library({ 106 library: wp.media.query( options.library ), 107 multiple: options.multiple, 108 title: options.title, 109 content: 'browse', 110 toolbar: 'select', 111 contentUserSetting: false, 112 filterable: 'all', 113 autoSelect: false 114 }) 115 ]); 116 }, 117 118 /** 119 * Bind region mode activation events to proper handlers. 120 */ 121 bindRegionModeHandlers: function() { 122 this.on( 'content:create:browse', this.browseContent, this ); 123 124 // Handle a frame-level event for editing an attachment. 125 this.on( 'edit:attachment', this.openEditAttachmentModal, this ); 126 127 this.on( 'select:activate', this.bindKeydown, this ); 128 this.on( 'select:deactivate', this.unbindKeydown, this ); 129 }, 130 131 handleKeydown: function( e ) { 132 if ( 27 === e.which ) { 133 e.preventDefault(); 134 this.deactivateMode( 'select' ).activateMode( 'edit' ); 135 } 136 }, 137 138 bindKeydown: function() { 139 this.$body.on( 'keydown.select', _.bind( this.handleKeydown, this ) ); 140 }, 141 142 unbindKeydown: function() { 143 this.$body.off( 'keydown.select' ); 144 }, 145 146 fixPosition: function() { 147 var $browser, $toolbar; 148 if ( ! this.isModeActive( 'select' ) ) { 149 return; 150 } 151 152 $browser = this.$('.attachments-browser'); 153 $toolbar = $browser.find('.media-toolbar'); 154 155 // Offset doesn't appear to take top margin into account, hence +16 156 if ( ( $browser.offset().top + 16 ) < this.$window.scrollTop() + this.$adminBar.height() ) { 157 $browser.addClass( 'fixed' ); 158 $toolbar.css('width', $browser.width() + 'px'); 159 } else { 160 $browser.removeClass( 'fixed' ); 161 $toolbar.css('width', ''); 162 } 163 }, 164 165 /** 166 * Click handler for the `Add New` button. 167 */ 168 addNewClickHandler: function( event ) { 169 event.preventDefault(); 170 this.trigger( 'toggle:upload:attachment' ); 171 }, 172 173 /** 174 * Open the Edit Attachment modal. 175 */ 176 openEditAttachmentModal: function( model ) { 177 // Create a new EditAttachment frame, passing along the library and the attachment model. 178 wp.media( { 179 frame: 'edit-attachments', 180 controller: this, 181 library: this.state().get('library'), 182 model: model 183 } ); 184 }, 185 186 /** 187 * Create an attachments browser view within the content region. 188 * 189 * @param {Object} contentRegion Basic object with a `view` property, which 190 * should be set with the proper region view. 191 * @this wp.media.controller.Region 192 */ 193 browseContent: function( contentRegion ) { 194 var state = this.state(); 195 196 // Browse our library of attachments. 197 this.browserView = contentRegion.view = new AttachmentsBrowser({ 198 controller: this, 199 collection: state.get('library'), 200 selection: state.get('selection'), 201 model: state, 202 sortable: state.get('sortable'), 203 search: state.get('searchable'), 204 filters: state.get('filterable'), 205 display: state.get('displaySettings'), 206 dragInfo: state.get('dragInfo'), 207 sidebar: 'errors', 208 209 suggestedWidth: state.get('suggestedWidth'), 210 suggestedHeight: state.get('suggestedHeight'), 211 212 AttachmentView: state.get('AttachmentView'), 213 214 scrollElement: document 215 }); 216 this.browserView.on( 'ready', _.bind( this.bindDeferred, this ) ); 217 218 this.errors = wp.Uploader.errors; 219 this.errors.on( 'add remove reset', this.sidebarVisibility, this ); 220 }, 221 222 sidebarVisibility: function() { 223 this.browserView.$( '.media-sidebar' ).toggle( !! this.errors.length ); 224 }, 225 226 bindDeferred: function() { 227 if ( ! this.browserView.dfd ) { 228 return; 229 } 230 this.browserView.dfd.done( _.bind( this.startHistory, this ) ); 231 }, 232 233 startHistory: function() { 234 // Verify pushState support and activate 235 if ( window.history && window.history.pushState ) { 236 Backbone.history.start( { 237 root: _wpMediaGridSettings.adminUrl, 238 pushState: true 239 } ); 240 } 241 } 242 }); 243 244 module.exports = Manage; 245 No newline at end of file -
src/wp-includes/js/media/views/frame/media-details.js
1 /** 2 * wp.media.view.MediaFrame.MediaDetails 3 * 4 * @constructor 5 * @augments wp.media.view.MediaFrame.Select 6 * @augments wp.media.view.MediaFrame 7 * @augments wp.media.view.Frame 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 * @mixes wp.media.controller.StateMachine 12 */ 13 var View = require( '../view.js' ), 14 Toolbar = require( '../toolbar.js' ), 15 Select = require( './select.js' ), 16 Selection = require( '../../models/selection.js' ), 17 PostMedia = require( '../../models/post-media.js' ), 18 l10n = wp.media.view.l10n, 19 MediaDetails; 20 21 MediaDetails = Select.extend({ 22 defaults: { 23 id: 'media', 24 url: '', 25 menu: 'media-details', 26 content: 'media-details', 27 toolbar: 'media-details', 28 type: 'link', 29 priority: 120 30 }, 31 32 initialize: function( options ) { 33 this.DetailsView = options.DetailsView; 34 this.cancelText = options.cancelText; 35 this.addText = options.addText; 36 37 this.media = new PostMedia( options.metadata ); 38 this.options.selection = new Selection( this.media.attachment, { multiple: false } ); 39 Select.prototype.initialize.apply( this, arguments ); 40 }, 41 42 bindHandlers: function() { 43 var menu = this.defaults.menu; 44 45 Select.prototype.bindHandlers.apply( this, arguments ); 46 47 this.on( 'menu:create:' + menu, this.createMenu, this ); 48 this.on( 'content:render:' + menu, this.renderDetailsContent, this ); 49 this.on( 'menu:render:' + menu, this.renderMenu, this ); 50 this.on( 'toolbar:render:' + menu, this.renderDetailsToolbar, this ); 51 }, 52 53 renderDetailsContent: function() { 54 var view = new this.DetailsView({ 55 controller: this, 56 model: this.state().media, 57 attachment: this.state().media.attachment 58 }).render(); 59 60 this.content.set( view ); 61 }, 62 63 renderMenu: function( view ) { 64 var lastState = this.lastState(), 65 previous = lastState && lastState.id, 66 frame = this; 67 68 view.set({ 69 cancel: { 70 text: this.cancelText, 71 priority: 20, 72 click: function() { 73 if ( previous ) { 74 frame.setState( previous ); 75 } else { 76 frame.close(); 77 } 78 } 79 }, 80 separateCancel: new View({ 81 className: 'separator', 82 priority: 40 83 }) 84 }); 85 86 }, 87 88 setPrimaryButton: function(text, handler) { 89 this.toolbar.set( new Toolbar({ 90 controller: this, 91 items: { 92 button: { 93 style: 'primary', 94 text: text, 95 priority: 80, 96 click: function() { 97 var controller = this.controller; 98 handler.call( this, controller, controller.state() ); 99 // Restore and reset the default state. 100 controller.setState( controller.options.state ); 101 controller.reset(); 102 } 103 } 104 } 105 }) ); 106 }, 107 108 renderDetailsToolbar: function() { 109 this.setPrimaryButton( l10n.update, function( controller, state ) { 110 controller.close(); 111 state.trigger( 'update', controller.media.toJSON() ); 112 } ); 113 }, 114 115 renderReplaceToolbar: function() { 116 this.setPrimaryButton( l10n.replace, function( controller, state ) { 117 var attachment = state.get( 'selection' ).single(); 118 controller.media.changeAttachment( attachment ); 119 state.trigger( 'replace', controller.media.toJSON() ); 120 } ); 121 }, 122 123 renderAddSourceToolbar: function() { 124 this.setPrimaryButton( this.addText, function( controller, state ) { 125 var attachment = state.get( 'selection' ).single(); 126 controller.media.setSource( attachment ); 127 state.trigger( 'add-source', controller.media.toJSON() ); 128 } ); 129 } 130 }); 131 132 module.exports = MediaDetails; 133 No newline at end of file -
src/wp-includes/js/media/views/frame/post.js
1 /** 2 * wp.media.view.MediaFrame.Post 3 * 4 * The frame for manipulating media on the Edit Post page. 5 * 6 * @class 7 * @augments wp.media.view.MediaFrame.Select 8 * @augments wp.media.view.MediaFrame 9 * @augments wp.media.view.Frame 10 * @augments wp.media.View 11 * @augments wp.Backbone.View 12 * @augments Backbone.View 13 * @mixes wp.media.controller.StateMachine 14 */ 15 var View = require( '../view.js' ), 16 Select = require( './select.js' ), 17 Library = require( '../../controllers/library.js' ), 18 Embed = require( '../embed.js' ), 19 EditImage = require( '../edit-image.js' ), 20 EditSelection = require( '../attachment/edit-selection.js' ), 21 Toolbar = require( '../toolbar.js' ), 22 ToolbarEmbed = require( '../toolbar/embed.js' ), 23 PlaylistSettings = require( '../settings/playlist.js' ), 24 AttachmentsBrowser = require( '../attachments/browser.js' ), 25 SelectionModel = require( '../../models/selection.js' ), 26 SelectionView = require( '../selection.js' ), 27 EmbedController = require( '../../controllers/embed.js' ), 28 EditImageController = require( '../../controllers/edit-image.js' ), 29 GalleryEditController = require( '../../controllers/gallery-edit.js' ), 30 GalleryAddController = require( '../../controllers/gallery-add.js' ), 31 CollectionEditController = require( '../../controllers/collection-edit.js' ), 32 CollectionAddController = require( '../../controllers/collection-add.js' ), 33 FeaturedImageController = require( '../../controllers/featured-image.js' ), 34 l10n = wp.media.view.l10n, 35 Post; 36 37 Post = Select.extend({ 38 initialize: function() { 39 this.counts = { 40 audio: { 41 count: wp.media.view.settings.attachmentCounts.audio, 42 state: 'playlist' 43 }, 44 video: { 45 count: wp.media.view.settings.attachmentCounts.video, 46 state: 'video-playlist' 47 } 48 }; 49 50 _.defaults( this.options, { 51 multiple: true, 52 editing: false, 53 state: 'insert', 54 metadata: {} 55 }); 56 57 // Call 'initialize' directly on the parent class. 58 Select.prototype.initialize.apply( this, arguments ); 59 this.createIframeStates(); 60 61 }, 62 63 /** 64 * Create the default states. 65 */ 66 createStates: function() { 67 var options = this.options; 68 69 this.states.add([ 70 // Main states. 71 new Library({ 72 id: 'insert', 73 title: l10n.insertMediaTitle, 74 priority: 20, 75 toolbar: 'main-insert', 76 filterable: 'all', 77 library: wp.media.query( options.library ), 78 multiple: options.multiple ? 'reset' : false, 79 editable: true, 80 81 // If the user isn't allowed to edit fields, 82 // can they still edit it locally? 83 allowLocalEdits: true, 84 85 // Show the attachment display settings. 86 displaySettings: true, 87 // Update user settings when users adjust the 88 // attachment display settings. 89 displayUserSettings: true 90 }), 91 92 new Library({ 93 id: 'gallery', 94 title: l10n.createGalleryTitle, 95 priority: 40, 96 toolbar: 'main-gallery', 97 filterable: 'uploaded', 98 multiple: 'add', 99 editable: false, 100 101 library: wp.media.query( _.defaults({ 102 type: 'image' 103 }, options.library ) ) 104 }), 105 106 // Embed states. 107 new EmbedController( { metadata: options.metadata } ), 108 109 new EditImageController( { model: options.editImage } ), 110 111 // Gallery states. 112 new GalleryEditController({ 113 library: options.selection, 114 editing: options.editing, 115 menu: 'gallery' 116 }), 117 118 new GalleryAddController(), 119 120 new Library({ 121 id: 'playlist', 122 title: l10n.createPlaylistTitle, 123 priority: 60, 124 toolbar: 'main-playlist', 125 filterable: 'uploaded', 126 multiple: 'add', 127 editable: false, 128 129 library: wp.media.query( _.defaults({ 130 type: 'audio' 131 }, options.library ) ) 132 }), 133 134 // Playlist states. 135 new CollectionEditController({ 136 type: 'audio', 137 collectionType: 'playlist', 138 title: l10n.editPlaylistTitle, 139 SettingsView: PlaylistSettings, 140 library: options.selection, 141 editing: options.editing, 142 menu: 'playlist', 143 dragInfoText: l10n.playlistDragInfo, 144 dragInfo: false 145 }), 146 147 new CollectionAddController({ 148 type: 'audio', 149 collectionType: 'playlist', 150 title: l10n.addToPlaylistTitle 151 }), 152 153 new Library({ 154 id: 'video-playlist', 155 title: l10n.createVideoPlaylistTitle, 156 priority: 60, 157 toolbar: 'main-video-playlist', 158 filterable: 'uploaded', 159 multiple: 'add', 160 editable: false, 161 162 library: wp.media.query( _.defaults({ 163 type: 'video' 164 }, options.library ) ) 165 }), 166 167 new CollectionEditController({ 168 type: 'video', 169 collectionType: 'playlist', 170 title: l10n.editVideoPlaylistTitle, 171 SettingsView: PlaylistSettings, 172 library: options.selection, 173 editing: options.editing, 174 menu: 'video-playlist', 175 dragInfoText: l10n.videoPlaylistDragInfo, 176 dragInfo: false 177 }), 178 179 new CollectionAddController({ 180 type: 'video', 181 collectionType: 'playlist', 182 title: l10n.addToVideoPlaylistTitle 183 }) 184 ]); 185 186 if ( wp.media.view.settings.post.featuredImageId ) { 187 this.states.add( new FeaturedImageController() ); 188 } 189 }, 190 191 bindHandlers: function() { 192 var handlers, checkCounts; 193 194 Select.prototype.bindHandlers.apply( this, arguments ); 195 196 this.on( 'activate', this.activate, this ); 197 198 // Only bother checking media type counts if one of the counts is zero 199 checkCounts = _.find( this.counts, function( type ) { 200 return type.count === 0; 201 } ); 202 203 if ( typeof checkCounts !== 'undefined' ) { 204 this.listenTo( wp.media.model.Attachments.all, 'change:type', this.mediaTypeCounts ); 205 } 206 207 this.on( 'menu:create:gallery', this.createMenu, this ); 208 this.on( 'menu:create:playlist', this.createMenu, this ); 209 this.on( 'menu:create:video-playlist', this.createMenu, this ); 210 this.on( 'toolbar:create:main-insert', this.createToolbar, this ); 211 this.on( 'toolbar:create:main-gallery', this.createToolbar, this ); 212 this.on( 'toolbar:create:main-playlist', this.createToolbar, this ); 213 this.on( 'toolbar:create:main-video-playlist', this.createToolbar, this ); 214 this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this ); 215 this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this ); 216 217 handlers = { 218 menu: { 219 'default': 'mainMenu', 220 'gallery': 'galleryMenu', 221 'playlist': 'playlistMenu', 222 'video-playlist': 'videoPlaylistMenu' 223 }, 224 225 content: { 226 'embed': 'embedContent', 227 'edit-image': 'editImageContent', 228 'edit-selection': 'editSelectionContent' 229 }, 230 231 toolbar: { 232 'main-insert': 'mainInsertToolbar', 233 'main-gallery': 'mainGalleryToolbar', 234 'gallery-edit': 'galleryEditToolbar', 235 'gallery-add': 'galleryAddToolbar', 236 'main-playlist': 'mainPlaylistToolbar', 237 'playlist-edit': 'playlistEditToolbar', 238 'playlist-add': 'playlistAddToolbar', 239 'main-video-playlist': 'mainVideoPlaylistToolbar', 240 'video-playlist-edit': 'videoPlaylistEditToolbar', 241 'video-playlist-add': 'videoPlaylistAddToolbar' 242 } 243 }; 244 245 _.each( handlers, function( regionHandlers, region ) { 246 _.each( regionHandlers, function( callback, handler ) { 247 this.on( region + ':render:' + handler, this[ callback ], this ); 248 }, this ); 249 }, this ); 250 }, 251 252 activate: function() { 253 // Hide menu items for states tied to particular media types if there are no items 254 _.each( this.counts, function( type ) { 255 if ( type.count < 1 ) { 256 this.menuItemVisibility( type.state, 'hide' ); 257 } 258 }, this ); 259 }, 260 261 mediaTypeCounts: function( model, attr ) { 262 if ( typeof this.counts[ attr ] !== 'undefined' && this.counts[ attr ].count < 1 ) { 263 this.counts[ attr ].count++; 264 this.menuItemVisibility( this.counts[ attr ].state, 'show' ); 265 } 266 }, 267 268 // Menus 269 /** 270 * @param {wp.Backbone.View} view 271 */ 272 mainMenu: function( view ) { 273 view.set({ 274 'library-separator': new View({ 275 className: 'separator', 276 priority: 100 277 }) 278 }); 279 }, 280 281 menuItemVisibility: function( state, visibility ) { 282 var menu = this.menu.get(); 283 if ( visibility === 'hide' ) { 284 menu.hide( state ); 285 } else if ( visibility === 'show' ) { 286 menu.show( state ); 287 } 288 }, 289 /** 290 * @param {wp.Backbone.View} view 291 */ 292 galleryMenu: function( view ) { 293 var lastState = this.lastState(), 294 previous = lastState && lastState.id, 295 frame = this; 296 297 view.set({ 298 cancel: { 299 text: l10n.cancelGalleryTitle, 300 priority: 20, 301 click: function() { 302 if ( previous ) { 303 frame.setState( previous ); 304 } else { 305 frame.close(); 306 } 307 308 // Keep focus inside media modal 309 // after canceling a gallery 310 this.controller.modal.focusManager.focus(); 311 } 312 }, 313 separateCancel: new View({ 314 className: 'separator', 315 priority: 40 316 }) 317 }); 318 }, 319 320 playlistMenu: function( view ) { 321 var lastState = this.lastState(), 322 previous = lastState && lastState.id, 323 frame = this; 324 325 view.set({ 326 cancel: { 327 text: l10n.cancelPlaylistTitle, 328 priority: 20, 329 click: function() { 330 if ( previous ) { 331 frame.setState( previous ); 332 } else { 333 frame.close(); 334 } 335 } 336 }, 337 separateCancel: new View({ 338 className: 'separator', 339 priority: 40 340 }) 341 }); 342 }, 343 344 videoPlaylistMenu: function( view ) { 345 var lastState = this.lastState(), 346 previous = lastState && lastState.id, 347 frame = this; 348 349 view.set({ 350 cancel: { 351 text: l10n.cancelVideoPlaylistTitle, 352 priority: 20, 353 click: function() { 354 if ( previous ) { 355 frame.setState( previous ); 356 } else { 357 frame.close(); 358 } 359 } 360 }, 361 separateCancel: new View({ 362 className: 'separator', 363 priority: 40 364 }) 365 }); 366 }, 367 368 // Content 369 embedContent: function() { 370 var view = new Embed({ 371 controller: this, 372 model: this.state() 373 }).render(); 374 375 this.content.set( view ); 376 377 if ( ! wp.media.isTouchDevice ) { 378 view.url.focus(); 379 } 380 }, 381 382 editSelectionContent: function() { 383 var state = this.state(), 384 selection = state.get('selection'), 385 view; 386 387 view = new AttachmentsBrowser({ 388 controller: this, 389 collection: selection, 390 selection: selection, 391 model: state, 392 sortable: true, 393 search: false, 394 dragInfo: true, 395 396 AttachmentView: EditSelection 397 }).render(); 398 399 view.toolbar.set( 'backToLibrary', { 400 text: l10n.returnToLibrary, 401 priority: -100, 402 403 click: function() { 404 this.controller.content.mode('browse'); 405 } 406 }); 407 408 // Browse our library of attachments. 409 this.content.set( view ); 410 411 // Trigger the controller to set focus 412 this.trigger( 'edit:selection', this ); 413 }, 414 415 editImageContent: function() { 416 var image = this.state().get('image'), 417 view = new EditImage( { model: image, controller: this } ).render(); 418 419 this.content.set( view ); 420 421 // after creating the wrapper view, load the actual editor via an ajax call 422 view.loadEditor(); 423 424 }, 425 426 // Toolbars 427 428 /** 429 * @param {wp.Backbone.View} view 430 */ 431 selectionStatusToolbar: function( view ) { 432 var editable = this.state().get('editable'); 433 434 view.set( 'selection', new SelectionView({ 435 controller: this, 436 collection: this.state().get('selection'), 437 priority: -40, 438 439 // If the selection is editable, pass the callback to 440 // switch the content mode. 441 editable: editable && function() { 442 this.controller.content.mode('edit-selection'); 443 } 444 }).render() ); 445 }, 446 447 /** 448 * @param {wp.Backbone.View} view 449 */ 450 mainInsertToolbar: function( view ) { 451 var controller = this; 452 453 this.selectionStatusToolbar( view ); 454 455 view.set( 'insert', { 456 style: 'primary', 457 priority: 80, 458 text: l10n.insertIntoPost, 459 requires: { selection: true }, 460 461 /** 462 * @fires wp.media.controller.State#insert 463 */ 464 click: function() { 465 var state = controller.state(), 466 selection = state.get('selection'); 467 468 controller.close(); 469 state.trigger( 'insert', selection ).reset(); 470 } 471 }); 472 }, 473 474 /** 475 * @param {wp.Backbone.View} view 476 */ 477 mainGalleryToolbar: function( view ) { 478 var controller = this; 479 480 this.selectionStatusToolbar( view ); 481 482 view.set( 'gallery', { 483 style: 'primary', 484 text: l10n.createNewGallery, 485 priority: 60, 486 requires: { selection: true }, 487 488 click: function() { 489 var selection = controller.state().get('selection'), 490 edit = controller.state('gallery-edit'), 491 models = selection.where({ type: 'image' }); 492 493 edit.set( 'library', new SelectionModel( models, { 494 props: selection.props.toJSON(), 495 multiple: true 496 }) ); 497 498 this.controller.setState('gallery-edit'); 499 500 // Keep focus inside media modal 501 // after jumping to gallery view 502 this.controller.modal.focusManager.focus(); 503 } 504 }); 505 }, 506 507 mainPlaylistToolbar: function( view ) { 508 var controller = this; 509 510 this.selectionStatusToolbar( view ); 511 512 view.set( 'playlist', { 513 style: 'primary', 514 text: l10n.createNewPlaylist, 515 priority: 100, 516 requires: { selection: true }, 517 518 click: function() { 519 var selection = controller.state().get('selection'), 520 edit = controller.state('playlist-edit'), 521 models = selection.where({ type: 'audio' }); 522 523 edit.set( 'library', new SelectionModel( models, { 524 props: selection.props.toJSON(), 525 multiple: true 526 }) ); 527 528 this.controller.setState('playlist-edit'); 529 530 // Keep focus inside media modal 531 // after jumping to playlist view 532 this.controller.modal.focusManager.focus(); 533 } 534 }); 535 }, 536 537 mainVideoPlaylistToolbar: function( view ) { 538 var controller = this; 539 540 this.selectionStatusToolbar( view ); 541 542 view.set( 'video-playlist', { 543 style: 'primary', 544 text: l10n.createNewVideoPlaylist, 545 priority: 100, 546 requires: { selection: true }, 547 548 click: function() { 549 var selection = controller.state().get('selection'), 550 edit = controller.state('video-playlist-edit'), 551 models = selection.where({ type: 'video' }); 552 553 edit.set( 'library', new SelectionModel( models, { 554 props: selection.props.toJSON(), 555 multiple: true 556 }) ); 557 558 this.controller.setState('video-playlist-edit'); 559 560 // Keep focus inside media modal 561 // after jumping to video playlist view 562 this.controller.modal.focusManager.focus(); 563 } 564 }); 565 }, 566 567 featuredImageToolbar: function( toolbar ) { 568 this.createSelectToolbar( toolbar, { 569 text: l10n.setFeaturedImage, 570 state: this.options.state 571 }); 572 }, 573 574 mainEmbedToolbar: function( toolbar ) { 575 toolbar.view = new ToolbarEmbed({ 576 controller: this 577 }); 578 }, 579 580 galleryEditToolbar: function() { 581 var editing = this.state().get('editing'); 582 this.toolbar.set( new Toolbar({ 583 controller: this, 584 items: { 585 insert: { 586 style: 'primary', 587 text: editing ? l10n.updateGallery : l10n.insertGallery, 588 priority: 80, 589 requires: { library: true }, 590 591 /** 592 * @fires wp.media.controller.State#update 593 */ 594 click: function() { 595 var controller = this.controller, 596 state = controller.state(); 597 598 controller.close(); 599 state.trigger( 'update', state.get('library') ); 600 601 // Restore and reset the default state. 602 controller.setState( controller.options.state ); 603 controller.reset(); 604 } 605 } 606 } 607 }) ); 608 }, 609 610 galleryAddToolbar: function() { 611 this.toolbar.set( new Toolbar({ 612 controller: this, 613 items: { 614 insert: { 615 style: 'primary', 616 text: l10n.addToGallery, 617 priority: 80, 618 requires: { selection: true }, 619 620 /** 621 * @fires wp.media.controller.State#reset 622 */ 623 click: function() { 624 var controller = this.controller, 625 state = controller.state(), 626 edit = controller.state('gallery-edit'); 627 628 edit.get('library').add( state.get('selection').models ); 629 state.trigger('reset'); 630 controller.setState('gallery-edit'); 631 } 632 } 633 } 634 }) ); 635 }, 636 637 playlistEditToolbar: function() { 638 var editing = this.state().get('editing'); 639 this.toolbar.set( new Toolbar({ 640 controller: this, 641 items: { 642 insert: { 643 style: 'primary', 644 text: editing ? l10n.updatePlaylist : l10n.insertPlaylist, 645 priority: 80, 646 requires: { library: true }, 647 648 /** 649 * @fires wp.media.controller.State#update 650 */ 651 click: function() { 652 var controller = this.controller, 653 state = controller.state(); 654 655 controller.close(); 656 state.trigger( 'update', state.get('library') ); 657 658 // Restore and reset the default state. 659 controller.setState( controller.options.state ); 660 controller.reset(); 661 } 662 } 663 } 664 }) ); 665 }, 666 667 playlistAddToolbar: function() { 668 this.toolbar.set( new Toolbar({ 669 controller: this, 670 items: { 671 insert: { 672 style: 'primary', 673 text: l10n.addToPlaylist, 674 priority: 80, 675 requires: { selection: true }, 676 677 /** 678 * @fires wp.media.controller.State#reset 679 */ 680 click: function() { 681 var controller = this.controller, 682 state = controller.state(), 683 edit = controller.state('playlist-edit'); 684 685 edit.get('library').add( state.get('selection').models ); 686 state.trigger('reset'); 687 controller.setState('playlist-edit'); 688 } 689 } 690 } 691 }) ); 692 }, 693 694 videoPlaylistEditToolbar: function() { 695 var editing = this.state().get('editing'); 696 this.toolbar.set( new Toolbar({ 697 controller: this, 698 items: { 699 insert: { 700 style: 'primary', 701 text: editing ? l10n.updateVideoPlaylist : l10n.insertVideoPlaylist, 702 priority: 140, 703 requires: { library: true }, 704 705 click: function() { 706 var controller = this.controller, 707 state = controller.state(), 708 library = state.get('library'); 709 710 library.type = 'video'; 711 712 controller.close(); 713 state.trigger( 'update', library ); 714 715 // Restore and reset the default state. 716 controller.setState( controller.options.state ); 717 controller.reset(); 718 } 719 } 720 } 721 }) ); 722 }, 723 724 videoPlaylistAddToolbar: function() { 725 this.toolbar.set( new Toolbar({ 726 controller: this, 727 items: { 728 insert: { 729 style: 'primary', 730 text: l10n.addToVideoPlaylist, 731 priority: 140, 732 requires: { selection: true }, 733 734 click: function() { 735 var controller = this.controller, 736 state = controller.state(), 737 edit = controller.state('video-playlist-edit'); 738 739 edit.get('library').add( state.get('selection').models ); 740 state.trigger('reset'); 741 controller.setState('video-playlist-edit'); 742 } 743 } 744 } 745 }) ); 746 } 747 }); 748 749 module.exports = Post; 750 No newline at end of file -
src/wp-includes/js/media/views/frame/select.js
1 /** 2 * wp.media.view.MediaFrame.Select 3 * 4 * A frame for selecting an item or items from the media library. 5 * 6 * @class 7 * @augments wp.media.view.MediaFrame 8 * @augments wp.media.view.Frame 9 * @augments wp.media.View 10 * @augments wp.Backbone.View 11 * @augments Backbone.View 12 * @mixes wp.media.controller.StateMachine 13 */ 14 15 var MediaFrame = require( '../media-frame.js' ), 16 Library = require( '../../controllers/library.js' ), 17 AttachmentsModel = require( '../../models/attachments.js' ), 18 SelectionModel = require( '../../models/selection.js' ), 19 AttachmentsBrowser = require( '../attachments/browser.js' ), 20 UploaderInline = require( '../uploader/inline.js' ), 21 ToolbarSelect = require( '../toolbar/select.js' ), 22 l10n = wp.media.view.l10n, 23 Select; 24 25 Select = MediaFrame.extend({ 26 initialize: function() { 27 // Call 'initialize' directly on the parent class. 28 MediaFrame.prototype.initialize.apply( this, arguments ); 29 30 _.defaults( this.options, { 31 selection: [], 32 library: {}, 33 multiple: false, 34 state: 'library' 35 }); 36 37 this.createSelection(); 38 this.createStates(); 39 this.bindHandlers(); 40 }, 41 42 /** 43 * Attach a selection collection to the frame. 44 * 45 * A selection is a collection of attachments used for a specific purpose 46 * by a media frame. e.g. Selecting an attachment (or many) to insert into 47 * post content. 48 * 49 * @see media.model.Selection 50 */ 51 createSelection: function() { 52 var selection = this.options.selection; 53 54 if ( ! (selection instanceof SelectionModel) ) { 55 this.options.selection = new SelectionModel( selection, { 56 multiple: this.options.multiple 57 }); 58 } 59 60 this._selection = { 61 attachments: new AttachmentsModel(), 62 difference: [] 63 }; 64 }, 65 66 /** 67 * Create the default states on the frame. 68 */ 69 createStates: function() { 70 var options = this.options; 71 72 if ( this.options.states ) { 73 return; 74 } 75 76 // Add the default states. 77 this.states.add([ 78 // Main states. 79 new Library({ 80 library: wp.media.query( options.library ), 81 multiple: options.multiple, 82 title: options.title, 83 priority: 20 84 }) 85 ]); 86 }, 87 88 /** 89 * Bind region mode event callbacks. 90 * 91 * @see media.controller.Region.render 92 */ 93 bindHandlers: function() { 94 this.on( 'router:create:browse', this.createRouter, this ); 95 this.on( 'router:render:browse', this.browseRouter, this ); 96 this.on( 'content:create:browse', this.browseContent, this ); 97 this.on( 'content:render:upload', this.uploadContent, this ); 98 this.on( 'toolbar:create:select', this.createSelectToolbar, this ); 99 }, 100 101 /** 102 * Render callback for the router region in the `browse` mode. 103 * 104 * @param {wp.media.view.Router} routerView 105 */ 106 browseRouter: function( routerView ) { 107 routerView.set({ 108 upload: { 109 text: l10n.uploadFilesTitle, 110 priority: 20 111 }, 112 browse: { 113 text: l10n.mediaLibraryTitle, 114 priority: 40 115 } 116 }); 117 }, 118 119 /** 120 * Render callback for the content region in the `browse` mode. 121 * 122 * @param {wp.media.controller.Region} contentRegion 123 */ 124 browseContent: function( contentRegion ) { 125 var state = this.state(); 126 127 this.$el.removeClass('hide-toolbar'); 128 129 // Browse our library of attachments. 130 contentRegion.view = new AttachmentsBrowser({ 131 controller: this, 132 collection: state.get('library'), 133 selection: state.get('selection'), 134 model: state, 135 sortable: state.get('sortable'), 136 search: state.get('searchable'), 137 filters: state.get('filterable'), 138 display: state.has('display') ? state.get('display') : state.get('displaySettings'), 139 dragInfo: state.get('dragInfo'), 140 141 idealColumnWidth: state.get('idealColumnWidth'), 142 suggestedWidth: state.get('suggestedWidth'), 143 suggestedHeight: state.get('suggestedHeight'), 144 145 AttachmentView: state.get('AttachmentView') 146 }); 147 }, 148 149 /** 150 * Render callback for the content region in the `upload` mode. 151 */ 152 uploadContent: function() { 153 this.$el.removeClass( 'hide-toolbar' ); 154 this.content.set( new UploaderInline({ 155 controller: this 156 }) ); 157 }, 158 159 /** 160 * Toolbars 161 * 162 * @param {Object} toolbar 163 * @param {Object} [options={}] 164 * @this wp.media.controller.Region 165 */ 166 createSelectToolbar: function( toolbar, options ) { 167 options = options || this.options.button || {}; 168 options.controller = this; 169 170 toolbar.view = new ToolbarSelect( options ); 171 } 172 }); 173 174 module.exports = Select; 175 No newline at end of file -
src/wp-includes/js/media/views/frame/video-details.js
1 /** 2 * wp.media.view.MediaFrame.VideoDetails 3 * 4 * @constructor 5 * @augments wp.media.view.MediaFrame.MediaDetails 6 * @augments wp.media.view.MediaFrame.Select 7 * @augments wp.media.view.MediaFrame 8 * @augments wp.media.view.Frame 9 * @augments wp.media.View 10 * @augments wp.Backbone.View 11 * @augments Backbone.View 12 * @mixes wp.media.controller.StateMachine 13 */ 14 var MediaDetails = require( './media-details' ), 15 MediaLibrary = require( '../../controllers/media-library.js' ), 16 VideoDetailsView = require( '../video-details.js' ), 17 VideoDetailsController = require( '../../controllers/video-details.js' ), 18 l10n = wp.media.view.l10n, 19 VideoDetails; 20 21 VideoDetails = MediaDetails.extend({ 22 defaults: { 23 id: 'video', 24 url: '', 25 menu: 'video-details', 26 content: 'video-details', 27 toolbar: 'video-details', 28 type: 'link', 29 title: l10n.videoDetailsTitle, 30 priority: 120 31 }, 32 33 initialize: function( options ) { 34 options.DetailsView = VideoDetailsView; 35 options.cancelText = l10n.videoDetailsCancel; 36 options.addText = l10n.videoAddSourceTitle; 37 38 MediaDetails.prototype.initialize.call( this, options ); 39 }, 40 41 bindHandlers: function() { 42 MediaDetails.prototype.bindHandlers.apply( this, arguments ); 43 44 this.on( 'toolbar:render:replace-video', this.renderReplaceToolbar, this ); 45 this.on( 'toolbar:render:add-video-source', this.renderAddSourceToolbar, this ); 46 this.on( 'toolbar:render:select-poster-image', this.renderSelectPosterImageToolbar, this ); 47 this.on( 'toolbar:render:add-track', this.renderAddTrackToolbar, this ); 48 }, 49 50 createStates: function() { 51 this.states.add([ 52 new VideoDetailsController({ 53 media: this.media 54 }), 55 56 new MediaLibrary( { 57 type: 'video', 58 id: 'replace-video', 59 title: l10n.videoReplaceTitle, 60 toolbar: 'replace-video', 61 media: this.media, 62 menu: 'video-details' 63 } ), 64 65 new MediaLibrary( { 66 type: 'video', 67 id: 'add-video-source', 68 title: l10n.videoAddSourceTitle, 69 toolbar: 'add-video-source', 70 media: this.media, 71 menu: false 72 } ), 73 74 new MediaLibrary( { 75 type: 'image', 76 id: 'select-poster-image', 77 title: l10n.videoSelectPosterImageTitle, 78 toolbar: 'select-poster-image', 79 media: this.media, 80 menu: 'video-details' 81 } ), 82 83 new MediaLibrary( { 84 type: 'text', 85 id: 'add-track', 86 title: l10n.videoAddTrackTitle, 87 toolbar: 'add-track', 88 media: this.media, 89 menu: 'video-details' 90 } ) 91 ]); 92 }, 93 94 renderSelectPosterImageToolbar: function() { 95 this.setPrimaryButton( l10n.videoSelectPosterImageTitle, function( controller, state ) { 96 var urls = [], attachment = state.get( 'selection' ).single(); 97 98 controller.media.set( 'poster', attachment.get( 'url' ) ); 99 state.trigger( 'set-poster-image', controller.media.toJSON() ); 100 101 _.each( wp.media.view.settings.embedExts, function (ext) { 102 if ( controller.media.get( ext ) ) { 103 urls.push( controller.media.get( ext ) ); 104 } 105 } ); 106 107 wp.ajax.send( 'set-attachment-thumbnail', { 108 data : { 109 urls: urls, 110 thumbnail_id: attachment.get( 'id' ) 111 } 112 } ); 113 } ); 114 }, 115 116 renderAddTrackToolbar: function() { 117 this.setPrimaryButton( l10n.videoAddTrackTitle, function( controller, state ) { 118 var attachment = state.get( 'selection' ).single(), 119 content = controller.media.get( 'content' ); 120 121 if ( -1 === content.indexOf( attachment.get( 'url' ) ) ) { 122 content += [ 123 '<track srclang="en" label="English"kind="subtitles" src="', 124 attachment.get( 'url' ), 125 '" />' 126 ].join(''); 127 128 controller.media.set( 'content', content ); 129 } 130 state.trigger( 'add-track', controller.media.toJSON() ); 131 } ); 132 } 133 }); 134 135 module.exports = VideoDetails; 136 No newline at end of file -
src/wp-includes/js/media/views/frame.js
1 /** 2 * wp.media.view.Frame 3 * 4 * A frame is a composite view consisting of one or more regions and one or more 5 * states. 6 * 7 * @see wp.media.controller.State 8 * @see wp.media.controller.Region 9 * 10 * @class 11 * @augments wp.media.View 12 * @augments wp.Backbone.View 13 * @augments Backbone.View 14 * @mixes wp.media.controller.StateMachine 15 */ 16 var StateMachine = require( '../controllers/state-machine.js' ), 17 State = require( '../controllers/state.js' ), 18 Region = require( '../controllers/region.js' ), 19 View = require( './view.js' ), 20 Frame; 21 22 Frame = View.extend({ 23 initialize: function() { 24 _.defaults( this.options, { 25 mode: [ 'select' ] 26 }); 27 this._createRegions(); 28 this._createStates(); 29 this._createModes(); 30 }, 31 32 _createRegions: function() { 33 // Clone the regions array. 34 this.regions = this.regions ? this.regions.slice() : []; 35 36 // Initialize regions. 37 _.each( this.regions, function( region ) { 38 this[ region ] = new Region({ 39 view: this, 40 id: region, 41 selector: '.media-frame-' + region 42 }); 43 }, this ); 44 }, 45 /** 46 * Create the frame's states. 47 * 48 * @see wp.media.controller.State 49 * @see wp.media.controller.StateMachine 50 * 51 * @fires wp.media.controller.State#ready 52 */ 53 _createStates: function() { 54 // Create the default `states` collection. 55 this.states = new Backbone.Collection( null, { 56 model: State 57 }); 58 59 // Ensure states have a reference to the frame. 60 this.states.on( 'add', function( model ) { 61 model.frame = this; 62 model.trigger('ready'); 63 }, this ); 64 65 if ( this.options.states ) { 66 this.states.add( this.options.states ); 67 } 68 }, 69 70 /** 71 * A frame can be in a mode or multiple modes at one time. 72 * 73 * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode. 74 */ 75 _createModes: function() { 76 // Store active "modes" that the frame is in. Unrelated to region modes. 77 this.activeModes = new Backbone.Collection(); 78 this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) ); 79 80 _.each( this.options.mode, function( mode ) { 81 this.activateMode( mode ); 82 }, this ); 83 }, 84 /** 85 * Reset all states on the frame to their defaults. 86 * 87 * @returns {wp.media.view.Frame} Returns itself to allow chaining 88 */ 89 reset: function() { 90 this.states.invoke( 'trigger', 'reset' ); 91 return this; 92 }, 93 /** 94 * Map activeMode collection events to the frame. 95 */ 96 triggerModeEvents: function( model, collection, options ) { 97 var collectionEvent, 98 modeEventMap = { 99 add: 'activate', 100 remove: 'deactivate' 101 }, 102 eventToTrigger; 103 // Probably a better way to do this. 104 _.each( options, function( value, key ) { 105 if ( value ) { 106 collectionEvent = key; 107 } 108 } ); 109 110 if ( ! _.has( modeEventMap, collectionEvent ) ) { 111 return; 112 } 113 114 eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent]; 115 this.trigger( eventToTrigger ); 116 }, 117 /** 118 * Activate a mode on the frame. 119 * 120 * @param string mode Mode ID. 121 * @returns {this} Returns itself to allow chaining. 122 */ 123 activateMode: function( mode ) { 124 // Bail if the mode is already active. 125 if ( this.isModeActive( mode ) ) { 126 return; 127 } 128 this.activeModes.add( [ { id: mode } ] ); 129 // Add a CSS class to the frame so elements can be styled for the mode. 130 this.$el.addClass( 'mode-' + mode ); 131 132 return this; 133 }, 134 /** 135 * Deactivate a mode on the frame. 136 * 137 * @param string mode Mode ID. 138 * @returns {this} Returns itself to allow chaining. 139 */ 140 deactivateMode: function( mode ) { 141 // Bail if the mode isn't active. 142 if ( ! this.isModeActive( mode ) ) { 143 return this; 144 } 145 this.activeModes.remove( this.activeModes.where( { id: mode } ) ); 146 this.$el.removeClass( 'mode-' + mode ); 147 /** 148 * Frame mode deactivation event. 149 * 150 * @event this#{mode}:deactivate 151 */ 152 this.trigger( mode + ':deactivate' ); 153 154 return this; 155 }, 156 /** 157 * Check if a mode is enabled on the frame. 158 * 159 * @param string mode Mode ID. 160 * @return bool 161 */ 162 isModeActive: function( mode ) { 163 return Boolean( this.activeModes.where( { id: mode } ).length ); 164 } 165 }); 166 167 // Make the `Frame` a `StateMachine`. 168 _.extend( Frame.prototype, StateMachine.prototype ); 169 170 module.exports = Frame; 171 No newline at end of file -
src/wp-includes/js/media/views/iframe.js
1 /** 2 * wp.media.view.Iframe 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 Iframe; 11 12 Iframe = View.extend({ 13 className: 'media-iframe', 14 /** 15 * @returns {wp.media.view.Iframe} Returns itself to allow chaining 16 */ 17 render: function() { 18 this.views.detach(); 19 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' ); 20 this.views.render(); 21 return this; 22 } 23 }); 24 25 module.exports = Iframe; 26 No newline at end of file -
src/wp-includes/js/media/views/image-details.js
1 /** 2 * wp.media.view.ImageDetails 3 * 4 * @class 5 * @augments wp.media.view.Settings.AttachmentDisplay 6 * @augments wp.media.view.Settings 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var AttachmentDisplay = require( './settings/attachment-display.js' ), 12 $ = jQuery, 13 ImageDetails; 14 15 ImageDetails = AttachmentDisplay.extend({ 16 className: 'image-details', 17 template: wp.template('image-details'), 18 events: _.defaults( AttachmentDisplay.prototype.events, { 19 'click .edit-attachment': 'editAttachment', 20 'click .replace-attachment': 'replaceAttachment', 21 'click .advanced-toggle': 'onToggleAdvanced', 22 'change [data-setting="customWidth"]': 'onCustomSize', 23 'change [data-setting="customHeight"]': 'onCustomSize', 24 'keyup [data-setting="customWidth"]': 'onCustomSize', 25 'keyup [data-setting="customHeight"]': 'onCustomSize' 26 } ), 27 initialize: function() { 28 // used in AttachmentDisplay.prototype.updateLinkTo 29 this.options.attachment = this.model.attachment; 30 this.listenTo( this.model, 'change:url', this.updateUrl ); 31 this.listenTo( this.model, 'change:link', this.toggleLinkSettings ); 32 this.listenTo( this.model, 'change:size', this.toggleCustomSize ); 33 34 AttachmentDisplay.prototype.initialize.apply( this, arguments ); 35 }, 36 37 prepare: function() { 38 var attachment = false; 39 40 if ( this.model.attachment ) { 41 attachment = this.model.attachment.toJSON(); 42 } 43 return _.defaults({ 44 model: this.model.toJSON(), 45 attachment: attachment 46 }, this.options ); 47 }, 48 49 render: function() { 50 var self = this, 51 args = arguments; 52 53 if ( this.model.attachment && 'pending' === this.model.dfd.state() ) { 54 this.model.dfd.done( function() { 55 AttachmentDisplay.prototype.render.apply( self, args ); 56 self.postRender(); 57 } ).fail( function() { 58 self.model.attachment = false; 59 AttachmentDisplay.prototype.render.apply( self, args ); 60 self.postRender(); 61 } ); 62 } else { 63 AttachmentDisplay.prototype.render.apply( this, arguments ); 64 this.postRender(); 65 } 66 67 return this; 68 }, 69 70 postRender: function() { 71 setTimeout( _.bind( this.resetFocus, this ), 10 ); 72 this.toggleLinkSettings(); 73 if ( getUserSetting( 'advImgDetails' ) === 'show' ) { 74 this.toggleAdvanced( true ); 75 } 76 this.trigger( 'post-render' ); 77 }, 78 79 resetFocus: function() { 80 this.$( '.link-to-custom' ).blur(); 81 this.$( '.embed-media-settings' ).scrollTop( 0 ); 82 }, 83 84 updateUrl: function() { 85 this.$( '.image img' ).attr( 'src', this.model.get( 'url' ) ); 86 this.$( '.url' ).val( this.model.get( 'url' ) ); 87 }, 88 89 toggleLinkSettings: function() { 90 if ( this.model.get( 'link' ) === 'none' ) { 91 this.$( '.link-settings' ).addClass('hidden'); 92 } else { 93 this.$( '.link-settings' ).removeClass('hidden'); 94 } 95 }, 96 97 toggleCustomSize: function() { 98 if ( this.model.get( 'size' ) !== 'custom' ) { 99 this.$( '.custom-size' ).addClass('hidden'); 100 } else { 101 this.$( '.custom-size' ).removeClass('hidden'); 102 } 103 }, 104 105 onCustomSize: function( event ) { 106 var dimension = $( event.target ).data('setting'), 107 num = $( event.target ).val(), 108 value; 109 110 // Ignore bogus input 111 if ( ! /^\d+/.test( num ) || parseInt( num, 10 ) < 1 ) { 112 event.preventDefault(); 113 return; 114 } 115 116 if ( dimension === 'customWidth' ) { 117 value = Math.round( 1 / this.model.get( 'aspectRatio' ) * num ); 118 this.model.set( 'customHeight', value, { silent: true } ); 119 this.$( '[data-setting="customHeight"]' ).val( value ); 120 } else { 121 value = Math.round( this.model.get( 'aspectRatio' ) * num ); 122 this.model.set( 'customWidth', value, { silent: true } ); 123 this.$( '[data-setting="customWidth"]' ).val( value ); 124 } 125 }, 126 127 onToggleAdvanced: function( event ) { 128 event.preventDefault(); 129 this.toggleAdvanced(); 130 }, 131 132 toggleAdvanced: function( show ) { 133 var $advanced = this.$el.find( '.advanced-section' ), 134 mode; 135 136 if ( $advanced.hasClass('advanced-visible') || show === false ) { 137 $advanced.removeClass('advanced-visible'); 138 $advanced.find('.advanced-settings').addClass('hidden'); 139 mode = 'hide'; 140 } else { 141 $advanced.addClass('advanced-visible'); 142 $advanced.find('.advanced-settings').removeClass('hidden'); 143 mode = 'show'; 144 } 145 146 setUserSetting( 'advImgDetails', mode ); 147 }, 148 149 editAttachment: function( event ) { 150 var editState = this.controller.states.get( 'edit-image' ); 151 152 if ( window.imageEdit && editState ) { 153 event.preventDefault(); 154 editState.set( 'image', this.model.attachment ); 155 this.controller.setState( 'edit-image' ); 156 } 157 }, 158 159 replaceAttachment: function( event ) { 160 event.preventDefault(); 161 this.controller.setState( 'replace-image' ); 162 } 163 }); 164 165 module.exports = AttachmentDisplay; 166 No newline at end of file -
src/wp-includes/js/media/views/label.js
1 /** 2 * @class 3 * @augments wp.media.View 4 * @augments wp.Backbone.View 5 * @augments Backbone.View 6 */ 7 var View = require( './view.js' ), 8 Label; 9 10 Label = View.extend({ 11 tagName: 'label', 12 className: 'screen-reader-text', 13 14 initialize: function() { 15 this.value = this.options.value; 16 }, 17 18 render: function() { 19 this.$el.html( this.value ); 20 21 return this; 22 } 23 }); 24 25 module.exports = Label; 26 No newline at end of file -
src/wp-includes/js/media/views/media-details.js
1 /** 2 * wp.media.view.MediaDetails 3 * 4 * @constructor 5 * @augments wp.media.view.Settings.AttachmentDisplay 6 * @augments wp.media.view.Settings 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var AttachmentDisplay = require( './settings/attachment-display.js' ), 12 $ = jQuery, 13 MediaDetails; 14 15 MediaDetails = AttachmentDisplay.extend({ 16 initialize: function() { 17 _.bindAll(this, 'success'); 18 this.players = []; 19 this.listenTo( this.controller, 'close', wp.media.mixin.unsetPlayers ); 20 this.on( 'ready', this.setPlayer ); 21 this.on( 'media:setting:remove', wp.media.mixin.unsetPlayers, this ); 22 this.on( 'media:setting:remove', this.render ); 23 this.on( 'media:setting:remove', this.setPlayer ); 24 this.events = _.extend( this.events, { 25 'click .remove-setting' : 'removeSetting', 26 'change .content-track' : 'setTracks', 27 'click .remove-track' : 'setTracks', 28 'click .add-media-source' : 'addSource' 29 } ); 30 31 AttachmentDisplay.prototype.initialize.apply( this, arguments ); 32 }, 33 34 prepare: function() { 35 return _.defaults({ 36 model: this.model.toJSON() 37 }, this.options ); 38 }, 39 40 /** 41 * Remove a setting's UI when the model unsets it 42 * 43 * @fires wp.media.view.MediaDetails#media:setting:remove 44 * 45 * @param {Event} e 46 */ 47 removeSetting : function(e) { 48 var wrap = $( e.currentTarget ).parent(), setting; 49 setting = wrap.find( 'input' ).data( 'setting' ); 50 51 if ( setting ) { 52 this.model.unset( setting ); 53 this.trigger( 'media:setting:remove', this ); 54 } 55 56 wrap.remove(); 57 }, 58 59 /** 60 * 61 * @fires wp.media.view.MediaDetails#media:setting:remove 62 */ 63 setTracks : function() { 64 var tracks = ''; 65 66 _.each( this.$('.content-track'), function(track) { 67 tracks += $( track ).val(); 68 } ); 69 70 this.model.set( 'content', tracks ); 71 this.trigger( 'media:setting:remove', this ); 72 }, 73 74 addSource : function( e ) { 75 this.controller.lastMime = $( e.currentTarget ).data( 'mime' ); 76 this.controller.setState( 'add-' + this.controller.defaults.id + '-source' ); 77 }, 78 79 /** 80 * @global MediaElementPlayer 81 */ 82 setPlayer : function() { 83 if ( ! this.players.length && this.media ) { 84 this.players.push( new MediaElementPlayer( this.media, this.settings ) ); 85 } 86 }, 87 88 /** 89 * @abstract 90 */ 91 setMedia : function() { 92 return this; 93 }, 94 95 success : function(mejs) { 96 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; 97 98 if ( 'flash' === mejs.pluginType && autoplay ) { 99 mejs.addEventListener( 'canplay', function() { 100 mejs.play(); 101 }, false ); 102 } 103 104 this.mejs = mejs; 105 }, 106 107 /** 108 * @returns {media.view.MediaDetails} Returns itself to allow chaining 109 */ 110 render: function() { 111 var self = this; 112 113 AttachmentDisplay.prototype.render.apply( this, arguments ); 114 setTimeout( function() { self.resetFocus(); }, 10 ); 115 116 this.settings = _.defaults( { 117 success : this.success 118 }, wp.media.mixin.mejsSettings ); 119 120 return this.setMedia(); 121 }, 122 123 resetFocus: function() { 124 this.$( '.embed-media-settings' ).scrollTop( 0 ); 125 } 126 }, { 127 instances : 0, 128 /** 129 * When multiple players in the DOM contain the same src, things get weird. 130 * 131 * @param {HTMLElement} elem 132 * @returns {HTMLElement} 133 */ 134 prepareSrc : function( elem ) { 135 var i = MediaDetails.instances++; 136 _.each( $( elem ).find( 'source' ), function( source ) { 137 source.src = [ 138 source.src, 139 source.src.indexOf('?') > -1 ? '&' : '?', 140 '_=', 141 i 142 ].join(''); 143 } ); 144 145 return elem; 146 } 147 }); 148 149 module.exports = MediaDetails; 150 No newline at end of file -
src/wp-includes/js/media/views/media-frame.js
1 /** 2 * wp.media.view.MediaFrame 3 * 4 * The frame used to create the media modal. 5 * 6 * @class 7 * @augments wp.media.view.Frame 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 * @mixes wp.media.controller.StateMachine 12 */ 13 var View = require( './view.js' ), 14 Frame = require( './frame.js' ), 15 Modal = require( './modal.js' ), 16 UploaderWindow = require( './uploader/window.js' ), 17 Menu = require( './menu.js' ), 18 Toolbar = require( './toolbar.js' ), 19 Router = require( './router.js' ), 20 Iframe = require( './iframe.js' ), 21 $ = jQuery, 22 MediaFrame; 23 24 MediaFrame = Frame.extend({ 25 className: 'media-frame', 26 template: wp.template('media-frame'), 27 regions: ['menu','title','content','toolbar','router'], 28 29 events: { 30 'click div.media-frame-title h1': 'toggleMenu' 31 }, 32 33 /** 34 * @global wp.Uploader 35 */ 36 initialize: function() { 37 Frame.prototype.initialize.apply( this, arguments ); 38 39 _.defaults( this.options, { 40 title: '', 41 modal: true, 42 uploader: true 43 }); 44 45 // Ensure core UI is enabled. 46 this.$el.addClass('wp-core-ui'); 47 48 // Initialize modal container view. 49 if ( this.options.modal ) { 50 this.modal = new Modal({ 51 controller: this, 52 title: this.options.title 53 }); 54 55 this.modal.content( this ); 56 } 57 58 // Force the uploader off if the upload limit has been exceeded or 59 // if the browser isn't supported. 60 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 61 this.options.uploader = false; 62 } 63 64 // Initialize window-wide uploader. 65 if ( this.options.uploader ) { 66 this.uploader = new UploaderWindow({ 67 controller: this, 68 uploader: { 69 dropzone: this.modal ? this.modal.$el : this.$el, 70 container: this.$el 71 } 72 }); 73 this.views.set( '.media-frame-uploader', this.uploader ); 74 } 75 76 this.on( 'attach', _.bind( this.views.ready, this.views ), this ); 77 78 // Bind default title creation. 79 this.on( 'title:create:default', this.createTitle, this ); 80 this.title.mode('default'); 81 82 this.on( 'title:render', function( view ) { 83 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' ); 84 }); 85 86 // Bind default menu. 87 this.on( 'menu:create:default', this.createMenu, this ); 88 }, 89 /** 90 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 91 */ 92 render: function() { 93 // Activate the default state if no active state exists. 94 if ( ! this.state() && this.options.state ) { 95 this.setState( this.options.state ); 96 } 97 /** 98 * call 'render' directly on the parent class 99 */ 100 return Frame.prototype.render.apply( this, arguments ); 101 }, 102 /** 103 * @param {Object} title 104 * @this wp.media.controller.Region 105 */ 106 createTitle: function( title ) { 107 title.view = new View({ 108 controller: this, 109 tagName: 'h1' 110 }); 111 }, 112 /** 113 * @param {Object} menu 114 * @this wp.media.controller.Region 115 */ 116 createMenu: function( menu ) { 117 menu.view = new Menu({ 118 controller: this 119 }); 120 }, 121 122 toggleMenu: function() { 123 this.$el.find( '.media-menu' ).toggleClass( 'visible' ); 124 }, 125 126 /** 127 * @param {Object} toolbar 128 * @this wp.media.controller.Region 129 */ 130 createToolbar: function( toolbar ) { 131 toolbar.view = new Toolbar({ 132 controller: this 133 }); 134 }, 135 /** 136 * @param {Object} router 137 * @this wp.media.controller.Region 138 */ 139 createRouter: function( router ) { 140 router.view = new Router({ 141 controller: this 142 }); 143 }, 144 /** 145 * @param {Object} options 146 */ 147 createIframeStates: function( options ) { 148 var settings = wp.media.view.settings, 149 tabs = settings.tabs, 150 tabUrl = settings.tabUrl, 151 $postId; 152 153 if ( ! tabs || ! tabUrl ) { 154 return; 155 } 156 157 // Add the post ID to the tab URL if it exists. 158 $postId = $('#post_ID'); 159 if ( $postId.length ) { 160 tabUrl += '&post_id=' + $postId.val(); 161 } 162 163 // Generate the tab states. 164 _.each( tabs, function( title, id ) { 165 this.state( 'iframe:' + id ).set( _.defaults({ 166 tab: id, 167 src: tabUrl + '&tab=' + id, 168 title: title, 169 content: 'iframe', 170 menu: 'default' 171 }, options ) ); 172 }, this ); 173 174 this.on( 'content:create:iframe', this.iframeContent, this ); 175 this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this ); 176 this.on( 'menu:render:default', this.iframeMenu, this ); 177 this.on( 'open', this.hijackThickbox, this ); 178 this.on( 'close', this.restoreThickbox, this ); 179 }, 180 181 /** 182 * @param {Object} content 183 * @this wp.media.controller.Region 184 */ 185 iframeContent: function( content ) { 186 this.$el.addClass('hide-toolbar'); 187 content.view = new Iframe({ 188 controller: this 189 }); 190 }, 191 192 iframeContentCleanup: function() { 193 this.$el.removeClass('hide-toolbar'); 194 }, 195 196 iframeMenu: function( view ) { 197 var views = {}; 198 199 if ( ! view ) { 200 return; 201 } 202 203 _.each( wp.media.view.settings.tabs, function( title, id ) { 204 views[ 'iframe:' + id ] = { 205 text: this.state( 'iframe:' + id ).get('title'), 206 priority: 200 207 }; 208 }, this ); 209 210 view.set( views ); 211 }, 212 213 hijackThickbox: function() { 214 var frame = this; 215 216 if ( ! window.tb_remove || this._tb_remove ) { 217 return; 218 } 219 220 this._tb_remove = window.tb_remove; 221 window.tb_remove = function() { 222 frame.close(); 223 frame.reset(); 224 frame.setState( frame.options.state ); 225 frame._tb_remove.call( window ); 226 }; 227 }, 228 229 restoreThickbox: function() { 230 if ( ! this._tb_remove ) { 231 return; 232 } 233 234 window.tb_remove = this._tb_remove; 235 delete this._tb_remove; 236 } 237 }); 238 239 // Map some of the modal's methods to the frame. 240 _.each(['open','close','attach','detach','escape'], function( method ) { 241 /** 242 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 243 */ 244 MediaFrame.prototype[ method ] = function() { 245 if ( this.modal ) { 246 this.modal[ method ].apply( this.modal, arguments ); 247 } 248 return this; 249 }; 250 }); 251 252 module.exports = MediaFrame; 253 No newline at end of file -
src/wp-includes/js/media/views/menu-item.js
1 /** 2 * wp.media.view.MenuItem 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 $ = jQuery, 11 MenuItem; 12 13 MenuItem = View.extend({ 14 tagName: 'a', 15 className: 'media-menu-item', 16 17 attributes: { 18 href: '#' 19 }, 20 21 events: { 22 'click': '_click' 23 }, 24 /** 25 * @param {Object} event 26 */ 27 _click: function( event ) { 28 var clickOverride = this.options.click; 29 30 if ( event ) { 31 event.preventDefault(); 32 } 33 34 if ( clickOverride ) { 35 clickOverride.call( this ); 36 } else { 37 this.click(); 38 } 39 40 // When selecting a tab along the left side, 41 // focus should be transferred into the main panel 42 if ( ! wp.media.isTouchDevice ) { 43 $('.media-frame-content input').first().focus(); 44 } 45 }, 46 47 click: function() { 48 var state = this.options.state; 49 50 if ( state ) { 51 this.controller.setState( state ); 52 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below 53 } 54 }, 55 /** 56 * @returns {wp.media.view.MenuItem} returns itself to allow chaining 57 */ 58 render: function() { 59 var options = this.options; 60 61 if ( options.text ) { 62 this.$el.text( options.text ); 63 } else if ( options.html ) { 64 this.$el.html( options.html ); 65 } 66 67 return this; 68 } 69 }); 70 71 module.exports = MenuItem; 72 No newline at end of file -
src/wp-includes/js/media/views/menu.js
1 /** 2 * wp.media.view.Menu 3 * 4 * @class 5 * @augments wp.media.view.PriorityList 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var MenuItem = require( './menu-item.js' ), 11 PriorityList = require( './priority-list.js' ), 12 Menu; 13 14 Menu = PriorityList.extend({ 15 tagName: 'div', 16 className: 'media-menu', 17 property: 'state', 18 ItemView: MenuItem, 19 region: 'menu', 20 21 /* TODO: alternatively hide on any click anywhere 22 events: { 23 'click': 'click' 24 }, 25 26 click: function() { 27 this.$el.removeClass( 'visible' ); 28 }, 29 */ 30 31 /** 32 * @param {Object} options 33 * @param {string} id 34 * @returns {wp.media.View} 35 */ 36 toView: function( options, id ) { 37 options = options || {}; 38 options[ this.property ] = options[ this.property ] || id; 39 return new this.ItemView( options ).render(); 40 }, 41 42 ready: function() { 43 /** 44 * call 'ready' directly on the parent class 45 */ 46 PriorityList.prototype.ready.apply( this, arguments ); 47 this.visibility(); 48 }, 49 50 set: function() { 51 /** 52 * call 'set' directly on the parent class 53 */ 54 PriorityList.prototype.set.apply( this, arguments ); 55 this.visibility(); 56 }, 57 58 unset: function() { 59 /** 60 * call 'unset' directly on the parent class 61 */ 62 PriorityList.prototype.unset.apply( this, arguments ); 63 this.visibility(); 64 }, 65 66 visibility: function() { 67 var region = this.region, 68 view = this.controller[ region ].get(), 69 views = this.views.get(), 70 hide = ! views || views.length < 2; 71 72 if ( this === view ) { 73 this.controller.$el.toggleClass( 'hide-' + region, hide ); 74 } 75 }, 76 /** 77 * @param {string} id 78 */ 79 select: function( id ) { 80 var view = this.get( id ); 81 82 if ( ! view ) { 83 return; 84 } 85 86 this.deselect(); 87 view.$el.addClass('active'); 88 }, 89 90 deselect: function() { 91 this.$el.children().removeClass('active'); 92 }, 93 94 hide: function( id ) { 95 var view = this.get( id ); 96 97 if ( ! view ) { 98 return; 99 } 100 101 view.$el.addClass('hidden'); 102 }, 103 104 show: function( id ) { 105 var view = this.get( id ); 106 107 if ( ! view ) { 108 return; 109 } 110 111 view.$el.removeClass('hidden'); 112 } 113 }); 114 115 module.exports = Menu; 116 No newline at end of file -
src/wp-includes/js/media/views/modal.js
1 /** 2 * wp.media.view.Modal 3 * 4 * A modal view, which the media modal uses as its default container. 5 * 6 * @class 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var View = require( './view.js' ), 12 FocusManager = require( './focus-manager.js' ), 13 $ = jQuery, 14 Modal; 15 16 Modal = View.extend({ 17 tagName: 'div', 18 template: wp.template('media-modal'), 19 20 attributes: { 21 tabindex: 0 22 }, 23 24 events: { 25 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 26 'keydown': 'keydown' 27 }, 28 29 initialize: function() { 30 _.defaults( this.options, { 31 container: document.body, 32 title: '', 33 propagate: true, 34 freeze: true 35 }); 36 37 this.focusManager = new FocusManager({ 38 el: this.el 39 }); 40 }, 41 /** 42 * @returns {Object} 43 */ 44 prepare: function() { 45 return { 46 title: this.options.title 47 }; 48 }, 49 50 /** 51 * @returns {wp.media.view.Modal} Returns itself to allow chaining 52 */ 53 attach: function() { 54 if ( this.views.attached ) { 55 return this; 56 } 57 58 if ( ! this.views.rendered ) { 59 this.render(); 60 } 61 62 this.$el.appendTo( this.options.container ); 63 64 // Manually mark the view as attached and trigger ready. 65 this.views.attached = true; 66 this.views.ready(); 67 68 return this.propagate('attach'); 69 }, 70 71 /** 72 * @returns {wp.media.view.Modal} Returns itself to allow chaining 73 */ 74 detach: function() { 75 if ( this.$el.is(':visible') ) { 76 this.close(); 77 } 78 79 this.$el.detach(); 80 this.views.attached = false; 81 return this.propagate('detach'); 82 }, 83 84 /** 85 * @returns {wp.media.view.Modal} Returns itself to allow chaining 86 */ 87 open: function() { 88 var $el = this.$el, 89 options = this.options, 90 mceEditor; 91 92 if ( $el.is(':visible') ) { 93 return this; 94 } 95 96 if ( ! this.views.attached ) { 97 this.attach(); 98 } 99 100 // If the `freeze` option is set, record the window's scroll position. 101 if ( options.freeze ) { 102 this._freeze = { 103 scrollTop: $( window ).scrollTop() 104 }; 105 } 106 107 // Disable page scrolling. 108 $( 'body' ).addClass( 'modal-open' ); 109 110 $el.show(); 111 112 // Try to close the onscreen keyboard 113 if ( 'ontouchend' in document ) { 114 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) { 115 mceEditor.iframeElement.focus(); 116 mceEditor.iframeElement.blur(); 117 118 setTimeout( function() { 119 mceEditor.iframeElement.blur(); 120 }, 100 ); 121 } 122 } 123 124 this.$el.focus(); 125 126 return this.propagate('open'); 127 }, 128 129 /** 130 * @param {Object} options 131 * @returns {wp.media.view.Modal} Returns itself to allow chaining 132 */ 133 close: function( options ) { 134 var freeze = this._freeze; 135 136 if ( ! this.views.attached || ! this.$el.is(':visible') ) { 137 return this; 138 } 139 140 // Enable page scrolling. 141 $( 'body' ).removeClass( 'modal-open' ); 142 143 // Hide modal and remove restricted media modal tab focus once it's closed 144 this.$el.hide().undelegate( 'keydown' ); 145 146 // Put focus back in useful location once modal is closed 147 $('#wpbody-content').focus(); 148 149 this.propagate('close'); 150 151 // If the `freeze` option is set, restore the container's scroll position. 152 if ( freeze ) { 153 $( window ).scrollTop( freeze.scrollTop ); 154 } 155 156 if ( options && options.escape ) { 157 this.propagate('escape'); 158 } 159 160 return this; 161 }, 162 /** 163 * @returns {wp.media.view.Modal} Returns itself to allow chaining 164 */ 165 escape: function() { 166 return this.close({ escape: true }); 167 }, 168 /** 169 * @param {Object} event 170 */ 171 escapeHandler: function( event ) { 172 event.preventDefault(); 173 this.escape(); 174 }, 175 176 /** 177 * @param {Array|Object} content Views to register to '.media-modal-content' 178 * @returns {wp.media.view.Modal} Returns itself to allow chaining 179 */ 180 content: function( content ) { 181 this.views.set( '.media-modal-content', content ); 182 return this; 183 }, 184 185 /** 186 * Triggers a modal event and if the `propagate` option is set, 187 * forwards events to the modal's controller. 188 * 189 * @param {string} id 190 * @returns {wp.media.view.Modal} Returns itself to allow chaining 191 */ 192 propagate: function( id ) { 193 this.trigger( id ); 194 195 if ( this.options.propagate ) { 196 this.controller.trigger( id ); 197 } 198 199 return this; 200 }, 201 /** 202 * @param {Object} event 203 */ 204 keydown: function( event ) { 205 // Close the modal when escape is pressed. 206 if ( 27 === event.which && this.$el.is(':visible') ) { 207 this.escape(); 208 event.stopImmediatePropagation(); 209 } 210 } 211 }); 212 213 module.exports = Modal; 214 No newline at end of file -
src/wp-includes/js/media/views/priority-list.js
1 /** 2 * wp.media.view.PriorityList 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 PriorityList; 11 12 PriorityList = View.extend({ 13 tagName: 'div', 14 15 initialize: function() { 16 this._views = {}; 17 18 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 19 delete this.options.views; 20 21 if ( ! this.options.silent ) { 22 this.render(); 23 } 24 }, 25 /** 26 * @param {string} id 27 * @param {wp.media.View|Object} view 28 * @param {Object} options 29 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining 30 */ 31 set: function( id, view, options ) { 32 var priority, views, index; 33 34 options = options || {}; 35 36 // Accept an object with an `id` : `view` mapping. 37 if ( _.isObject( id ) ) { 38 _.each( id, function( view, id ) { 39 this.set( id, view ); 40 }, this ); 41 return this; 42 } 43 44 if ( ! (view instanceof Backbone.View) ) { 45 view = this.toView( view, id, options ); 46 } 47 view.controller = view.controller || this.controller; 48 49 this.unset( id ); 50 51 priority = view.options.priority || 10; 52 views = this.views.get() || []; 53 54 _.find( views, function( existing, i ) { 55 if ( existing.options.priority > priority ) { 56 index = i; 57 return true; 58 } 59 }); 60 61 this._views[ id ] = view; 62 this.views.add( view, { 63 at: _.isNumber( index ) ? index : views.length || 0 64 }); 65 66 return this; 67 }, 68 /** 69 * @param {string} id 70 * @returns {wp.media.View} 71 */ 72 get: function( id ) { 73 return this._views[ id ]; 74 }, 75 /** 76 * @param {string} id 77 * @returns {wp.media.view.PriorityList} 78 */ 79 unset: function( id ) { 80 var view = this.get( id ); 81 82 if ( view ) { 83 view.remove(); 84 } 85 86 delete this._views[ id ]; 87 return this; 88 }, 89 /** 90 * @param {Object} options 91 * @returns {wp.media.View} 92 */ 93 toView: function( options ) { 94 return new View( options ); 95 } 96 }); 97 98 module.exports = PriorityList; 99 No newline at end of file -
src/wp-includes/js/media/views/router-item.js
1 /** 2 * wp.media.view.RouterItem 3 * 4 * @class 5 * @augments wp.media.view.MenuItem 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var MenuItem = require( './menu-item.js' ), 11 RouterItem; 12 13 RouterItem = MenuItem.extend({ 14 /** 15 * On click handler to activate the content region's corresponding mode. 16 */ 17 click: function() { 18 var contentMode = this.options.contentMode; 19 if ( contentMode ) { 20 this.controller.content.mode( contentMode ); 21 } 22 } 23 }); 24 25 module.exports = RouterItem; 26 No newline at end of file -
src/wp-includes/js/media/views/router.js
1 /** 2 * wp.media.view.Router 3 * 4 * @class 5 * @augments wp.media.view.Menu 6 * @augments wp.media.view.PriorityList 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var Menu = require( './menu.js' ), 12 RouterItem = require( './router-item.js' ), 13 Router; 14 15 Router = Menu.extend({ 16 tagName: 'div', 17 className: 'media-router', 18 property: 'contentMode', 19 ItemView: RouterItem, 20 region: 'router', 21 22 initialize: function() { 23 this.controller.on( 'content:render', this.update, this ); 24 // Call 'initialize' directly on the parent class. 25 Menu.prototype.initialize.apply( this, arguments ); 26 }, 27 28 update: function() { 29 var mode = this.controller.content.mode(); 30 if ( mode ) { 31 this.select( mode ); 32 } 33 } 34 }); 35 36 module.exports = Router; 37 No newline at end of file -
src/wp-includes/js/media/views/search.js
1 /** 2 * wp.media.view.Search 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 l10n = wp.media.view.l10n, 11 Search; 12 13 Search = View.extend({ 14 tagName: 'input', 15 className: 'search', 16 id: 'media-search-input', 17 18 attributes: { 19 type: 'search', 20 placeholder: l10n.search 21 }, 22 23 events: { 24 'input': 'search', 25 'keyup': 'search', 26 'change': 'search', 27 'search': 'search' 28 }, 29 30 /** 31 * @returns {wp.media.view.Search} Returns itself to allow chaining 32 */ 33 render: function() { 34 this.el.value = this.model.escape('search'); 35 return this; 36 }, 37 38 search: function( event ) { 39 if ( event.target.value ) { 40 this.model.set( 'search', event.target.value ); 41 } else { 42 this.model.unset('search'); 43 } 44 } 45 }); 46 47 module.exports = Search; 48 No newline at end of file -
src/wp-includes/js/media/views/selection.js
1 /** 2 * wp.media.view.Selection 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 AttachmentsSelection = require( './attachments/selection.js' ), 11 l10n = wp.media.view.l10n, 12 Selection; 13 14 Selection = View.extend({ 15 tagName: 'div', 16 className: 'media-selection', 17 template: wp.template('media-selection'), 18 19 events: { 20 'click .edit-selection': 'edit', 21 'click .clear-selection': 'clear' 22 }, 23 24 initialize: function() { 25 _.defaults( this.options, { 26 editable: false, 27 clearable: true 28 }); 29 30 /** 31 * @member {wp.media.view.Attachments.Selection} 32 */ 33 this.attachments = new AttachmentsSelection({ 34 controller: this.controller, 35 collection: this.collection, 36 selection: this.collection, 37 model: new Backbone.Model() 38 }); 39 40 this.views.set( '.selection-view', this.attachments ); 41 this.collection.on( 'add remove reset', this.refresh, this ); 42 this.controller.on( 'content:activate', this.refresh, this ); 43 }, 44 45 ready: function() { 46 this.refresh(); 47 }, 48 49 refresh: function() { 50 // If the selection hasn't been rendered, bail. 51 if ( ! this.$el.children().length ) { 52 return; 53 } 54 55 var collection = this.collection, 56 editing = 'edit-selection' === this.controller.content.mode(); 57 58 // If nothing is selected, display nothing. 59 this.$el.toggleClass( 'empty', ! collection.length ); 60 this.$el.toggleClass( 'one', 1 === collection.length ); 61 this.$el.toggleClass( 'editing', editing ); 62 63 this.$('.count').text( l10n.selected.replace('%d', collection.length) ); 64 }, 65 66 edit: function( event ) { 67 event.preventDefault(); 68 if ( this.options.editable ) { 69 this.options.editable.call( this, this.collection ); 70 } 71 }, 72 73 clear: function( event ) { 74 event.preventDefault(); 75 this.collection.reset(); 76 77 // Keep focus inside media modal 78 // after clear link is selected 79 this.controller.modal.focusManager.focus(); 80 } 81 }); 82 83 module.exports = Selection; 84 No newline at end of file -
src/wp-includes/js/media/views/settings/attachment-display.js
1 /** 2 * wp.media.view.Settings.AttachmentDisplay 3 * 4 * @class 5 * @augments wp.media.view.Settings 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Settings = require( '../settings.js' ), 11 AttachmentDisplay; 12 13 AttachmentDisplay = Settings.extend({ 14 className: 'attachment-display-settings', 15 template: wp.template('attachment-display-settings'), 16 17 initialize: function() { 18 var attachment = this.options.attachment; 19 20 _.defaults( this.options, { 21 userSettings: false 22 }); 23 // Call 'initialize' directly on the parent class. 24 Settings.prototype.initialize.apply( this, arguments ); 25 this.model.on( 'change:link', this.updateLinkTo, this ); 26 27 if ( attachment ) { 28 attachment.on( 'change:uploading', this.render, this ); 29 } 30 }, 31 32 dispose: function() { 33 var attachment = this.options.attachment; 34 if ( attachment ) { 35 attachment.off( null, null, this ); 36 } 37 /** 38 * call 'dispose' directly on the parent class 39 */ 40 Settings.prototype.dispose.apply( this, arguments ); 41 }, 42 /** 43 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining 44 */ 45 render: function() { 46 var attachment = this.options.attachment; 47 if ( attachment ) { 48 _.extend( this.options, { 49 sizes: attachment.get('sizes'), 50 type: attachment.get('type') 51 }); 52 } 53 /** 54 * call 'render' directly on the parent class 55 */ 56 Settings.prototype.render.call( this ); 57 this.updateLinkTo(); 58 return this; 59 }, 60 61 updateLinkTo: function() { 62 var linkTo = this.model.get('link'), 63 $input = this.$('.link-to-custom'), 64 attachment = this.options.attachment; 65 66 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { 67 $input.addClass( 'hidden' ); 68 return; 69 } 70 71 if ( attachment ) { 72 if ( 'post' === linkTo ) { 73 $input.val( attachment.get('link') ); 74 } else if ( 'file' === linkTo ) { 75 $input.val( attachment.get('url') ); 76 } else if ( ! this.model.get('linkUrl') ) { 77 $input.val('http://'); 78 } 79 80 $input.prop( 'readonly', 'custom' !== linkTo ); 81 } 82 83 $input.removeClass( 'hidden' ); 84 85 // If the input is visible, focus and select its contents. 86 if ( ! wp.media.isTouchDevice && $input.is(':visible') ) { 87 $input.focus()[0].select(); 88 } 89 } 90 }); 91 92 module.exports = AttachmentDisplay; 93 No newline at end of file -
src/wp-includes/js/media/views/settings/gallery.js
1 /** 2 * wp.media.view.Settings.Gallery 3 * 4 * @class 5 * @augments wp.media.view.Settings 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Settings = require( '../settings.js' ), 11 Gallery; 12 13 Gallery = Settings.extend({ 14 className: 'collection-settings gallery-settings', 15 template: wp.template('gallery-settings') 16 }); 17 18 module.exports = Gallery; 19 No newline at end of file -
src/wp-includes/js/media/views/settings/playlist.js
1 /** 2 * wp.media.view.Settings.Playlist 3 * 4 * @class 5 * @augments wp.media.view.Settings 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Settings = require( '../settings.js' ), 11 Playlist; 12 13 Playlist = Settings.extend({ 14 className: 'collection-settings playlist-settings', 15 template: wp.template('playlist-settings') 16 }); 17 18 module.exports = Playlist; 19 No newline at end of file -
src/wp-includes/js/media/views/settings.js
1 /** 2 * wp.media.view.Settings 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 $ = jQuery, 11 Settings; 12 13 Settings = View.extend({ 14 events: { 15 'click button': 'updateHandler', 16 'change input': 'updateHandler', 17 'change select': 'updateHandler', 18 'change textarea': 'updateHandler' 19 }, 20 21 initialize: function() { 22 this.model = this.model || new Backbone.Model(); 23 this.model.on( 'change', this.updateChanges, this ); 24 }, 25 26 prepare: function() { 27 return _.defaults({ 28 model: this.model.toJSON() 29 }, this.options ); 30 }, 31 /** 32 * @returns {wp.media.view.Settings} Returns itself to allow chaining 33 */ 34 render: function() { 35 View.prototype.render.apply( this, arguments ); 36 // Select the correct values. 37 _( this.model.attributes ).chain().keys().each( this.update, this ); 38 return this; 39 }, 40 /** 41 * @param {string} key 42 */ 43 update: function( key ) { 44 var value = this.model.get( key ), 45 $setting = this.$('[data-setting="' + key + '"]'), 46 $buttons, $value; 47 48 // Bail if we didn't find a matching setting. 49 if ( ! $setting.length ) { 50 return; 51 } 52 53 // Attempt to determine how the setting is rendered and update 54 // the selected value. 55 56 // Handle dropdowns. 57 if ( $setting.is('select') ) { 58 $value = $setting.find('[value="' + value + '"]'); 59 60 if ( $value.length ) { 61 $setting.find('option').prop( 'selected', false ); 62 $value.prop( 'selected', true ); 63 } else { 64 // If we can't find the desired value, record what *is* selected. 65 this.model.set( key, $setting.find(':selected').val() ); 66 } 67 68 // Handle button groups. 69 } else if ( $setting.hasClass('button-group') ) { 70 $buttons = $setting.find('button').removeClass('active'); 71 $buttons.filter( '[value="' + value + '"]' ).addClass('active'); 72 73 // Handle text inputs and textareas. 74 } else if ( $setting.is('input[type="text"], textarea') ) { 75 if ( ! $setting.is(':focus') ) { 76 $setting.val( value ); 77 } 78 // Handle checkboxes. 79 } else if ( $setting.is('input[type="checkbox"]') ) { 80 $setting.prop( 'checked', !! value && 'false' !== value ); 81 } 82 }, 83 /** 84 * @param {Object} event 85 */ 86 updateHandler: function( event ) { 87 var $setting = $( event.target ).closest('[data-setting]'), 88 value = event.target.value, 89 userSetting; 90 91 event.preventDefault(); 92 93 if ( ! $setting.length ) { 94 return; 95 } 96 97 // Use the correct value for checkboxes. 98 if ( $setting.is('input[type="checkbox"]') ) { 99 value = $setting[0].checked; 100 } 101 102 // Update the corresponding setting. 103 this.model.set( $setting.data('setting'), value ); 104 105 // If the setting has a corresponding user setting, 106 // update that as well. 107 if ( userSetting = $setting.data('userSetting') ) { 108 setUserSetting( userSetting, value ); 109 } 110 }, 111 112 updateChanges: function( model ) { 113 if ( model.hasChanged() ) { 114 _( model.changed ).chain().keys().each( this.update, this ); 115 } 116 } 117 }); 118 119 module.exports = Settings; 120 No newline at end of file -
src/wp-includes/js/media/views/sidebar.js
1 /** 2 * wp.media.view.Sidebar 3 * 4 * @class 5 * @augments wp.media.view.PriorityList 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var PriorityList = require( './priority-list.js' ), 11 Sidebar; 12 13 Sidebar = PriorityList.extend({ 14 className: 'media-sidebar' 15 }); 16 17 module.exports = Sidebar; 18 No newline at end of file -
src/wp-includes/js/media/views/spinner.js
1 /** 2 * wp.media.view.Spinner 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( './view.js' ), 10 Spinner; 11 12 Spinner = View.extend({ 13 tagName: 'span', 14 className: 'spinner', 15 spinnerTimeout: false, 16 delay: 400, 17 18 show: function() { 19 if ( ! this.spinnerTimeout ) { 20 this.spinnerTimeout = _.delay(function( $el ) { 21 $el.show(); 22 }, this.delay, this.$el ); 23 } 24 25 return this; 26 }, 27 28 hide: function() { 29 this.$el.hide(); 30 this.spinnerTimeout = clearTimeout( this.spinnerTimeout ); 31 32 return this; 33 } 34 }); 35 36 module.exports = Spinner; 37 No newline at end of file -
src/wp-includes/js/media/views/toolbar/embed.js
1 /** 2 * wp.media.view.Toolbar.Embed 3 * 4 * @class 5 * @augments wp.media.view.Toolbar.Select 6 * @augments wp.media.view.Toolbar 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var Select = require( './select.js' ), 12 l10n = wp.media.view.l10n, 13 Embed; 14 15 Embed = Select.extend({ 16 initialize: function() { 17 _.defaults( this.options, { 18 text: l10n.insertIntoPost, 19 requires: false 20 }); 21 // Call 'initialize' directly on the parent class. 22 Select.prototype.initialize.apply( this, arguments ); 23 }, 24 25 refresh: function() { 26 var url = this.controller.state().props.get('url'); 27 this.get('select').model.set( 'disabled', ! url || url === 'http://' ); 28 /** 29 * call 'refresh' directly on the parent class 30 */ 31 Select.prototype.refresh.apply( this, arguments ); 32 } 33 }); 34 35 module.exports = Embed; 36 No newline at end of file -
src/wp-includes/js/media/views/toolbar/select.js
1 /** 2 * wp.media.view.Toolbar.Select 3 * 4 * @class 5 * @augments wp.media.view.Toolbar 6 * @augments wp.media.View 7 * @augments wp.Backbone.View 8 * @augments Backbone.View 9 */ 10 var Toolbar = require( '../toolbar.js' ), 11 l10n = wp.media.view.l10n, 12 Select; 13 14 Select = Toolbar.extend({ 15 initialize: function() { 16 var options = this.options; 17 18 _.bindAll( this, 'clickSelect' ); 19 20 _.defaults( options, { 21 event: 'select', 22 state: false, 23 reset: true, 24 close: true, 25 text: l10n.select, 26 27 // Does the button rely on the selection? 28 requires: { 29 selection: true 30 } 31 }); 32 33 options.items = _.defaults( options.items || {}, { 34 select: { 35 style: 'primary', 36 text: options.text, 37 priority: 80, 38 click: this.clickSelect, 39 requires: options.requires 40 } 41 }); 42 // Call 'initialize' directly on the parent class. 43 Toolbar.prototype.initialize.apply( this, arguments ); 44 }, 45 46 clickSelect: function() { 47 var options = this.options, 48 controller = this.controller; 49 50 if ( options.close ) { 51 controller.close(); 52 } 53 54 if ( options.event ) { 55 controller.state().trigger( options.event ); 56 } 57 58 if ( options.state ) { 59 controller.setState( options.state ); 60 } 61 62 if ( options.reset ) { 63 controller.reset(); 64 } 65 } 66 }); 67 68 module.exports = Select; 69 No newline at end of file -
src/wp-includes/js/media/views/toolbar.js
1 /** 2 * wp.media.view.Toolbar 3 * 4 * A toolbar which consists of a primary and a secondary section. Each sections 5 * can be filled with views. 6 * 7 * @class 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 */ 12 var View = require( './view.js' ), 13 Button = require( './button.js' ), 14 PriorityList = require( './priority-list.js' ), 15 Toolbar; 16 17 Toolbar = View.extend({ 18 tagName: 'div', 19 className: 'media-toolbar', 20 21 initialize: function() { 22 var state = this.controller.state(), 23 selection = this.selection = state.get('selection'), 24 library = this.library = state.get('library'); 25 26 this._views = {}; 27 28 // The toolbar is composed of two `PriorityList` views. 29 this.primary = new PriorityList(); 30 this.secondary = new PriorityList(); 31 this.primary.$el.addClass('media-toolbar-primary search-form'); 32 this.secondary.$el.addClass('media-toolbar-secondary'); 33 34 this.views.set([ this.secondary, this.primary ]); 35 36 if ( this.options.items ) { 37 this.set( this.options.items, { silent: true }); 38 } 39 40 if ( ! this.options.silent ) { 41 this.render(); 42 } 43 44 if ( selection ) { 45 selection.on( 'add remove reset', this.refresh, this ); 46 } 47 48 if ( library ) { 49 library.on( 'add remove reset', this.refresh, this ); 50 } 51 }, 52 /** 53 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining 54 */ 55 dispose: function() { 56 if ( this.selection ) { 57 this.selection.off( null, null, this ); 58 } 59 60 if ( this.library ) { 61 this.library.off( null, null, this ); 62 } 63 /** 64 * call 'dispose' directly on the parent class 65 */ 66 return View.prototype.dispose.apply( this, arguments ); 67 }, 68 69 ready: function() { 70 this.refresh(); 71 }, 72 73 /** 74 * @param {string} id 75 * @param {Backbone.View|Object} view 76 * @param {Object} [options={}] 77 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 78 */ 79 set: function( id, view, options ) { 80 var list; 81 options = options || {}; 82 83 // Accept an object with an `id` : `view` mapping. 84 if ( _.isObject( id ) ) { 85 _.each( id, function( view, id ) { 86 this.set( id, view, { silent: true }); 87 }, this ); 88 89 } else { 90 if ( ! ( view instanceof Backbone.View ) ) { 91 view.classes = [ 'media-button-' + id ].concat( view.classes || [] ); 92 view = new Button( view ).render(); 93 } 94 95 view.controller = view.controller || this.controller; 96 97 this._views[ id ] = view; 98 99 list = view.options.priority < 0 ? 'secondary' : 'primary'; 100 this[ list ].set( id, view, options ); 101 } 102 103 if ( ! options.silent ) { 104 this.refresh(); 105 } 106 107 return this; 108 }, 109 /** 110 * @param {string} id 111 * @returns {wp.media.view.Button} 112 */ 113 get: function( id ) { 114 return this._views[ id ]; 115 }, 116 /** 117 * @param {string} id 118 * @param {Object} options 119 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 120 */ 121 unset: function( id, options ) { 122 delete this._views[ id ]; 123 this.primary.unset( id, options ); 124 this.secondary.unset( id, options ); 125 126 if ( ! options || ! options.silent ) { 127 this.refresh(); 128 } 129 return this; 130 }, 131 132 refresh: function() { 133 var state = this.controller.state(), 134 library = state.get('library'), 135 selection = state.get('selection'); 136 137 _.each( this._views, function( button ) { 138 if ( ! button.model || ! button.options || ! button.options.requires ) { 139 return; 140 } 141 142 var requires = button.options.requires, 143 disabled = false; 144 145 // Prevent insertion of attachments if any of them are still uploading 146 disabled = _.some( selection.models, function( attachment ) { 147 return attachment.get('uploading') === true; 148 }); 149 150 if ( requires.selection && selection && ! selection.length ) { 151 disabled = true; 152 } else if ( requires.library && library && ! library.length ) { 153 disabled = true; 154 } 155 button.model.set( 'disabled', disabled ); 156 }); 157 } 158 }); 159 160 module.exports = Toolbar; 161 No newline at end of file -
src/wp-includes/js/media/views/uploader/editor.js
1 /** 2 * Creates a dropzone on WP editor instances (elements with .wp-editor-wrap 3 * or #wp-fullscreen-body) and relays drag'n'dropped files to a media workflow. 4 * 5 * wp.media.view.EditorUploader 6 * 7 * @class 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 */ 12 var View = require( '../view.js' ), 13 l10n = wp.media.view.l10n, 14 $ = jQuery, 15 EditorUploader; 16 17 EditorUploader = View.extend({ 18 tagName: 'div', 19 className: 'uploader-editor', 20 template: wp.template( 'uploader-editor' ), 21 22 localDrag: false, 23 overContainer: false, 24 overDropzone: false, 25 draggingFile: null, 26 27 /** 28 * Bind drag'n'drop events to callbacks. 29 */ 30 initialize: function() { 31 var self = this; 32 33 this.initialized = false; 34 35 // Bail if not enabled or UA does not support drag'n'drop or File API. 36 if ( ! window.tinyMCEPreInit || ! window.tinyMCEPreInit.dragDropUpload || ! this.browserSupport() ) { 37 return this; 38 } 39 40 this.$document = $(document); 41 this.dropzones = []; 42 this.files = []; 43 44 this.$document.on( 'drop', '.uploader-editor', _.bind( this.drop, this ) ); 45 this.$document.on( 'dragover', '.uploader-editor', _.bind( this.dropzoneDragover, this ) ); 46 this.$document.on( 'dragleave', '.uploader-editor', _.bind( this.dropzoneDragleave, this ) ); 47 this.$document.on( 'click', '.uploader-editor', _.bind( this.click, this ) ); 48 49 this.$document.on( 'dragover', _.bind( this.containerDragover, this ) ); 50 this.$document.on( 'dragleave', _.bind( this.containerDragleave, this ) ); 51 52 this.$document.on( 'dragstart dragend drop', function( event ) { 53 self.localDrag = event.type === 'dragstart'; 54 }); 55 56 this.initialized = true; 57 return this; 58 }, 59 60 /** 61 * Check browser support for drag'n'drop. 62 * 63 * @return Boolean 64 */ 65 browserSupport: function() { 66 var supports = false, div = document.createElement('div'); 67 68 supports = ( 'draggable' in div ) || ( 'ondragstart' in div && 'ondrop' in div ); 69 supports = supports && !! ( window.File && window.FileList && window.FileReader ); 70 return supports; 71 }, 72 73 isDraggingFile: function( event ) { 74 if ( this.draggingFile !== null ) { 75 return this.draggingFile; 76 } 77 78 if ( _.isUndefined( event.originalEvent ) || _.isUndefined( event.originalEvent.dataTransfer ) ) { 79 return false; 80 } 81 82 this.draggingFile = _.indexOf( event.originalEvent.dataTransfer.types, 'Files' ) > -1 && 83 _.indexOf( event.originalEvent.dataTransfer.types, 'text/plain' ) === -1; 84 85 return this.draggingFile; 86 }, 87 88 refresh: function( e ) { 89 var dropzone_id; 90 for ( dropzone_id in this.dropzones ) { 91 // Hide the dropzones only if dragging has left the screen. 92 this.dropzones[ dropzone_id ].toggle( this.overContainer || this.overDropzone ); 93 } 94 95 if ( ! _.isUndefined( e ) ) { 96 $( e.target ).closest( '.uploader-editor' ).toggleClass( 'droppable', this.overDropzone ); 97 } 98 99 if ( ! this.overContainer && ! this.overDropzone ) { 100 this.draggingFile = null; 101 } 102 103 return this; 104 }, 105 106 render: function() { 107 if ( ! this.initialized ) { 108 return this; 109 } 110 111 View.prototype.render.apply( this, arguments ); 112 $( '.wp-editor-wrap, #wp-fullscreen-body' ).each( _.bind( this.attach, this ) ); 113 return this; 114 }, 115 116 attach: function( index, editor ) { 117 // Attach a dropzone to an editor. 118 var dropzone = this.$el.clone(); 119 this.dropzones.push( dropzone ); 120 $( editor ).append( dropzone ); 121 return this; 122 }, 123 124 /** 125 * When a file is dropped on the editor uploader, open up an editor media workflow 126 * and upload the file immediately. 127 * 128 * @param {jQuery.Event} event The 'drop' event. 129 */ 130 drop: function( event ) { 131 var $wrap = null, uploadView; 132 133 this.containerDragleave( event ); 134 this.dropzoneDragleave( event ); 135 136 this.files = event.originalEvent.dataTransfer.files; 137 if ( this.files.length < 1 ) { 138 return; 139 } 140 141 // Set the active editor to the drop target. 142 $wrap = $( event.target ).parents( '.wp-editor-wrap' ); 143 if ( $wrap.length > 0 && $wrap[0].id ) { 144 window.wpActiveEditor = $wrap[0].id.slice( 3, -5 ); 145 } 146 147 if ( ! this.workflow ) { 148 this.workflow = wp.media.editor.open( 'content', { 149 frame: 'post', 150 state: 'insert', 151 title: l10n.addMedia, 152 multiple: true 153 }); 154 uploadView = this.workflow.uploader; 155 if ( uploadView.uploader && uploadView.uploader.ready ) { 156 this.addFiles.apply( this ); 157 } else { 158 this.workflow.on( 'uploader:ready', this.addFiles, this ); 159 } 160 } else { 161 this.workflow.state().reset(); 162 this.addFiles.apply( this ); 163 this.workflow.open(); 164 } 165 166 return false; 167 }, 168 169 /** 170 * Add the files to the uploader. 171 */ 172 addFiles: function() { 173 if ( this.files.length ) { 174 this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) ); 175 this.files = []; 176 } 177 return this; 178 }, 179 180 containerDragover: function( event ) { 181 if ( this.localDrag || ! this.isDraggingFile( event ) ) { 182 return; 183 } 184 185 this.overContainer = true; 186 this.refresh(); 187 }, 188 189 containerDragleave: function() { 190 this.overContainer = false; 191 192 // Throttle dragleave because it's called when bouncing from some elements to others. 193 _.delay( _.bind( this.refresh, this ), 50 ); 194 }, 195 196 dropzoneDragover: function( event ) { 197 if ( this.localDrag || ! this.isDraggingFile( event ) ) { 198 return; 199 } 200 201 this.overDropzone = true; 202 this.refresh( event ); 203 return false; 204 }, 205 206 dropzoneDragleave: function( e ) { 207 this.overDropzone = false; 208 _.delay( _.bind( this.refresh, this, e ), 50 ); 209 }, 210 211 click: function( e ) { 212 // In the rare case where the dropzone gets stuck, hide it on click. 213 this.containerDragleave( e ); 214 this.dropzoneDragleave( e ); 215 this.localDrag = false; 216 } 217 }); 218 219 module.exports = EditorUploader; 220 No newline at end of file -
src/wp-includes/js/media/views/uploader/inline.js
1 /** 2 * wp.media.view.UploaderInline 3 * 4 * The inline uploader that shows up in the 'Upload Files' tab. 5 * 6 * @class 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var View = require( '../view.js' ), 12 UploaderStatus = require( './status.js' ), 13 UploaderInline; 14 15 UploaderInline = View.extend({ 16 tagName: 'div', 17 className: 'uploader-inline', 18 template: wp.template('uploader-inline'), 19 20 events: { 21 'click .close': 'hide' 22 }, 23 24 initialize: function() { 25 _.defaults( this.options, { 26 message: '', 27 status: true, 28 canClose: false 29 }); 30 31 if ( ! this.options.$browser && this.controller.uploader ) { 32 this.options.$browser = this.controller.uploader.$browser; 33 } 34 35 if ( _.isUndefined( this.options.postId ) ) { 36 this.options.postId = wp.media.view.settings.post.id; 37 } 38 39 if ( this.options.status ) { 40 this.views.set( '.upload-inline-status', new UploaderStatus({ 41 controller: this.controller 42 }) ); 43 } 44 }, 45 46 prepare: function() { 47 var suggestedWidth = this.controller.state().get('suggestedWidth'), 48 suggestedHeight = this.controller.state().get('suggestedHeight'), 49 data = {}; 50 51 data.message = this.options.message; 52 data.canClose = this.options.canClose; 53 54 if ( suggestedWidth && suggestedHeight ) { 55 data.suggestedWidth = suggestedWidth; 56 data.suggestedHeight = suggestedHeight; 57 } 58 59 return data; 60 }, 61 /** 62 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 63 */ 64 dispose: function() { 65 if ( this.disposing ) { 66 /** 67 * call 'dispose' directly on the parent class 68 */ 69 return View.prototype.dispose.apply( this, arguments ); 70 } 71 72 // Run remove on `dispose`, so we can be sure to refresh the 73 // uploader with a view-less DOM. Track whether we're disposing 74 // so we don't trigger an infinite loop. 75 this.disposing = true; 76 return this.remove(); 77 }, 78 /** 79 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 80 */ 81 remove: function() { 82 /** 83 * call 'remove' directly on the parent class 84 */ 85 var result = View.prototype.remove.apply( this, arguments ); 86 87 _.defer( _.bind( this.refresh, this ) ); 88 return result; 89 }, 90 91 refresh: function() { 92 var uploader = this.controller.uploader; 93 94 if ( uploader ) { 95 uploader.refresh(); 96 } 97 }, 98 /** 99 * @returns {wp.media.view.UploaderInline} 100 */ 101 ready: function() { 102 var $browser = this.options.$browser, 103 $placeholder; 104 105 if ( this.controller.uploader ) { 106 $placeholder = this.$('.browser'); 107 108 // Check if we've already replaced the placeholder. 109 if ( $placeholder[0] === $browser[0] ) { 110 return; 111 } 112 113 $browser.detach().text( $placeholder.text() ); 114 $browser[0].className = $placeholder[0].className; 115 $placeholder.replaceWith( $browser.show() ); 116 } 117 118 this.refresh(); 119 return this; 120 }, 121 show: function() { 122 this.$el.removeClass( 'hidden' ); 123 }, 124 hide: function() { 125 this.$el.addClass( 'hidden' ); 126 } 127 128 }); 129 130 module.exports = UploaderInline; 131 No newline at end of file -
src/wp-includes/js/media/views/uploader/status-error.js
1 /** 2 * wp.media.view.UploaderStatusError 3 * 4 * @class 5 * @augments wp.media.View 6 * @augments wp.Backbone.View 7 * @augments Backbone.View 8 */ 9 var View = require( '../view.js' ), 10 UploaderStatusError; 11 12 UploaderStatusError = View.extend({ 13 className: 'upload-error', 14 template: wp.template('uploader-status-error') 15 }); 16 17 module.exports = UploaderStatusError; 18 No newline at end of file -
src/wp-includes/js/media/views/uploader/status.js
1 /** 2 * wp.media.view.UploaderStatus 3 * 4 * An uploader status for on-going uploads. 5 * 6 * @class 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var View = require( '../view.js' ), 12 UploaderStatusError = require( './status-error.js' ), 13 UploaderStatus; 14 15 UploaderStatus = View.extend({ 16 className: 'media-uploader-status', 17 template: wp.template('uploader-status'), 18 19 events: { 20 'click .upload-dismiss-errors': 'dismiss' 21 }, 22 23 initialize: function() { 24 this.queue = wp.Uploader.queue; 25 this.queue.on( 'add remove reset', this.visibility, this ); 26 this.queue.on( 'add remove reset change:percent', this.progress, this ); 27 this.queue.on( 'add remove reset change:uploading', this.info, this ); 28 29 this.errors = wp.Uploader.errors; 30 this.errors.reset(); 31 this.errors.on( 'add remove reset', this.visibility, this ); 32 this.errors.on( 'add', this.error, this ); 33 }, 34 /** 35 * @global wp.Uploader 36 * @returns {wp.media.view.UploaderStatus} 37 */ 38 dispose: function() { 39 wp.Uploader.queue.off( null, null, this ); 40 /** 41 * call 'dispose' directly on the parent class 42 */ 43 View.prototype.dispose.apply( this, arguments ); 44 return this; 45 }, 46 47 visibility: function() { 48 this.$el.toggleClass( 'uploading', !! this.queue.length ); 49 this.$el.toggleClass( 'errors', !! this.errors.length ); 50 this.$el.toggle( !! this.queue.length || !! this.errors.length ); 51 }, 52 53 ready: function() { 54 _.each({ 55 '$bar': '.media-progress-bar div', 56 '$index': '.upload-index', 57 '$total': '.upload-total', 58 '$filename': '.upload-filename' 59 }, function( selector, key ) { 60 this[ key ] = this.$( selector ); 61 }, this ); 62 63 this.visibility(); 64 this.progress(); 65 this.info(); 66 }, 67 68 progress: function() { 69 var queue = this.queue, 70 $bar = this.$bar; 71 72 if ( ! $bar || ! queue.length ) { 73 return; 74 } 75 76 $bar.width( ( queue.reduce( function( memo, attachment ) { 77 if ( ! attachment.get('uploading') ) { 78 return memo + 100; 79 } 80 81 var percent = attachment.get('percent'); 82 return memo + ( _.isNumber( percent ) ? percent : 100 ); 83 }, 0 ) / queue.length ) + '%' ); 84 }, 85 86 info: function() { 87 var queue = this.queue, 88 index = 0, active; 89 90 if ( ! queue.length ) { 91 return; 92 } 93 94 active = this.queue.find( function( attachment, i ) { 95 index = i; 96 return attachment.get('uploading'); 97 }); 98 99 this.$index.text( index + 1 ); 100 this.$total.text( queue.length ); 101 this.$filename.html( active ? this.filename( active.get('filename') ) : '' ); 102 }, 103 /** 104 * @param {string} filename 105 * @returns {string} 106 */ 107 filename: function( filename ) { 108 return wp.media.truncate( _.escape( filename ), 24 ); 109 }, 110 /** 111 * @param {Backbone.Model} error 112 */ 113 error: function( error ) { 114 this.views.add( '.upload-errors', new UploaderStatusError({ 115 filename: this.filename( error.get('file').name ), 116 message: error.get('message') 117 }), { at: 0 }); 118 }, 119 120 /** 121 * @global wp.Uploader 122 * 123 * @param {Object} event 124 */ 125 dismiss: function( event ) { 126 var errors = this.views.get('.upload-errors'); 127 128 event.preventDefault(); 129 130 if ( errors ) { 131 _.invoke( errors, 'remove' ); 132 } 133 wp.Uploader.errors.reset(); 134 } 135 }); 136 137 module.exports = UploaderStatus; 138 No newline at end of file -
src/wp-includes/js/media/views/uploader/window.js
1 /** 2 * wp.media.view.UploaderWindow 3 * 4 * An uploader window that allows for dragging and dropping media. 5 * 6 * @class 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 * 11 * @param {object} [options] Options hash passed to the view. 12 * @param {object} [options.uploader] Uploader properties. 13 * @param {jQuery} [options.uploader.browser] 14 * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone. 15 * @param {object} [options.uploader.params] 16 */ 17 var View = require( '../view.js' ), 18 $ = jQuery, 19 UploaderWindow; 20 21 UploaderWindow = View.extend({ 22 tagName: 'div', 23 className: 'uploader-window', 24 template: wp.template('uploader-window'), 25 26 initialize: function() { 27 var uploader; 28 29 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body'); 30 31 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { 32 dropzone: this.$el, 33 browser: this.$browser, 34 params: {} 35 }); 36 37 // Ensure the dropzone is a jQuery collection. 38 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) { 39 uploader.dropzone = $( uploader.dropzone ); 40 } 41 42 this.controller.on( 'activate', this.refresh, this ); 43 44 this.controller.on( 'detach', function() { 45 this.$browser.remove(); 46 }, this ); 47 }, 48 49 refresh: function() { 50 if ( this.uploader ) { 51 this.uploader.refresh(); 52 } 53 }, 54 55 ready: function() { 56 var postId = wp.media.view.settings.post.id, 57 dropzone; 58 59 // If the uploader already exists, bail. 60 if ( this.uploader ) { 61 return; 62 } 63 64 if ( postId ) { 65 this.options.uploader.params.post_id = postId; 66 } 67 this.uploader = new wp.Uploader( this.options.uploader ); 68 69 dropzone = this.uploader.dropzone; 70 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) ); 71 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) ); 72 73 $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) ); 74 }, 75 76 _ready: function() { 77 this.controller.trigger( 'uploader:ready' ); 78 }, 79 80 show: function() { 81 var $el = this.$el.show(); 82 83 // Ensure that the animation is triggered by waiting until 84 // the transparent element is painted into the DOM. 85 _.defer( function() { 86 $el.css({ opacity: 1 }); 87 }); 88 }, 89 90 hide: function() { 91 var $el = this.$el.css({ opacity: 0 }); 92 93 wp.media.transition( $el ).done( function() { 94 // Transition end events are subject to race conditions. 95 // Make sure that the value is set as intended. 96 if ( '0' === $el.css('opacity') ) { 97 $el.hide(); 98 } 99 }); 100 101 // https://core.trac.wordpress.org/ticket/27341 102 _.delay( function() { 103 if ( '0' === $el.css('opacity') && $el.is(':visible') ) { 104 $el.hide(); 105 } 106 }, 500 ); 107 } 108 }); 109 110 module.exports = UploaderWindow; 111 No newline at end of file -
src/wp-includes/js/media/views/video-details.js
1 /** 2 * wp.media.view.VideoDetails 3 * 4 * @constructor 5 * @augments wp.media.view.MediaDetails 6 * @augments wp.media.view.Settings.AttachmentDisplay 7 * @augments wp.media.view.Settings 8 * @augments wp.media.View 9 * @augments wp.Backbone.View 10 * @augments Backbone.View 11 */ 12 var MediaDetails = require( './media-details' ), 13 VideoDetails; 14 15 VideoDetails = MediaDetails.extend({ 16 className: 'video-details', 17 template: wp.template('video-details'), 18 19 setMedia: function() { 20 var video = this.$('.wp-video-shortcode'); 21 22 if ( video.find( 'source' ).length ) { 23 if ( video.is(':hidden') ) { 24 video.show(); 25 } 26 27 if ( ! video.hasClass('youtube-video') ) { 28 this.media = MediaDetails.prepareSrc( video.get(0) ); 29 } else { 30 this.media = video.get(0); 31 } 32 } else { 33 video.hide(); 34 this.media = false; 35 } 36 37 return this; 38 } 39 }); 40 41 module.exports = VideoDetails; 42 No newline at end of file -
src/wp-includes/js/media/views/view.js
1 /** 2 * wp.media.View 3 * 4 * The base view class for media. 5 * 6 * Undelegating events, removing events from the model, and 7 * removing events from the controller mirror the code for 8 * `Backbone.View.dispose` in Backbone 0.9.8 development. 9 * 10 * This behavior has since been removed, and should not be used 11 * outside of the media manager. 12 * 13 * @class 14 * @augments wp.Backbone.View 15 * @augments Backbone.View 16 */ 17 var View = wp.Backbone.View.extend({ 18 constructor: function( options ) { 19 if ( options && options.controller ) { 20 this.controller = options.controller; 21 } 22 wp.Backbone.View.apply( this, arguments ); 23 }, 24 /** 25 * @todo The internal comment mentions this might have been a stop-gap 26 * before Backbone 0.9.8 came out. Figure out if Backbone core takes 27 * care of this in Backbone.View now. 28 * 29 * @returns {wp.media.View} Returns itself to allow chaining 30 */ 31 dispose: function() { 32 // Undelegating events, removing events from the model, and 33 // removing events from the controller mirror the code for 34 // `Backbone.View.dispose` in Backbone 0.9.8 development. 35 this.undelegateEvents(); 36 37 if ( this.model && this.model.off ) { 38 this.model.off( null, null, this ); 39 } 40 41 if ( this.collection && this.collection.off ) { 42 this.collection.off( null, null, this ); 43 } 44 45 // Unbind controller events. 46 if ( this.controller && this.controller.off ) { 47 this.controller.off( null, null, this ); 48 } 49 50 return this; 51 }, 52 /** 53 * @returns {wp.media.View} Returns itself to allow chaining 54 */ 55 remove: function() { 56 this.dispose(); 57 /** 58 * call 'remove' directly on the parent class 59 */ 60 return wp.Backbone.View.prototype.remove.apply( this, arguments ); 61 } 62 }); 63 64 module.exports = View; 65 No newline at end of file -
src/wp-includes/js/media/views.js
1 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 2 /** 3 * wp.media.controller.CollectionAdd 4 * 5 * A state for adding attachments to a collection (e.g. video playlist). 6 * 7 * @class 8 * @augments wp.media.controller.Library 9 * @augments wp.media.controller.State 10 * @augments Backbone.Model 11 * 12 * @param {object} [attributes] The attributes hash passed to the state. 13 * @param {string} [attributes.id=library] Unique identifier. 14 * @param {string} attributes.title Title for the state. Displays in the frame's title region. 15 * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. 16 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 17 * If one is not supplied, a collection of attachments of the specified type will be created. 18 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 19 * Accepts 'all', 'uploaded', or 'unattached'. 20 * @param {string} [attributes.menu=gallery] Initial mode for the menu region. 21 * @param {string} [attributes.content=upload] Initial mode for the content region. 22 * Overridden by persistent user setting if 'contentUserSetting' is true. 23 * @param {string} [attributes.router=browse] Initial mode for the router region. 24 * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region. 25 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 26 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 27 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 28 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 29 * @param {int} [attributes.priority=100] The priority for the state link in the media menu. 30 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 31 * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. 32 * @param {string} attributes.type The collection's media type. (e.g. 'video'). 33 * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). 34 */ 35 var Selection = require( '../models/selection.js' ), 36 Library = require( './library.js' ), 37 CollectionAdd; 38 39 CollectionAdd = Library.extend({ 40 defaults: _.defaults( { 41 // Selection defaults. @see media.model.Selection 42 multiple: 'add', 43 // Attachments browser defaults. @see media.view.AttachmentsBrowser 44 filterable: 'uploaded', 45 46 priority: 100, 47 syncSelection: false 48 }, Library.prototype.defaults ), 49 50 /** 51 * @since 3.9.0 52 */ 53 initialize: function() { 54 var collectionType = this.get('collectionType'); 55 56 if ( 'video' === this.get( 'type' ) ) { 57 collectionType = 'video-' + collectionType; 58 } 59 60 this.set( 'id', collectionType + '-library' ); 61 this.set( 'toolbar', collectionType + '-add' ); 62 this.set( 'menu', collectionType ); 63 64 // If we haven't been provided a `library`, create a `Selection`. 65 if ( ! this.get('library') ) { 66 this.set( 'library', wp.media.query({ type: this.get('type') }) ); 67 } 68 Library.prototype.initialize.apply( this, arguments ); 69 }, 70 71 /** 72 * @since 3.9.0 73 */ 74 activate: function() { 75 var library = this.get('library'), 76 editLibrary = this.get('editLibrary'), 77 edit = this.frame.state( this.get('collectionType') + '-edit' ).get('library'); 78 79 if ( editLibrary && editLibrary !== edit ) { 80 library.unobserve( editLibrary ); 81 } 82 83 // Accepts attachments that exist in the original library and 84 // that do not exist in gallery's library. 85 library.validator = function( attachment ) { 86 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && Selection.prototype.validator.apply( this, arguments ); 87 }; 88 89 // Reset the library to ensure that all attachments are re-added 90 // to the collection. Do so silently, as calling `observe` will 91 // trigger the `reset` event. 92 library.reset( library.mirroring.models, { silent: true }); 93 library.observe( edit ); 94 this.set('editLibrary', edit); 95 96 Library.prototype.activate.apply( this, arguments ); 97 } 98 }); 99 100 module.exports = CollectionAdd; 101 },{"../models/selection.js":20,"./library.js":10}],2:[function(require,module,exports){ 102 /** 103 * wp.media.controller.CollectionEdit 104 * 105 * A state for editing a collection, which is used by audio and video playlists, 106 * and can be used for other collections. 107 * 108 * @class 109 * @augments wp.media.controller.Library 110 * @augments wp.media.controller.State 111 * @augments Backbone.Model 112 * 113 * @param {object} [attributes] The attributes hash passed to the state. 114 * @param {string} attributes.title Title for the state. Displays in the media menu and the frame's title region. 115 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to edit. 116 * If one is not supplied, an empty media.model.Selection collection is created. 117 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 118 * @param {string} [attributes.content=browse] Initial mode for the content region. 119 * @param {string} attributes.menu Initial mode for the menu region. @todo this needs a better explanation. 120 * @param {boolean} [attributes.searchable=false] Whether the library is searchable. 121 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 122 * @param {boolean} [attributes.describe=true] Whether to offer UI to describe the attachments - e.g. captioning images in a gallery. 123 * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable. 124 * @param {boolean} [attributes.dragInfoText] Instructional text about the attachments being sortable. 125 * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments. 126 * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance. 127 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 128 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 129 * Defaults to false for this state, because the library passed in *is* the selection. 130 * @param {view} [attributes.SettingsView] The view to edit the collection instance settings (e.g. Playlist settings with "Show tracklist" checkbox). 131 * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`. 132 * If none supplied, defaults to wp.media.view.Attachment.EditLibrary. 133 * @param {string} attributes.type The collection's media type. (e.g. 'video'). 134 * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). 135 */ 136 var SelectionModel = require( '../models/selection.js' ), 137 Library = require( './library.js' ), 138 View = require( '../views/view.js' ), 139 EditLibraryView = require( '../views/attachment/edit-library.js' ), 140 l10n = wp.media.view.l10n, 141 CollectionEdit; 142 143 CollectionEdit = Library.extend({ 144 defaults: { 145 multiple: false, 146 sortable: true, 147 searchable: false, 148 content: 'browse', 149 describe: true, 150 dragInfo: true, 151 idealColumnWidth: 170, 152 editing: false, 153 priority: 60, 154 SettingsView: false, 155 syncSelection: false 156 }, 157 158 /** 159 * @since 3.9.0 160 */ 161 initialize: function() { 162 var collectionType = this.get('collectionType'); 163 164 if ( 'video' === this.get( 'type' ) ) { 165 collectionType = 'video-' + collectionType; 166 } 167 168 this.set( 'id', collectionType + '-edit' ); 169 this.set( 'toolbar', collectionType + '-edit' ); 170 171 // If we haven't been provided a `library`, create a `Selection`. 172 if ( ! this.get('library') ) { 173 this.set( 'library', new SelectionModel() ); 174 } 175 // The single `Attachment` view to be used in the `Attachments` view. 176 if ( ! this.get('AttachmentView') ) { 177 this.set( 'AttachmentView', EditLibraryView ); 178 } 179 Library.prototype.initialize.apply( this, arguments ); 180 }, 181 182 /** 183 * @since 3.9.0 184 */ 185 activate: function() { 186 var library = this.get('library'); 187 188 // Limit the library to images only. 189 library.props.set( 'type', this.get( 'type' ) ); 190 191 // Watch for uploaded attachments. 192 this.get('library').observe( wp.Uploader.queue ); 193 194 this.frame.on( 'content:render:browse', this.renderSettings, this ); 195 196 Library.prototype.activate.apply( this, arguments ); 197 }, 198 199 /** 200 * @since 3.9.0 201 */ 202 deactivate: function() { 203 // Stop watching for uploaded attachments. 204 this.get('library').unobserve( wp.Uploader.queue ); 205 206 this.frame.off( 'content:render:browse', this.renderSettings, this ); 207 208 Library.prototype.deactivate.apply( this, arguments ); 209 }, 210 211 /** 212 * Render the collection embed settings view in the browser sidebar. 213 * 214 * @todo This is against the pattern elsewhere in media. Typically the frame 215 * is responsible for adding region mode callbacks. Explain. 216 * 217 * @since 3.9.0 218 * 219 * @param {wp.media.view.attachmentsBrowser} The attachments browser view. 220 */ 221 renderSettings: function( attachmentsBrowserView ) { 222 var library = this.get('library'), 223 collectionType = this.get('collectionType'), 224 dragInfoText = this.get('dragInfoText'), 225 SettingsView = this.get('SettingsView'), 226 obj = {}; 227 228 if ( ! library || ! attachmentsBrowserView ) { 229 return; 230 } 231 232 library[ collectionType ] = library[ collectionType ] || new Backbone.Model(); 233 234 obj[ collectionType ] = new SettingsView({ 235 controller: this, 236 model: library[ collectionType ], 237 priority: 40 238 }); 239 240 attachmentsBrowserView.sidebar.set( obj ); 241 242 if ( dragInfoText ) { 243 attachmentsBrowserView.toolbar.set( 'dragInfo', new View({ 244 el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0], 245 priority: -40 246 }) ); 247 } 248 249 // Add the 'Reverse order' button to the toolbar. 250 attachmentsBrowserView.toolbar.set( 'reverse', { 251 text: l10n.reverseOrder, 252 priority: 80, 253 254 click: function() { 255 library.reset( library.toArray().reverse() ); 256 } 257 }); 258 } 259 }); 260 261 module.exports = CollectionEdit; 262 },{"../models/selection.js":20,"../views/attachment/edit-library.js":30,"../views/view.js":76,"./library.js":10}],3:[function(require,module,exports){ 263 /** 264 * wp.media.controller.Cropper 265 * 266 * A state for cropping an image. 267 * 268 * @class 269 * @augments wp.media.controller.State 270 * @augments Backbone.Model 271 */ 272 var State = require( './state.js' ), 273 ToolbarView = require( '../views/toolbar.js' ), 274 CropperView = require( '../views/cropper.js' ), 275 l10n = wp.media.view.l10n, 276 Cropper; 277 278 Cropper = State.extend({ 279 defaults: { 280 id: 'cropper', 281 title: l10n.cropImage, 282 // Region mode defaults. 283 toolbar: 'crop', 284 content: 'crop', 285 router: false, 286 287 canSkipCrop: false 288 }, 289 290 activate: function() { 291 this.frame.on( 'content:create:crop', this.createCropContent, this ); 292 this.frame.on( 'close', this.removeCropper, this ); 293 this.set('selection', new Backbone.Collection(this.frame._selection.single)); 294 }, 295 296 deactivate: function() { 297 this.frame.toolbar.mode('browse'); 298 }, 299 300 createCropContent: function() { 301 this.cropperView = new CropperView({ 302 controller: this, 303 attachment: this.get('selection').first() 304 }); 305 this.cropperView.on('image-loaded', this.createCropToolbar, this); 306 this.frame.content.set(this.cropperView); 307 308 }, 309 removeCropper: function() { 310 this.imgSelect.cancelSelection(); 311 this.imgSelect.setOptions({remove: true}); 312 this.imgSelect.update(); 313 this.cropperView.remove(); 314 }, 315 createCropToolbar: function() { 316 var canSkipCrop, toolbarOptions; 317 318 canSkipCrop = this.get('canSkipCrop') || false; 319 320 toolbarOptions = { 321 controller: this.frame, 322 items: { 323 insert: { 324 style: 'primary', 325 text: l10n.cropImage, 326 priority: 80, 327 requires: { library: false, selection: false }, 328 329 click: function() { 330 var self = this, 331 selection = this.controller.state().get('selection').first(); 332 333 selection.set({cropDetails: this.controller.state().imgSelect.getSelection()}); 334 335 this.$el.text(l10n.cropping); 336 this.$el.attr('disabled', true); 337 this.controller.state().doCrop( selection ).done( function( croppedImage ) { 338 self.controller.trigger('cropped', croppedImage ); 339 self.controller.close(); 340 }).fail( function() { 341 self.controller.trigger('content:error:crop'); 342 }); 343 } 344 } 345 } 346 }; 347 348 if ( canSkipCrop ) { 349 _.extend( toolbarOptions.items, { 350 skip: { 351 style: 'secondary', 352 text: l10n.skipCropping, 353 priority: 70, 354 requires: { library: false, selection: false }, 355 click: function() { 356 var selection = this.controller.state().get('selection').first(); 357 this.controller.state().cropperView.remove(); 358 this.controller.trigger('skippedcrop', selection); 359 this.controller.close(); 360 } 361 } 362 }); 363 } 364 365 this.frame.toolbar.set( new ToolbarView(toolbarOptions) ); 366 }, 367 368 doCrop: function( attachment ) { 369 return wp.ajax.post( 'custom-header-crop', { 370 nonce: attachment.get('nonces').edit, 371 id: attachment.get('id'), 372 cropDetails: attachment.get('cropDetails') 373 } ); 374 } 375 }); 376 377 module.exports = Cropper; 378 },{"../views/cropper.js":39,"../views/toolbar.js":68,"./state.js":15}],4:[function(require,module,exports){ 379 /** 380 * wp.media.controller.EditImage 381 * 382 * A state for editing (cropping, etc.) an image. 383 * 384 * @class 385 * @augments wp.media.controller.State 386 * @augments Backbone.Model 387 * 388 * @param {object} attributes The attributes hash passed to the state. 389 * @param {wp.media.model.Attachment} attributes.model The attachment. 390 * @param {string} [attributes.id=edit-image] Unique identifier. 391 * @param {string} [attributes.title=Edit Image] Title for the state. Displays in the media menu and the frame's title region. 392 * @param {string} [attributes.content=edit-image] Initial mode for the content region. 393 * @param {string} [attributes.toolbar=edit-image] Initial mode for the toolbar region. 394 * @param {string} [attributes.menu=false] Initial mode for the menu region. 395 * @param {string} [attributes.url] Unused. @todo Consider removal. 396 */ 397 var State = require( './state.js' ), 398 ToolbarView = require( '../views/toolbar.js' ), 399 l10n = wp.media.view.l10n, 400 EditImage; 401 402 EditImage = State.extend({ 403 defaults: { 404 id: 'edit-image', 405 title: l10n.editImage, 406 menu: false, 407 toolbar: 'edit-image', 408 content: 'edit-image', 409 url: '' 410 }, 411 412 /** 413 * @since 3.9.0 414 */ 415 activate: function() { 416 this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar ); 417 }, 418 419 /** 420 * @since 3.9.0 421 */ 422 deactivate: function() { 423 this.stopListening( this.frame ); 424 }, 425 426 /** 427 * @since 3.9.0 428 */ 429 toolbar: function() { 430 var frame = this.frame, 431 lastState = frame.lastState(), 432 previous = lastState && lastState.id; 433 434 frame.toolbar.set( new ToolbarView({ 435 controller: frame, 436 items: { 437 back: { 438 style: 'primary', 439 text: l10n.back, 440 priority: 20, 441 click: function() { 442 if ( previous ) { 443 frame.setState( previous ); 444 } else { 445 frame.close(); 446 } 447 } 448 } 449 } 450 }) ); 451 } 452 }); 453 454 module.exports = EditImage; 455 },{"../views/toolbar.js":68,"./state.js":15}],5:[function(require,module,exports){ 456 /** 457 * wp.media.controller.Embed 458 * 459 * A state for embedding media from a URL. 460 * 461 * @class 462 * @augments wp.media.controller.State 463 * @augments Backbone.Model 464 * 465 * @param {object} attributes The attributes hash passed to the state. 466 * @param {string} [attributes.id=embed] Unique identifier. 467 * @param {string} [attributes.title=Insert From URL] Title for the state. Displays in the media menu and the frame's title region. 468 * @param {string} [attributes.content=embed] Initial mode for the content region. 469 * @param {string} [attributes.menu=default] Initial mode for the menu region. 470 * @param {string} [attributes.toolbar=main-embed] Initial mode for the toolbar region. 471 * @param {string} [attributes.menu=false] Initial mode for the menu region. 472 * @param {int} [attributes.priority=120] The priority for the state link in the media menu. 473 * @param {string} [attributes.type=link] The type of embed. Currently only link is supported. 474 * @param {string} [attributes.url] The embed URL. 475 * @param {object} [attributes.metadata={}] Properties of the embed, which will override attributes.url if set. 476 */ 477 var State = require( './state.js' ), 478 l10n = wp.media.view.l10n, 479 Embed; 480 481 Embed = State.extend({ 482 defaults: { 483 id: 'embed', 484 title: l10n.insertFromUrlTitle, 485 content: 'embed', 486 menu: 'default', 487 toolbar: 'main-embed', 488 priority: 120, 489 type: 'link', 490 url: '', 491 metadata: {} 492 }, 493 494 // The amount of time used when debouncing the scan. 495 sensitivity: 200, 496 497 initialize: function(options) { 498 this.metadata = options.metadata; 499 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity ); 500 this.props = new Backbone.Model( this.metadata || { url: '' }); 501 this.props.on( 'change:url', this.debouncedScan, this ); 502 this.props.on( 'change:url', this.refresh, this ); 503 this.on( 'scan', this.scanImage, this ); 504 }, 505 506 /** 507 * Trigger a scan of the embedded URL's content for metadata required to embed. 508 * 509 * @fires wp.media.controller.Embed#scan 510 */ 511 scan: function() { 512 var scanners, 513 embed = this, 514 attributes = { 515 type: 'link', 516 scanners: [] 517 }; 518 519 // Scan is triggered with the list of `attributes` to set on the 520 // state, useful for the 'type' attribute and 'scanners' attribute, 521 // an array of promise objects for asynchronous scan operations. 522 if ( this.props.get('url') ) { 523 this.trigger( 'scan', attributes ); 524 } 525 526 if ( attributes.scanners.length ) { 527 scanners = attributes.scanners = $.when.apply( $, attributes.scanners ); 528 scanners.always( function() { 529 if ( embed.get('scanners') === scanners ) { 530 embed.set( 'loading', false ); 531 } 532 }); 533 } else { 534 attributes.scanners = null; 535 } 536 537 attributes.loading = !! attributes.scanners; 538 this.set( attributes ); 539 }, 540 /** 541 * Try scanning the embed as an image to discover its dimensions. 542 * 543 * @param {Object} attributes 544 */ 545 scanImage: function( attributes ) { 546 var frame = this.frame, 547 state = this, 548 url = this.props.get('url'), 549 image = new Image(), 550 deferred = $.Deferred(); 551 552 attributes.scanners.push( deferred.promise() ); 553 554 // Try to load the image and find its width/height. 555 image.onload = function() { 556 deferred.resolve(); 557 558 if ( state !== frame.state() || url !== state.props.get('url') ) { 559 return; 560 } 561 562 state.set({ 563 type: 'image' 564 }); 565 566 state.props.set({ 567 width: image.width, 568 height: image.height 569 }); 570 }; 571 572 image.onerror = deferred.reject; 573 image.src = url; 574 }, 575 576 refresh: function() { 577 this.frame.toolbar.get().refresh(); 578 }, 579 580 reset: function() { 581 this.props.clear().set({ url: '' }); 582 583 if ( this.active ) { 584 this.refresh(); 585 } 586 } 587 }); 588 589 module.exports = Embed; 590 },{"./state.js":15}],6:[function(require,module,exports){ 591 /** 592 * wp.media.controller.FeaturedImage 593 * 594 * A state for selecting a featured image for a post. 595 * 596 * @class 597 * @augments wp.media.controller.Library 598 * @augments wp.media.controller.State 599 * @augments Backbone.Model 600 * 601 * @param {object} [attributes] The attributes hash passed to the state. 602 * @param {string} [attributes.id=featured-image] Unique identifier. 603 * @param {string} [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region. 604 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 605 * If one is not supplied, a collection of all images will be created. 606 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 607 * @param {string} [attributes.content=upload] Initial mode for the content region. 608 * Overridden by persistent user setting if 'contentUserSetting' is true. 609 * @param {string} [attributes.menu=default] Initial mode for the menu region. 610 * @param {string} [attributes.router=browse] Initial mode for the router region. 611 * @param {string} [attributes.toolbar=featured-image] Initial mode for the toolbar region. 612 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 613 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 614 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 615 * Accepts 'all', 'uploaded', or 'unattached'. 616 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 617 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 618 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 619 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 620 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 621 */ 622 var Attachment = require( '../models/attachment.js' ), 623 Library = require( './library.js' ), 624 l10n = wp.media.view.l10n, 625 FeaturedImage; 626 627 FeaturedImage = Library.extend({ 628 defaults: _.defaults({ 629 id: 'featured-image', 630 title: l10n.setFeaturedImageTitle, 631 multiple: false, 632 filterable: 'uploaded', 633 toolbar: 'featured-image', 634 priority: 60, 635 syncSelection: true 636 }, Library.prototype.defaults ), 637 638 /** 639 * @since 3.5.0 640 */ 641 initialize: function() { 642 var library, comparator; 643 644 // If we haven't been provided a `library`, create a `Selection`. 645 if ( ! this.get('library') ) { 646 this.set( 'library', wp.media.query({ type: 'image' }) ); 647 } 648 649 Library.prototype.initialize.apply( this, arguments ); 650 651 library = this.get('library'); 652 comparator = library.comparator; 653 654 // Overload the library's comparator to push items that are not in 655 // the mirrored query to the front of the aggregate collection. 656 library.comparator = function( a, b ) { 657 var aInQuery = !! this.mirroring.get( a.cid ), 658 bInQuery = !! this.mirroring.get( b.cid ); 659 660 if ( ! aInQuery && bInQuery ) { 661 return -1; 662 } else if ( aInQuery && ! bInQuery ) { 663 return 1; 664 } else { 665 return comparator.apply( this, arguments ); 666 } 667 }; 668 669 // Add all items in the selection to the library, so any featured 670 // images that are not initially loaded still appear. 671 library.observe( this.get('selection') ); 672 }, 673 674 /** 675 * @since 3.5.0 676 */ 677 activate: function() { 678 this.updateSelection(); 679 this.frame.on( 'open', this.updateSelection, this ); 680 681 Library.prototype.activate.apply( this, arguments ); 682 }, 683 684 /** 685 * @since 3.5.0 686 */ 687 deactivate: function() { 688 this.frame.off( 'open', this.updateSelection, this ); 689 690 Library.prototype.deactivate.apply( this, arguments ); 691 }, 692 693 /** 694 * @since 3.5.0 695 */ 696 updateSelection: function() { 697 var selection = this.get('selection'), 698 id = wp.media.view.settings.post.featuredImageId, 699 attachment; 700 701 if ( '' !== id && -1 !== id ) { 702 attachment = Attachment.get( id ); 703 attachment.fetch(); 704 } 705 706 selection.reset( attachment ? [ attachment ] : [] ); 707 } 708 }); 709 710 module.exports = FeaturedImage; 711 },{"../models/attachment.js":16,"./library.js":10}],7:[function(require,module,exports){ 712 /** 713 * A state for selecting more images to add to a gallery. 714 * 715 * @class 716 * @augments wp.media.controller.Library 717 * @augments wp.media.controller.State 718 * @augments Backbone.Model 719 * 720 * @param {object} [attributes] The attributes hash passed to the state. 721 * @param {string} [attributes.id=gallery-library] Unique identifier. 722 * @param {string} [attributes.title=Add to Gallery] Title for the state. Displays in the frame's title region. 723 * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. 724 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 725 * If one is not supplied, a collection of all images will be created. 726 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 727 * Accepts 'all', 'uploaded', or 'unattached'. 728 * @param {string} [attributes.menu=gallery] Initial mode for the menu region. 729 * @param {string} [attributes.content=upload] Initial mode for the content region. 730 * Overridden by persistent user setting if 'contentUserSetting' is true. 731 * @param {string} [attributes.router=browse] Initial mode for the router region. 732 * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region. 733 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 734 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 735 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 736 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 737 * @param {int} [attributes.priority=100] The priority for the state link in the media menu. 738 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 739 * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. 740 */ 741 var Selection = require( '../models/selection.js' ), 742 Library = require( './library.js' ), 743 l10n = wp.media.view.l10n, 744 GalleryAdd; 745 746 GalleryAdd = Library.extend({ 747 defaults: _.defaults({ 748 id: 'gallery-library', 749 title: l10n.addToGalleryTitle, 750 multiple: 'add', 751 filterable: 'uploaded', 752 menu: 'gallery', 753 toolbar: 'gallery-add', 754 priority: 100, 755 syncSelection: false 756 }, Library.prototype.defaults ), 757 758 /** 759 * @since 3.5.0 760 */ 761 initialize: function() { 762 // If a library wasn't supplied, create a library of images. 763 if ( ! this.get('library') ) 764 this.set( 'library', wp.media.query({ type: 'image' }) ); 765 766 Library.prototype.initialize.apply( this, arguments ); 767 }, 768 769 /** 770 * @since 3.5.0 771 */ 772 activate: function() { 773 var library = this.get('library'), 774 edit = this.frame.state('gallery-edit').get('library'); 775 776 if ( this.editLibrary && this.editLibrary !== edit ) 777 library.unobserve( this.editLibrary ); 778 779 // Accepts attachments that exist in the original library and 780 // that do not exist in gallery's library. 781 library.validator = function( attachment ) { 782 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && Selection.prototype.validator.apply( this, arguments ); 783 }; 784 785 // Reset the library to ensure that all attachments are re-added 786 // to the collection. Do so silently, as calling `observe` will 787 // trigger the `reset` event. 788 library.reset( library.mirroring.models, { silent: true }); 789 library.observe( edit ); 790 this.editLibrary = edit; 791 792 Library.prototype.activate.apply( this, arguments ); 793 } 794 }); 795 796 module.exports = GalleryAdd; 797 },{"../models/selection.js":20,"./library.js":10}],8:[function(require,module,exports){ 798 /** 799 * wp.media.controller.GalleryEdit 800 * 801 * A state for editing a gallery's images and settings. 802 * 803 * @class 804 * @augments wp.media.controller.Library 805 * @augments wp.media.controller.State 806 * @augments Backbone.Model 807 * 808 * @param {object} [attributes] The attributes hash passed to the state. 809 * @param {string} [attributes.id=gallery-edit] Unique identifier. 810 * @param {string} [attributes.title=Edit Gallery] Title for the state. Displays in the frame's title region. 811 * @param {wp.media.model.Attachments} [attributes.library] The collection of attachments in the gallery. 812 * If one is not supplied, an empty media.model.Selection collection is created. 813 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 814 * @param {boolean} [attributes.searchable=false] Whether the library is searchable. 815 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 816 * @param {string|false} [attributes.content=browse] Initial mode for the content region. 817 * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region. 818 * @param {boolean} [attributes.describe=true] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 819 * @param {boolean} [attributes.displaySettings=true] Whether to show the attachment display settings interface. 820 * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable. 821 * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments. 822 * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance. 823 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 824 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 825 * Defaults to false for this state, because the library passed in *is* the selection. 826 * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`. 827 * If none supplied, defaults to wp.media.view.Attachment.EditLibrary. 828 */ 829 var Selection = require( '../models/selection.js' ), 830 Library = require( './library.js' ), 831 EditLibraryView = require( '../views/attachment/edit-library.js' ), 832 GallerySettingsView = require( '../views/settings/gallery.js' ), 833 l10n = wp.media.view.l10n, 834 GalleryEdit; 835 836 GalleryEdit = Library.extend({ 837 defaults: { 838 id: 'gallery-edit', 839 title: l10n.editGalleryTitle, 840 multiple: false, 841 searchable: false, 842 sortable: true, 843 display: false, 844 content: 'browse', 845 toolbar: 'gallery-edit', 846 describe: true, 847 displaySettings: true, 848 dragInfo: true, 849 idealColumnWidth: 170, 850 editing: false, 851 priority: 60, 852 syncSelection: false 853 }, 854 855 /** 856 * @since 3.5.0 857 */ 858 initialize: function() { 859 // If we haven't been provided a `library`, create a `Selection`. 860 if ( ! this.get('library') ) 861 this.set( 'library', new Selection() ); 862 863 // The single `Attachment` view to be used in the `Attachments` view. 864 if ( ! this.get('AttachmentView') ) 865 this.set( 'AttachmentView', EditLibraryView ); 866 Library.prototype.initialize.apply( this, arguments ); 867 }, 868 869 /** 870 * @since 3.5.0 871 */ 872 activate: function() { 873 var library = this.get('library'); 874 875 // Limit the library to images only. 876 library.props.set( 'type', 'image' ); 877 878 // Watch for uploaded attachments. 879 this.get('library').observe( wp.Uploader.queue ); 880 881 this.frame.on( 'content:render:browse', this.gallerySettings, this ); 882 883 Library.prototype.activate.apply( this, arguments ); 884 }, 885 886 /** 887 * @since 3.5.0 888 */ 889 deactivate: function() { 890 // Stop watching for uploaded attachments. 891 this.get('library').unobserve( wp.Uploader.queue ); 892 893 this.frame.off( 'content:render:browse', this.gallerySettings, this ); 894 895 Library.prototype.deactivate.apply( this, arguments ); 896 }, 897 898 /** 899 * @since 3.5.0 900 * 901 * @param browser 902 */ 903 gallerySettings: function( browser ) { 904 if ( ! this.get('displaySettings') ) { 905 return; 906 } 907 908 var library = this.get('library'); 909 910 if ( ! library || ! browser ) { 911 return; 912 } 913 914 library.gallery = library.gallery || new Backbone.Model(); 915 916 browser.sidebar.set({ 917 gallery: new GallerySettingsView({ 918 controller: this, 919 model: library.gallery, 920 priority: 40 921 }) 922 }); 923 924 browser.toolbar.set( 'reverse', { 925 text: l10n.reverseOrder, 926 priority: 80, 927 928 click: function() { 929 library.reset( library.toArray().reverse() ); 930 } 931 }); 932 } 933 }); 934 935 module.exports = GalleryEdit; 936 },{"../models/selection.js":20,"../views/attachment/edit-library.js":30,"../views/settings/gallery.js":64,"./library.js":10}],9:[function(require,module,exports){ 937 /** 938 * wp.media.controller.ImageDetails 939 * 940 * A state for editing the attachment display settings of an image that's been 941 * inserted into the editor. 942 * 943 * @class 944 * @augments wp.media.controller.State 945 * @augments Backbone.Model 946 * 947 * @param {object} [attributes] The attributes hash passed to the state. 948 * @param {string} [attributes.id=image-details] Unique identifier. 949 * @param {string} [attributes.title=Image Details] Title for the state. Displays in the frame's title region. 950 * @param {wp.media.model.Attachment} attributes.image The image's model. 951 * @param {string|false} [attributes.content=image-details] Initial mode for the content region. 952 * @param {string|false} [attributes.menu=false] Initial mode for the menu region. 953 * @param {string|false} [attributes.router=false] Initial mode for the router region. 954 * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region. 955 * @param {boolean} [attributes.editing=false] Unused. 956 * @param {int} [attributes.priority=60] Unused. 957 * 958 * @todo This state inherits some defaults from media.controller.Library.prototype.defaults, 959 * however this may not do anything. 960 */ 961 var State = require( './state.js' ), 962 Library = require( './library.js' ), 963 l10n = wp.media.view.l10n, 964 ImageDetails; 965 966 ImageDetails = State.extend({ 967 defaults: _.defaults({ 968 id: 'image-details', 969 title: l10n.imageDetailsTitle, 970 content: 'image-details', 971 menu: false, 972 router: false, 973 toolbar: 'image-details', 974 editing: false, 975 priority: 60 976 }, Library.prototype.defaults ), 977 978 /** 979 * @since 3.9.0 980 * 981 * @param options Attributes 982 */ 983 initialize: function( options ) { 984 this.image = options.image; 985 State.prototype.initialize.apply( this, arguments ); 986 }, 987 988 /** 989 * @since 3.9.0 990 */ 991 activate: function() { 992 this.frame.modal.$el.addClass('image-details'); 993 } 994 }); 995 996 module.exports = ImageDetails; 997 },{"./library.js":10,"./state.js":15}],10:[function(require,module,exports){ 998 /** 999 * wp.media.controller.Library 1000 * 1001 * A state for choosing an attachment or group of attachments from the media library. 1002 * 1003 * @class 1004 * @augments wp.media.controller.State 1005 * @augments Backbone.Model 1006 * @mixes media.selectionSync 1007 * 1008 * @param {object} [attributes] The attributes hash passed to the state. 1009 * @param {string} [attributes.id=library] Unique identifier. 1010 * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region. 1011 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 1012 * If one is not supplied, a collection of all attachments will be created. 1013 * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state. 1014 * If the 'selection' attribute is a plain JS object, 1015 * a Selection will be created using its values as the selection instance's `props` model. 1016 * Otherwise, it will copy the library's `props` model. 1017 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 1018 * @param {string} [attributes.content=upload] Initial mode for the content region. 1019 * Overridden by persistent user setting if 'contentUserSetting' is true. 1020 * @param {string} [attributes.menu=default] Initial mode for the menu region. 1021 * @param {string} [attributes.router=browse] Initial mode for the router region. 1022 * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region. 1023 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 1024 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 1025 * Accepts 'all', 'uploaded', or 'unattached'. 1026 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1027 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 1028 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 1029 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 1030 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 1031 */ 1032 var selectionSync = require( '../utils/selection-sync.js' ), 1033 SelectionModel = require( '../models/selection.js' ), 1034 State = require( './state.js' ), 1035 l10n = wp.media.view.l10n, 1036 Library; 1037 1038 Library = State.extend({ 1039 defaults: { 1040 id: 'library', 1041 title: l10n.mediaLibraryTitle, 1042 multiple: false, 1043 content: 'upload', 1044 menu: 'default', 1045 router: 'browse', 1046 toolbar: 'select', 1047 searchable: true, 1048 filterable: false, 1049 sortable: true, 1050 autoSelect: true, 1051 describe: false, 1052 contentUserSetting: true, 1053 syncSelection: true 1054 }, 1055 1056 /** 1057 * If a library isn't provided, query all media items. 1058 * If a selection instance isn't provided, create one. 1059 * 1060 * @since 3.5.0 1061 */ 1062 initialize: function() { 1063 var selection = this.get('selection'), 1064 props; 1065 1066 if ( ! this.get('library') ) { 1067 this.set( 'library', wp.media.query() ); 1068 } 1069 1070 if ( ! (selection instanceof SelectionModel) ) { 1071 props = selection; 1072 1073 if ( ! props ) { 1074 props = this.get('library').props.toJSON(); 1075 props = _.omit( props, 'orderby', 'query' ); 1076 } 1077 1078 this.set( 'selection', new SelectionModel( null, { 1079 multiple: this.get('multiple'), 1080 props: props 1081 }) ); 1082 } 1083 1084 this.resetDisplays(); 1085 }, 1086 1087 /** 1088 * @since 3.5.0 1089 */ 1090 activate: function() { 1091 this.syncSelection(); 1092 1093 wp.Uploader.queue.on( 'add', this.uploading, this ); 1094 1095 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 1096 1097 if ( this.get( 'router' ) && this.get('contentUserSetting') ) { 1098 this.frame.on( 'content:activate', this.saveContentMode, this ); 1099 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 1100 } 1101 }, 1102 1103 /** 1104 * @since 3.5.0 1105 */ 1106 deactivate: function() { 1107 this.recordSelection(); 1108 1109 this.frame.off( 'content:activate', this.saveContentMode, this ); 1110 1111 // Unbind all event handlers that use this state as the context 1112 // from the selection. 1113 this.get('selection').off( null, null, this ); 1114 1115 wp.Uploader.queue.off( null, null, this ); 1116 }, 1117 1118 /** 1119 * Reset the library to its initial state. 1120 * 1121 * @since 3.5.0 1122 */ 1123 reset: function() { 1124 this.get('selection').reset(); 1125 this.resetDisplays(); 1126 this.refreshContent(); 1127 }, 1128 1129 /** 1130 * Reset the attachment display settings defaults to the site options. 1131 * 1132 * If site options don't define them, fall back to a persistent user setting. 1133 * 1134 * @since 3.5.0 1135 */ 1136 resetDisplays: function() { 1137 var defaultProps = wp.media.view.settings.defaultProps; 1138 this._displays = []; 1139 this._defaultDisplaySettings = { 1140 align: defaultProps.align || getUserSetting( 'align', 'none' ), 1141 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 1142 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 1143 }; 1144 }, 1145 1146 /** 1147 * Create a model to represent display settings (alignment, etc.) for an attachment. 1148 * 1149 * @since 3.5.0 1150 * 1151 * @param {wp.media.model.Attachment} attachment 1152 * @returns {Backbone.Model} 1153 */ 1154 display: function( attachment ) { 1155 var displays = this._displays; 1156 1157 if ( ! displays[ attachment.cid ] ) { 1158 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 1159 } 1160 return displays[ attachment.cid ]; 1161 }, 1162 1163 /** 1164 * Given an attachment, create attachment display settings properties. 1165 * 1166 * @since 3.6.0 1167 * 1168 * @param {wp.media.model.Attachment} attachment 1169 * @returns {Object} 1170 */ 1171 defaultDisplaySettings: function( attachment ) { 1172 var settings = this._defaultDisplaySettings; 1173 if ( settings.canEmbed = this.canEmbed( attachment ) ) { 1174 settings.link = 'embed'; 1175 } 1176 return settings; 1177 }, 1178 1179 /** 1180 * Whether an attachment can be embedded (audio or video). 1181 * 1182 * @since 3.6.0 1183 * 1184 * @param {wp.media.model.Attachment} attachment 1185 * @returns {Boolean} 1186 */ 1187 canEmbed: function( attachment ) { 1188 // If uploading, we know the filename but not the mime type. 1189 if ( ! attachment.get('uploading') ) { 1190 var type = attachment.get('type'); 1191 if ( type !== 'audio' && type !== 'video' ) { 1192 return false; 1193 } 1194 } 1195 1196 return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 1197 }, 1198 1199 1200 /** 1201 * If the state is active, no items are selected, and the current 1202 * content mode is not an option in the state's router (provided 1203 * the state has a router), reset the content mode to the default. 1204 * 1205 * @since 3.5.0 1206 */ 1207 refreshContent: function() { 1208 var selection = this.get('selection'), 1209 frame = this.frame, 1210 router = frame.router.get(), 1211 mode = frame.content.mode(); 1212 1213 if ( this.active && ! selection.length && router && ! router.get( mode ) ) { 1214 this.frame.content.render( this.get('content') ); 1215 } 1216 }, 1217 1218 /** 1219 * Callback handler when an attachment is uploaded. 1220 * 1221 * Switch to the Media Library if uploaded from the 'Upload Files' tab. 1222 * 1223 * Adds any uploading attachments to the selection. 1224 * 1225 * If the state only supports one attachment to be selected and multiple 1226 * attachments are uploaded, the last attachment in the upload queue will 1227 * be selected. 1228 * 1229 * @since 3.5.0 1230 * 1231 * @param {wp.media.model.Attachment} attachment 1232 */ 1233 uploading: function( attachment ) { 1234 var content = this.frame.content; 1235 1236 if ( 'upload' === content.mode() ) { 1237 this.frame.content.mode('browse'); 1238 } 1239 1240 if ( this.get( 'autoSelect' ) ) { 1241 this.get('selection').add( attachment ); 1242 this.frame.trigger( 'library:selection:add' ); 1243 } 1244 }, 1245 1246 /** 1247 * Persist the mode of the content region as a user setting. 1248 * 1249 * @since 3.5.0 1250 */ 1251 saveContentMode: function() { 1252 if ( 'browse' !== this.get('router') ) { 1253 return; 1254 } 1255 1256 var mode = this.frame.content.mode(), 1257 view = this.frame.router.get(); 1258 1259 if ( view && view.get( mode ) ) { 1260 setUserSetting( 'libraryContent', mode ); 1261 } 1262 } 1263 }); 1264 1265 // Make selectionSync available on any Media Library state. 1266 _.extend( Library.prototype, selectionSync ); 1267 1268 module.exports = Library; 1269 },{"../models/selection.js":20,"../utils/selection-sync.js":21,"./state.js":15}],11:[function(require,module,exports){ 1270 /** 1271 * wp.media.controller.MediaLibrary 1272 *