Ticket #28510: browserify.2.diff
File browserify.2.diff, 1014.2 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 123 frame = wp.media({ 124 frame: 'audio', 125 state: 'audio-details', 126 metadata: _.defaults( shortcode.attrs.named, this.defaults ) 127 }); 128 129 return frame; 130 }, 131 132 shortcode : function( model ) { 133 var self = this, content; 134 135 _.each( this.defaults, function( value, key ) { 136 model[ key ] = self.coerce( model, key ); 137 138 if ( value === model[ key ] ) { 139 delete model[ key ]; 140 } 141 }); 142 143 content = model.content; 144 delete model.content; 145 146 return new wp.shortcode({ 147 tag: 'audio', 148 attrs: model, 149 content: content 150 }); 151 } 152 }; 153 154 /** 155 * Shortcode modeling for video 156 * `edit()` prepares the shortcode for the media modal 157 * `shortcode()` builds the new shortcode after update 158 * 159 * @namespace 160 */ 161 wp.media.video = { 162 coerce : wp.media.coerce, 163 164 defaults : { 165 id : wp.media.view.settings.post.id, 166 src : '', 167 poster : '', 168 loop : false, 169 autoplay : false, 170 preload : 'metadata', 171 content : '', 172 width : 640, 173 height : 360 174 }, 175 176 edit : function( data ) { 177 var frame, 178 shortcode = wp.shortcode.next( 'video', data ).shortcode, 179 attrs; 180 181 attrs = shortcode.attrs.named; 182 attrs.content = shortcode.content; 183 184 frame = wp.media({ 185 frame: 'video', 186 state: 'video-details', 187 metadata: _.defaults( attrs, this.defaults ) 188 }); 189 190 return frame; 191 }, 192 193 shortcode : function( model ) { 194 var self = this, content; 195 196 _.each( this.defaults, function( value, key ) { 197 model[ key ] = self.coerce( model, key ); 198 199 if ( value === model[ key ] ) { 200 delete model[ key ]; 201 } 202 }); 203 204 content = model.content; 205 delete model.content; 206 207 return new wp.shortcode({ 208 tag: 'video', 209 attrs: model, 210 content: content 211 }); 212 } 213 }; 214 215 media.model.PostMedia = require( './models/post-media.js' ); 216 media.controller.AudioDetails = require( './controllers/audio-details.js' ); 217 media.controller.VideoDetails = require( './controllers/video-details.js' ); 218 media.view.MediaFrame.MediaDetails = require( './views/frame/media-details.js' ); 219 media.view.MediaFrame.AudioDetails = require( './views/frame/audio-details.js' ); 220 media.view.MediaFrame.VideoDetails = require( './views/frame/video-details.js' ); 221 media.view.MediaDetails = require( './views/media-details.js' ); 222 media.view.AudioDetails = require( './views/audio-details.js' ); 223 media.view.VideoDetails = require( './views/video-details.js' ); 224 225 }(jQuery, _, Backbone)); 226 227 },{"./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){ 228 /** 229 * The controller for the Audio Details state 230 * 231 * @constructor 232 * @augments wp.media.controller.State 233 * @augments Backbone.Model 234 */ 235 var State = require( './state.js' ), 236 l10n = wp.media.view.l10n, 237 AudioDetails; 238 239 AudioDetails = State.extend({ 240 defaults: { 241 id: 'audio-details', 242 toolbar: 'audio-details', 243 title: l10n.audioDetailsTitle, 244 content: 'audio-details', 245 menu: 'audio-details', 246 router: false, 247 priority: 60 248 }, 249 250 initialize: function( options ) { 251 this.media = options.media; 252 State.prototype.initialize.apply( this, arguments ); 253 } 254 }); 255 256 module.exports = AudioDetails; 257 258 },{"./state.js":7}],3:[function(require,module,exports){ 259 /** 260 * wp.media.controller.Library 261 * 262 * A state for choosing an attachment or group of attachments from the media library. 263 * 264 * @class 265 * @augments wp.media.controller.State 266 * @augments Backbone.Model 267 * @mixes media.selectionSync 268 * 269 * @param {object} [attributes] The attributes hash passed to the state. 270 * @param {string} [attributes.id=library] Unique identifier. 271 * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region. 272 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 273 * If one is not supplied, a collection of all attachments will be created. 274 * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state. 275 * If the 'selection' attribute is a plain JS object, 276 * a Selection will be created using its values as the selection instance's `props` model. 277 * Otherwise, it will copy the library's `props` model. 278 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 279 * @param {string} [attributes.content=upload] Initial mode for the content region. 280 * Overridden by persistent user setting if 'contentUserSetting' is true. 281 * @param {string} [attributes.menu=default] Initial mode for the menu region. 282 * @param {string} [attributes.router=browse] Initial mode for the router region. 283 * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region. 284 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 285 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 286 * Accepts 'all', 'uploaded', or 'unattached'. 287 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 288 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 289 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 290 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 291 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 292 */ 293 var selectionSync = require( '../utils/selection-sync.js' ), 294 SelectionModel = require( '../models/selection.js' ), 295 State = require( './state.js' ), 296 l10n = wp.media.view.l10n, 297 Library; 298 299 Library = State.extend({ 300 defaults: { 301 id: 'library', 302 title: l10n.mediaLibraryTitle, 303 multiple: false, 304 content: 'upload', 305 menu: 'default', 306 router: 'browse', 307 toolbar: 'select', 308 searchable: true, 309 filterable: false, 310 sortable: true, 311 autoSelect: true, 312 describe: false, 313 contentUserSetting: true, 314 syncSelection: true 315 }, 316 317 /** 318 * If a library isn't provided, query all media items. 319 * If a selection instance isn't provided, create one. 320 * 321 * @since 3.5.0 322 */ 323 initialize: function() { 324 var selection = this.get('selection'), 325 props; 326 327 if ( ! this.get('library') ) { 328 this.set( 'library', wp.media.query() ); 329 } 330 331 if ( ! (selection instanceof SelectionModel) ) { 332 props = selection; 333 334 if ( ! props ) { 335 props = this.get('library').props.toJSON(); 336 props = _.omit( props, 'orderby', 'query' ); 337 } 338 339 this.set( 'selection', new SelectionModel( null, { 340 multiple: this.get('multiple'), 341 props: props 342 }) ); 343 } 344 345 this.resetDisplays(); 346 }, 347 348 /** 349 * @since 3.5.0 350 */ 351 activate: function() { 352 this.syncSelection(); 353 354 wp.Uploader.queue.on( 'add', this.uploading, this ); 355 356 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 357 358 if ( this.get( 'router' ) && this.get('contentUserSetting') ) { 359 this.frame.on( 'content:activate', this.saveContentMode, this ); 360 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 361 } 362 }, 363 364 /** 365 * @since 3.5.0 366 */ 367 deactivate: function() { 368 this.recordSelection(); 369 370 this.frame.off( 'content:activate', this.saveContentMode, this ); 371 372 // Unbind all event handlers that use this state as the context 373 // from the selection. 374 this.get('selection').off( null, null, this ); 375 376 wp.Uploader.queue.off( null, null, this ); 377 }, 378 379 /** 380 * Reset the library to its initial state. 381 * 382 * @since 3.5.0 383 */ 384 reset: function() { 385 this.get('selection').reset(); 386 this.resetDisplays(); 387 this.refreshContent(); 388 }, 389 390 /** 391 * Reset the attachment display settings defaults to the site options. 392 * 393 * If site options don't define them, fall back to a persistent user setting. 394 * 395 * @since 3.5.0 396 */ 397 resetDisplays: function() { 398 var defaultProps = wp.media.view.settings.defaultProps; 399 this._displays = []; 400 this._defaultDisplaySettings = { 401 align: defaultProps.align || getUserSetting( 'align', 'none' ), 402 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 403 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 404 }; 405 }, 406 407 /** 408 * Create a model to represent display settings (alignment, etc.) for an attachment. 409 * 410 * @since 3.5.0 411 * 412 * @param {wp.media.model.Attachment} attachment 413 * @returns {Backbone.Model} 414 */ 415 display: function( attachment ) { 416 var displays = this._displays; 417 418 if ( ! displays[ attachment.cid ] ) { 419 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 420 } 421 return displays[ attachment.cid ]; 422 }, 423 424 /** 425 * Given an attachment, create attachment display settings properties. 426 * 427 * @since 3.6.0 428 * 429 * @param {wp.media.model.Attachment} attachment 430 * @returns {Object} 431 */ 432 defaultDisplaySettings: function( attachment ) { 433 var settings = this._defaultDisplaySettings; 434 if ( settings.canEmbed = this.canEmbed( attachment ) ) { 435 settings.link = 'embed'; 436 } 437 return settings; 438 }, 439 440 /** 441 * Whether an attachment can be embedded (audio or video). 442 * 443 * @since 3.6.0 444 * 445 * @param {wp.media.model.Attachment} attachment 446 * @returns {Boolean} 447 */ 448 canEmbed: function( attachment ) { 449 // If uploading, we know the filename but not the mime type. 450 if ( ! attachment.get('uploading') ) { 451 var type = attachment.get('type'); 452 if ( type !== 'audio' && type !== 'video' ) { 453 return false; 454 } 455 } 456 457 return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 458 }, 459 460 461 /** 462 * If the state is active, no items are selected, and the current 463 * content mode is not an option in the state's router (provided 464 * the state has a router), reset the content mode to the default. 465 * 466 * @since 3.5.0 467 */ 468 refreshContent: function() { 469 var selection = this.get('selection'), 470 frame = this.frame, 471 router = frame.router.get(), 472 mode = frame.content.mode(); 473 474 if ( this.active && ! selection.length && router && ! router.get( mode ) ) { 475 this.frame.content.render( this.get('content') ); 476 } 477 }, 478 479 /** 480 * Callback handler when an attachment is uploaded. 481 * 482 * Switch to the Media Library if uploaded from the 'Upload Files' tab. 483 * 484 * Adds any uploading attachments to the selection. 485 * 486 * If the state only supports one attachment to be selected and multiple 487 * attachments are uploaded, the last attachment in the upload queue will 488 * be selected. 489 * 490 * @since 3.5.0 491 * 492 * @param {wp.media.model.Attachment} attachment 493 */ 494 uploading: function( attachment ) { 495 var content = this.frame.content; 496 497 if ( 'upload' === content.mode() ) { 498 this.frame.content.mode('browse'); 499 } 500 501 if ( this.get( 'autoSelect' ) ) { 502 this.get('selection').add( attachment ); 503 this.frame.trigger( 'library:selection:add' ); 504 } 505 }, 506 507 /** 508 * Persist the mode of the content region as a user setting. 509 * 510 * @since 3.5.0 511 */ 512 saveContentMode: function() { 513 if ( 'browse' !== this.get('router') ) { 514 return; 515 } 516 517 var mode = this.frame.content.mode(), 518 view = this.frame.router.get(); 519 520 if ( view && view.get( mode ) ) { 521 setUserSetting( 'libraryContent', mode ); 522 } 523 } 524 }); 525 526 // Make selectionSync available on any Media Library state. 527 _.extend( Library.prototype, selectionSync ); 528 529 module.exports = Library; 530 },{"../models/selection.js":13,"../utils/selection-sync.js":14,"./state.js":7}],4:[function(require,module,exports){ 531 /** 532 * wp.media.controller.MediaLibrary 533 * 534 * @class 535 * @augments wp.media.controller.Library 536 * @augments wp.media.controller.State 537 * @augments Backbone.Model 538 */ 539 var Library = require( './library.js' ), 540 MediaLibrary; 541 542 MediaLibrary = Library.extend({ 543 defaults: _.defaults({ 544 // Attachments browser defaults. @see media.view.AttachmentsBrowser 545 filterable: 'uploaded', 546 547 displaySettings: false, 548 priority: 80, 549 syncSelection: false 550 }, Library.prototype.defaults ), 551 552 /** 553 * @since 3.9.0 554 * 555 * @param options 556 */ 557 initialize: function( options ) { 558 this.media = options.media; 559 this.type = options.type; 560 this.set( 'library', wp.media.query({ type: this.type }) ); 561 562 Library.prototype.initialize.apply( this, arguments ); 563 }, 564 565 /** 566 * @since 3.9.0 567 */ 568 activate: function() { 569 // @todo this should use this.frame. 570 if ( wp.media.frame.lastMime ) { 571 this.set( 'library', wp.media.query({ type: wp.media.frame.lastMime }) ); 572 delete wp.media.frame.lastMime; 573 } 574 Library.prototype.activate.apply( this, arguments ); 575 } 576 }); 577 578 module.exports = MediaLibrary; 579 },{"./library.js":3}],5:[function(require,module,exports){ 580 /** 581 * wp.media.controller.Region 582 * 583 * A region is a persistent application layout area. 584 * 585 * A region assumes one mode at any time, and can be switched to another. 586 * 587 * When mode changes, events are triggered on the region's parent view. 588 * The parent view will listen to specific events and fill the region with an 589 * appropriate view depending on mode. For example, a frame listens for the 590 * 'browse' mode t be activated on the 'content' view and then fills the region 591 * with an AttachmentsBrowser view. 592 * 593 * @class 594 * 595 * @param {object} options Options hash for the region. 596 * @param {string} options.id Unique identifier for the region. 597 * @param {Backbone.View} options.view A parent view the region exists within. 598 * @param {string} options.selector jQuery selector for the region within the parent view. 599 */ 600 var Region = function( options ) { 601 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); 602 }; 603 604 // Use Backbone's self-propagating `extend` inheritance method. 605 Region.extend = Backbone.Model.extend; 606 607 _.extend( Region.prototype, { 608 /** 609 * Activate a mode. 610 * 611 * @since 3.5.0 612 * 613 * @param {string} mode 614 * 615 * @fires this.view#{this.id}:activate:{this._mode} 616 * @fires this.view#{this.id}:activate 617 * @fires this.view#{this.id}:deactivate:{this._mode} 618 * @fires this.view#{this.id}:deactivate 619 * 620 * @returns {wp.media.controller.Region} Returns itself to allow chaining. 621 */ 622 mode: function( mode ) { 623 if ( ! mode ) { 624 return this._mode; 625 } 626 // Bail if we're trying to change to the current mode. 627 if ( mode === this._mode ) { 628 return this; 629 } 630 631 /** 632 * Region mode deactivation event. 633 * 634 * @event this.view#{this.id}:deactivate:{this._mode} 635 * @event this.view#{this.id}:deactivate 636 */ 637 this.trigger('deactivate'); 638 639 this._mode = mode; 640 this.render( mode ); 641 642 /** 643 * Region mode activation event. 644 * 645 * @event this.view#{this.id}:activate:{this._mode} 646 * @event this.view#{this.id}:activate 647 */ 648 this.trigger('activate'); 649 return this; 650 }, 651 /** 652 * Render a mode. 653 * 654 * @since 3.5.0 655 * 656 * @param {string} mode 657 * 658 * @fires this.view#{this.id}:create:{this._mode} 659 * @fires this.view#{this.id}:create 660 * @fires this.view#{this.id}:render:{this._mode} 661 * @fires this.view#{this.id}:render 662 * 663 * @returns {wp.media.controller.Region} Returns itself to allow chaining 664 */ 665 render: function( mode ) { 666 // If the mode isn't active, activate it. 667 if ( mode && mode !== this._mode ) { 668 return this.mode( mode ); 669 } 670 671 var set = { view: null }, 672 view; 673 674 /** 675 * Create region view event. 676 * 677 * Region view creation takes place in an event callback on the frame. 678 * 679 * @event this.view#{this.id}:create:{this._mode} 680 * @event this.view#{this.id}:create 681 */ 682 this.trigger( 'create', set ); 683 view = set.view; 684 685 /** 686 * Render region view event. 687 * 688 * Region view creation takes place in an event callback on the frame. 689 * 690 * @event this.view#{this.id}:create:{this._mode} 691 * @event this.view#{this.id}:create 692 */ 693 this.trigger( 'render', view ); 694 if ( view ) { 695 this.set( view ); 696 } 697 return this; 698 }, 699 700 /** 701 * Get the region's view. 702 * 703 * @since 3.5.0 704 * 705 * @returns {wp.media.View} 706 */ 707 get: function() { 708 return this.view.views.first( this.selector ); 709 }, 710 711 /** 712 * Set the region's view as a subview of the frame. 713 * 714 * @since 3.5.0 715 * 716 * @param {Array|Object} views 717 * @param {Object} [options={}] 718 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining 719 */ 720 set: function( views, options ) { 721 if ( options ) { 722 options.add = false; 723 } 724 return this.view.views.set( this.selector, views, options ); 725 }, 726 727 /** 728 * Trigger regional view events on the frame. 729 * 730 * @since 3.5.0 731 * 732 * @param {string} event 733 * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining. 734 */ 735 trigger: function( event ) { 736 var base, args; 737 738 if ( ! this._mode ) { 739 return; 740 } 741 742 args = _.toArray( arguments ); 743 base = this.id + ':' + event; 744 745 // Trigger `{this.id}:{event}:{this._mode}` event on the frame. 746 args[0] = base + ':' + this._mode; 747 this.view.trigger.apply( this.view, args ); 748 749 // Trigger `{this.id}:{event}` event on the frame. 750 args[0] = base; 751 this.view.trigger.apply( this.view, args ); 752 return this; 753 } 754 }); 755 756 module.exports = Region; 757 },{}],6:[function(require,module,exports){ 758 /** 759 * wp.media.controller.StateMachine 760 * 761 * A state machine keeps track of state. It is in one state at a time, 762 * and can change from one state to another. 763 * 764 * States are stored as models in a Backbone collection. 765 * 766 * @since 3.5.0 767 * 768 * @class 769 * @augments Backbone.Model 770 * @mixin 771 * @mixes Backbone.Events 772 * 773 * @param {Array} states 774 */ 775 var StateMachine = function( states ) { 776 // @todo This is dead code. The states collection gets created in media.view.Frame._createStates. 777 this.states = new Backbone.Collection( states ); 778 }; 779 780 // Use Backbone's self-propagating `extend` inheritance method. 781 StateMachine.extend = Backbone.Model.extend; 782 783 _.extend( StateMachine.prototype, Backbone.Events, { 784 /** 785 * Fetch a state. 786 * 787 * If no `id` is provided, returns the active state. 788 * 789 * Implicitly creates states. 790 * 791 * Ensure that the `states` collection exists so the `StateMachine` 792 * can be used as a mixin. 793 * 794 * @since 3.5.0 795 * 796 * @param {string} id 797 * @returns {wp.media.controller.State} Returns a State model 798 * from the StateMachine collection 799 */ 800 state: function( id ) { 801 this.states = this.states || new Backbone.Collection(); 802 803 // Default to the active state. 804 id = id || this._state; 805 806 if ( id && ! this.states.get( id ) ) { 807 this.states.add({ id: id }); 808 } 809 return this.states.get( id ); 810 }, 811 812 /** 813 * Sets the active state. 814 * 815 * Bail if we're trying to select the current state, if we haven't 816 * created the `states` collection, or are trying to select a state 817 * that does not exist. 818 * 819 * @since 3.5.0 820 * 821 * @param {string} id 822 * 823 * @fires wp.media.controller.State#deactivate 824 * @fires wp.media.controller.State#activate 825 * 826 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining 827 */ 828 setState: function( id ) { 829 var previous = this.state(); 830 831 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) { 832 return this; 833 } 834 835 if ( previous ) { 836 previous.trigger('deactivate'); 837 this._lastState = previous.id; 838 } 839 840 this._state = id; 841 this.state().trigger('activate'); 842 843 return this; 844 }, 845 846 /** 847 * Returns the previous active state. 848 * 849 * Call the `state()` method with no parameters to retrieve the current 850 * active state. 851 * 852 * @since 3.5.0 853 * 854 * @returns {wp.media.controller.State} Returns a State model 855 * from the StateMachine collection 856 */ 857 lastState: function() { 858 if ( this._lastState ) { 859 return this.state( this._lastState ); 860 } 861 } 862 }); 863 864 // Map all event binding and triggering on a StateMachine to its `states` collection. 865 _.each([ 'on', 'off', 'trigger' ], function( method ) { 866 /** 867 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. 868 */ 869 StateMachine.prototype[ method ] = function() { 870 // Ensure that the `states` collection exists so the `StateMachine` 871 // can be used as a mixin. 872 this.states = this.states || new Backbone.Collection(); 873 // Forward the method to the `states` collection. 874 this.states[ method ].apply( this.states, arguments ); 875 return this; 876 }; 877 }); 878 879 module.exports = StateMachine; 880 },{}],7:[function(require,module,exports){ 881 /** 882 * wp.media.controller.State 883 * 884 * A state is a step in a workflow that when set will trigger the controllers 885 * for the regions to be updated as specified in the frame. 886 * 887 * A state has an event-driven lifecycle: 888 * 889 * 'ready' triggers when a state is added to a state machine's collection. 890 * 'activate' triggers when a state is activated by a state machine. 891 * 'deactivate' triggers when a state is deactivated by a state machine. 892 * 'reset' is not triggered automatically. It should be invoked by the 893 * proper controller to reset the state to its default. 894 * 895 * @class 896 * @augments Backbone.Model 897 */ 898 var State = Backbone.Model.extend({ 899 /** 900 * Constructor. 901 * 902 * @since 3.5.0 903 */ 904 constructor: function() { 905 this.on( 'activate', this._preActivate, this ); 906 this.on( 'activate', this.activate, this ); 907 this.on( 'activate', this._postActivate, this ); 908 this.on( 'deactivate', this._deactivate, this ); 909 this.on( 'deactivate', this.deactivate, this ); 910 this.on( 'reset', this.reset, this ); 911 this.on( 'ready', this._ready, this ); 912 this.on( 'ready', this.ready, this ); 913 /** 914 * Call parent constructor with passed arguments 915 */ 916 Backbone.Model.apply( this, arguments ); 917 this.on( 'change:menu', this._updateMenu, this ); 918 }, 919 /** 920 * Ready event callback. 921 * 922 * @abstract 923 * @since 3.5.0 924 */ 925 ready: function() {}, 926 927 /** 928 * Activate event callback. 929 * 930 * @abstract 931 * @since 3.5.0 932 */ 933 activate: function() {}, 934 935 /** 936 * Deactivate event callback. 937 * 938 * @abstract 939 * @since 3.5.0 940 */ 941 deactivate: function() {}, 942 943 /** 944 * Reset event callback. 945 * 946 * @abstract 947 * @since 3.5.0 948 */ 949 reset: function() {}, 950 951 /** 952 * @access private 953 * @since 3.5.0 954 */ 955 _ready: function() { 956 this._updateMenu(); 957 }, 958 959 /** 960 * @access private 961 * @since 3.5.0 962 */ 963 _preActivate: function() { 964 this.active = true; 965 }, 966 967 /** 968 * @access private 969 * @since 3.5.0 970 */ 971 _postActivate: function() { 972 this.on( 'change:menu', this._menu, this ); 973 this.on( 'change:titleMode', this._title, this ); 974 this.on( 'change:content', this._content, this ); 975 this.on( 'change:toolbar', this._toolbar, this ); 976 977 this.frame.on( 'title:render:default', this._renderTitle, this ); 978 979 this._title(); 980 this._menu(); 981 this._toolbar(); 982 this._content(); 983 this._router(); 984 }, 985 986 /** 987 * @access private 988 * @since 3.5.0 989 */ 990 _deactivate: function() { 991 this.active = false; 992 993 this.frame.off( 'title:render:default', this._renderTitle, this ); 994 995 this.off( 'change:menu', this._menu, this ); 996 this.off( 'change:titleMode', this._title, this ); 997 this.off( 'change:content', this._content, this ); 998 this.off( 'change:toolbar', this._toolbar, this ); 999 }, 1000 1001 /** 1002 * @access private 1003 * @since 3.5.0 1004 */ 1005 _title: function() { 1006 this.frame.title.render( this.get('titleMode') || 'default' ); 1007 }, 1008 1009 /** 1010 * @access private 1011 * @since 3.5.0 1012 */ 1013 _renderTitle: function( view ) { 1014 view.$el.text( this.get('title') || '' ); 1015 }, 1016 1017 /** 1018 * @access private 1019 * @since 3.5.0 1020 */ 1021 _router: function() { 1022 var router = this.frame.router, 1023 mode = this.get('router'), 1024 view; 1025 1026 this.frame.$el.toggleClass( 'hide-router', ! mode ); 1027 if ( ! mode ) { 1028 return; 1029 } 1030 1031 this.frame.router.render( mode ); 1032 1033 view = router.get(); 1034 if ( view && view.select ) { 1035 view.select( this.frame.content.mode() ); 1036 } 1037 }, 1038 1039 /** 1040 * @access private 1041 * @since 3.5.0 1042 */ 1043 _menu: function() { 1044 var menu = this.frame.menu, 1045 mode = this.get('menu'), 1046 view; 1047 1048 this.frame.$el.toggleClass( 'hide-menu', ! mode ); 1049 if ( ! mode ) { 1050 return; 1051 } 1052 1053 menu.mode( mode ); 1054 1055 view = menu.get(); 1056 if ( view && view.select ) { 1057 view.select( this.id ); 1058 } 1059 }, 1060 1061 /** 1062 * @access private 1063 * @since 3.5.0 1064 */ 1065 _updateMenu: function() { 1066 var previous = this.previous('menu'), 1067 menu = this.get('menu'); 1068 1069 if ( previous ) { 1070 this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); 1071 } 1072 1073 if ( menu ) { 1074 this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); 1075 } 1076 }, 1077 1078 /** 1079 * Create a view in the media menu for the state. 1080 * 1081 * @access private 1082 * @since 3.5.0 1083 * 1084 * @param {media.view.Menu} view The menu view. 1085 */ 1086 _renderMenu: function( view ) { 1087 var menuItem = this.get('menuItem'), 1088 title = this.get('title'), 1089 priority = this.get('priority'); 1090 1091 if ( ! menuItem && title ) { 1092 menuItem = { text: title }; 1093 1094 if ( priority ) { 1095 menuItem.priority = priority; 1096 } 1097 } 1098 1099 if ( ! menuItem ) { 1100 return; 1101 } 1102 1103 view.set( this.id, menuItem ); 1104 } 1105 }); 1106 1107 _.each(['toolbar','content'], function( region ) { 1108 /** 1109 * @access private 1110 */ 1111 State.prototype[ '_' + region ] = function() { 1112 var mode = this.get( region ); 1113 if ( mode ) { 1114 this.frame[ region ].render( mode ); 1115 } 1116 }; 1117 }); 1118 1119 module.exports = State; 1120 },{}],8:[function(require,module,exports){ 1121 /** 1122 * The controller for the Video Details state 1123 * 1124 * @constructor 1125 * @augments wp.media.controller.State 1126 * @augments Backbone.Model 1127 */ 1128 var State = require( './state.js' ), 1129 l10n = wp.media.view.l10n, 1130 VideoDetails; 1131 1132 VideoDetails = State.extend({ 1133 defaults: { 1134 id: 'video-details', 1135 toolbar: 'video-details', 1136 title: l10n.videoDetailsTitle, 1137 content: 'video-details', 1138 menu: 'video-details', 1139 router: false, 1140 priority: 60 1141 }, 1142 1143 initialize: function( options ) { 1144 this.media = options.media; 1145 State.prototype.initialize.apply( this, arguments ); 1146 } 1147 }); 1148 1149 module.exports = VideoDetails; 1150 },{"./state.js":7}],9:[function(require,module,exports){ 1151 /** 1152 * wp.media.model.Attachment 1153 * 1154 * @class 1155 * @augments Backbone.Model 1156 */ 1157 var $ = jQuery, 1158 Attachment; 1159 1160 Attachment = Backbone.Model.extend({ 1161 /** 1162 * Triggered when attachment details change 1163 * Overrides Backbone.Model.sync 1164 * 1165 * @param {string} method 1166 * @param {wp.media.model.Attachment} model 1167 * @param {Object} [options={}] 1168 * 1169 * @returns {Promise} 1170 */ 1171 sync: function( method, model, options ) { 1172 // If the attachment does not yet have an `id`, return an instantly 1173 // rejected promise. Otherwise, all of our requests will fail. 1174 if ( _.isUndefined( this.id ) ) { 1175 return $.Deferred().rejectWith( this ).promise(); 1176 } 1177 1178 // Overload the `read` request so Attachment.fetch() functions correctly. 1179 if ( 'read' === method ) { 1180 options = options || {}; 1181 options.context = this; 1182 options.data = _.extend( options.data || {}, { 1183 action: 'get-attachment', 1184 id: this.id 1185 }); 1186 return wp.media.ajax( options ); 1187 1188 // Overload the `update` request so properties can be saved. 1189 } else if ( 'update' === method ) { 1190 // If we do not have the necessary nonce, fail immeditately. 1191 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 1192 return $.Deferred().rejectWith( this ).promise(); 1193 } 1194 1195 options = options || {}; 1196 options.context = this; 1197 1198 // Set the action and ID. 1199 options.data = _.extend( options.data || {}, { 1200 action: 'save-attachment', 1201 id: this.id, 1202 nonce: this.get('nonces').update, 1203 post_id: wp.media.model.settings.post.id 1204 }); 1205 1206 // Record the values of the changed attributes. 1207 if ( model.hasChanged() ) { 1208 options.data.changes = {}; 1209 1210 _.each( model.changed, function( value, key ) { 1211 options.data.changes[ key ] = this.get( key ); 1212 }, this ); 1213 } 1214 1215 return wp.media.ajax( options ); 1216 1217 // Overload the `delete` request so attachments can be removed. 1218 // This will permanently delete an attachment. 1219 } else if ( 'delete' === method ) { 1220 options = options || {}; 1221 1222 if ( ! options.wait ) { 1223 this.destroyed = true; 1224 } 1225 1226 options.context = this; 1227 options.data = _.extend( options.data || {}, { 1228 action: 'delete-post', 1229 id: this.id, 1230 _wpnonce: this.get('nonces')['delete'] 1231 }); 1232 1233 return wp.media.ajax( options ).done( function() { 1234 this.destroyed = true; 1235 }).fail( function() { 1236 this.destroyed = false; 1237 }); 1238 1239 // Otherwise, fall back to `Backbone.sync()`. 1240 } else { 1241 /** 1242 * Call `sync` directly on Backbone.Model 1243 */ 1244 return Backbone.Model.prototype.sync.apply( this, arguments ); 1245 } 1246 }, 1247 /** 1248 * Convert date strings into Date objects. 1249 * 1250 * @param {Object} resp The raw response object, typically returned by fetch() 1251 * @returns {Object} The modified response object, which is the attributes hash 1252 * to be set on the model. 1253 */ 1254 parse: function( resp ) { 1255 if ( ! resp ) { 1256 return resp; 1257 } 1258 1259 resp.date = new Date( resp.date ); 1260 resp.modified = new Date( resp.modified ); 1261 return resp; 1262 }, 1263 /** 1264 * @param {Object} data The properties to be saved. 1265 * @param {Object} options Sync options. e.g. patch, wait, success, error. 1266 * 1267 * @this Backbone.Model 1268 * 1269 * @returns {Promise} 1270 */ 1271 saveCompat: function( data, options ) { 1272 var model = this; 1273 1274 // If we do not have the necessary nonce, fail immeditately. 1275 if ( ! this.get('nonces') || ! this.get('nonces').update ) { 1276 return $.Deferred().rejectWith( this ).promise(); 1277 } 1278 1279 return media.post( 'save-attachment-compat', _.defaults({ 1280 id: this.id, 1281 nonce: this.get('nonces').update, 1282 post_id: wp.media.model.settings.post.id 1283 }, data ) ).done( function( resp, status, xhr ) { 1284 model.set( model.parse( resp, xhr ), options ); 1285 }); 1286 } 1287 }, { 1288 /** 1289 * Create a new model on the static 'all' attachments collection and return it. 1290 * 1291 * @static 1292 * @param {Object} attrs 1293 * @returns {wp.media.model.Attachment} 1294 */ 1295 create: function( attrs ) { 1296 var Attachments = require( './attachments.js' ); 1297 return Attachments.all.push( attrs ); 1298 }, 1299 /** 1300 * Create a new model on the static 'all' attachments collection and return it. 1301 * 1302 * If this function has already been called for the id, 1303 * it returns the specified attachment. 1304 * 1305 * @static 1306 * @param {string} id A string used to identify a model. 1307 * @param {Backbone.Model|undefined} attachment 1308 * @returns {wp.media.model.Attachment} 1309 */ 1310 get: _.memoize( function( id, attachment ) { 1311 var Attachments = require( './attachments.js' ); 1312 return Attachments.all.push( attachment || { id: id } ); 1313 }) 1314 }); 1315 1316 module.exports = Attachment; 1317 },{"./attachments.js":10}],10:[function(require,module,exports){ 1318 /** 1319 * wp.media.model.Attachments 1320 * 1321 * A collection of attachments. 1322 * 1323 * This collection has no persistence with the server without supplying 1324 * 'options.props.query = true', which will mirror the collection 1325 * to an Attachments Query collection - @see wp.media.model.Attachments.mirror(). 1326 * 1327 * @class 1328 * @augments Backbone.Collection 1329 * 1330 * @param {array} [models] Models to initialize with the collection. 1331 * @param {object} [options] Options hash for the collection. 1332 * @param {string} [options.props] Options hash for the initial query properties. 1333 * @param {string} [options.props.order] Initial order (ASC or DESC) for the collection. 1334 * @param {string} [options.props.orderby] Initial attribute key to order the collection by. 1335 * @param {string} [options.props.query] Whether the collection is linked to an attachments query. 1336 * @param {string} [options.observe] 1337 * @param {string} [options.filters] 1338 * 1339 */ 1340 var Attachment = require( './attachment.js' ), 1341 Attachments; 1342 1343 Attachments = Backbone.Collection.extend({ 1344 /** 1345 * @type {wp.media.model.Attachment} 1346 */ 1347 model: Attachment, 1348 /** 1349 * @param {Array} [models=[]] Array of models used to populate the collection. 1350 * @param {Object} [options={}] 1351 */ 1352 initialize: function( models, options ) { 1353 options = options || {}; 1354 1355 this.props = new Backbone.Model(); 1356 this.filters = options.filters || {}; 1357 1358 // Bind default `change` events to the `props` model. 1359 this.props.on( 'change', this._changeFilteredProps, this ); 1360 1361 this.props.on( 'change:order', this._changeOrder, this ); 1362 this.props.on( 'change:orderby', this._changeOrderby, this ); 1363 this.props.on( 'change:query', this._changeQuery, this ); 1364 1365 this.props.set( _.defaults( options.props || {} ) ); 1366 1367 if ( options.observe ) { 1368 this.observe( options.observe ); 1369 } 1370 }, 1371 /** 1372 * Sort the collection when the order attribute changes. 1373 * 1374 * @access private 1375 */ 1376 _changeOrder: function() { 1377 if ( this.comparator ) { 1378 this.sort(); 1379 } 1380 }, 1381 /** 1382 * Set the default comparator only when the `orderby` property is set. 1383 * 1384 * @access private 1385 * 1386 * @param {Backbone.Model} model 1387 * @param {string} orderby 1388 */ 1389 _changeOrderby: function( model, orderby ) { 1390 // If a different comparator is defined, bail. 1391 if ( this.comparator && this.comparator !== Attachments.comparator ) { 1392 return; 1393 } 1394 1395 if ( orderby && 'post__in' !== orderby ) { 1396 this.comparator = Attachments.comparator; 1397 } else { 1398 delete this.comparator; 1399 } 1400 }, 1401 /** 1402 * If the `query` property is set to true, query the server using 1403 * the `props` values, and sync the results to this collection. 1404 * 1405 * @access private 1406 * 1407 * @param {Backbone.Model} model 1408 * @param {Boolean} query 1409 */ 1410 _changeQuery: function( model, query ) { 1411 if ( query ) { 1412 this.props.on( 'change', this._requery, this ); 1413 this._requery(); 1414 } else { 1415 this.props.off( 'change', this._requery, this ); 1416 } 1417 }, 1418 /** 1419 * @access private 1420 * 1421 * @param {Backbone.Model} model 1422 */ 1423 _changeFilteredProps: function( model ) { 1424 // If this is a query, updating the collection will be handled by 1425 // `this._requery()`. 1426 if ( this.props.get('query') ) { 1427 return; 1428 } 1429 1430 var changed = _.chain( model.changed ).map( function( t, prop ) { 1431 var filter = Attachments.filters[ prop ], 1432 term = model.get( prop ); 1433 1434 if ( ! filter ) { 1435 return; 1436 } 1437 1438 if ( term && ! this.filters[ prop ] ) { 1439 this.filters[ prop ] = filter; 1440 } else if ( ! term && this.filters[ prop ] === filter ) { 1441 delete this.filters[ prop ]; 1442 } else { 1443 return; 1444 } 1445 1446 // Record the change. 1447 return true; 1448 }, this ).any().value(); 1449 1450 if ( ! changed ) { 1451 return; 1452 } 1453 1454 // If no `Attachments` model is provided to source the searches 1455 // from, then automatically generate a source from the existing 1456 // models. 1457 if ( ! this._source ) { 1458 this._source = new Attachments( this.models ); 1459 } 1460 1461 this.reset( this._source.filter( this.validator, this ) ); 1462 }, 1463 1464 validateDestroyed: false, 1465 /** 1466 * Checks whether an attachment is valid. 1467 * 1468 * @param {wp.media.model.Attachment} attachment 1469 * @returns {Boolean} 1470 */ 1471 validator: function( attachment ) { 1472 if ( ! this.validateDestroyed && attachment.destroyed ) { 1473 return false; 1474 } 1475 return _.all( this.filters, function( filter ) { 1476 return !! filter.call( this, attachment ); 1477 }, this ); 1478 }, 1479 /** 1480 * Add or remove an attachment to the collection depending on its validity. 1481 * 1482 * @param {wp.media.model.Attachment} attachment 1483 * @param {Object} options 1484 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1485 */ 1486 validate: function( attachment, options ) { 1487 var valid = this.validator( attachment ), 1488 hasAttachment = !! this.get( attachment.cid ); 1489 1490 if ( ! valid && hasAttachment ) { 1491 this.remove( attachment, options ); 1492 } else if ( valid && ! hasAttachment ) { 1493 this.add( attachment, options ); 1494 } 1495 1496 return this; 1497 }, 1498 1499 /** 1500 * Add or remove all attachments from another collection depending on each one's validity. 1501 * 1502 * @param {wp.media.model.Attachments} attachments 1503 * @param {object} [options={}] 1504 * 1505 * @fires wp.media.model.Attachments#reset 1506 * 1507 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1508 */ 1509 validateAll: function( attachments, options ) { 1510 options = options || {}; 1511 1512 _.each( attachments.models, function( attachment ) { 1513 this.validate( attachment, { silent: true }); 1514 }, this ); 1515 1516 if ( ! options.silent ) { 1517 this.trigger( 'reset', this, options ); 1518 } 1519 return this; 1520 }, 1521 /** 1522 * Start observing another attachments collection change events 1523 * and replicate them on this collection. 1524 * 1525 * @param {wp.media.model.Attachments} The attachments collection to observe. 1526 * @returns {wp.media.model.Attachments} Returns itself to allow chaining. 1527 */ 1528 observe: function( attachments ) { 1529 this.observers = this.observers || []; 1530 this.observers.push( attachments ); 1531 1532 attachments.on( 'add change remove', this._validateHandler, this ); 1533 attachments.on( 'reset', this._validateAllHandler, this ); 1534 this.validateAll( attachments ); 1535 return this; 1536 }, 1537 /** 1538 * Stop replicating collection change events from another attachments collection. 1539 * 1540 * @param {wp.media.model.Attachments} The attachments collection to stop observing. 1541 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1542 */ 1543 unobserve: function( attachments ) { 1544 if ( attachments ) { 1545 attachments.off( null, null, this ); 1546 this.observers = _.without( this.observers, attachments ); 1547 1548 } else { 1549 _.each( this.observers, function( attachments ) { 1550 attachments.off( null, null, this ); 1551 }, this ); 1552 delete this.observers; 1553 } 1554 1555 return this; 1556 }, 1557 /** 1558 * @access private 1559 * 1560 * @param {wp.media.model.Attachments} attachment 1561 * @param {wp.media.model.Attachments} attachments 1562 * @param {Object} options 1563 * 1564 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1565 */ 1566 _validateHandler: function( attachment, attachments, options ) { 1567 // If we're not mirroring this `attachments` collection, 1568 // only retain the `silent` option. 1569 options = attachments === this.mirroring ? options : { 1570 silent: options && options.silent 1571 }; 1572 1573 return this.validate( attachment, options ); 1574 }, 1575 /** 1576 * @access private 1577 * 1578 * @param {wp.media.model.Attachments} attachments 1579 * @param {Object} options 1580 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1581 */ 1582 _validateAllHandler: function( attachments, options ) { 1583 return this.validateAll( attachments, options ); 1584 }, 1585 /** 1586 * Start mirroring another attachments collection, clearing out any models already 1587 * in the collection. 1588 * 1589 * @param {wp.media.model.Attachments} The attachments collection to mirror. 1590 * @returns {wp.media.model.Attachments} Returns itself to allow chaining 1591 */ 1592 mirror: function( attachments ) { 1593 if ( this.mirroring && this.mirroring === attachments ) { 1594 return this; 1595 } 1596 1597 this.unmirror(); 1598 this.mirroring = attachments; 1599 1600 // Clear the collection silently. A `reset` event will be fired 1601 // when `observe()` calls `validateAll()`. 1602 this.reset( [], { silent: true } ); 1603 this.observe( attachments ); 1604 1605 return this; 1606 }, 1607 /** 1608 * Stop mirroring another attachments collection. 1609 */ 1610 unmirror: function() { 1611 if ( ! this.mirroring ) { 1612 return; 1613 } 1614 1615 this.unobserve( this.mirroring ); 1616 delete this.mirroring; 1617 }, 1618 /** 1619 * Retrive more attachments from the server for the collection. 1620 * 1621 * Only works if the collection is mirroring a Query Attachments collection, 1622 * and forwards to its `more` method. This collection class doesn't have 1623 * server persistence by itself. 1624 * 1625 * @param {object} options 1626 * @returns {Promise} 1627 */ 1628 more: function( options ) { 1629 var deferred = jQuery.Deferred(), 1630 mirroring = this.mirroring, 1631 attachments = this; 1632 1633 if ( ! mirroring || ! mirroring.more ) { 1634 return deferred.resolveWith( this ).promise(); 1635 } 1636 // If we're mirroring another collection, forward `more` to 1637 // the mirrored collection. Account for a race condition by 1638 // checking if we're still mirroring that collection when 1639 // the request resolves. 1640 mirroring.more( options ).done( function() { 1641 if ( this === attachments.mirroring ) 1642 deferred.resolveWith( this ); 1643 }); 1644 1645 return deferred.promise(); 1646 }, 1647 /** 1648 * Whether there are more attachments that haven't been sync'd from the server 1649 * that match the collection's query. 1650 * 1651 * Only works if the collection is mirroring a Query Attachments collection, 1652 * and forwards to its `hasMore` method. This collection class doesn't have 1653 * server persistence by itself. 1654 * 1655 * @returns {boolean} 1656 */ 1657 hasMore: function() { 1658 return this.mirroring ? this.mirroring.hasMore() : false; 1659 }, 1660 /** 1661 * A custom AJAX-response parser. 1662 * 1663 * See trac ticket #24753 1664 * 1665 * @param {Object|Array} resp The raw response Object/Array. 1666 * @param {Object} xhr 1667 * @returns {Array} The array of model attributes to be added to the collection 1668 */ 1669 parse: function( resp, xhr ) { 1670 if ( ! _.isArray( resp ) ) { 1671 resp = [resp]; 1672 } 1673 1674 return _.map( resp, function( attrs ) { 1675 var id, attachment, newAttributes; 1676 1677 if ( attrs instanceof Backbone.Model ) { 1678 id = attrs.get( 'id' ); 1679 attrs = attrs.attributes; 1680 } else { 1681 id = attrs.id; 1682 } 1683 1684 attachment = Attachment.get( id ); 1685 newAttributes = attachment.parse( attrs, xhr ); 1686 1687 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) { 1688 attachment.set( newAttributes ); 1689 } 1690 1691 return attachment; 1692 }); 1693 }, 1694 /** 1695 * If the collection is a query, create and mirror an Attachments Query collection. 1696 * 1697 * @access private 1698 */ 1699 _requery: function( refresh ) { 1700 var props, Query; 1701 if ( this.props.get('query') ) { 1702 Query = require( './query.js' ); 1703 props = this.props.toJSON(); 1704 props.cache = ( true !== refresh ); 1705 this.mirror( Query.get( props ) ); 1706 } 1707 }, 1708 /** 1709 * If this collection is sorted by `menuOrder`, recalculates and saves 1710 * the menu order to the database. 1711 * 1712 * @returns {undefined|Promise} 1713 */ 1714 saveMenuOrder: function() { 1715 if ( 'menuOrder' !== this.props.get('orderby') ) { 1716 return; 1717 } 1718 1719 // Removes any uploading attachments, updates each attachment's 1720 // menu order, and returns an object with an { id: menuOrder } 1721 // mapping to pass to the request. 1722 var attachments = this.chain().filter( function( attachment ) { 1723 return ! _.isUndefined( attachment.id ); 1724 }).map( function( attachment, index ) { 1725 // Indices start at 1. 1726 index = index + 1; 1727 attachment.set( 'menuOrder', index ); 1728 return [ attachment.id, index ]; 1729 }).object().value(); 1730 1731 if ( _.isEmpty( attachments ) ) { 1732 return; 1733 } 1734 1735 return wp.media.post( 'save-attachment-order', { 1736 nonce: wp.media.model.settings.post.nonce, 1737 post_id: wp.media.model.settings.post.id, 1738 attachments: attachments 1739 }); 1740 } 1741 }, { 1742 /** 1743 * A function to compare two attachment models in an attachments collection. 1744 * 1745 * Used as the default comparator for instances of wp.media.model.Attachments 1746 * and its subclasses. @see wp.media.model.Attachments._changeOrderby(). 1747 * 1748 * @static 1749 * 1750 * @param {Backbone.Model} a 1751 * @param {Backbone.Model} b 1752 * @param {Object} options 1753 * @returns {Number} -1 if the first model should come before the second, 1754 * 0 if they are of the same rank and 1755 * 1 if the first model should come after. 1756 */ 1757 comparator: function( a, b, options ) { 1758 var key = this.props.get('orderby'), 1759 order = this.props.get('order') || 'DESC', 1760 ac = a.cid, 1761 bc = b.cid; 1762 1763 a = a.get( key ); 1764 b = b.get( key ); 1765 1766 if ( 'date' === key || 'modified' === key ) { 1767 a = a || new Date(); 1768 b = b || new Date(); 1769 } 1770 1771 // If `options.ties` is set, don't enforce the `cid` tiebreaker. 1772 if ( options && options.ties ) { 1773 ac = bc = null; 1774 } 1775 1776 return ( 'DESC' === order ) ? wp.media.compare( a, b, ac, bc ) : wp.media.compare( b, a, bc, ac ); 1777 }, 1778 /** 1779 * @namespace 1780 */ 1781 filters: { 1782 /** 1783 * @static 1784 * Note that this client-side searching is *not* equivalent 1785 * to our server-side searching. 1786 * 1787 * @param {wp.media.model.Attachment} attachment 1788 * 1789 * @this wp.media.model.Attachments 1790 * 1791 * @returns {Boolean} 1792 */ 1793 search: function( attachment ) { 1794 if ( ! this.props.get('search') ) { 1795 return true; 1796 } 1797 1798 return _.any(['title','filename','description','caption','name'], function( key ) { 1799 var value = attachment.get( key ); 1800 return value && -1 !== value.search( this.props.get('search') ); 1801 }, this ); 1802 }, 1803 /** 1804 * @static 1805 * @param {wp.media.model.Attachment} attachment 1806 * 1807 * @this wp.media.model.Attachments 1808 * 1809 * @returns {Boolean} 1810 */ 1811 type: function( attachment ) { 1812 var type = this.props.get('type'); 1813 return ! type || -1 !== type.indexOf( attachment.get('type') ); 1814 }, 1815 /** 1816 * @static 1817 * @param {wp.media.model.Attachment} attachment 1818 * 1819 * @this wp.media.model.Attachments 1820 * 1821 * @returns {Boolean} 1822 */ 1823 uploadedTo: function( attachment ) { 1824 var uploadedTo = this.props.get('uploadedTo'); 1825 if ( _.isUndefined( uploadedTo ) ) { 1826 return true; 1827 } 1828 1829 return uploadedTo === attachment.get('uploadedTo'); 1830 }, 1831 /** 1832 * @static 1833 * @param {wp.media.model.Attachment} attachment 1834 * 1835 * @this wp.media.model.Attachments 1836 * 1837 * @returns {Boolean} 1838 */ 1839 status: function( attachment ) { 1840 var status = this.props.get('status'); 1841 if ( _.isUndefined( status ) ) { 1842 return true; 1843 } 1844 1845 return status === attachment.get('status'); 1846 } 1847 } 1848 }); 1849 1850 module.exports = Attachments; 1851 },{"./attachment.js":9,"./query.js":12}],11:[function(require,module,exports){ 1852 /** 1853 * Shared model class for audio and video. Updates the model after 1854 * "Add Audio|Video Source" and "Replace Audio|Video" states return 1855 * 1856 * @constructor 1857 * @augments Backbone.Model 1858 */ 1859 var PostMedia = Backbone.Model.extend({ 1860 initialize: function() { 1861 this.attachment = false; 1862 }, 1863 1864 setSource: function( attachment ) { 1865 this.attachment = attachment; 1866 this.extension = attachment.get( 'filename' ).split('.').pop(); 1867 1868 if ( this.get( 'src' ) && this.extension === this.get( 'src' ).split('.').pop() ) { 1869 this.unset( 'src' ); 1870 } 1871 1872 if ( _.contains( wp.media.view.settings.embedExts, this.extension ) ) { 1873 this.set( this.extension, this.attachment.get( 'url' ) ); 1874 } else { 1875 this.unset( this.extension ); 1876 } 1877 }, 1878 1879 changeAttachment: function( attachment ) { 1880 var self = this; 1881 1882 this.setSource( attachment ); 1883 1884 this.unset( 'src' ); 1885 _.each( _.without( wp.media.view.settings.embedExts, this.extension ), function( ext ) { 1886 self.unset( ext ); 1887 } ); 1888 } 1889 }); 1890 1891 module.exports = PostMedia; 1892 },{}],12:[function(require,module,exports){ 1893 /** 1894 * wp.media.model.Query 1895 * 1896 * A collection of attachments that match the supplied query arguments. 1897 * 1898 * Note: Do NOT change this.args after the query has been initialized. 1899 * Things will break. 1900 * 1901 * @class 1902 * @augments wp.media.model.Attachments 1903 * @augments Backbone.Collection 1904 * 1905 * @param {array} [models] Models to initialize with the collection. 1906 * @param {object} [options] Options hash. 1907 * @param {object} [options.args] Attachments query arguments. 1908 * @param {object} [options.args.posts_per_page] 1909 */ 1910 var Attachments = require( './attachments.js' ), 1911 Query; 1912 1913 Query = Attachments.extend({ 1914 /** 1915 * @global wp.Uploader 1916 * 1917 * @param {array} [models=[]] Array of initial models to populate the collection. 1918 * @param {object} [options={}] 1919 */ 1920 initialize: function( models, options ) { 1921 var allowed; 1922 1923 options = options || {}; 1924 Attachments.prototype.initialize.apply( this, arguments ); 1925 1926 this.args = options.args; 1927 this._hasMore = true; 1928 this.created = new Date(); 1929 1930 this.filters.order = function( attachment ) { 1931 var orderby = this.props.get('orderby'), 1932 order = this.props.get('order'); 1933 1934 if ( ! this.comparator ) { 1935 return true; 1936 } 1937 1938 // We want any items that can be placed before the last 1939 // item in the set. If we add any items after the last 1940 // item, then we can't guarantee the set is complete. 1941 if ( this.length ) { 1942 return 1 !== this.comparator( attachment, this.last(), { ties: true }); 1943 1944 // Handle the case where there are no items yet and 1945 // we're sorting for recent items. In that case, we want 1946 // changes that occurred after we created the query. 1947 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { 1948 return attachment.get( orderby ) >= this.created; 1949 1950 // If we're sorting by menu order and we have no items, 1951 // accept any items that have the default menu order (0). 1952 } else if ( 'ASC' === order && 'menuOrder' === orderby ) { 1953 return attachment.get( orderby ) === 0; 1954 } 1955 1956 // Otherwise, we don't want any items yet. 1957 return false; 1958 }; 1959 1960 // Observe the central `wp.Uploader.queue` collection to watch for 1961 // new matches for the query. 1962 // 1963 // Only observe when a limited number of query args are set. There 1964 // are no filters for other properties, so observing will result in 1965 // false positives in those queries. 1966 allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; 1967 if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { 1968 this.observe( wp.Uploader.queue ); 1969 } 1970 }, 1971 /** 1972 * Whether there are more attachments that haven't been sync'd from the server 1973 * that match the collection's query. 1974 * 1975 * @returns {boolean} 1976 */ 1977 hasMore: function() { 1978 return this._hasMore; 1979 }, 1980 /** 1981 * Fetch more attachments from the server for the collection. 1982 * 1983 * @param {object} [options={}] 1984 * @returns {Promise} 1985 */ 1986 more: function( options ) { 1987 var query = this; 1988 1989 // If there is already a request pending, return early with the Deferred object. 1990 if ( this._more && 'pending' === this._more.state() ) { 1991 return this._more; 1992 } 1993 1994 if ( ! this.hasMore() ) { 1995 return jQuery.Deferred().resolveWith( this ).promise(); 1996 } 1997 1998 options = options || {}; 1999 options.remove = false; 2000 2001 return this._more = this.fetch( options ).done( function( resp ) { 2002 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { 2003 query._hasMore = false; 2004 } 2005 }); 2006 }, 2007 /** 2008 * Overrides Backbone.Collection.sync 2009 * Overrides wp.media.model.Attachments.sync 2010 * 2011 * @param {String} method 2012 * @param {Backbone.Model} model 2013 * @param {Object} [options={}] 2014 * @returns {Promise} 2015 */ 2016 sync: function( method, model, options ) { 2017 var args, fallback; 2018 2019 // Overload the read method so Attachment.fetch() functions correctly. 2020 if ( 'read' === method ) { 2021 options = options || {}; 2022 options.context = this; 2023 options.data = _.extend( options.data || {}, { 2024 action: 'query-attachments', 2025 post_id: wp.media.model.settings.post.id 2026 }); 2027 2028 // Clone the args so manipulation is non-destructive. 2029 args = _.clone( this.args ); 2030 2031 // Determine which page to query. 2032 if ( -1 !== args.posts_per_page ) { 2033 args.paged = Math.floor( this.length / args.posts_per_page ) + 1; 2034 } 2035 2036 options.data.query = args; 2037 return wp.media.ajax( options ); 2038 2039 // Otherwise, fall back to Backbone.sync() 2040 } else { 2041 /** 2042 * Call wp.media.model.Attachments.sync or Backbone.sync 2043 */ 2044 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; 2045 return fallback.sync.apply( this, arguments ); 2046 } 2047 } 2048 }, { 2049 /** 2050 * @readonly 2051 */ 2052 defaultProps: { 2053 orderby: 'date', 2054 order: 'DESC' 2055 }, 2056 /** 2057 * @readonly 2058 */ 2059 defaultArgs: { 2060 posts_per_page: 40 2061 }, 2062 /** 2063 * @readonly 2064 */ 2065 orderby: { 2066 allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], 2067 /** 2068 * A map of JavaScript orderby values to their WP_Query equivalents. 2069 * @type {Object} 2070 */ 2071 valuemap: { 2072 'id': 'ID', 2073 'uploadedTo': 'parent', 2074 'menuOrder': 'menu_order ID' 2075 } 2076 }, 2077 /** 2078 * A map of JavaScript query properties to their WP_Query equivalents. 2079 * 2080 * @readonly 2081 */ 2082 propmap: { 2083 'search': 's', 2084 'type': 'post_mime_type', 2085 'perPage': 'posts_per_page', 2086 'menuOrder': 'menu_order', 2087 'uploadedTo': 'post_parent', 2088 'status': 'post_status', 2089 'include': 'post__in', 2090 'exclude': 'post__not_in' 2091 }, 2092 /** 2093 * Creates and returns an Attachments Query collection given the properties. 2094 * 2095 * Caches query objects and reuses where possible. 2096 * 2097 * @static 2098 * @method 2099 * 2100 * @param {object} [props] 2101 * @param {Object} [props.cache=true] Whether to use the query cache or not. 2102 * @param {Object} [props.order] 2103 * @param {Object} [props.orderby] 2104 * @param {Object} [props.include] 2105 * @param {Object} [props.exclude] 2106 * @param {Object} [props.s] 2107 * @param {Object} [props.post_mime_type] 2108 * @param {Object} [props.posts_per_page] 2109 * @param {Object} [props.menu_order] 2110 * @param {Object} [props.post_parent] 2111 * @param {Object} [props.post_status] 2112 * @param {Object} [options] 2113 * 2114 * @returns {wp.media.model.Query} A new Attachments Query collection. 2115 */ 2116 get: (function(){ 2117 /** 2118 * @static 2119 * @type Array 2120 */ 2121 var queries = []; 2122 2123 /** 2124 * @returns {Query} 2125 */ 2126 return function( props, options ) { 2127 var args = {}, 2128 orderby = Query.orderby, 2129 defaults = Query.defaultProps, 2130 query, 2131 cache = !! props.cache || _.isUndefined( props.cache ); 2132 2133 // Remove the `query` property. This isn't linked to a query, 2134 // this *is* the query. 2135 delete props.query; 2136 delete props.cache; 2137 2138 // Fill default args. 2139 _.defaults( props, defaults ); 2140 2141 // Normalize the order. 2142 props.order = props.order.toUpperCase(); 2143 if ( 'DESC' !== props.order && 'ASC' !== props.order ) { 2144 props.order = defaults.order.toUpperCase(); 2145 } 2146 2147 // Ensure we have a valid orderby value. 2148 if ( ! _.contains( orderby.allowed, props.orderby ) ) { 2149 props.orderby = defaults.orderby; 2150 } 2151 2152 _.each( [ 'include', 'exclude' ], function( prop ) { 2153 if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { 2154 props[ prop ] = [ props[ prop ] ]; 2155 } 2156 } ); 2157 2158 // Generate the query `args` object. 2159 // Correct any differing property names. 2160 _.each( props, function( value, prop ) { 2161 if ( _.isNull( value ) ) { 2162 return; 2163 } 2164 2165 args[ Query.propmap[ prop ] || prop ] = value; 2166 }); 2167 2168 // Fill any other default query args. 2169 _.defaults( args, Query.defaultArgs ); 2170 2171 // `props.orderby` does not always map directly to `args.orderby`. 2172 // Substitute exceptions specified in orderby.keymap. 2173 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; 2174 2175 // Search the query cache for a matching query. 2176 if ( cache ) { 2177 query = _.find( queries, function( query ) { 2178 return _.isEqual( query.args, args ); 2179 }); 2180 } else { 2181 queries = []; 2182 } 2183 2184 // Otherwise, create a new query and add it to the cache. 2185 if ( ! query ) { 2186 query = new Query( [], _.extend( options || {}, { 2187 props: props, 2188 args: args 2189 } ) ); 2190 queries.push( query ); 2191 } 2192 2193 return query; 2194 }; 2195 }()) 2196 }); 2197 2198 module.exports = Query; 2199 },{"./attachments.js":10}],13:[function(require,module,exports){ 2200 /** 2201 * wp.media.model.Selection 2202 * 2203 * A selection of attachments. 2204 * 2205 * @class 2206 * @augments wp.media.model.Attachments 2207 * @augments Backbone.Collection 2208 */ 2209 var Attachments = require( './attachments.js' ), 2210 Selection; 2211 2212 Selection = Attachments.extend({ 2213 /** 2214 * Refresh the `single` model whenever the selection changes. 2215 * Binds `single` instead of using the context argument to ensure 2216 * it receives no parameters. 2217 * 2218 * @param {Array} [models=[]] Array of models used to populate the collection. 2219 * @param {Object} [options={}] 2220 */ 2221 initialize: function( models, options ) { 2222 /** 2223 * call 'initialize' directly on the parent class 2224 */ 2225 Attachments.prototype.initialize.apply( this, arguments ); 2226 this.multiple = options && options.multiple; 2227 2228 this.on( 'add remove reset', _.bind( this.single, this, false ) ); 2229 }, 2230 2231 /** 2232 * If the workflow does not support multi-select, clear out the selection 2233 * before adding a new attachment to it. 2234 * 2235 * @param {Array} models 2236 * @param {Object} options 2237 * @returns {wp.media.model.Attachment[]} 2238 */ 2239 add: function( models, options ) { 2240 if ( ! this.multiple ) { 2241 this.remove( this.models ); 2242 } 2243 /** 2244 * call 'add' directly on the parent class 2245 */ 2246 return Attachments.prototype.add.call( this, models, options ); 2247 }, 2248 2249 /** 2250 * Fired when toggling (clicking on) an attachment in the modal. 2251 * 2252 * @param {undefined|boolean|wp.media.model.Attachment} model 2253 * 2254 * @fires wp.media.model.Selection#selection:single 2255 * @fires wp.media.model.Selection#selection:unsingle 2256 * 2257 * @returns {Backbone.Model} 2258 */ 2259 single: function( model ) { 2260 var previous = this._single; 2261 2262 // If a `model` is provided, use it as the single model. 2263 if ( model ) { 2264 this._single = model; 2265 } 2266 // If the single model isn't in the selection, remove it. 2267 if ( this._single && ! this.get( this._single.cid ) ) { 2268 delete this._single; 2269 } 2270 2271 this._single = this._single || this.last(); 2272 2273 // If single has changed, fire an event. 2274 if ( this._single !== previous ) { 2275 if ( previous ) { 2276 previous.trigger( 'selection:unsingle', previous, this ); 2277 2278 // If the model was already removed, trigger the collection 2279 // event manually. 2280 if ( ! this.get( previous.cid ) ) { 2281 this.trigger( 'selection:unsingle', previous, this ); 2282 } 2283 } 2284 if ( this._single ) { 2285 this._single.trigger( 'selection:single', this._single, this ); 2286 } 2287 } 2288 2289 // Return the single model, or the last model as a fallback. 2290 return this._single; 2291 } 2292 }); 2293 2294 module.exports = Selection; 2295 },{"./attachments.js":10}],14:[function(require,module,exports){ 2296 /** 2297 * wp.media.selectionSync 2298 * 2299 * Sync an attachments selection in a state with another state. 2300 * 2301 * Allows for selecting multiple images in the Insert Media workflow, and then 2302 * switching to the Insert Gallery workflow while preserving the attachments selection. 2303 * 2304 * @mixin 2305 */ 2306 var selectionSync = { 2307 /** 2308 * @since 3.5.0 2309 */ 2310 syncSelection: function() { 2311 var selection = this.get('selection'), 2312 manager = this.frame._selection; 2313 2314 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 2315 return; 2316 } 2317 2318 // If the selection supports multiple items, validate the stored 2319 // attachments based on the new selection's conditions. Record 2320 // the attachments that are not included; we'll maintain a 2321 // reference to those. Other attachments are considered in flux. 2322 if ( selection.multiple ) { 2323 selection.reset( [], { silent: true }); 2324 selection.validateAll( manager.attachments ); 2325 manager.difference = _.difference( manager.attachments.models, selection.models ); 2326 } 2327 2328 // Sync the selection's single item with the master. 2329 selection.single( manager.single ); 2330 }, 2331 2332 /** 2333 * Record the currently active attachments, which is a combination 2334 * of the selection's attachments and the set of selected 2335 * attachments that this specific selection considered invalid. 2336 * Reset the difference and record the single attachment. 2337 * 2338 * @since 3.5.0 2339 */ 2340 recordSelection: function() { 2341 var selection = this.get('selection'), 2342 manager = this.frame._selection; 2343 2344 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 2345 return; 2346 } 2347 2348 if ( selection.multiple ) { 2349 manager.attachments.reset( selection.toArray().concat( manager.difference ) ); 2350 manager.difference = []; 2351 } else { 2352 manager.attachments.add( selection.toArray() ); 2353 } 2354 2355 manager.single = selection._single; 2356 } 2357 }; 2358 2359 module.exports = selectionSync; 2360 },{}],15:[function(require,module,exports){ 2361 /** 2362 * wp.media.view.AttachmentCompat 2363 * 2364 * A view to display fields added via the `attachment_fields_to_edit` filter. 2365 * 2366 * @class 2367 * @augments wp.media.View 2368 * @augments wp.Backbone.View 2369 * @augments Backbone.View 2370 */ 2371 var View = require( './view.js' ), 2372 AttachmentCompat; 2373 2374 AttachmentCompat = View.extend({ 2375 tagName: 'form', 2376 className: 'compat-item', 2377 2378 events: { 2379 'submit': 'preventDefault', 2380 'change input': 'save', 2381 'change select': 'save', 2382 'change textarea': 'save' 2383 }, 2384 2385 initialize: function() { 2386 this.model.on( 'change:compat', this.render, this ); 2387 }, 2388 /** 2389 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 2390 */ 2391 dispose: function() { 2392 if ( this.$(':focus').length ) { 2393 this.save(); 2394 } 2395 /** 2396 * call 'dispose' directly on the parent class 2397 */ 2398 return View.prototype.dispose.apply( this, arguments ); 2399 }, 2400 /** 2401 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 2402 */ 2403 render: function() { 2404 var compat = this.model.get('compat'); 2405 if ( ! compat || ! compat.item ) { 2406 return; 2407 } 2408 2409 this.views.detach(); 2410 this.$el.html( compat.item ); 2411 this.views.render(); 2412 return this; 2413 }, 2414 /** 2415 * @param {Object} event 2416 */ 2417 preventDefault: function( event ) { 2418 event.preventDefault(); 2419 }, 2420 /** 2421 * @param {Object} event 2422 */ 2423 save: function( event ) { 2424 var data = {}; 2425 2426 if ( event ) { 2427 event.preventDefault(); 2428 } 2429 2430 _.each( this.$el.serializeArray(), function( pair ) { 2431 data[ pair.name ] = pair.value; 2432 }); 2433 2434 this.controller.trigger( 'attachment:compat:waiting', ['waiting'] ); 2435 this.model.saveCompat( data ).always( _.bind( this.postSave, this ) ); 2436 }, 2437 2438 postSave: function() { 2439 this.controller.trigger( 'attachment:compat:ready', ['ready'] ); 2440 } 2441 }); 2442 2443 module.exports = AttachmentCompat; 2444 },{"./view.js":55}],16:[function(require,module,exports){ 2445 /** 2446 * wp.media.view.AttachmentFilters 2447 * 2448 * @class 2449 * @augments wp.media.View 2450 * @augments wp.Backbone.View 2451 * @augments Backbone.View 2452 */ 2453 var View = require( './view.js' ), 2454 $ = jQuery, 2455 AttachmentFilters; 2456 2457 AttachmentFilters = View.extend({ 2458 tagName: 'select', 2459 className: 'attachment-filters', 2460 id: 'media-attachment-filters', 2461 2462 events: { 2463 change: 'change' 2464 }, 2465 2466 keys: [], 2467 2468 initialize: function() { 2469 this.createFilters(); 2470 _.extend( this.filters, this.options.filters ); 2471 2472 // Build `<option>` elements. 2473 this.$el.html( _.chain( this.filters ).map( function( filter, value ) { 2474 return { 2475 el: $( '<option></option>' ).val( value ).html( filter.text )[0], 2476 priority: filter.priority || 50 2477 }; 2478 }, this ).sortBy('priority').pluck('el').value() ); 2479 2480 this.model.on( 'change', this.select, this ); 2481 this.select(); 2482 }, 2483 2484 /** 2485 * @abstract 2486 */ 2487 createFilters: function() { 2488 this.filters = {}; 2489 }, 2490 2491 /** 2492 * When the selected filter changes, update the Attachment Query properties to match. 2493 */ 2494 change: function() { 2495 var filter = this.filters[ this.el.value ]; 2496 if ( filter ) { 2497 this.model.set( filter.props ); 2498 } 2499 }, 2500 2501 select: function() { 2502 var model = this.model, 2503 value = 'all', 2504 props = model.toJSON(); 2505 2506 _.find( this.filters, function( filter, id ) { 2507 var equal = _.all( filter.props, function( prop, key ) { 2508 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); 2509 }); 2510 2511 if ( equal ) { 2512 return value = id; 2513 } 2514 }); 2515 2516 this.$el.val( value ); 2517 } 2518 }); 2519 2520 module.exports = AttachmentFilters; 2521 },{"./view.js":55}],17:[function(require,module,exports){ 2522 /** 2523 * wp.media.view.AttachmentFilters.All 2524 * 2525 * @class 2526 * @augments wp.media.view.AttachmentFilters 2527 * @augments wp.media.View 2528 * @augments wp.Backbone.View 2529 * @augments Backbone.View 2530 */ 2531 var AttachmentFilters = require( '../attachment-filters.js' ), 2532 l10n = wp.media.view.l10n, 2533 All; 2534 2535 All = AttachmentFilters.extend({ 2536 createFilters: function() { 2537 var filters = {}; 2538 2539 _.each( wp.media.view.settings.mimeTypes || {}, function( text, key ) { 2540 filters[ key ] = { 2541 text: text, 2542 props: { 2543 status: null, 2544 type: key, 2545 uploadedTo: null, 2546 orderby: 'date', 2547 order: 'DESC' 2548 } 2549 }; 2550 }); 2551 2552 filters.all = { 2553 text: l10n.allMediaItems, 2554 props: { 2555 status: null, 2556 type: null, 2557 uploadedTo: null, 2558 orderby: 'date', 2559 order: 'DESC' 2560 }, 2561 priority: 10 2562 }; 2563 2564 if ( wp.media.view.settings.post.id ) { 2565 filters.uploaded = { 2566 text: l10n.uploadedToThisPost, 2567 props: { 2568 status: null, 2569 type: null, 2570 uploadedTo: wp.media.view.settings.post.id, 2571 orderby: 'menuOrder', 2572 order: 'ASC' 2573 }, 2574 priority: 20 2575 }; 2576 } 2577 2578 filters.unattached = { 2579 text: l10n.unattached, 2580 props: { 2581 status: null, 2582 uploadedTo: 0, 2583 type: null, 2584 orderby: 'menuOrder', 2585 order: 'ASC' 2586 }, 2587 priority: 50 2588 }; 2589 2590 if ( wp.media.view.settings.mediaTrash && 2591 this.controller.isModeActive( 'grid' ) ) { 2592 2593 filters.trash = { 2594 text: l10n.trash, 2595 props: { 2596 uploadedTo: null, 2597 status: 'trash', 2598 type: null, 2599 orderby: 'date', 2600 order: 'DESC' 2601 }, 2602 priority: 50 2603 }; 2604 } 2605 2606 this.filters = filters; 2607 } 2608 }); 2609 2610 module.exports = All; 2611 },{"../attachment-filters.js":16}],18:[function(require,module,exports){ 2612 /** 2613 * A filter dropdown for month/dates. 2614 * 2615 * @class 2616 * @augments wp.media.view.AttachmentFilters 2617 * @augments wp.media.View 2618 * @augments wp.Backbone.View 2619 * @augments Backbone.View 2620 */ 2621 var AttachmentFilters = require( '../attachment-filters.js' ), 2622 l10n = wp.media.view.l10n, 2623 DateFilter; 2624 2625 DateFilter = AttachmentFilters.extend({ 2626 id: 'media-attachment-date-filters', 2627 2628 createFilters: function() { 2629 var filters = {}; 2630 _.each( wp.media.view.settings.months || {}, function( value, index ) { 2631 filters[ index ] = { 2632 text: value.text, 2633 props: { 2634 year: value.year, 2635 monthnum: value.month 2636 } 2637 }; 2638 }); 2639 filters.all = { 2640 text: l10n.allDates, 2641 props: { 2642 monthnum: false, 2643 year: false 2644 }, 2645 priority: 10 2646 }; 2647 this.filters = filters; 2648 } 2649 }); 2650 2651 module.exports = DateFilter; 2652 },{"../attachment-filters.js":16}],19:[function(require,module,exports){ 2653 /** 2654 * wp.media.view.AttachmentFilters.Uploaded 2655 * 2656 * @class 2657 * @augments wp.media.view.AttachmentFilters 2658 * @augments wp.media.View 2659 * @augments wp.Backbone.View 2660 * @augments Backbone.View 2661 */ 2662 var AttachmentFilters = require( '../attachment-filters.js' ), 2663 l10n = wp.media.view.l10n, 2664 Uploaded; 2665 2666 Uploaded = AttachmentFilters.extend({ 2667 createFilters: function() { 2668 var type = this.model.get('type'), 2669 types = wp.media.view.settings.mimeTypes, 2670 text; 2671 2672 if ( types && type ) { 2673 text = types[ type ]; 2674 } 2675 2676 this.filters = { 2677 all: { 2678 text: text || l10n.allMediaItems, 2679 props: { 2680 uploadedTo: null, 2681 orderby: 'date', 2682 order: 'DESC' 2683 }, 2684 priority: 10 2685 }, 2686 2687 uploaded: { 2688 text: l10n.uploadedToThisPost, 2689 props: { 2690 uploadedTo: wp.media.view.settings.post.id, 2691 orderby: 'menuOrder', 2692 order: 'ASC' 2693 }, 2694 priority: 20 2695 }, 2696 2697 unattached: { 2698 text: l10n.unattached, 2699 props: { 2700 uploadedTo: 0, 2701 orderby: 'menuOrder', 2702 order: 'ASC' 2703 }, 2704 priority: 50 2705 } 2706 }; 2707 } 2708 }); 2709 2710 module.exports = Uploaded; 2711 },{"../attachment-filters.js":16}],20:[function(require,module,exports){ 2712 /** 2713 * wp.media.view.Attachment 2714 * 2715 * @class 2716 * @augments wp.media.View 2717 * @augments wp.Backbone.View 2718 * @augments Backbone.View 2719 */ 2720 var View = require( './view.js' ), 2721 $ = jQuery, 2722 Attachment; 2723 2724 Attachment = View.extend({ 2725 tagName: 'li', 2726 className: 'attachment', 2727 template: wp.template('attachment'), 2728 2729 attributes: function() { 2730 return { 2731 'tabIndex': 0, 2732 'role': 'checkbox', 2733 'aria-label': this.model.get( 'title' ), 2734 'aria-checked': false, 2735 'data-id': this.model.get( 'id' ) 2736 }; 2737 }, 2738 2739 events: { 2740 'click .js--select-attachment': 'toggleSelectionHandler', 2741 'change [data-setting]': 'updateSetting', 2742 'change [data-setting] input': 'updateSetting', 2743 'change [data-setting] select': 'updateSetting', 2744 'change [data-setting] textarea': 'updateSetting', 2745 'click .close': 'removeFromLibrary', 2746 'click .check': 'checkClickHandler', 2747 'click a': 'preventDefault', 2748 'keydown .close': 'removeFromLibrary', 2749 'keydown': 'toggleSelectionHandler' 2750 }, 2751 2752 buttons: {}, 2753 2754 initialize: function() { 2755 var selection = this.options.selection, 2756 options = _.defaults( this.options, { 2757 rerenderOnModelChange: true 2758 } ); 2759 2760 if ( options.rerenderOnModelChange ) { 2761 this.model.on( 'change', this.render, this ); 2762 } else { 2763 this.model.on( 'change:percent', this.progress, this ); 2764 } 2765 this.model.on( 'change:title', this._syncTitle, this ); 2766 this.model.on( 'change:caption', this._syncCaption, this ); 2767 this.model.on( 'change:artist', this._syncArtist, this ); 2768 this.model.on( 'change:album', this._syncAlbum, this ); 2769 2770 // Update the selection. 2771 this.model.on( 'add', this.select, this ); 2772 this.model.on( 'remove', this.deselect, this ); 2773 if ( selection ) { 2774 selection.on( 'reset', this.updateSelect, this ); 2775 // Update the model's details view. 2776 this.model.on( 'selection:single selection:unsingle', this.details, this ); 2777 this.details( this.model, this.controller.state().get('selection') ); 2778 } 2779 2780 this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); 2781 }, 2782 /** 2783 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2784 */ 2785 dispose: function() { 2786 var selection = this.options.selection; 2787 2788 // Make sure all settings are saved before removing the view. 2789 this.updateAll(); 2790 2791 if ( selection ) { 2792 selection.off( null, null, this ); 2793 } 2794 /** 2795 * call 'dispose' directly on the parent class 2796 */ 2797 View.prototype.dispose.apply( this, arguments ); 2798 return this; 2799 }, 2800 /** 2801 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 2802 */ 2803 render: function() { 2804 var options = _.defaults( this.model.toJSON(), { 2805 orientation: 'landscape', 2806 uploading: false, 2807 type: '', 2808 subtype: '', 2809 icon: '', 2810 filename: '', 2811 caption: '', 2812 title: '', 2813 dateFormatted: '', 2814 width: '', 2815 height: '', 2816 compat: false, 2817 alt: '', 2818 description: '' 2819 }, this.options ); 2820 2821 options.buttons = this.buttons; 2822 options.describe = this.controller.state().get('describe'); 2823 2824 if ( 'image' === options.type ) { 2825 options.size = this.imageSize(); 2826 } 2827 2828 options.can = {}; 2829 if ( options.nonces ) { 2830 options.can.remove = !! options.nonces['delete']; 2831 options.can.save = !! options.nonces.update; 2832 } 2833 2834 if ( this.controller.state().get('allowLocalEdits') ) { 2835 options.allowLocalEdits = true; 2836 } 2837 2838 if ( options.uploading && ! options.percent ) { 2839 options.percent = 0; 2840 } 2841 2842 this.views.detach(); 2843 this.$el.html( this.template( options ) ); 2844 2845 this.$el.toggleClass( 'uploading', options.uploading ); 2846 2847 if ( options.uploading ) { 2848 this.$bar = this.$('.media-progress-bar div'); 2849 } else { 2850 delete this.$bar; 2851 } 2852 2853 // Check if the model is selected. 2854 this.updateSelect(); 2855 2856 // Update the save status. 2857 this.updateSave(); 2858 2859 this.views.render(); 2860 2861 return this; 2862 }, 2863 2864 progress: function() { 2865 if ( this.$bar && this.$bar.length ) { 2866 this.$bar.width( this.model.get('percent') + '%' ); 2867 } 2868 }, 2869 2870 /** 2871 * @param {Object} event 2872 */ 2873 toggleSelectionHandler: function( event ) { 2874 var method; 2875 2876 // Don't do anything inside inputs. 2877 if ( 'INPUT' === event.target.nodeName ) { 2878 return; 2879 } 2880 2881 // Catch arrow events 2882 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 2883 this.controller.trigger( 'attachment:keydown:arrow', event ); 2884 return; 2885 } 2886 2887 // Catch enter and space events 2888 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 2889 return; 2890 } 2891 2892 event.preventDefault(); 2893 2894 // In the grid view, bubble up an edit:attachment event to the controller. 2895 if ( this.controller.isModeActive( 'grid' ) ) { 2896 if ( this.controller.isModeActive( 'edit' ) ) { 2897 // Pass the current target to restore focus when closing 2898 this.controller.trigger( 'edit:attachment', this.model, event.currentTarget ); 2899 return; 2900 } 2901 2902 if ( this.controller.isModeActive( 'select' ) ) { 2903 method = 'toggle'; 2904 } 2905 } 2906 2907 if ( event.shiftKey ) { 2908 method = 'between'; 2909 } else if ( event.ctrlKey || event.metaKey ) { 2910 method = 'toggle'; 2911 } 2912 2913 this.toggleSelection({ 2914 method: method 2915 }); 2916 2917 this.controller.trigger( 'selection:toggle' ); 2918 }, 2919 /** 2920 * @param {Object} options 2921 */ 2922 toggleSelection: function( options ) { 2923 var collection = this.collection, 2924 selection = this.options.selection, 2925 model = this.model, 2926 method = options && options.method, 2927 single, models, singleIndex, modelIndex; 2928 2929 if ( ! selection ) { 2930 return; 2931 } 2932 2933 single = selection.single(); 2934 method = _.isUndefined( method ) ? selection.multiple : method; 2935 2936 // If the `method` is set to `between`, select all models that 2937 // exist between the current and the selected model. 2938 if ( 'between' === method && single && selection.multiple ) { 2939 // If the models are the same, short-circuit. 2940 if ( single === model ) { 2941 return; 2942 } 2943 2944 singleIndex = collection.indexOf( single ); 2945 modelIndex = collection.indexOf( this.model ); 2946 2947 if ( singleIndex < modelIndex ) { 2948 models = collection.models.slice( singleIndex, modelIndex + 1 ); 2949 } else { 2950 models = collection.models.slice( modelIndex, singleIndex + 1 ); 2951 } 2952 2953 selection.add( models ); 2954 selection.single( model ); 2955 return; 2956 2957 // If the `method` is set to `toggle`, just flip the selection 2958 // status, regardless of whether the model is the single model. 2959 } else if ( 'toggle' === method ) { 2960 selection[ this.selected() ? 'remove' : 'add' ]( model ); 2961 selection.single( model ); 2962 return; 2963 } else if ( 'add' === method ) { 2964 selection.add( model ); 2965 selection.single( model ); 2966 return; 2967 } 2968 2969 // Fixes bug that loses focus when selecting a featured image 2970 if ( ! method ) { 2971 method = 'add'; 2972 } 2973 2974 if ( method !== 'add' ) { 2975 method = 'reset'; 2976 } 2977 2978 if ( this.selected() ) { 2979 // If the model is the single model, remove it. 2980 // If it is not the same as the single model, 2981 // it now becomes the single model. 2982 selection[ single === model ? 'remove' : 'single' ]( model ); 2983 } else { 2984 // If the model is not selected, run the `method` on the 2985 // selection. By default, we `reset` the selection, but the 2986 // `method` can be set to `add` the model to the selection. 2987 selection[ method ]( model ); 2988 selection.single( model ); 2989 } 2990 }, 2991 2992 updateSelect: function() { 2993 this[ this.selected() ? 'select' : 'deselect' ](); 2994 }, 2995 /** 2996 * @returns {unresolved|Boolean} 2997 */ 2998 selected: function() { 2999 var selection = this.options.selection; 3000 if ( selection ) { 3001 return !! selection.get( this.model.cid ); 3002 } 3003 }, 3004 /** 3005 * @param {Backbone.Model} model 3006 * @param {Backbone.Collection} collection 3007 */ 3008 select: function( model, collection ) { 3009 var selection = this.options.selection, 3010 controller = this.controller; 3011 3012 // Check if a selection exists and if it's the collection provided. 3013 // If they're not the same collection, bail; we're in another 3014 // selection's event loop. 3015 if ( ! selection || ( collection && collection !== selection ) ) { 3016 return; 3017 } 3018 3019 // Bail if the model is already selected. 3020 if ( this.$el.hasClass( 'selected' ) ) { 3021 return; 3022 } 3023 3024 // Add 'selected' class to model, set aria-checked to true. 3025 this.$el.addClass( 'selected' ).attr( 'aria-checked', true ); 3026 // Make the checkbox tabable, except in media grid (bulk select mode). 3027 if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) { 3028 this.$( '.check' ).attr( 'tabindex', '0' ); 3029 } 3030 }, 3031 /** 3032 * @param {Backbone.Model} model 3033 * @param {Backbone.Collection} collection 3034 */ 3035 deselect: function( model, collection ) { 3036 var selection = this.options.selection; 3037 3038 // Check if a selection exists and if it's the collection provided. 3039 // If they're not the same collection, bail; we're in another 3040 // selection's event loop. 3041 if ( ! selection || ( collection && collection !== selection ) ) { 3042 return; 3043 } 3044 this.$el.removeClass( 'selected' ).attr( 'aria-checked', false ) 3045 .find( '.check' ).attr( 'tabindex', '-1' ); 3046 }, 3047 /** 3048 * @param {Backbone.Model} model 3049 * @param {Backbone.Collection} collection 3050 */ 3051 details: function( model, collection ) { 3052 var selection = this.options.selection, 3053 details; 3054 3055 if ( selection !== collection ) { 3056 return; 3057 } 3058 3059 details = selection.single(); 3060 this.$el.toggleClass( 'details', details === this.model ); 3061 }, 3062 /** 3063 * @param {Object} event 3064 */ 3065 preventDefault: function( event ) { 3066 event.preventDefault(); 3067 }, 3068 /** 3069 * @param {string} size 3070 * @returns {Object} 3071 */ 3072 imageSize: function( size ) { 3073 var sizes = this.model.get('sizes'); 3074 3075 size = size || 'medium'; 3076 3077 // Use the provided image size if possible. 3078 if ( sizes && sizes[ size ] ) { 3079 return _.clone( sizes[ size ] ); 3080 } else { 3081 return { 3082 url: this.model.get('url'), 3083 width: this.model.get('width'), 3084 height: this.model.get('height'), 3085 orientation: this.model.get('orientation') 3086 }; 3087 } 3088 }, 3089 /** 3090 * @param {Object} event 3091 */ 3092 updateSetting: function( event ) { 3093 var $setting = $( event.target ).closest('[data-setting]'), 3094 setting, value; 3095 3096 if ( ! $setting.length ) { 3097 return; 3098 } 3099 3100 setting = $setting.data('setting'); 3101 value = event.target.value; 3102 3103 if ( this.model.get( setting ) !== value ) { 3104 this.save( setting, value ); 3105 } 3106 }, 3107 3108 /** 3109 * Pass all the arguments to the model's save method. 3110 * 3111 * Records the aggregate status of all save requests and updates the 3112 * view's classes accordingly. 3113 */ 3114 save: function() { 3115 var view = this, 3116 save = this._save = this._save || { status: 'ready' }, 3117 request = this.model.save.apply( this.model, arguments ), 3118 requests = save.requests ? $.when( request, save.requests ) : request; 3119 3120 // If we're waiting to remove 'Saved.', stop. 3121 if ( save.savedTimer ) { 3122 clearTimeout( save.savedTimer ); 3123 } 3124 3125 this.updateSave('waiting'); 3126 save.requests = requests; 3127 requests.always( function() { 3128 // If we've performed another request since this one, bail. 3129 if ( save.requests !== requests ) { 3130 return; 3131 } 3132 3133 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' ); 3134 save.savedTimer = setTimeout( function() { 3135 view.updateSave('ready'); 3136 delete save.savedTimer; 3137 }, 2000 ); 3138 }); 3139 }, 3140 /** 3141 * @param {string} status 3142 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 3143 */ 3144 updateSave: function( status ) { 3145 var save = this._save = this._save || { status: 'ready' }; 3146 3147 if ( status && status !== save.status ) { 3148 this.$el.removeClass( 'save-' + save.status ); 3149 save.status = status; 3150 } 3151 3152 this.$el.addClass( 'save-' + save.status ); 3153 return this; 3154 }, 3155 3156 updateAll: function() { 3157 var $settings = this.$('[data-setting]'), 3158 model = this.model, 3159 changed; 3160 3161 changed = _.chain( $settings ).map( function( el ) { 3162 var $input = $('input, textarea, select, [value]', el ), 3163 setting, value; 3164 3165 if ( ! $input.length ) { 3166 return; 3167 } 3168 3169 setting = $(el).data('setting'); 3170 value = $input.val(); 3171 3172 // Record the value if it changed. 3173 if ( model.get( setting ) !== value ) { 3174 return [ setting, value ]; 3175 } 3176 }).compact().object().value(); 3177 3178 if ( ! _.isEmpty( changed ) ) { 3179 model.save( changed ); 3180 } 3181 }, 3182 /** 3183 * @param {Object} event 3184 */ 3185 removeFromLibrary: function( event ) { 3186 // Catch enter and space events 3187 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 3188 return; 3189 } 3190 3191 // Stop propagation so the model isn't selected. 3192 event.stopPropagation(); 3193 3194 this.collection.remove( this.model ); 3195 }, 3196 3197 /** 3198 * Add the model if it isn't in the selection, if it is in the selection, 3199 * remove it. 3200 * 3201 * @param {[type]} event [description] 3202 * @return {[type]} [description] 3203 */ 3204 checkClickHandler: function ( event ) { 3205 var selection = this.options.selection; 3206 if ( ! selection ) { 3207 return; 3208 } 3209 event.stopPropagation(); 3210 if ( selection.where( { id: this.model.get( 'id' ) } ).length ) { 3211 selection.remove( this.model ); 3212 // Move focus back to the attachment tile (from the check). 3213 this.$el.focus(); 3214 } else { 3215 selection.add( this.model ); 3216 } 3217 } 3218 }); 3219 3220 // Ensure settings remain in sync between attachment views. 3221 _.each({ 3222 caption: '_syncCaption', 3223 title: '_syncTitle', 3224 artist: '_syncArtist', 3225 album: '_syncAlbum' 3226 }, function( method, setting ) { 3227 /** 3228 * @param {Backbone.Model} model 3229 * @param {string} value 3230 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 3231 */ 3232 Attachment.prototype[ method ] = function( model, value ) { 3233 var $setting = this.$('[data-setting="' + setting + '"]'); 3234 3235 if ( ! $setting.length ) { 3236 return this; 3237 } 3238 3239 // If the updated value is in sync with the value in the DOM, there 3240 // is no need to re-render. If we're currently editing the value, 3241 // it will automatically be in sync, suppressing the re-render for 3242 // the view we're editing, while updating any others. 3243 if ( value === $setting.find('input, textarea, select, [value]').val() ) { 3244 return this; 3245 } 3246 3247 return this.render(); 3248 }; 3249 }); 3250 3251 module.exports = Attachment; 3252 },{"./view.js":55}],21:[function(require,module,exports){ 3253 /** 3254 * wp.media.view.Attachment.Details 3255 * 3256 * @class 3257 * @augments wp.media.view.Attachment 3258 * @augments wp.media.View 3259 * @augments wp.Backbone.View 3260 * @augments Backbone.View 3261 */ 3262 var Attachment = require( '../attachment.js' ), 3263 l10n = wp.media.view.l10n, 3264 Details; 3265 3266 Details = Attachment.extend({ 3267 tagName: 'div', 3268 className: 'attachment-details', 3269 template: wp.template('attachment-details'), 3270 3271 attributes: function() { 3272 return { 3273 'tabIndex': 0, 3274 'data-id': this.model.get( 'id' ) 3275 }; 3276 }, 3277 3278 events: { 3279 'change [data-setting]': 'updateSetting', 3280 'change [data-setting] input': 'updateSetting', 3281 'change [data-setting] select': 'updateSetting', 3282 'change [data-setting] textarea': 'updateSetting', 3283 'click .delete-attachment': 'deleteAttachment', 3284 'click .trash-attachment': 'trashAttachment', 3285 'click .untrash-attachment': 'untrashAttachment', 3286 'click .edit-attachment': 'editAttachment', 3287 'click .refresh-attachment': 'refreshAttachment', 3288 'keydown': 'toggleSelectionHandler' 3289 }, 3290 3291 initialize: function() { 3292 this.options = _.defaults( this.options, { 3293 rerenderOnModelChange: false 3294 }); 3295 3296 this.on( 'ready', this.initialFocus ); 3297 // Call 'initialize' directly on the parent class. 3298 Attachment.prototype.initialize.apply( this, arguments ); 3299 }, 3300 3301 initialFocus: function() { 3302 if ( ! wp.media.isTouchDevice ) { 3303 this.$( ':input' ).eq( 0 ).focus(); 3304 } 3305 }, 3306 /** 3307 * @param {Object} event 3308 */ 3309 deleteAttachment: function( event ) { 3310 event.preventDefault(); 3311 3312 if ( confirm( l10n.warnDelete ) ) { 3313 this.model.destroy(); 3314 // Keep focus inside media modal 3315 // after image is deleted 3316 this.controller.modal.focusManager.focus(); 3317 } 3318 }, 3319 /** 3320 * @param {Object} event 3321 */ 3322 trashAttachment: function( event ) { 3323 var library = this.controller.library; 3324 event.preventDefault(); 3325 3326 if ( wp.media.view.settings.mediaTrash && 3327 'edit-metadata' === this.controller.content.mode() ) { 3328 3329 this.model.set( 'status', 'trash' ); 3330 this.model.save().done( function() { 3331 library._requery( true ); 3332 } ); 3333 } else { 3334 this.model.destroy(); 3335 } 3336 }, 3337 /** 3338 * @param {Object} event 3339 */ 3340 untrashAttachment: function( event ) { 3341 var library = this.controller.library; 3342 event.preventDefault(); 3343 3344 this.model.set( 'status', 'inherit' ); 3345 this.model.save().done( function() { 3346 library._requery( true ); 3347 } ); 3348 }, 3349 /** 3350 * @param {Object} event 3351 */ 3352 editAttachment: function( event ) { 3353 var editState = this.controller.states.get( 'edit-image' ); 3354 if ( window.imageEdit && editState ) { 3355 event.preventDefault(); 3356 3357 editState.set( 'image', this.model ); 3358 this.controller.setState( 'edit-image' ); 3359 } else { 3360 this.$el.addClass('needs-refresh'); 3361 } 3362 }, 3363 /** 3364 * @param {Object} event 3365 */ 3366 refreshAttachment: function( event ) { 3367 this.$el.removeClass('needs-refresh'); 3368 event.preventDefault(); 3369 this.model.fetch(); 3370 }, 3371 /** 3372 * When reverse tabbing(shift+tab) out of the right details panel, deliver 3373 * the focus to the item in the list that was being edited. 3374 * 3375 * @param {Object} event 3376 */ 3377 toggleSelectionHandler: function( event ) { 3378 if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { 3379 this.controller.trigger( 'attachment:details:shift-tab', event ); 3380 return false; 3381 } 3382 3383 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 3384 this.controller.trigger( 'attachment:keydown:arrow', event ); 3385 return; 3386 } 3387 } 3388 }); 3389 3390 module.exports = Details; 3391 },{"../attachment.js":20}],22:[function(require,module,exports){ 3392 /** 3393 * wp.media.view.Attachment.Library 3394 * 3395 * @class 3396 * @augments wp.media.view.Attachment 3397 * @augments wp.media.View 3398 * @augments wp.Backbone.View 3399 * @augments Backbone.View 3400 */ 3401 var Attachment = require( '../attachment.js' ), 3402 Library; 3403 3404 Library = Attachment.extend({ 3405 buttons: { 3406 check: true 3407 } 3408 }); 3409 3410 module.exports = Library; 3411 },{"../attachment.js":20}],23:[function(require,module,exports){ 3412 /** 3413 * wp.media.view.Attachments 3414 * 3415 * @class 3416 * @augments wp.media.View 3417 * @augments wp.Backbone.View 3418 * @augments Backbone.View 3419 */ 3420 var View = require( './view.js' ), 3421 Attachment = require( './attachment.js' ), 3422 $ = jQuery, 3423 Attachments; 3424 3425 Attachments = View.extend({ 3426 tagName: 'ul', 3427 className: 'attachments', 3428 3429 attributes: { 3430 tabIndex: -1 3431 }, 3432 3433 initialize: function() { 3434 this.el.id = _.uniqueId('__attachments-view-'); 3435 3436 _.defaults( this.options, { 3437 refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, 3438 refreshThreshold: 3, 3439 AttachmentView: Attachment, 3440 sortable: false, 3441 resize: true, 3442 idealColumnWidth: $( window ).width() < 640 ? 135 : 150 3443 }); 3444 3445 this._viewsByCid = {}; 3446 this.$window = $( window ); 3447 this.resizeEvent = 'resize.media-modal-columns'; 3448 3449 this.collection.on( 'add', function( attachment ) { 3450 this.views.add( this.createAttachmentView( attachment ), { 3451 at: this.collection.indexOf( attachment ) 3452 }); 3453 }, this ); 3454 3455 this.collection.on( 'remove', function( attachment ) { 3456 var view = this._viewsByCid[ attachment.cid ]; 3457 delete this._viewsByCid[ attachment.cid ]; 3458 3459 if ( view ) { 3460 view.remove(); 3461 } 3462 }, this ); 3463 3464 this.collection.on( 'reset', this.render, this ); 3465 3466 this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); 3467 3468 // Throttle the scroll handler and bind this. 3469 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 3470 3471 this.options.scrollElement = this.options.scrollElement || this.el; 3472 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 3473 3474 this.initSortable(); 3475 3476 _.bindAll( this, 'setColumns' ); 3477 3478 if ( this.options.resize ) { 3479 this.on( 'ready', this.bindEvents ); 3480 this.controller.on( 'open', this.setColumns ); 3481 3482 // Call this.setColumns() after this view has been rendered in the DOM so 3483 // attachments get proper width applied. 3484 _.defer( this.setColumns, this ); 3485 } 3486 }, 3487 3488 bindEvents: function() { 3489 this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); 3490 }, 3491 3492 attachmentFocus: function() { 3493 this.$( 'li:first' ).focus(); 3494 }, 3495 3496 restoreFocus: function() { 3497 this.$( 'li.selected:first' ).focus(); 3498 }, 3499 3500 arrowEvent: function( event ) { 3501 var attachments = this.$el.children( 'li' ), 3502 perRow = this.columns, 3503 index = attachments.filter( ':focus' ).index(), 3504 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); 3505 3506 if ( index === -1 ) { 3507 return; 3508 } 3509 3510 // Left arrow 3511 if ( 37 === event.keyCode ) { 3512 if ( 0 === index ) { 3513 return; 3514 } 3515 attachments.eq( index - 1 ).focus(); 3516 } 3517 3518 // Up arrow 3519 if ( 38 === event.keyCode ) { 3520 if ( 1 === row ) { 3521 return; 3522 } 3523 attachments.eq( index - perRow ).focus(); 3524 } 3525 3526 // Right arrow 3527 if ( 39 === event.keyCode ) { 3528 if ( attachments.length === index ) { 3529 return; 3530 } 3531 attachments.eq( index + 1 ).focus(); 3532 } 3533 3534 // Down arrow 3535 if ( 40 === event.keyCode ) { 3536 if ( Math.ceil( attachments.length / perRow ) === row ) { 3537 return; 3538 } 3539 attachments.eq( index + perRow ).focus(); 3540 } 3541 }, 3542 3543 dispose: function() { 3544 this.collection.props.off( null, null, this ); 3545 if ( this.options.resize ) { 3546 this.$window.off( this.resizeEvent ); 3547 } 3548 3549 /** 3550 * call 'dispose' directly on the parent class 3551 */ 3552 View.prototype.dispose.apply( this, arguments ); 3553 }, 3554 3555 setColumns: function() { 3556 var prev = this.columns, 3557 width = this.$el.width(); 3558 3559 if ( width ) { 3560 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; 3561 3562 if ( ! prev || prev !== this.columns ) { 3563 this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); 3564 } 3565 } 3566 }, 3567 3568 initSortable: function() { 3569 var collection = this.collection; 3570 3571 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 3572 return; 3573 } 3574 3575 this.$el.sortable( _.extend({ 3576 // If the `collection` has a `comparator`, disable sorting. 3577 disabled: !! collection.comparator, 3578 3579 // Change the position of the attachment as soon as the 3580 // mouse pointer overlaps a thumbnail. 3581 tolerance: 'pointer', 3582 3583 // Record the initial `index` of the dragged model. 3584 start: function( event, ui ) { 3585 ui.item.data('sortableIndexStart', ui.item.index()); 3586 }, 3587 3588 // Update the model's index in the collection. 3589 // Do so silently, as the view is already accurate. 3590 update: function( event, ui ) { 3591 var model = collection.at( ui.item.data('sortableIndexStart') ), 3592 comparator = collection.comparator; 3593 3594 // Temporarily disable the comparator to prevent `add` 3595 // from re-sorting. 3596 delete collection.comparator; 3597 3598 // Silently shift the model to its new index. 3599 collection.remove( model, { 3600 silent: true 3601 }); 3602 collection.add( model, { 3603 silent: true, 3604 at: ui.item.index() 3605 }); 3606 3607 // Restore the comparator. 3608 collection.comparator = comparator; 3609 3610 // Fire the `reset` event to ensure other collections sync. 3611 collection.trigger( 'reset', collection ); 3612 3613 // If the collection is sorted by menu order, 3614 // update the menu order. 3615 collection.saveMenuOrder(); 3616 } 3617 }, this.options.sortable ) ); 3618 3619 // If the `orderby` property is changed on the `collection`, 3620 // check to see if we have a `comparator`. If so, disable sorting. 3621 collection.props.on( 'change:orderby', function() { 3622 this.$el.sortable( 'option', 'disabled', !! collection.comparator ); 3623 }, this ); 3624 3625 this.collection.props.on( 'change:orderby', this.refreshSortable, this ); 3626 this.refreshSortable(); 3627 }, 3628 3629 refreshSortable: function() { 3630 if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 3631 return; 3632 } 3633 3634 // If the `collection` has a `comparator`, disable sorting. 3635 var collection = this.collection, 3636 orderby = collection.props.get('orderby'), 3637 enabled = 'menuOrder' === orderby || ! collection.comparator; 3638 3639 this.$el.sortable( 'option', 'disabled', ! enabled ); 3640 }, 3641 3642 /** 3643 * @param {wp.media.model.Attachment} attachment 3644 * @returns {wp.media.View} 3645 */ 3646 createAttachmentView: function( attachment ) { 3647 var view = new this.options.AttachmentView({ 3648 controller: this.controller, 3649 model: attachment, 3650 collection: this.collection, 3651 selection: this.options.selection 3652 }); 3653 3654 return this._viewsByCid[ attachment.cid ] = view; 3655 }, 3656 3657 prepare: function() { 3658 // Create all of the Attachment views, and replace 3659 // the list in a single DOM operation. 3660 if ( this.collection.length ) { 3661 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 3662 3663 // If there are no elements, clear the views and load some. 3664 } else { 3665 this.views.unset(); 3666 this.collection.more().done( this.scroll ); 3667 } 3668 }, 3669 3670 ready: function() { 3671 // Trigger the scroll event to check if we're within the 3672 // threshold to query for additional attachments. 3673 this.scroll(); 3674 }, 3675 3676 scroll: function() { 3677 var view = this, 3678 el = this.options.scrollElement, 3679 scrollTop = el.scrollTop, 3680 toolbar; 3681 3682 // The scroll event occurs on the document, but the element 3683 // that should be checked is the document body. 3684 if ( el == document ) { 3685 el = document.body; 3686 scrollTop = $(document).scrollTop(); 3687 } 3688 3689 if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { 3690 return; 3691 } 3692 3693 toolbar = this.views.parent.toolbar; 3694 3695 // Show the spinner only if we are close to the bottom. 3696 if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { 3697 toolbar.get('spinner').show(); 3698 } 3699 3700 if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { 3701 this.collection.more().done(function() { 3702 view.scroll(); 3703 toolbar.get('spinner').hide(); 3704 }); 3705 } 3706 } 3707 }); 3708 3709 module.exports = Attachments; 3710 },{"./attachment.js":20,"./view.js":55}],24:[function(require,module,exports){ 3711 /** 3712 * wp.media.view.AttachmentsBrowser 3713 * 3714 * @class 3715 * @augments wp.media.View 3716 * @augments wp.Backbone.View 3717 * @augments Backbone.View 3718 * 3719 * @param {object} options 3720 * @param {object} [options.filters=false] Which filters to show in the browser's toolbar. 3721 * Accepts 'uploaded' and 'all'. 3722 * @param {object} [options.search=true] Whether to show the search interface in the 3723 * browser's toolbar. 3724 * @param {object} [options.display=false] Whether to show the attachments display settings 3725 * view in the sidebar. 3726 * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser. 3727 * Accepts true, false, and 'errors'. 3728 */ 3729 var View = require( '../view.js' ), 3730 Library = require( '../attachment/library.js' ), 3731 Toolbar = require( '../toolbar.js' ), 3732 Spinner = require( '../spinner.js' ), 3733 Search = require( '../search.js' ), 3734 Label = require( '../label.js' ), 3735 Uploaded = require( '../attachment-filters/uploaded.js' ), 3736 All = require( '../attachment-filters/all.js' ), 3737 DateFilter = require( '../attachment-filters/date.js' ), 3738 UploaderInline = require( '../uploader/inline.js' ), 3739 Attachments = require( '../attachments.js' ), 3740 Sidebar = require( '../sidebar.js' ), 3741 UploaderStatus = require( '../uploader/status.js' ), 3742 Details = require( '../attachment/details.js' ), 3743 AttachmentCompat = require( '../attachment-compat.js' ), 3744 AttachmentDisplay = require( '../settings/attachment-display.js' ), 3745 mediaTrash = wp.media.view.settings.mediaTrash, 3746 l10n = wp.media.view.l10n, 3747 $ = jQuery, 3748 AttachmentsBrowser; 3749 3750 AttachmentsBrowser = View.extend({ 3751 tagName: 'div', 3752 className: 'attachments-browser', 3753 3754 initialize: function() { 3755 _.defaults( this.options, { 3756 filters: false, 3757 search: true, 3758 display: false, 3759 sidebar: true, 3760 AttachmentView: Library 3761 }); 3762 3763 this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) ); 3764 this.controller.on( 'edit:selection', this.editSelection ); 3765 this.createToolbar(); 3766 if ( this.options.sidebar ) { 3767 this.createSidebar(); 3768 } 3769 this.createUploader(); 3770 this.createAttachments(); 3771 this.updateContent(); 3772 3773 if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) { 3774 this.$el.addClass( 'hide-sidebar' ); 3775 3776 if ( 'errors' === this.options.sidebar ) { 3777 this.$el.addClass( 'sidebar-for-errors' ); 3778 } 3779 } 3780 3781 this.collection.on( 'add remove reset', this.updateContent, this ); 3782 }, 3783 3784 editSelection: function( modal ) { 3785 modal.$( '.media-button-backToLibrary' ).focus(); 3786 }, 3787 3788 /** 3789 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining 3790 */ 3791 dispose: function() { 3792 this.options.selection.off( null, null, this ); 3793 View.prototype.dispose.apply( this, arguments ); 3794 return this; 3795 }, 3796 3797 createToolbar: function() { 3798 var LibraryViewSwitcher, Filters, toolbarOptions; 3799 3800 toolbarOptions = { 3801 controller: this.controller 3802 }; 3803 3804 if ( this.controller.isModeActive( 'grid' ) ) { 3805 toolbarOptions.className = 'media-toolbar wp-filter'; 3806 } 3807 3808 /** 3809 * @member {wp.media.view.Toolbar} 3810 */ 3811 this.toolbar = new Toolbar( toolbarOptions ); 3812 3813 this.views.add( this.toolbar ); 3814 3815 this.toolbar.set( 'spinner', new Spinner({ 3816 priority: -60 3817 }) ); 3818 3819 if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { 3820 // "Filters" will return a <select>, need to render 3821 // screen reader text before 3822 this.toolbar.set( 'filtersLabel', new Label({ 3823 value: l10n.filterByType, 3824 attributes: { 3825 'for': 'media-attachment-filters' 3826 }, 3827 priority: -80 3828 }).render() ); 3829 3830 if ( 'uploaded' === this.options.filters ) { 3831 this.toolbar.set( 'filters', new Uploaded({ 3832 controller: this.controller, 3833 model: this.collection.props, 3834 priority: -80 3835 }).render() ); 3836 } else { 3837 Filters = new All({ 3838 controller: this.controller, 3839 model: this.collection.props, 3840 priority: -80 3841 }); 3842 3843 this.toolbar.set( 'filters', Filters.render() ); 3844 } 3845 } 3846 3847 // Feels odd to bring the global media library switcher into the Attachment 3848 // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); 3849 // which the controller can tap into and add this view? 3850 if ( this.controller.isModeActive( 'grid' ) ) { 3851 LibraryViewSwitcher = View.extend({ 3852 className: 'view-switch media-grid-view-switch', 3853 template: wp.template( 'media-library-view-switcher') 3854 }); 3855 3856 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({ 3857 controller: this.controller, 3858 priority: -90 3859 }).render() ); 3860 3861 // DateFilter is a <select>, screen reader text needs to be rendered before 3862 this.toolbar.set( 'dateFilterLabel', new Label({ 3863 value: l10n.filterByDate, 3864 attributes: { 3865 'for': 'media-attachment-date-filters' 3866 }, 3867 priority: -75 3868 }).render() ); 3869 this.toolbar.set( 'dateFilter', new DateFilter({ 3870 controller: this.controller, 3871 model: this.collection.props, 3872 priority: -75 3873 }).render() ); 3874 3875 // BulkSelection is a <div> with subviews, including screen reader text 3876 this.toolbar.set( 'selectModeToggleButton', new wp.media.view.SelectModeToggleButton({ 3877 text: l10n.bulkSelect, 3878 controller: this.controller, 3879 priority: -70 3880 }).render() ); 3881 3882 this.toolbar.set( 'deleteSelectedButton', new wp.media.view.DeleteSelectedButton({ 3883 filters: Filters, 3884 style: 'primary', 3885 disabled: true, 3886 text: mediaTrash ? l10n.trashSelected : l10n.deleteSelected, 3887 controller: this.controller, 3888 priority: -60, 3889 click: function() { 3890 var changed = [], removed = [], self = this, 3891 selection = this.controller.state().get( 'selection' ), 3892 library = this.controller.state().get( 'library' ); 3893 3894 if ( ! selection.length ) { 3895 return; 3896 } 3897 3898 if ( ! mediaTrash && ! confirm( l10n.warnBulkDelete ) ) { 3899 return; 3900 } 3901 3902 if ( mediaTrash && 3903 'trash' !== selection.at( 0 ).get( 'status' ) && 3904 ! confirm( l10n.warnBulkTrash ) ) { 3905 3906 return; 3907 } 3908 3909 selection.each( function( model ) { 3910 if ( ! model.get( 'nonces' )['delete'] ) { 3911 removed.push( model ); 3912 return; 3913 } 3914 3915 if ( mediaTrash && 'trash' === model.get( 'status' ) ) { 3916 model.set( 'status', 'inherit' ); 3917 changed.push( model.save() ); 3918 removed.push( model ); 3919 } else if ( mediaTrash ) { 3920 model.set( 'status', 'trash' ); 3921 changed.push( model.save() ); 3922 removed.push( model ); 3923 } else { 3924 model.destroy({wait: true}); 3925 } 3926 } ); 3927 3928 if ( changed.length ) { 3929 selection.remove( removed ); 3930 3931 $.when.apply( null, changed ).then( function() { 3932 library._requery( true ); 3933 self.controller.trigger( 'selection:action:done' ); 3934 } ); 3935 } else { 3936 this.controller.trigger( 'selection:action:done' ); 3937 } 3938 } 3939 }).render() ); 3940 3941 if ( mediaTrash ) { 3942 this.toolbar.set( 'deleteSelectedPermanentlyButton', new wp.media.view.DeleteSelectedPermanentlyButton({ 3943 filters: Filters, 3944 style: 'primary', 3945 disabled: true, 3946 text: l10n.deleteSelected, 3947 controller: this.controller, 3948 priority: -55, 3949 click: function() { 3950 var removed = [], selection = this.controller.state().get( 'selection' ); 3951 3952 if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) { 3953 return; 3954 } 3955 3956 selection.each( function( model ) { 3957 if ( ! model.get( 'nonces' )['delete'] ) { 3958 removed.push( model ); 3959 return; 3960 } 3961 3962 model.destroy(); 3963 } ); 3964 3965 selection.remove( removed ); 3966 this.controller.trigger( 'selection:action:done' ); 3967 } 3968 }).render() ); 3969 } 3970 3971 } else { 3972 // DateFilter is a <select>, screen reader text needs to be rendered before 3973 this.toolbar.set( 'dateFilterLabel', new Label({ 3974 value: l10n.filterByDate, 3975 attributes: { 3976 'for': 'media-attachment-date-filters' 3977 }, 3978 priority: -75 3979 }).render() ); 3980 this.toolbar.set( 'dateFilter', new DateFilter({ 3981 controller: this.controller, 3982 model: this.collection.props, 3983 priority: -75 3984 }).render() ); 3985 } 3986 3987 if ( this.options.search ) { 3988 // Search is an input, screen reader text needs to be rendered before 3989 this.toolbar.set( 'searchLabel', new Label({ 3990 value: l10n.searchMediaLabel, 3991 attributes: { 3992 'for': 'media-search-input' 3993 }, 3994 priority: 60 3995 }).render() ); 3996 this.toolbar.set( 'search', new Search({ 3997 controller: this.controller, 3998 model: this.collection.props, 3999 priority: 60 4000 }).render() ); 4001 } 4002 4003 if ( this.options.dragInfo ) { 4004 this.toolbar.set( 'dragInfo', new View({ 4005 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0], 4006 priority: -40 4007 }) ); 4008 } 4009 4010 if ( this.options.suggestedWidth && this.options.suggestedHeight ) { 4011 this.toolbar.set( 'suggestedDimensions', new View({ 4012 el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '</div>' )[0], 4013 priority: -40 4014 }) ); 4015 } 4016 }, 4017 4018 updateContent: function() { 4019 var view = this, 4020 noItemsView; 4021 4022 if ( this.controller.isModeActive( 'grid' ) ) { 4023 noItemsView = view.attachmentsNoResults; 4024 } else { 4025 noItemsView = view.uploader; 4026 } 4027 4028 if ( ! this.collection.length ) { 4029 this.toolbar.get( 'spinner' ).show(); 4030 this.dfd = this.collection.more().done( function() { 4031 if ( ! view.collection.length ) { 4032 noItemsView.$el.removeClass( 'hidden' ); 4033 } else { 4034 noItemsView.$el.addClass( 'hidden' ); 4035 } 4036 view.toolbar.get( 'spinner' ).hide(); 4037 } ); 4038 } else { 4039 noItemsView.$el.addClass( 'hidden' ); 4040 view.toolbar.get( 'spinner' ).hide(); 4041 } 4042 }, 4043 4044 createUploader: function() { 4045 this.uploader = new UploaderInline({ 4046 controller: this.controller, 4047 status: false, 4048 message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound, 4049 canClose: this.controller.isModeActive( 'grid' ) 4050 }); 4051 4052 this.uploader.hide(); 4053 this.views.add( this.uploader ); 4054 }, 4055 4056 toggleUploader: function() { 4057 if ( this.uploader.$el.hasClass( 'hidden' ) ) { 4058 this.uploader.show(); 4059 } else { 4060 this.uploader.hide(); 4061 } 4062 }, 4063 4064 createAttachments: function() { 4065 this.attachments = new Attachments({ 4066 controller: this.controller, 4067 collection: this.collection, 4068 selection: this.options.selection, 4069 model: this.model, 4070 sortable: this.options.sortable, 4071 scrollElement: this.options.scrollElement, 4072 idealColumnWidth: this.options.idealColumnWidth, 4073 4074 // The single `Attachment` view to be used in the `Attachments` view. 4075 AttachmentView: this.options.AttachmentView 4076 }); 4077 4078 // Add keydown listener to the instance of the Attachments view 4079 this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent ); 4080 this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus ); 4081 4082 this.views.add( this.attachments ); 4083 4084 4085 if ( this.controller.isModeActive( 'grid' ) ) { 4086 this.attachmentsNoResults = new View({ 4087 controller: this.controller, 4088 tagName: 'p' 4089 }); 4090 4091 this.attachmentsNoResults.$el.addClass( 'hidden no-media' ); 4092 this.attachmentsNoResults.$el.html( l10n.noMedia ); 4093 4094 this.views.add( this.attachmentsNoResults ); 4095 } 4096 }, 4097 4098 createSidebar: function() { 4099 var options = this.options, 4100 selection = options.selection, 4101 sidebar = this.sidebar = new Sidebar({ 4102 controller: this.controller 4103 }); 4104 4105 this.views.add( sidebar ); 4106 4107 if ( this.controller.uploader ) { 4108 sidebar.set( 'uploads', new UploaderStatus({ 4109 controller: this.controller, 4110 priority: 40 4111 }) ); 4112 } 4113 4114 selection.on( 'selection:single', this.createSingle, this ); 4115 selection.on( 'selection:unsingle', this.disposeSingle, this ); 4116 4117 if ( selection.single() ) { 4118 this.createSingle(); 4119 } 4120 }, 4121 4122 createSingle: function() { 4123 var sidebar = this.sidebar, 4124 single = this.options.selection.single(); 4125 4126 sidebar.set( 'details', new Details({ 4127 controller: this.controller, 4128 model: single, 4129 priority: 80 4130 }) ); 4131 4132 sidebar.set( 'compat', new AttachmentCompat({ 4133 controller: this.controller, 4134 model: single, 4135 priority: 120 4136 }) ); 4137 4138 if ( this.options.display ) { 4139 sidebar.set( 'display', new AttachmentDisplay({ 4140 controller: this.controller, 4141 model: this.model.display( single ), 4142 attachment: single, 4143 priority: 160, 4144 userSettings: this.model.get('displayUserSettings') 4145 }) ); 4146 } 4147 4148 // Show the sidebar on mobile 4149 if ( this.model.id === 'insert' ) { 4150 sidebar.$el.addClass( 'visible' ); 4151 } 4152 }, 4153 4154 disposeSingle: function() { 4155 var sidebar = this.sidebar; 4156 sidebar.unset('details'); 4157 sidebar.unset('compat'); 4158 sidebar.unset('display'); 4159 // Hide the sidebar on mobile 4160 sidebar.$el.removeClass( 'visible' ); 4161 } 4162 }); 4163 4164 module.exports = AttachmentsBrowser; 4165 },{"../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){ 4166 /** 4167 * wp.media.view.AudioDetails 4168 * 4169 * @constructor 4170 * @augments wp.media.view.MediaDetails 4171 * @augments wp.media.view.Settings.AttachmentDisplay 4172 * @augments wp.media.view.Settings 4173 * @augments wp.media.View 4174 * @augments wp.Backbone.View 4175 * @augments Backbone.View 4176 */ 4177 var MediaDetails = require( './media-details' ), 4178 AudioDetails; 4179 4180 AudioDetails = MediaDetails.extend({ 4181 className: 'audio-details', 4182 template: wp.template('audio-details'), 4183 4184 setMedia: function() { 4185 var audio = this.$('.wp-audio-shortcode'); 4186 4187 if ( audio.find( 'source' ).length ) { 4188 if ( audio.is(':hidden') ) { 4189 audio.show(); 4190 } 4191 this.media = MediaDetails.prepareSrc( audio.get(0) ); 4192 } else { 4193 audio.hide(); 4194 this.media = false; 4195 } 4196 4197 return this; 4198 } 4199 }); 4200 4201 module.exports = AudioDetails; 4202 },{"./media-details":35}],26:[function(require,module,exports){ 4203 /** 4204 * wp.media.view.Button 4205 * 4206 * @class 4207 * @augments wp.media.View 4208 * @augments wp.Backbone.View 4209 * @augments Backbone.View 4210 */ 4211 var View = require( './view.js' ), 4212 Button; 4213 4214 Button = View.extend({ 4215 tagName: 'a', 4216 className: 'media-button', 4217 attributes: { href: '#' }, 4218 4219 events: { 4220 'click': 'click' 4221 }, 4222 4223 defaults: { 4224 text: '', 4225 style: '', 4226 size: 'large', 4227 disabled: false 4228 }, 4229 4230 initialize: function() { 4231 /** 4232 * Create a model with the provided `defaults`. 4233 * 4234 * @member {Backbone.Model} 4235 */ 4236 this.model = new Backbone.Model( this.defaults ); 4237 4238 // If any of the `options` have a key from `defaults`, apply its 4239 // value to the `model` and remove it from the `options object. 4240 _.each( this.defaults, function( def, key ) { 4241 var value = this.options[ key ]; 4242 if ( _.isUndefined( value ) ) { 4243 return; 4244 } 4245 4246 this.model.set( key, value ); 4247 delete this.options[ key ]; 4248 }, this ); 4249 4250 this.model.on( 'change', this.render, this ); 4251 }, 4252 /** 4253 * @returns {wp.media.view.Button} Returns itself to allow chaining 4254 */ 4255 render: function() { 4256 var classes = [ 'button', this.className ], 4257 model = this.model.toJSON(); 4258 4259 if ( model.style ) { 4260 classes.push( 'button-' + model.style ); 4261 } 4262 4263 if ( model.size ) { 4264 classes.push( 'button-' + model.size ); 4265 } 4266 4267 classes = _.uniq( classes.concat( this.options.classes ) ); 4268 this.el.className = classes.join(' '); 4269 4270 this.$el.attr( 'disabled', model.disabled ); 4271 this.$el.text( this.model.get('text') ); 4272 4273 return this; 4274 }, 4275 /** 4276 * @param {Object} event 4277 */ 4278 click: function( event ) { 4279 if ( '#' === this.attributes.href ) { 4280 event.preventDefault(); 4281 } 4282 4283 if ( this.options.click && ! this.model.get('disabled') ) { 4284 this.options.click.apply( this, arguments ); 4285 } 4286 } 4287 }); 4288 4289 module.exports = Button; 4290 },{"./view.js":55}],27:[function(require,module,exports){ 4291 /** 4292 * wp.media.view.FocusManager 4293 * 4294 * @class 4295 * @augments wp.media.View 4296 * @augments wp.Backbone.View 4297 * @augments Backbone.View 4298 */ 4299 var View = require( './view.js' ), 4300 FocusManager; 4301 4302 FocusManager = View.extend({ 4303 4304 events: { 4305 'keydown': 'constrainTabbing' 4306 }, 4307 4308 focus: function() { // Reset focus on first left menu item 4309 this.$('.media-menu-item').first().focus(); 4310 }, 4311 /** 4312 * @param {Object} event 4313 */ 4314 constrainTabbing: function( event ) { 4315 var tabbables; 4316 4317 // Look for the tab key. 4318 if ( 9 !== event.keyCode ) { 4319 return; 4320 } 4321 4322 tabbables = this.$( ':tabbable' ); 4323 4324 // Keep tab focus within media modal while it's open 4325 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { 4326 tabbables.first().focus(); 4327 return false; 4328 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { 4329 tabbables.last().focus(); 4330 return false; 4331 } 4332 } 4333 4334 }); 4335 4336 module.exports = FocusManager; 4337 },{"./view.js":55}],28:[function(require,module,exports){ 4338 /** 4339 * wp.media.view.Frame 4340 * 4341 * A frame is a composite view consisting of one or more regions and one or more 4342 * states. 4343 * 4344 * @see wp.media.controller.State 4345 * @see wp.media.controller.Region 4346 * 4347 * @class 4348 * @augments wp.media.View 4349 * @augments wp.Backbone.View 4350 * @augments Backbone.View 4351 * @mixes wp.media.controller.StateMachine 4352 */ 4353 var StateMachine = require( '../controllers/state-machine.js' ), 4354 State = require( '../controllers/state.js' ), 4355 Region = require( '../controllers/region.js' ), 4356 View = require( './view.js' ), 4357 Frame; 4358 4359 Frame = View.extend({ 4360 initialize: function() { 4361 _.defaults( this.options, { 4362 mode: [ 'select' ] 4363 }); 4364 this._createRegions(); 4365 this._createStates(); 4366 this._createModes(); 4367 }, 4368 4369 _createRegions: function() { 4370 // Clone the regions array. 4371 this.regions = this.regions ? this.regions.slice() : []; 4372 4373 // Initialize regions. 4374 _.each( this.regions, function( region ) { 4375 this[ region ] = new Region({ 4376 view: this, 4377 id: region, 4378 selector: '.media-frame-' + region 4379 }); 4380 }, this ); 4381 }, 4382 /** 4383 * Create the frame's states. 4384 * 4385 * @see wp.media.controller.State 4386 * @see wp.media.controller.StateMachine 4387 * 4388 * @fires wp.media.controller.State#ready 4389 */ 4390 _createStates: function() { 4391 // Create the default `states` collection. 4392 this.states = new Backbone.Collection( null, { 4393 model: State 4394 }); 4395 4396 // Ensure states have a reference to the frame. 4397 this.states.on( 'add', function( model ) { 4398 model.frame = this; 4399 model.trigger('ready'); 4400 }, this ); 4401 4402 if ( this.options.states ) { 4403 this.states.add( this.options.states ); 4404 } 4405 }, 4406 4407 /** 4408 * A frame can be in a mode or multiple modes at one time. 4409 * 4410 * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode. 4411 */ 4412 _createModes: function() { 4413 // Store active "modes" that the frame is in. Unrelated to region modes. 4414 this.activeModes = new Backbone.Collection(); 4415 this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) ); 4416 4417 _.each( this.options.mode, function( mode ) { 4418 this.activateMode( mode ); 4419 }, this ); 4420 }, 4421 /** 4422 * Reset all states on the frame to their defaults. 4423 * 4424 * @returns {wp.media.view.Frame} Returns itself to allow chaining 4425 */ 4426 reset: function() { 4427 this.states.invoke( 'trigger', 'reset' ); 4428 return this; 4429 }, 4430 /** 4431 * Map activeMode collection events to the frame. 4432 */ 4433 triggerModeEvents: function( model, collection, options ) { 4434 var collectionEvent, 4435 modeEventMap = { 4436 add: 'activate', 4437 remove: 'deactivate' 4438 }, 4439 eventToTrigger; 4440 // Probably a better way to do this. 4441 _.each( options, function( value, key ) { 4442 if ( value ) { 4443 collectionEvent = key; 4444 } 4445 } ); 4446 4447 if ( ! _.has( modeEventMap, collectionEvent ) ) { 4448 return; 4449 } 4450 4451 eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent]; 4452 this.trigger( eventToTrigger ); 4453 }, 4454 /** 4455 * Activate a mode on the frame. 4456 * 4457 * @param string mode Mode ID. 4458 * @returns {this} Returns itself to allow chaining. 4459 */ 4460 activateMode: function( mode ) { 4461 // Bail if the mode is already active. 4462 if ( this.isModeActive( mode ) ) { 4463 return; 4464 } 4465 this.activeModes.add( [ { id: mode } ] ); 4466 // Add a CSS class to the frame so elements can be styled for the mode. 4467 this.$el.addClass( 'mode-' + mode ); 4468 4469 return this; 4470 }, 4471 /** 4472 * Deactivate a mode on the frame. 4473 * 4474 * @param string mode Mode ID. 4475 * @returns {this} Returns itself to allow chaining. 4476 */ 4477 deactivateMode: function( mode ) { 4478 // Bail if the mode isn't active. 4479 if ( ! this.isModeActive( mode ) ) { 4480 return this; 4481 } 4482 this.activeModes.remove( this.activeModes.where( { id: mode } ) ); 4483 this.$el.removeClass( 'mode-' + mode ); 4484 /** 4485 * Frame mode deactivation event. 4486 * 4487 * @event this#{mode}:deactivate 4488 */ 4489 this.trigger( mode + ':deactivate' ); 4490 4491 return this; 4492 }, 4493 /** 4494 * Check if a mode is enabled on the frame. 4495 * 4496 * @param string mode Mode ID. 4497 * @return bool 4498 */ 4499 isModeActive: function( mode ) { 4500 return Boolean( this.activeModes.where( { id: mode } ).length ); 4501 } 4502 }); 4503 4504 // Make the `Frame` a `StateMachine`. 4505 _.extend( Frame.prototype, StateMachine.prototype ); 4506 4507 module.exports = Frame; 4508 },{"../controllers/region.js":5,"../controllers/state-machine.js":6,"../controllers/state.js":7,"./view.js":55}],29:[function(require,module,exports){ 4509 /** 4510 * wp.media.view.MediaFrame.AudioDetails 4511 * 4512 * @constructor 4513 * @augments wp.media.view.MediaFrame.MediaDetails 4514 * @augments wp.media.view.MediaFrame.Select 4515 * @augments wp.media.view.MediaFrame 4516 * @augments wp.media.view.Frame 4517 * @augments wp.media.View 4518 * @augments wp.Backbone.View 4519 * @augments Backbone.View 4520 * @mixes wp.media.controller.StateMachine 4521 */ 4522 var MediaDetails = require( './media-details' ), 4523 MediaLibrary = require( '../../controllers/media-library.js' ), 4524 AudioDetailsView = require( '../audio-details.js' ), 4525 AudioDetailsController = require( '../../controllers/audio-details.js' ), 4526 l10n = wp.media.view.l10n, 4527 AudioDetails; 4528 4529 AudioDetails = MediaDetails.extend({ 4530 defaults: { 4531 id: 'audio', 4532 url: '', 4533 menu: 'audio-details', 4534 content: 'audio-details', 4535 toolbar: 'audio-details', 4536 type: 'link', 4537 title: l10n.audioDetailsTitle, 4538 priority: 120 4539 }, 4540 4541 initialize: function( options ) { 4542 options.DetailsView = AudioDetailsView; 4543 options.cancelText = l10n.audioDetailsCancel; 4544 options.addText = l10n.audioAddSourceTitle; 4545 4546 MediaDetails.prototype.initialize.call( this, options ); 4547 }, 4548 4549 bindHandlers: function() { 4550 MediaDetails.prototype.bindHandlers.apply( this, arguments ); 4551 4552 this.on( 'toolbar:render:replace-audio', this.renderReplaceToolbar, this ); 4553 this.on( 'toolbar:render:add-audio-source', this.renderAddSourceToolbar, this ); 4554 }, 4555 4556 createStates: function() { 4557 this.states.add([ 4558 new AudioDetailsController( { 4559 media: this.media 4560 } ), 4561 4562 new MediaLibrary( { 4563 type: 'audio', 4564 id: 'replace-audio', 4565 title: l10n.audioReplaceTitle, 4566 toolbar: 'replace-audio', 4567 media: this.media, 4568 menu: 'audio-details' 4569 } ), 4570 4571 new MediaLibrary( { 4572 type: 'audio', 4573 id: 'add-audio-source', 4574 title: l10n.audioAddSourceTitle, 4575 toolbar: 'add-audio-source', 4576 media: this.media, 4577 menu: false 4578 } ) 4579 ]); 4580 } 4581 }); 4582 4583 module.exports = AudioDetails; 4584 },{"../../controllers/audio-details.js":2,"../../controllers/media-library.js":4,"../audio-details.js":25,"./media-details":30}],30:[function(require,module,exports){ 4585 /** 4586 * wp.media.view.MediaFrame.MediaDetails 4587 * 4588 * @constructor 4589 * @augments wp.media.view.MediaFrame.Select 4590 * @augments wp.media.view.MediaFrame 4591 * @augments wp.media.view.Frame 4592 * @augments wp.media.View 4593 * @augments wp.Backbone.View 4594 * @augments Backbone.View 4595 * @mixes wp.media.controller.StateMachine 4596 */ 4597 var View = require( '../view.js' ), 4598 Toolbar = require( '../toolbar.js' ), 4599 Select = require( './select.js' ), 4600 Selection = require( '../../models/selection.js' ), 4601 PostMedia = require( '../../models/post-media.js' ), 4602 l10n = wp.media.view.l10n, 4603 MediaDetails; 4604 4605 MediaDetails = Select.extend({ 4606 defaults: { 4607 id: 'media', 4608 url: '', 4609 menu: 'media-details', 4610 content: 'media-details', 4611 toolbar: 'media-details', 4612 type: 'link', 4613 priority: 120 4614 }, 4615 4616 initialize: function( options ) { 4617 this.DetailsView = options.DetailsView; 4618 this.cancelText = options.cancelText; 4619 this.addText = options.addText; 4620 4621 this.media = new PostMedia( options.metadata ); 4622 this.options.selection = new Selection( this.media.attachment, { multiple: false } ); 4623 Select.prototype.initialize.apply( this, arguments ); 4624 }, 4625 4626 bindHandlers: function() { 4627 var menu = this.defaults.menu; 4628 4629 Select.prototype.bindHandlers.apply( this, arguments ); 4630 4631 this.on( 'menu:create:' + menu, this.createMenu, this ); 4632 this.on( 'content:render:' + menu, this.renderDetailsContent, this ); 4633 this.on( 'menu:render:' + menu, this.renderMenu, this ); 4634 this.on( 'toolbar:render:' + menu, this.renderDetailsToolbar, this ); 4635 }, 4636 4637 renderDetailsContent: function() { 4638 var view = new this.DetailsView({ 4639 controller: this, 4640 model: this.state().media, 4641 attachment: this.state().media.attachment 4642 }).render(); 4643 4644 this.content.set( view ); 4645 }, 4646 4647 renderMenu: function( view ) { 4648 var lastState = this.lastState(), 4649 previous = lastState && lastState.id, 4650 frame = this; 4651 4652 view.set({ 4653 cancel: { 4654 text: this.cancelText, 4655 priority: 20, 4656 click: function() { 4657 if ( previous ) { 4658 frame.setState( previous ); 4659 } else { 4660 frame.close(); 4661 } 4662 } 4663 }, 4664 separateCancel: new View({ 4665 className: 'separator', 4666 priority: 40 4667 }) 4668 }); 4669 4670 }, 4671 4672 setPrimaryButton: function(text, handler) { 4673 this.toolbar.set( new Toolbar({ 4674 controller: this, 4675 items: { 4676 button: { 4677 style: 'primary', 4678 text: text, 4679 priority: 80, 4680 click: function() { 4681 var controller = this.controller; 4682 handler.call( this, controller, controller.state() ); 4683 // Restore and reset the default state. 4684 controller.setState( controller.options.state ); 4685 controller.reset(); 4686 } 4687 } 4688 } 4689 }) ); 4690 }, 4691 4692 renderDetailsToolbar: function() { 4693 this.setPrimaryButton( l10n.update, function( controller, state ) { 4694 controller.close(); 4695 state.trigger( 'update', controller.media.toJSON() ); 4696 } ); 4697 }, 4698 4699 renderReplaceToolbar: function() { 4700 this.setPrimaryButton( l10n.replace, function( controller, state ) { 4701 var attachment = state.get( 'selection' ).single(); 4702 controller.media.changeAttachment( attachment ); 4703 state.trigger( 'replace', controller.media.toJSON() ); 4704 } ); 4705 }, 4706 4707 renderAddSourceToolbar: function() { 4708 this.setPrimaryButton( this.addText, function( controller, state ) { 4709 var attachment = state.get( 'selection' ).single(); 4710 controller.media.setSource( attachment ); 4711 state.trigger( 'add-source', controller.media.toJSON() ); 4712 } ); 4713 } 4714 }); 4715 4716 module.exports = MediaDetails; 4717 },{"../../models/post-media.js":11,"../../models/selection.js":13,"../toolbar.js":48,"../view.js":55,"./select.js":31}],31:[function(require,module,exports){ 4718 /** 4719 * wp.media.view.MediaFrame.Select 4720 * 4721 * A frame for selecting an item or items from the media library. 4722 * 4723 * @class 4724 * @augments wp.media.view.MediaFrame 4725 * @augments wp.media.view.Frame 4726 * @augments wp.media.View 4727 * @augments wp.Backbone.View 4728 * @augments Backbone.View 4729 * @mixes wp.media.controller.StateMachine 4730 */ 4731 4732 var MediaFrame = require( '../media-frame.js' ), 4733 Library = require( '../../controllers/library.js' ), 4734 AttachmentsModel = require( '../../models/attachments.js' ), 4735 SelectionModel = require( '../../models/selection.js' ), 4736 AttachmentsBrowser = require( '../attachments/browser.js' ), 4737 UploaderInline = require( '../uploader/inline.js' ), 4738 ToolbarSelect = require( '../toolbar/select.js' ), 4739 l10n = wp.media.view.l10n, 4740 Select; 4741 4742 Select = MediaFrame.extend({ 4743 initialize: function() { 4744 // Call 'initialize' directly on the parent class. 4745 MediaFrame.prototype.initialize.apply( this, arguments ); 4746 4747 _.defaults( this.options, { 4748 selection: [], 4749 library: {}, 4750 multiple: false, 4751 state: 'library' 4752 }); 4753 4754 this.createSelection(); 4755 this.createStates(); 4756 this.bindHandlers(); 4757 }, 4758 4759 /** 4760 * Attach a selection collection to the frame. 4761 * 4762 * A selection is a collection of attachments used for a specific purpose 4763 * by a media frame. e.g. Selecting an attachment (or many) to insert into 4764 * post content. 4765 * 4766 * @see media.model.Selection 4767 */ 4768 createSelection: function() { 4769 var selection = this.options.selection; 4770 4771 if ( ! (selection instanceof SelectionModel) ) { 4772 this.options.selection = new SelectionModel( selection, { 4773 multiple: this.options.multiple 4774 }); 4775 } 4776 4777 this._selection = { 4778 attachments: new AttachmentsModel(), 4779 difference: [] 4780 }; 4781 }, 4782 4783 /** 4784 * Create the default states on the frame. 4785 */ 4786 createStates: function() { 4787 var options = this.options; 4788 4789 if ( this.options.states ) { 4790 return; 4791 } 4792 4793 // Add the default states. 4794 this.states.add([ 4795 // Main states. 4796 new Library({ 4797 library: wp.media.query( options.library ), 4798 multiple: options.multiple, 4799 title: options.title, 4800 priority: 20 4801 }) 4802 ]); 4803 }, 4804 4805 /** 4806 * Bind region mode event callbacks. 4807 * 4808 * @see media.controller.Region.render 4809 */ 4810 bindHandlers: function() { 4811 this.on( 'router:create:browse', this.createRouter, this ); 4812 this.on( 'router:render:browse', this.browseRouter, this ); 4813 this.on( 'content:create:browse', this.browseContent, this ); 4814 this.on( 'content:render:upload', this.uploadContent, this ); 4815 this.on( 'toolbar:create:select', this.createSelectToolbar, this ); 4816 }, 4817 4818 /** 4819 * Render callback for the router region in the `browse` mode. 4820 * 4821 * @param {wp.media.view.Router} routerView 4822 */ 4823 browseRouter: function( routerView ) { 4824 routerView.set({ 4825 upload: { 4826 text: l10n.uploadFilesTitle, 4827 priority: 20 4828 }, 4829 browse: { 4830 text: l10n.mediaLibraryTitle, 4831 priority: 40 4832 } 4833 }); 4834 }, 4835 4836 /** 4837 * Render callback for the content region in the `browse` mode. 4838 * 4839 * @param {wp.media.controller.Region} contentRegion 4840 */ 4841 browseContent: function( contentRegion ) { 4842 var state = this.state(); 4843 4844 this.$el.removeClass('hide-toolbar'); 4845 4846 // Browse our library of attachments. 4847 contentRegion.view = new AttachmentsBrowser({ 4848 controller: this, 4849 collection: state.get('library'), 4850 selection: state.get('selection'), 4851 model: state, 4852 sortable: state.get('sortable'), 4853 search: state.get('searchable'), 4854 filters: state.get('filterable'), 4855 display: state.has('display') ? state.get('display') : state.get('displaySettings'), 4856 dragInfo: state.get('dragInfo'), 4857 4858 idealColumnWidth: state.get('idealColumnWidth'), 4859 suggestedWidth: state.get('suggestedWidth'), 4860 suggestedHeight: state.get('suggestedHeight'), 4861 4862 AttachmentView: state.get('AttachmentView') 4863 }); 4864 }, 4865 4866 /** 4867 * Render callback for the content region in the `upload` mode. 4868 */ 4869 uploadContent: function() { 4870 this.$el.removeClass( 'hide-toolbar' ); 4871 this.content.set( new UploaderInline({ 4872 controller: this 4873 }) ); 4874 }, 4875 4876 /** 4877 * Toolbars 4878 * 4879 * @param {Object} toolbar 4880 * @param {Object} [options={}] 4881 * @this wp.media.controller.Region 4882 */ 4883 createSelectToolbar: function( toolbar, options ) { 4884 options = options || this.options.button || {}; 4885 options.controller = this; 4886 4887 toolbar.view = new ToolbarSelect( options ); 4888 } 4889 }); 4890 4891 module.exports = Select; 4892 },{"../../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){ 4893 /** 4894 * wp.media.view.MediaFrame.VideoDetails 4895 * 4896 * @constructor 4897 * @augments wp.media.view.MediaFrame.MediaDetails 4898 * @augments wp.media.view.MediaFrame.Select 4899 * @augments wp.media.view.MediaFrame 4900 * @augments wp.media.view.Frame 4901 * @augments wp.media.View 4902 * @augments wp.Backbone.View 4903 * @augments Backbone.View 4904 * @mixes wp.media.controller.StateMachine 4905 */ 4906 var MediaDetails = require( './media-details' ), 4907 MediaLibrary = require( '../../controllers/media-library.js' ), 4908 VideoDetailsView = require( '../video-details.js' ), 4909 VideoDetailsController = require( '../../controllers/video-details.js' ), 4910 l10n = wp.media.view.l10n, 4911 VideoDetails; 4912 4913 VideoDetails = MediaDetails.extend({ 4914 defaults: { 4915 id: 'video', 4916 url: '', 4917 menu: 'video-details', 4918 content: 'video-details', 4919 toolbar: 'video-details', 4920 type: 'link', 4921 title: l10n.videoDetailsTitle, 4922 priority: 120 4923 }, 4924 4925 initialize: function( options ) { 4926 options.DetailsView = VideoDetailsView; 4927 options.cancelText = l10n.videoDetailsCancel; 4928 options.addText = l10n.videoAddSourceTitle; 4929 4930 MediaDetails.prototype.initialize.call( this, options ); 4931 }, 4932 4933 bindHandlers: function() { 4934 MediaDetails.prototype.bindHandlers.apply( this, arguments ); 4935 4936 this.on( 'toolbar:render:replace-video', this.renderReplaceToolbar, this ); 4937 this.on( 'toolbar:render:add-video-source', this.renderAddSourceToolbar, this ); 4938 this.on( 'toolbar:render:select-poster-image', this.renderSelectPosterImageToolbar, this ); 4939 this.on( 'toolbar:render:add-track', this.renderAddTrackToolbar, this ); 4940 }, 4941 4942 createStates: function() { 4943 this.states.add([ 4944 new VideoDetailsController({ 4945 media: this.media 4946 }), 4947 4948 new MediaLibrary( { 4949 type: 'video', 4950 id: 'replace-video', 4951 title: l10n.videoReplaceTitle, 4952 toolbar: 'replace-video', 4953 media: this.media, 4954 menu: 'video-details' 4955 } ), 4956 4957 new MediaLibrary( { 4958 type: 'video', 4959 id: 'add-video-source', 4960 title: l10n.videoAddSourceTitle, 4961 toolbar: 'add-video-source', 4962 media: this.media, 4963 menu: false 4964 } ), 4965 4966 new MediaLibrary( { 4967 type: 'image', 4968 id: 'select-poster-image', 4969 title: l10n.videoSelectPosterImageTitle, 4970 toolbar: 'select-poster-image', 4971 media: this.media, 4972 menu: 'video-details' 4973 } ), 4974 4975 new MediaLibrary( { 4976 type: 'text', 4977 id: 'add-track', 4978 title: l10n.videoAddTrackTitle, 4979 toolbar: 'add-track', 4980 media: this.media, 4981 menu: 'video-details' 4982 } ) 4983 ]); 4984 }, 4985 4986 renderSelectPosterImageToolbar: function() { 4987 this.setPrimaryButton( l10n.videoSelectPosterImageTitle, function( controller, state ) { 4988 var urls = [], attachment = state.get( 'selection' ).single(); 4989 4990 controller.media.set( 'poster', attachment.get( 'url' ) ); 4991 state.trigger( 'set-poster-image', controller.media.toJSON() ); 4992 4993 _.each( wp.media.view.settings.embedExts, function (ext) { 4994 if ( controller.media.get( ext ) ) { 4995 urls.push( controller.media.get( ext ) ); 4996 } 4997 } ); 4998 4999 wp.ajax.send( 'set-attachment-thumbnail', { 5000 data : { 5001 urls: urls, 5002 thumbnail_id: attachment.get( 'id' ) 5003 } 5004 } ); 5005 } ); 5006 }, 5007 5008 renderAddTrackToolbar: function() { 5009 this.setPrimaryButton( l10n.videoAddTrackTitle, function( controller, state ) { 5010 var attachment = state.get( 'selection' ).single(), 5011 content = controller.media.get( 'content' ); 5012 5013 if ( -1 === content.indexOf( attachment.get( 'url' ) ) ) { 5014 content += [ 5015 '<track srclang="en" label="English"kind="subtitles" src="', 5016 attachment.get( 'url' ), 5017 '" />' 5018 ].join(''); 5019 5020 controller.media.set( 'content', content ); 5021 } 5022 state.trigger( 'add-track', controller.media.toJSON() ); 5023 } ); 5024 } 5025 }); 5026 5027 module.exports = VideoDetails; 5028 },{"../../controllers/media-library.js":4,"../../controllers/video-details.js":8,"../video-details.js":54,"./media-details":30}],33:[function(require,module,exports){ 5029 /** 5030 * wp.media.view.Iframe 5031 * 5032 * @class 5033 * @augments wp.media.View 5034 * @augments wp.Backbone.View 5035 * @augments Backbone.View 5036 */ 5037 var View = require( './view.js' ), 5038 Iframe; 5039 5040 Iframe = View.extend({ 5041 className: 'media-iframe', 5042 /** 5043 * @returns {wp.media.view.Iframe} Returns itself to allow chaining 5044 */ 5045 render: function() { 5046 this.views.detach(); 5047 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' ); 5048 this.views.render(); 5049 return this; 5050 } 5051 }); 5052 5053 module.exports = Iframe; 5054 },{"./view.js":55}],34:[function(require,module,exports){ 5055 /** 5056 * @class 5057 * @augments wp.media.View 5058 * @augments wp.Backbone.View 5059 * @augments Backbone.View 5060 */ 5061 var View = require( './view.js' ), 5062 Label; 5063 5064 Label = View.extend({ 5065 tagName: 'label', 5066 className: 'screen-reader-text', 5067 5068 initialize: function() { 5069 this.value = this.options.value; 5070 }, 5071 5072 render: function() { 5073 this.$el.html( this.value ); 5074 5075 return this; 5076 } 5077 }); 5078 5079 module.exports = Label; 5080 },{"./view.js":55}],35:[function(require,module,exports){ 5081 /** 5082 * wp.media.view.MediaDetails 5083 * 5084 * @constructor 5085 * @augments wp.media.view.Settings.AttachmentDisplay 5086 * @augments wp.media.view.Settings 5087 * @augments wp.media.View 5088 * @augments wp.Backbone.View 5089 * @augments Backbone.View 5090 */ 5091 var AttachmentDisplay = require( './settings/attachment-display.js' ), 5092 $ = jQuery, 5093 MediaDetails; 5094 5095 MediaDetails = AttachmentDisplay.extend({ 5096 initialize: function() { 5097 _.bindAll(this, 'success'); 5098 this.players = []; 5099 this.listenTo( this.controller, 'close', wp.media.mixin.unsetPlayers ); 5100 this.on( 'ready', this.setPlayer ); 5101 this.on( 'media:setting:remove', wp.media.mixin.unsetPlayers, this ); 5102 this.on( 'media:setting:remove', this.render ); 5103 this.on( 'media:setting:remove', this.setPlayer ); 5104 this.events = _.extend( this.events, { 5105 'click .remove-setting' : 'removeSetting', 5106 'change .content-track' : 'setTracks', 5107 'click .remove-track' : 'setTracks', 5108 'click .add-media-source' : 'addSource' 5109 } ); 5110 5111 AttachmentDisplay.prototype.initialize.apply( this, arguments ); 5112 }, 5113 5114 prepare: function() { 5115 return _.defaults({ 5116 model: this.model.toJSON() 5117 }, this.options ); 5118 }, 5119 5120 /** 5121 * Remove a setting's UI when the model unsets it 5122 * 5123 * @fires wp.media.view.MediaDetails#media:setting:remove 5124 * 5125 * @param {Event} e 5126 */ 5127 removeSetting : function(e) { 5128 var wrap = $( e.currentTarget ).parent(), setting; 5129 setting = wrap.find( 'input' ).data( 'setting' ); 5130 5131 if ( setting ) { 5132 this.model.unset( setting ); 5133 this.trigger( 'media:setting:remove', this ); 5134 } 5135 5136 wrap.remove(); 5137 }, 5138 5139 /** 5140 * 5141 * @fires wp.media.view.MediaDetails#media:setting:remove 5142 */ 5143 setTracks : function() { 5144 var tracks = ''; 5145 5146 _.each( this.$('.content-track'), function(track) { 5147 tracks += $( track ).val(); 5148 } ); 5149 5150 this.model.set( 'content', tracks ); 5151 this.trigger( 'media:setting:remove', this ); 5152 }, 5153 5154 addSource : function( e ) { 5155 this.controller.lastMime = $( e.currentTarget ).data( 'mime' ); 5156 this.controller.setState( 'add-' + this.controller.defaults.id + '-source' ); 5157 }, 5158 5159 /** 5160 * @global MediaElementPlayer 5161 */ 5162 setPlayer : function() { 5163 if ( ! this.players.length && this.media ) { 5164 this.players.push( new MediaElementPlayer( this.media, this.settings ) ); 5165 } 5166 }, 5167 5168 /** 5169 * @abstract 5170 */ 5171 setMedia : function() { 5172 return this; 5173 }, 5174 5175 success : function(mejs) { 5176 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; 5177 5178 if ( 'flash' === mejs.pluginType && autoplay ) { 5179 mejs.addEventListener( 'canplay', function() { 5180 mejs.play(); 5181 }, false ); 5182 } 5183 5184 this.mejs = mejs; 5185 }, 5186 5187 /** 5188 * @returns {media.view.MediaDetails} Returns itself to allow chaining 5189 */ 5190 render: function() { 5191 var self = this; 5192 5193 AttachmentDisplay.prototype.render.apply( this, arguments ); 5194 setTimeout( function() { self.resetFocus(); }, 10 ); 5195 5196 this.settings = _.defaults( { 5197 success : this.success 5198 }, wp.media.mixin.mejsSettings ); 5199 5200 return this.setMedia(); 5201 }, 5202 5203 resetFocus: function() { 5204 this.$( '.embed-media-settings' ).scrollTop( 0 ); 5205 } 5206 }, { 5207 instances : 0, 5208 /** 5209 * When multiple players in the DOM contain the same src, things get weird. 5210 * 5211 * @param {HTMLElement} elem 5212 * @returns {HTMLElement} 5213 */ 5214 prepareSrc : function( elem ) { 5215 var i = MediaDetails.instances++; 5216 _.each( $( elem ).find( 'source' ), function( source ) { 5217 source.src = [ 5218 source.src, 5219 source.src.indexOf('?') > -1 ? '&' : '?', 5220 '_=', 5221 i 5222 ].join(''); 5223 } ); 5224 5225 return elem; 5226 } 5227 }); 5228 5229 module.exports = MediaDetails; 5230 },{"./settings/attachment-display.js":45}],36:[function(require,module,exports){ 5231 /** 5232 * wp.media.view.MediaFrame 5233 * 5234 * The frame used to create the media modal. 5235 * 5236 * @class 5237 * @augments wp.media.view.Frame 5238 * @augments wp.media.View 5239 * @augments wp.Backbone.View 5240 * @augments Backbone.View 5241 * @mixes wp.media.controller.StateMachine 5242 */ 5243 var View = require( './view.js' ), 5244 Frame = require( './frame.js' ), 5245 Modal = require( './modal.js' ), 5246 UploaderWindow = require( './uploader/window.js' ), 5247 Menu = require( './menu.js' ), 5248 Toolbar = require( './toolbar.js' ), 5249 Router = require( './router.js' ), 5250 Iframe = require( './iframe.js' ), 5251 $ = jQuery, 5252 MediaFrame; 5253 5254 MediaFrame = Frame.extend({ 5255 className: 'media-frame', 5256 template: wp.template('media-frame'), 5257 regions: ['menu','title','content','toolbar','router'], 5258 5259 events: { 5260 'click div.media-frame-title h1': 'toggleMenu' 5261 }, 5262 5263 /** 5264 * @global wp.Uploader 5265 */ 5266 initialize: function() { 5267 Frame.prototype.initialize.apply( this, arguments ); 5268 5269 _.defaults( this.options, { 5270 title: '', 5271 modal: true, 5272 uploader: true 5273 }); 5274 5275 // Ensure core UI is enabled. 5276 this.$el.addClass('wp-core-ui'); 5277 5278 // Initialize modal container view. 5279 if ( this.options.modal ) { 5280 this.modal = new Modal({ 5281 controller: this, 5282 title: this.options.title 5283 }); 5284 5285 this.modal.content( this ); 5286 } 5287 5288 // Force the uploader off if the upload limit has been exceeded or 5289 // if the browser isn't supported. 5290 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 5291 this.options.uploader = false; 5292 } 5293 5294 // Initialize window-wide uploader. 5295 if ( this.options.uploader ) { 5296 this.uploader = new UploaderWindow({ 5297 controller: this, 5298 uploader: { 5299 dropzone: this.modal ? this.modal.$el : this.$el, 5300 container: this.$el 5301 } 5302 }); 5303 this.views.set( '.media-frame-uploader', this.uploader ); 5304 } 5305 5306 this.on( 'attach', _.bind( this.views.ready, this.views ), this ); 5307 5308 // Bind default title creation. 5309 this.on( 'title:create:default', this.createTitle, this ); 5310 this.title.mode('default'); 5311 5312 this.on( 'title:render', function( view ) { 5313 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' ); 5314 }); 5315 5316 // Bind default menu. 5317 this.on( 'menu:create:default', this.createMenu, this ); 5318 }, 5319 /** 5320 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 5321 */ 5322 render: function() { 5323 // Activate the default state if no active state exists. 5324 if ( ! this.state() && this.options.state ) { 5325 this.setState( this.options.state ); 5326 } 5327 /** 5328 * call 'render' directly on the parent class 5329 */ 5330 return Frame.prototype.render.apply( this, arguments ); 5331 }, 5332 /** 5333 * @param {Object} title 5334 * @this wp.media.controller.Region 5335 */ 5336 createTitle: function( title ) { 5337 title.view = new View({ 5338 controller: this, 5339 tagName: 'h1' 5340 }); 5341 }, 5342 /** 5343 * @param {Object} menu 5344 * @this wp.media.controller.Region 5345 */ 5346 createMenu: function( menu ) { 5347 menu.view = new Menu({ 5348 controller: this 5349 }); 5350 }, 5351 5352 toggleMenu: function() { 5353 this.$el.find( '.media-menu' ).toggleClass( 'visible' ); 5354 }, 5355 5356 /** 5357 * @param {Object} toolbar 5358 * @this wp.media.controller.Region 5359 */ 5360 createToolbar: function( toolbar ) { 5361 toolbar.view = new Toolbar({ 5362 controller: this 5363 }); 5364 }, 5365 /** 5366 * @param {Object} router 5367 * @this wp.media.controller.Region 5368 */ 5369 createRouter: function( router ) { 5370 router.view = new Router({ 5371 controller: this 5372 }); 5373 }, 5374 /** 5375 * @param {Object} options 5376 */ 5377 createIframeStates: function( options ) { 5378 var settings = wp.media.view.settings, 5379 tabs = settings.tabs, 5380 tabUrl = settings.tabUrl, 5381 $postId; 5382 5383 if ( ! tabs || ! tabUrl ) { 5384 return; 5385 } 5386 5387 // Add the post ID to the tab URL if it exists. 5388 $postId = $('#post_ID'); 5389 if ( $postId.length ) { 5390 tabUrl += '&post_id=' + $postId.val(); 5391 } 5392 5393 // Generate the tab states. 5394 _.each( tabs, function( title, id ) { 5395 this.state( 'iframe:' + id ).set( _.defaults({ 5396 tab: id, 5397 src: tabUrl + '&tab=' + id, 5398 title: title, 5399 content: 'iframe', 5400 menu: 'default' 5401 }, options ) ); 5402 }, this ); 5403 5404 this.on( 'content:create:iframe', this.iframeContent, this ); 5405 this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this ); 5406 this.on( 'menu:render:default', this.iframeMenu, this ); 5407 this.on( 'open', this.hijackThickbox, this ); 5408 this.on( 'close', this.restoreThickbox, this ); 5409 }, 5410 5411 /** 5412 * @param {Object} content 5413 * @this wp.media.controller.Region 5414 */ 5415 iframeContent: function( content ) { 5416 this.$el.addClass('hide-toolbar'); 5417 content.view = new Iframe({ 5418 controller: this 5419 }); 5420 }, 5421 5422 iframeContentCleanup: function() { 5423 this.$el.removeClass('hide-toolbar'); 5424 }, 5425 5426 iframeMenu: function( view ) { 5427 var views = {}; 5428 5429 if ( ! view ) { 5430 return; 5431 } 5432 5433 _.each( wp.media.view.settings.tabs, function( title, id ) { 5434 views[ 'iframe:' + id ] = { 5435 text: this.state( 'iframe:' + id ).get('title'), 5436 priority: 200 5437 }; 5438 }, this ); 5439 5440 view.set( views ); 5441 }, 5442 5443 hijackThickbox: function() { 5444 var frame = this; 5445 5446 if ( ! window.tb_remove || this._tb_remove ) { 5447 return; 5448 } 5449 5450 this._tb_remove = window.tb_remove; 5451 window.tb_remove = function() { 5452 frame.close(); 5453 frame.reset(); 5454 frame.setState( frame.options.state ); 5455 frame._tb_remove.call( window ); 5456 }; 5457 }, 5458 5459 restoreThickbox: function() { 5460 if ( ! this._tb_remove ) { 5461 return; 5462 } 5463 5464 window.tb_remove = this._tb_remove; 5465 delete this._tb_remove; 5466 } 5467 }); 5468 5469 // Map some of the modal's methods to the frame. 5470 _.each(['open','close','attach','detach','escape'], function( method ) { 5471 /** 5472 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 5473 */ 5474 MediaFrame.prototype[ method ] = function() { 5475 if ( this.modal ) { 5476 this.modal[ method ].apply( this.modal, arguments ); 5477 } 5478 return this; 5479 }; 5480 }); 5481 5482 module.exports = MediaFrame; 5483 },{"./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){ 5484 /** 5485 * wp.media.view.MenuItem 5486 * 5487 * @class 5488 * @augments wp.media.View 5489 * @augments wp.Backbone.View 5490 * @augments Backbone.View 5491 */ 5492 var View = require( './view.js' ), 5493 $ = jQuery, 5494 MenuItem; 5495 5496 MenuItem = View.extend({ 5497 tagName: 'a', 5498 className: 'media-menu-item', 5499 5500 attributes: { 5501 href: '#' 5502 }, 5503 5504 events: { 5505 'click': '_click' 5506 }, 5507 /** 5508 * @param {Object} event 5509 */ 5510 _click: function( event ) { 5511 var clickOverride = this.options.click; 5512 5513 if ( event ) { 5514 event.preventDefault(); 5515 } 5516 5517 if ( clickOverride ) { 5518 clickOverride.call( this ); 5519 } else { 5520 this.click(); 5521 } 5522 5523 // When selecting a tab along the left side, 5524 // focus should be transferred into the main panel 5525 if ( ! wp.media.isTouchDevice ) { 5526 $('.media-frame-content input').first().focus(); 5527 } 5528 }, 5529 5530 click: function() { 5531 var state = this.options.state; 5532 5533 if ( state ) { 5534 this.controller.setState( state ); 5535 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below 5536 } 5537 }, 5538 /** 5539 * @returns {wp.media.view.MenuItem} returns itself to allow chaining 5540 */ 5541 render: function() { 5542 var options = this.options; 5543 5544 if ( options.text ) { 5545 this.$el.text( options.text ); 5546 } else if ( options.html ) { 5547 this.$el.html( options.html ); 5548 } 5549 5550 return this; 5551 } 5552 }); 5553 5554 module.exports = MenuItem; 5555 },{"./view.js":55}],38:[function(require,module,exports){ 5556 /** 5557 * wp.media.view.Menu 5558 * 5559 * @class 5560 * @augments wp.media.view.PriorityList 5561 * @augments wp.media.View 5562 * @augments wp.Backbone.View 5563 * @augments Backbone.View 5564 */ 5565 var MenuItem = require( './menu-item.js' ), 5566 PriorityList = require( './priority-list.js' ), 5567 Menu; 5568 5569 Menu = PriorityList.extend({ 5570 tagName: 'div', 5571 className: 'media-menu', 5572 property: 'state', 5573 ItemView: MenuItem, 5574 region: 'menu', 5575 5576 /* TODO: alternatively hide on any click anywhere 5577 events: { 5578 'click': 'click' 5579 }, 5580 5581 click: function() { 5582 this.$el.removeClass( 'visible' ); 5583 }, 5584 */ 5585 5586 /** 5587 * @param {Object} options 5588 * @param {string} id 5589 * @returns {wp.media.View} 5590 */ 5591 toView: function( options, id ) { 5592 options = options || {}; 5593 options[ this.property ] = options[ this.property ] || id; 5594 return new this.ItemView( options ).render(); 5595 }, 5596 5597 ready: function() { 5598 /** 5599 * call 'ready' directly on the parent class 5600 */ 5601 PriorityList.prototype.ready.apply( this, arguments ); 5602 this.visibility(); 5603 }, 5604 5605 set: function() { 5606 /** 5607 * call 'set' directly on the parent class 5608 */ 5609 PriorityList.prototype.set.apply( this, arguments ); 5610 this.visibility(); 5611 }, 5612 5613 unset: function() { 5614 /** 5615 * call 'unset' directly on the parent class 5616 */ 5617 PriorityList.prototype.unset.apply( this, arguments ); 5618 this.visibility(); 5619 }, 5620 5621 visibility: function() { 5622 var region = this.region, 5623 view = this.controller[ region ].get(), 5624 views = this.views.get(), 5625 hide = ! views || views.length < 2; 5626 5627 if ( this === view ) { 5628 this.controller.$el.toggleClass( 'hide-' + region, hide ); 5629 } 5630 }, 5631 /** 5632 * @param {string} id 5633 */ 5634 select: function( id ) { 5635 var view = this.get( id ); 5636 5637 if ( ! view ) { 5638 return; 5639 } 5640 5641 this.deselect(); 5642 view.$el.addClass('active'); 5643 }, 5644 5645 deselect: function() { 5646 this.$el.children().removeClass('active'); 5647 }, 5648 5649 hide: function( id ) { 5650 var view = this.get( id ); 5651 5652 if ( ! view ) { 5653 return; 5654 } 5655 5656 view.$el.addClass('hidden'); 5657 }, 5658 5659 show: function( id ) { 5660 var view = this.get( id ); 5661 5662 if ( ! view ) { 5663 return; 5664 } 5665 5666 view.$el.removeClass('hidden'); 5667 } 5668 }); 5669 5670 module.exports = Menu; 5671 },{"./menu-item.js":37,"./priority-list.js":40}],39:[function(require,module,exports){ 5672 /** 5673 * wp.media.view.Modal 5674 * 5675 * A modal view, which the media modal uses as its default container. 5676 * 5677 * @class 5678 * @augments wp.media.View 5679 * @augments wp.Backbone.View 5680 * @augments Backbone.View 5681 */ 5682 var View = require( './view.js' ), 5683 FocusManager = require( './focus-manager.js' ), 5684 $ = jQuery, 5685 Modal; 5686 5687 Modal = View.extend({ 5688 tagName: 'div', 5689 template: wp.template('media-modal'), 5690 5691 attributes: { 5692 tabindex: 0 5693 }, 5694 5695 events: { 5696 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 5697 'keydown': 'keydown' 5698 }, 5699 5700 initialize: function() { 5701 _.defaults( this.options, { 5702 container: document.body, 5703 title: '', 5704 propagate: true, 5705 freeze: true 5706 }); 5707 5708 this.focusManager = new FocusManager({ 5709 el: this.el 5710 }); 5711 }, 5712 /** 5713 * @returns {Object} 5714 */ 5715 prepare: function() { 5716 return { 5717 title: this.options.title 5718 }; 5719 }, 5720 5721 /** 5722 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5723 */ 5724 attach: function() { 5725 if ( this.views.attached ) { 5726 return this; 5727 } 5728 5729 if ( ! this.views.rendered ) { 5730 this.render(); 5731 } 5732 5733 this.$el.appendTo( this.options.container ); 5734 5735 // Manually mark the view as attached and trigger ready. 5736 this.views.attached = true; 5737 this.views.ready(); 5738 5739 return this.propagate('attach'); 5740 }, 5741 5742 /** 5743 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5744 */ 5745 detach: function() { 5746 if ( this.$el.is(':visible') ) { 5747 this.close(); 5748 } 5749 5750 this.$el.detach(); 5751 this.views.attached = false; 5752 return this.propagate('detach'); 5753 }, 5754 5755 /** 5756 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5757 */ 5758 open: function() { 5759 var $el = this.$el, 5760 options = this.options, 5761 mceEditor; 5762 5763 if ( $el.is(':visible') ) { 5764 return this; 5765 } 5766 5767 if ( ! this.views.attached ) { 5768 this.attach(); 5769 } 5770 5771 // If the `freeze` option is set, record the window's scroll position. 5772 if ( options.freeze ) { 5773 this._freeze = { 5774 scrollTop: $( window ).scrollTop() 5775 }; 5776 } 5777 5778 // Disable page scrolling. 5779 $( 'body' ).addClass( 'modal-open' ); 5780 5781 $el.show(); 5782 5783 // Try to close the onscreen keyboard 5784 if ( 'ontouchend' in document ) { 5785 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) { 5786 mceEditor.iframeElement.focus(); 5787 mceEditor.iframeElement.blur(); 5788 5789 setTimeout( function() { 5790 mceEditor.iframeElement.blur(); 5791 }, 100 ); 5792 } 5793 } 5794 5795 this.$el.focus(); 5796 5797 return this.propagate('open'); 5798 }, 5799 5800 /** 5801 * @param {Object} options 5802 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5803 */ 5804 close: function( options ) { 5805 var freeze = this._freeze; 5806 5807 if ( ! this.views.attached || ! this.$el.is(':visible') ) { 5808 return this; 5809 } 5810 5811 // Enable page scrolling. 5812 $( 'body' ).removeClass( 'modal-open' ); 5813 5814 // Hide modal and remove restricted media modal tab focus once it's closed 5815 this.$el.hide().undelegate( 'keydown' ); 5816 5817 // Put focus back in useful location once modal is closed 5818 $('#wpbody-content').focus(); 5819 5820 this.propagate('close'); 5821 5822 // If the `freeze` option is set, restore the container's scroll position. 5823 if ( freeze ) { 5824 $( window ).scrollTop( freeze.scrollTop ); 5825 } 5826 5827 if ( options && options.escape ) { 5828 this.propagate('escape'); 5829 } 5830 5831 return this; 5832 }, 5833 /** 5834 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5835 */ 5836 escape: function() { 5837 return this.close({ escape: true }); 5838 }, 5839 /** 5840 * @param {Object} event 5841 */ 5842 escapeHandler: function( event ) { 5843 event.preventDefault(); 5844 this.escape(); 5845 }, 5846 5847 /** 5848 * @param {Array|Object} content Views to register to '.media-modal-content' 5849 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5850 */ 5851 content: function( content ) { 5852 this.views.set( '.media-modal-content', content ); 5853 return this; 5854 }, 5855 5856 /** 5857 * Triggers a modal event and if the `propagate` option is set, 5858 * forwards events to the modal's controller. 5859 * 5860 * @param {string} id 5861 * @returns {wp.media.view.Modal} Returns itself to allow chaining 5862 */ 5863 propagate: function( id ) { 5864 this.trigger( id ); 5865 5866 if ( this.options.propagate ) { 5867 this.controller.trigger( id ); 5868 } 5869 5870 return this; 5871 }, 5872 /** 5873 * @param {Object} event 5874 */ 5875 keydown: function( event ) { 5876 // Close the modal when escape is pressed. 5877 if ( 27 === event.which && this.$el.is(':visible') ) { 5878 this.escape(); 5879 event.stopImmediatePropagation(); 5880 } 5881 } 5882 }); 5883 5884 module.exports = Modal; 5885 },{"./focus-manager.js":27,"./view.js":55}],40:[function(require,module,exports){ 5886 /** 5887 * wp.media.view.PriorityList 5888 * 5889 * @class 5890 * @augments wp.media.View 5891 * @augments wp.Backbone.View 5892 * @augments Backbone.View 5893 */ 5894 var View = require( './view.js' ), 5895 PriorityList; 5896 5897 PriorityList = View.extend({ 5898 tagName: 'div', 5899 5900 initialize: function() { 5901 this._views = {}; 5902 5903 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 5904 delete this.options.views; 5905 5906 if ( ! this.options.silent ) { 5907 this.render(); 5908 } 5909 }, 5910 /** 5911 * @param {string} id 5912 * @param {wp.media.View|Object} view 5913 * @param {Object} options 5914 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining 5915 */ 5916 set: function( id, view, options ) { 5917 var priority, views, index; 5918 5919 options = options || {}; 5920 5921 // Accept an object with an `id` : `view` mapping. 5922 if ( _.isObject( id ) ) { 5923 _.each( id, function( view, id ) { 5924 this.set( id, view ); 5925 }, this ); 5926 return this; 5927 } 5928 5929 if ( ! (view instanceof Backbone.View) ) { 5930 view = this.toView( view, id, options ); 5931 } 5932 view.controller = view.controller || this.controller; 5933 5934 this.unset( id ); 5935 5936 priority = view.options.priority || 10; 5937 views = this.views.get() || []; 5938 5939 _.find( views, function( existing, i ) { 5940 if ( existing.options.priority > priority ) { 5941 index = i; 5942 return true; 5943 } 5944 }); 5945 5946 this._views[ id ] = view; 5947 this.views.add( view, { 5948 at: _.isNumber( index ) ? index : views.length || 0 5949 }); 5950 5951 return this; 5952 }, 5953 /** 5954 * @param {string} id 5955 * @returns {wp.media.View} 5956 */ 5957 get: function( id ) { 5958 return this._views[ id ]; 5959 }, 5960 /** 5961 * @param {string} id 5962 * @returns {wp.media.view.PriorityList} 5963 */ 5964 unset: function( id ) { 5965 var view = this.get( id ); 5966 5967 if ( view ) { 5968 view.remove(); 5969 } 5970 5971 delete this._views[ id ]; 5972 return this; 5973 }, 5974 /** 5975 * @param {Object} options 5976 * @returns {wp.media.View} 5977 */ 5978 toView: function( options ) { 5979 return new View( options ); 5980 } 5981 }); 5982 5983 module.exports = PriorityList; 5984 },{"./view.js":55}],41:[function(require,module,exports){ 5985 /** 5986 * wp.media.view.RouterItem 5987 * 5988 * @class 5989 * @augments wp.media.view.MenuItem 5990 * @augments wp.media.View 5991 * @augments wp.Backbone.View 5992 * @augments Backbone.View 5993 */ 5994 var MenuItem = require( './menu-item.js' ), 5995 RouterItem; 5996 5997 RouterItem = MenuItem.extend({ 5998 /** 5999 * On click handler to activate the content region's corresponding mode. 6000 */ 6001 click: function() { 6002 var contentMode = this.options.contentMode; 6003 if ( contentMode ) { 6004 this.controller.content.mode( contentMode ); 6005 } 6006 } 6007 }); 6008 6009 module.exports = RouterItem; 6010 },{"./menu-item.js":37}],42:[function(require,module,exports){ 6011 /** 6012 * wp.media.view.Router 6013 * 6014 * @class 6015 * @augments wp.media.view.Menu 6016 * @augments wp.media.view.PriorityList 6017 * @augments wp.media.View 6018 * @augments wp.Backbone.View 6019 * @augments Backbone.View 6020 */ 6021 var Menu = require( './menu.js' ), 6022 RouterItem = require( './router-item.js' ), 6023 Router; 6024 6025 Router = Menu.extend({ 6026 tagName: 'div', 6027 className: 'media-router', 6028 property: 'contentMode', 6029 ItemView: RouterItem, 6030 region: 'router', 6031 6032 initialize: function() { 6033 this.controller.on( 'content:render', this.update, this ); 6034 // Call 'initialize' directly on the parent class. 6035 Menu.prototype.initialize.apply( this, arguments ); 6036 }, 6037 6038 update: function() { 6039 var mode = this.controller.content.mode(); 6040 if ( mode ) { 6041 this.select( mode ); 6042 } 6043 } 6044 }); 6045 6046 module.exports = Router; 6047 },{"./menu.js":38,"./router-item.js":41}],43:[function(require,module,exports){ 6048 /** 6049 * wp.media.view.Search 6050 * 6051 * @class 6052 * @augments wp.media.View 6053 * @augments wp.Backbone.View 6054 * @augments Backbone.View 6055 */ 6056 var View = require( './view.js' ), 6057 l10n = wp.media.view.l10n, 6058 Search; 6059 6060 Search = View.extend({ 6061 tagName: 'input', 6062 className: 'search', 6063 id: 'media-search-input', 6064 6065 attributes: { 6066 type: 'search', 6067 placeholder: l10n.search 6068 }, 6069 6070 events: { 6071 'input': 'search', 6072 'keyup': 'search', 6073 'change': 'search', 6074 'search': 'search' 6075 }, 6076 6077 /** 6078 * @returns {wp.media.view.Search} Returns itself to allow chaining 6079 */ 6080 render: function() { 6081 this.el.value = this.model.escape('search'); 6082 return this; 6083 }, 6084 6085 search: function( event ) { 6086 if ( event.target.value ) { 6087 this.model.set( 'search', event.target.value ); 6088 } else { 6089 this.model.unset('search'); 6090 } 6091 } 6092 }); 6093 6094 module.exports = Search; 6095 },{"./view.js":55}],44:[function(require,module,exports){ 6096 /** 6097 * wp.media.view.Settings 6098 * 6099 * @class 6100 * @augments wp.media.View 6101 * @augments wp.Backbone.View 6102 * @augments Backbone.View 6103 */ 6104 var View = require( './view.js' ), 6105 $ = jQuery, 6106 Settings; 6107 6108 Settings = View.extend({ 6109 events: { 6110 'click button': 'updateHandler', 6111 'change input': 'updateHandler', 6112 'change select': 'updateHandler', 6113 'change textarea': 'updateHandler' 6114 }, 6115 6116 initialize: function() { 6117 this.model = this.model || new Backbone.Model(); 6118 this.model.on( 'change', this.updateChanges, this ); 6119 }, 6120 6121 prepare: function() { 6122 return _.defaults({ 6123 model: this.model.toJSON() 6124 }, this.options ); 6125 }, 6126 /** 6127 * @returns {wp.media.view.Settings} Returns itself to allow chaining 6128 */ 6129 render: function() { 6130 View.prototype.render.apply( this, arguments ); 6131 // Select the correct values. 6132 _( this.model.attributes ).chain().keys().each( this.update, this ); 6133 return this; 6134 }, 6135 /** 6136 * @param {string} key 6137 */ 6138 update: function( key ) { 6139 var value = this.model.get( key ), 6140 $setting = this.$('[data-setting="' + key + '"]'), 6141 $buttons, $value; 6142 6143 // Bail if we didn't find a matching setting. 6144 if ( ! $setting.length ) { 6145 return; 6146 } 6147 6148 // Attempt to determine how the setting is rendered and update 6149 // the selected value. 6150 6151 // Handle dropdowns. 6152 if ( $setting.is('select') ) { 6153 $value = $setting.find('[value="' + value + '"]'); 6154 6155 if ( $value.length ) { 6156 $setting.find('option').prop( 'selected', false ); 6157 $value.prop( 'selected', true ); 6158 } else { 6159 // If we can't find the desired value, record what *is* selected. 6160 this.model.set( key, $setting.find(':selected').val() ); 6161 } 6162 6163 // Handle button groups. 6164 } else if ( $setting.hasClass('button-group') ) { 6165 $buttons = $setting.find('button').removeClass('active'); 6166 $buttons.filter( '[value="' + value + '"]' ).addClass('active'); 6167 6168 // Handle text inputs and textareas. 6169 } else if ( $setting.is('input[type="text"], textarea') ) { 6170 if ( ! $setting.is(':focus') ) { 6171 $setting.val( value ); 6172 } 6173 // Handle checkboxes. 6174 } else if ( $setting.is('input[type="checkbox"]') ) { 6175 $setting.prop( 'checked', !! value && 'false' !== value ); 6176 } 6177 }, 6178 /** 6179 * @param {Object} event 6180 */ 6181 updateHandler: function( event ) { 6182 var $setting = $( event.target ).closest('[data-setting]'), 6183 value = event.target.value, 6184 userSetting; 6185 6186 event.preventDefault(); 6187 6188 if ( ! $setting.length ) { 6189 return; 6190 } 6191 6192 // Use the correct value for checkboxes. 6193 if ( $setting.is('input[type="checkbox"]') ) { 6194 value = $setting[0].checked; 6195 } 6196 6197 // Update the corresponding setting. 6198 this.model.set( $setting.data('setting'), value ); 6199 6200 // If the setting has a corresponding user setting, 6201 // update that as well. 6202 if ( userSetting = $setting.data('userSetting') ) { 6203 setUserSetting( userSetting, value ); 6204 } 6205 }, 6206 6207 updateChanges: function( model ) { 6208 if ( model.hasChanged() ) { 6209 _( model.changed ).chain().keys().each( this.update, this ); 6210 } 6211 } 6212 }); 6213 6214 module.exports = Settings; 6215 },{"./view.js":55}],45:[function(require,module,exports){ 6216 /** 6217 * wp.media.view.Settings.AttachmentDisplay 6218 * 6219 * @class 6220 * @augments wp.media.view.Settings 6221 * @augments wp.media.View 6222 * @augments wp.Backbone.View 6223 * @augments Backbone.View 6224 */ 6225 var Settings = require( '../settings.js' ), 6226 AttachmentDisplay; 6227 6228 AttachmentDisplay = Settings.extend({ 6229 className: 'attachment-display-settings', 6230 template: wp.template('attachment-display-settings'), 6231 6232 initialize: function() { 6233 var attachment = this.options.attachment; 6234 6235 _.defaults( this.options, { 6236 userSettings: false 6237 }); 6238 // Call 'initialize' directly on the parent class. 6239 Settings.prototype.initialize.apply( this, arguments ); 6240 this.model.on( 'change:link', this.updateLinkTo, this ); 6241 6242 if ( attachment ) { 6243 attachment.on( 'change:uploading', this.render, this ); 6244 } 6245 }, 6246 6247 dispose: function() { 6248 var attachment = this.options.attachment; 6249 if ( attachment ) { 6250 attachment.off( null, null, this ); 6251 } 6252 /** 6253 * call 'dispose' directly on the parent class 6254 */ 6255 Settings.prototype.dispose.apply( this, arguments ); 6256 }, 6257 /** 6258 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining 6259 */ 6260 render: function() { 6261 var attachment = this.options.attachment; 6262 if ( attachment ) { 6263 _.extend( this.options, { 6264 sizes: attachment.get('sizes'), 6265 type: attachment.get('type') 6266 }); 6267 } 6268 /** 6269 * call 'render' directly on the parent class 6270 */ 6271 Settings.prototype.render.call( this ); 6272 this.updateLinkTo(); 6273 return this; 6274 }, 6275 6276 updateLinkTo: function() { 6277 var linkTo = this.model.get('link'), 6278 $input = this.$('.link-to-custom'), 6279 attachment = this.options.attachment; 6280 6281 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { 6282 $input.addClass( 'hidden' ); 6283 return; 6284 } 6285 6286 if ( attachment ) { 6287 if ( 'post' === linkTo ) { 6288 $input.val( attachment.get('link') ); 6289 } else if ( 'file' === linkTo ) { 6290 $input.val( attachment.get('url') ); 6291 } else if ( ! this.model.get('linkUrl') ) { 6292 $input.val('http://'); 6293 } 6294 6295 $input.prop( 'readonly', 'custom' !== linkTo ); 6296 } 6297 6298 $input.removeClass( 'hidden' ); 6299 6300 // If the input is visible, focus and select its contents. 6301 if ( ! wp.media.isTouchDevice && $input.is(':visible') ) { 6302 $input.focus()[0].select(); 6303 } 6304 } 6305 }); 6306 6307 module.exports = AttachmentDisplay; 6308 },{"../settings.js":44}],46:[function(require,module,exports){ 6309 /** 6310 * wp.media.view.Sidebar 6311 * 6312 * @class 6313 * @augments wp.media.view.PriorityList 6314 * @augments wp.media.View 6315 * @augments wp.Backbone.View 6316 * @augments Backbone.View 6317 */ 6318 var PriorityList = require( './priority-list.js' ), 6319 Sidebar; 6320 6321 Sidebar = PriorityList.extend({ 6322 className: 'media-sidebar' 6323 }); 6324 6325 module.exports = Sidebar; 6326 },{"./priority-list.js":40}],47:[function(require,module,exports){ 6327 /** 6328 * wp.media.view.Spinner 6329 * 6330 * @class 6331 * @augments wp.media.View 6332 * @augments wp.Backbone.View 6333 * @augments Backbone.View 6334 */ 6335 var View = require( './view.js' ), 6336 Spinner; 6337 6338 Spinner = View.extend({ 6339 tagName: 'span', 6340 className: 'spinner', 6341 spinnerTimeout: false, 6342 delay: 400, 6343 6344 show: function() { 6345 if ( ! this.spinnerTimeout ) { 6346 this.spinnerTimeout = _.delay(function( $el ) { 6347 $el.show(); 6348 }, this.delay, this.$el ); 6349 } 6350 6351 return this; 6352 }, 6353 6354 hide: function() { 6355 this.$el.hide(); 6356 this.spinnerTimeout = clearTimeout( this.spinnerTimeout ); 6357 6358 return this; 6359 } 6360 }); 6361 6362 module.exports = Spinner; 6363 },{"./view.js":55}],48:[function(require,module,exports){ 6364 /** 6365 * wp.media.view.Toolbar 6366 * 6367 * A toolbar which consists of a primary and a secondary section. Each sections 6368 * can be filled with views. 6369 * 6370 * @class 6371 * @augments wp.media.View 6372 * @augments wp.Backbone.View 6373 * @augments Backbone.View 6374 */ 6375 var View = require( './view.js' ), 6376 Button = require( './button.js' ), 6377 PriorityList = require( './priority-list.js' ), 6378 Toolbar; 6379 6380 Toolbar = View.extend({ 6381 tagName: 'div', 6382 className: 'media-toolbar', 6383 6384 initialize: function() { 6385 var state = this.controller.state(), 6386 selection = this.selection = state.get('selection'), 6387 library = this.library = state.get('library'); 6388 6389 this._views = {}; 6390 6391 // The toolbar is composed of two `PriorityList` views. 6392 this.primary = new PriorityList(); 6393 this.secondary = new PriorityList(); 6394 this.primary.$el.addClass('media-toolbar-primary search-form'); 6395 this.secondary.$el.addClass('media-toolbar-secondary'); 6396 6397 this.views.set([ this.secondary, this.primary ]); 6398 6399 if ( this.options.items ) { 6400 this.set( this.options.items, { silent: true }); 6401 } 6402 6403 if ( ! this.options.silent ) { 6404 this.render(); 6405 } 6406 6407 if ( selection ) { 6408 selection.on( 'add remove reset', this.refresh, this ); 6409 } 6410 6411 if ( library ) { 6412 library.on( 'add remove reset', this.refresh, this ); 6413 } 6414 }, 6415 /** 6416 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining 6417 */ 6418 dispose: function() { 6419 if ( this.selection ) { 6420 this.selection.off( null, null, this ); 6421 } 6422 6423 if ( this.library ) { 6424 this.library.off( null, null, this ); 6425 } 6426 /** 6427 * call 'dispose' directly on the parent class 6428 */ 6429 return View.prototype.dispose.apply( this, arguments ); 6430 }, 6431 6432 ready: function() { 6433 this.refresh(); 6434 }, 6435 6436 /** 6437 * @param {string} id 6438 * @param {Backbone.View|Object} view 6439 * @param {Object} [options={}] 6440 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 6441 */ 6442 set: function( id, view, options ) { 6443 var list; 6444 options = options || {}; 6445 6446 // Accept an object with an `id` : `view` mapping. 6447 if ( _.isObject( id ) ) { 6448 _.each( id, function( view, id ) { 6449 this.set( id, view, { silent: true }); 6450 }, this ); 6451 6452 } else { 6453 if ( ! ( view instanceof Backbone.View ) ) { 6454 view.classes = [ 'media-button-' + id ].concat( view.classes || [] ); 6455 view = new Button( view ).render(); 6456 } 6457 6458 view.controller = view.controller || this.controller; 6459 6460 this._views[ id ] = view; 6461 6462 list = view.options.priority < 0 ? 'secondary' : 'primary'; 6463 this[ list ].set( id, view, options ); 6464 } 6465 6466 if ( ! options.silent ) { 6467 this.refresh(); 6468 } 6469 6470 return this; 6471 }, 6472 /** 6473 * @param {string} id 6474 * @returns {wp.media.view.Button} 6475 */ 6476 get: function( id ) { 6477 return this._views[ id ]; 6478 }, 6479 /** 6480 * @param {string} id 6481 * @param {Object} options 6482 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 6483 */ 6484 unset: function( id, options ) { 6485 delete this._views[ id ]; 6486 this.primary.unset( id, options ); 6487 this.secondary.unset( id, options ); 6488 6489 if ( ! options || ! options.silent ) { 6490 this.refresh(); 6491 } 6492 return this; 6493 }, 6494 6495 refresh: function() { 6496 var state = this.controller.state(), 6497 library = state.get('library'), 6498 selection = state.get('selection'); 6499 6500 _.each( this._views, function( button ) { 6501 if ( ! button.model || ! button.options || ! button.options.requires ) { 6502 return; 6503 } 6504 6505 var requires = button.options.requires, 6506 disabled = false; 6507 6508 // Prevent insertion of attachments if any of them are still uploading 6509 disabled = _.some( selection.models, function( attachment ) { 6510 return attachment.get('uploading') === true; 6511 }); 6512 6513 if ( requires.selection && selection && ! selection.length ) { 6514 disabled = true; 6515 } else if ( requires.library && library && ! library.length ) { 6516 disabled = true; 6517 } 6518 button.model.set( 'disabled', disabled ); 6519 }); 6520 } 6521 }); 6522 6523 module.exports = Toolbar; 6524 },{"./button.js":26,"./priority-list.js":40,"./view.js":55}],49:[function(require,module,exports){ 6525 /** 6526 * wp.media.view.Toolbar.Select 6527 * 6528 * @class 6529 * @augments wp.media.view.Toolbar 6530 * @augments wp.media.View 6531 * @augments wp.Backbone.View 6532 * @augments Backbone.View 6533 */ 6534 var Toolbar = require( '../toolbar.js' ), 6535 l10n = wp.media.view.l10n, 6536 Select; 6537 6538 Select = Toolbar.extend({ 6539 initialize: function() { 6540 var options = this.options; 6541 6542 _.bindAll( this, 'clickSelect' ); 6543 6544 _.defaults( options, { 6545 event: 'select', 6546 state: false, 6547 reset: true, 6548 close: true, 6549 text: l10n.select, 6550 6551 // Does the button rely on the selection? 6552 requires: { 6553 selection: true 6554 } 6555 }); 6556 6557 options.items = _.defaults( options.items || {}, { 6558 select: { 6559 style: 'primary', 6560 text: options.text, 6561 priority: 80, 6562 click: this.clickSelect, 6563 requires: options.requires 6564 } 6565 }); 6566 // Call 'initialize' directly on the parent class. 6567 Toolbar.prototype.initialize.apply( this, arguments ); 6568 }, 6569 6570 clickSelect: function() { 6571 var options = this.options, 6572 controller = this.controller; 6573 6574 if ( options.close ) { 6575 controller.close(); 6576 } 6577 6578 if ( options.event ) { 6579 controller.state().trigger( options.event ); 6580 } 6581 6582 if ( options.state ) { 6583 controller.setState( options.state ); 6584 } 6585 6586 if ( options.reset ) { 6587 controller.reset(); 6588 } 6589 } 6590 }); 6591 6592 module.exports = Select; 6593 },{"../toolbar.js":48}],50:[function(require,module,exports){ 6594 /** 6595 * wp.media.view.UploaderInline 6596 * 6597 * The inline uploader that shows up in the 'Upload Files' tab. 6598 * 6599 * @class 6600 * @augments wp.media.View 6601 * @augments wp.Backbone.View 6602 * @augments Backbone.View 6603 */ 6604 var View = require( '../view.js' ), 6605 UploaderStatus = require( './status.js' ), 6606 UploaderInline; 6607 6608 UploaderInline = View.extend({ 6609 tagName: 'div', 6610 className: 'uploader-inline', 6611 template: wp.template('uploader-inline'), 6612 6613 events: { 6614 'click .close': 'hide' 6615 }, 6616 6617 initialize: function() { 6618 _.defaults( this.options, { 6619 message: '', 6620 status: true, 6621 canClose: false 6622 }); 6623 6624 if ( ! this.options.$browser && this.controller.uploader ) { 6625 this.options.$browser = this.controller.uploader.$browser; 6626 } 6627 6628 if ( _.isUndefined( this.options.postId ) ) { 6629 this.options.postId = wp.media.view.settings.post.id; 6630 } 6631 6632 if ( this.options.status ) { 6633 this.views.set( '.upload-inline-status', new UploaderStatus({ 6634 controller: this.controller 6635 }) ); 6636 } 6637 }, 6638 6639 prepare: function() { 6640 var suggestedWidth = this.controller.state().get('suggestedWidth'), 6641 suggestedHeight = this.controller.state().get('suggestedHeight'), 6642 data = {}; 6643 6644 data.message = this.options.message; 6645 data.canClose = this.options.canClose; 6646 6647 if ( suggestedWidth && suggestedHeight ) { 6648 data.suggestedWidth = suggestedWidth; 6649 data.suggestedHeight = suggestedHeight; 6650 } 6651 6652 return data; 6653 }, 6654 /** 6655 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 6656 */ 6657 dispose: function() { 6658 if ( this.disposing ) { 6659 /** 6660 * call 'dispose' directly on the parent class 6661 */ 6662 return View.prototype.dispose.apply( this, arguments ); 6663 } 6664 6665 // Run remove on `dispose`, so we can be sure to refresh the 6666 // uploader with a view-less DOM. Track whether we're disposing 6667 // so we don't trigger an infinite loop. 6668 this.disposing = true; 6669 return this.remove(); 6670 }, 6671 /** 6672 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 6673 */ 6674 remove: function() { 6675 /** 6676 * call 'remove' directly on the parent class 6677 */ 6678 var result = View.prototype.remove.apply( this, arguments ); 6679 6680 _.defer( _.bind( this.refresh, this ) ); 6681 return result; 6682 }, 6683 6684 refresh: function() { 6685 var uploader = this.controller.uploader; 6686 6687 if ( uploader ) { 6688 uploader.refresh(); 6689 } 6690 }, 6691 /** 6692 * @returns {wp.media.view.UploaderInline} 6693 */ 6694 ready: function() { 6695 var $browser = this.options.$browser, 6696 $placeholder; 6697 6698 if ( this.controller.uploader ) { 6699 $placeholder = this.$('.browser'); 6700 6701 // Check if we've already replaced the placeholder. 6702 if ( $placeholder[0] === $browser[0] ) { 6703 return; 6704 } 6705 6706 $browser.detach().text( $placeholder.text() ); 6707 $browser[0].className = $placeholder[0].className; 6708 $placeholder.replaceWith( $browser.show() ); 6709 } 6710 6711 this.refresh(); 6712 return this; 6713 }, 6714 show: function() { 6715 this.$el.removeClass( 'hidden' ); 6716 }, 6717 hide: function() { 6718 this.$el.addClass( 'hidden' ); 6719 } 6720 6721 }); 6722 6723 module.exports = UploaderInline; 6724 },{"../view.js":55,"./status.js":52}],51:[function(require,module,exports){ 6725 /** 6726 * wp.media.view.UploaderStatusError 6727 * 6728 * @class 6729 * @augments wp.media.View 6730 * @augments wp.Backbone.View 6731 * @augments Backbone.View 6732 */ 6733 var View = require( '../view.js' ), 6734 UploaderStatusError; 6735 6736 UploaderStatusError = View.extend({ 6737 className: 'upload-error', 6738 template: wp.template('uploader-status-error') 6739 }); 6740 6741 module.exports = UploaderStatusError; 6742 },{"../view.js":55}],52:[function(require,module,exports){ 6743 /** 6744 * wp.media.view.UploaderStatus 6745 * 6746 * An uploader status for on-going uploads. 6747 * 6748 * @class 6749 * @augments wp.media.View 6750 * @augments wp.Backbone.View 6751 * @augments Backbone.View 6752 */ 6753 var View = require( '../view.js' ), 6754 UploaderStatusError = require( './status-error.js' ), 6755 UploaderStatus; 6756 6757 UploaderStatus = View.extend({ 6758 className: 'media-uploader-status', 6759 template: wp.template('uploader-status'), 6760 6761 events: { 6762 'click .upload-dismiss-errors': 'dismiss' 6763 }, 6764 6765 initialize: function() { 6766 this.queue = wp.Uploader.queue; 6767 this.queue.on( 'add remove reset', this.visibility, this ); 6768 this.queue.on( 'add remove reset change:percent', this.progress, this ); 6769 this.queue.on( 'add remove reset change:uploading', this.info, this ); 6770 6771 this.errors = wp.Uploader.errors; 6772 this.errors.reset(); 6773 this.errors.on( 'add remove reset', this.visibility, this ); 6774 this.errors.on( 'add', this.error, this ); 6775 }, 6776 /** 6777 * @global wp.Uploader 6778 * @returns {wp.media.view.UploaderStatus} 6779 */ 6780 dispose: function() { 6781 wp.Uploader.queue.off( null, null, this ); 6782 /** 6783 * call 'dispose' directly on the parent class 6784 */ 6785 View.prototype.dispose.apply( this, arguments ); 6786 return this; 6787 }, 6788 6789 visibility: function() { 6790 this.$el.toggleClass( 'uploading', !! this.queue.length ); 6791 this.$el.toggleClass( 'errors', !! this.errors.length ); 6792 this.$el.toggle( !! this.queue.length || !! this.errors.length ); 6793 }, 6794 6795 ready: function() { 6796 _.each({ 6797 '$bar': '.media-progress-bar div', 6798 '$index': '.upload-index', 6799 '$total': '.upload-total', 6800 '$filename': '.upload-filename' 6801 }, function( selector, key ) { 6802 this[ key ] = this.$( selector ); 6803 }, this ); 6804 6805 this.visibility(); 6806 this.progress(); 6807 this.info(); 6808 }, 6809 6810 progress: function() { 6811 var queue = this.queue, 6812 $bar = this.$bar; 6813 6814 if ( ! $bar || ! queue.length ) { 6815 return; 6816 } 6817 6818 $bar.width( ( queue.reduce( function( memo, attachment ) { 6819 if ( ! attachment.get('uploading') ) { 6820 return memo + 100; 6821 } 6822 6823 var percent = attachment.get('percent'); 6824 return memo + ( _.isNumber( percent ) ? percent : 100 ); 6825 }, 0 ) / queue.length ) + '%' ); 6826 }, 6827 6828 info: function() { 6829 var queue = this.queue, 6830 index = 0, active; 6831 6832 if ( ! queue.length ) { 6833 return; 6834 } 6835 6836 active = this.queue.find( function( attachment, i ) { 6837 index = i; 6838 return attachment.get('uploading'); 6839 }); 6840 6841 this.$index.text( index + 1 ); 6842 this.$total.text( queue.length ); 6843 this.$filename.html( active ? this.filename( active.get('filename') ) : '' ); 6844 }, 6845 /** 6846 * @param {string} filename 6847 * @returns {string} 6848 */ 6849 filename: function( filename ) { 6850 return wp.media.truncate( _.escape( filename ), 24 ); 6851 }, 6852 /** 6853 * @param {Backbone.Model} error 6854 */ 6855 error: function( error ) { 6856 this.views.add( '.upload-errors', new UploaderStatusError({ 6857 filename: this.filename( error.get('file').name ), 6858 message: error.get('message') 6859 }), { at: 0 }); 6860 }, 6861 6862 /** 6863 * @global wp.Uploader 6864 * 6865 * @param {Object} event 6866 */ 6867 dismiss: function( event ) { 6868 var errors = this.views.get('.upload-errors'); 6869 6870 event.preventDefault(); 6871 6872 if ( errors ) { 6873 _.invoke( errors, 'remove' ); 6874 } 6875 wp.Uploader.errors.reset(); 6876 } 6877 }); 6878 6879 module.exports = UploaderStatus; 6880 },{"../view.js":55,"./status-error.js":51}],53:[function(require,module,exports){ 6881 /** 6882 * wp.media.view.UploaderWindow 6883 * 6884 * An uploader window that allows for dragging and dropping media. 6885 * 6886 * @class 6887 * @augments wp.media.View 6888 * @augments wp.Backbone.View 6889 * @augments Backbone.View 6890 * 6891 * @param {object} [options] Options hash passed to the view. 6892 * @param {object} [options.uploader] Uploader properties. 6893 * @param {jQuery} [options.uploader.browser] 6894 * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone. 6895 * @param {object} [options.uploader.params] 6896 */ 6897 var View = require( '../view.js' ), 6898 $ = jQuery, 6899 UploaderWindow; 6900 6901 UploaderWindow = View.extend({ 6902 tagName: 'div', 6903 className: 'uploader-window', 6904 template: wp.template('uploader-window'), 6905 6906 initialize: function() { 6907 var uploader; 6908 6909 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body'); 6910 6911 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { 6912 dropzone: this.$el, 6913 browser: this.$browser, 6914 params: {} 6915 }); 6916 6917 // Ensure the dropzone is a jQuery collection. 6918 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) { 6919 uploader.dropzone = $( uploader.dropzone ); 6920 } 6921 6922 this.controller.on( 'activate', this.refresh, this ); 6923 6924 this.controller.on( 'detach', function() { 6925 this.$browser.remove(); 6926 }, this ); 6927 }, 6928 6929 refresh: function() { 6930 if ( this.uploader ) { 6931 this.uploader.refresh(); 6932 } 6933 }, 6934 6935 ready: function() { 6936 var postId = wp.media.view.settings.post.id, 6937 dropzone; 6938 6939 // If the uploader already exists, bail. 6940 if ( this.uploader ) { 6941 return; 6942 } 6943 6944 if ( postId ) { 6945 this.options.uploader.params.post_id = postId; 6946 } 6947 this.uploader = new wp.Uploader( this.options.uploader ); 6948 6949 dropzone = this.uploader.dropzone; 6950 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) ); 6951 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) ); 6952 6953 $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) ); 6954 }, 6955 6956 _ready: function() { 6957 this.controller.trigger( 'uploader:ready' ); 6958 }, 6959 6960 show: function() { 6961 var $el = this.$el.show(); 6962 6963 // Ensure that the animation is triggered by waiting until 6964 // the transparent element is painted into the DOM. 6965 _.defer( function() { 6966 $el.css({ opacity: 1 }); 6967 }); 6968 }, 6969 6970 hide: function() { 6971 var $el = this.$el.css({ opacity: 0 }); 6972 6973 wp.media.transition( $el ).done( function() { 6974 // Transition end events are subject to race conditions. 6975 // Make sure that the value is set as intended. 6976 if ( '0' === $el.css('opacity') ) { 6977 $el.hide(); 6978 } 6979 }); 6980 6981 // https://core.trac.wordpress.org/ticket/27341 6982 _.delay( function() { 6983 if ( '0' === $el.css('opacity') && $el.is(':visible') ) { 6984 $el.hide(); 6985 } 6986 }, 500 ); 6987 } 6988 }); 6989 6990 module.exports = UploaderWindow; 6991 },{"../view.js":55}],54:[function(require,module,exports){ 6992 /** 6993 * wp.media.view.VideoDetails 6994 * 6995 * @constructor 6996 * @augments wp.media.view.MediaDetails 6997 * @augments wp.media.view.Settings.AttachmentDisplay 6998 * @augments wp.media.view.Settings 6999 * @augments wp.media.View 7000 * @augments wp.Backbone.View 7001 * @augments Backbone.View 7002 */ 7003 var MediaDetails = require( './media-details' ), 7004 VideoDetails; 7005 7006 VideoDetails = MediaDetails.extend({ 7007 className: 'video-details', 7008 template: wp.template('video-details'), 7009 7010 setMedia: function() { 7011 var video = this.$('.wp-video-shortcode'); 7012 7013 if ( video.find( 'source' ).length ) { 7014 if ( video.is(':hidden') ) { 7015 video.show(); 7016 } 7017 7018 if ( ! video.hasClass('youtube-video') ) { 7019 this.media = MediaDetails.prepareSrc( video.get(0) ); 7020 } else { 7021 this.media = video.get(0); 7022 } 7023 } else { 7024 video.hide(); 7025 this.media = false; 7026 } 7027 7028 return this; 7029 } 7030 }); 7031 7032 module.exports = VideoDetails; 7033 },{"./media-details":35}],55:[function(require,module,exports){ 7034 /** 7035 * wp.media.View 7036 * 7037 * The base view class for media. 7038 * 7039 * Undelegating events, removing events from the model, and 7040 * removing events from the controller mirror the code for 7041 * `Backbone.View.dispose` in Backbone 0.9.8 development. 7042 * 7043 * This behavior has since been removed, and should not be used 7044 * outside of the media manager. 7045 * 7046 * @class 7047 * @augments wp.Backbone.View 7048 * @augments Backbone.View 7049 */ 7050 var View = wp.Backbone.View.extend({ 7051 constructor: function( options ) { 7052 if ( options && options.controller ) { 7053 this.controller = options.controller; 7054 } 7055 wp.Backbone.View.apply( this, arguments ); 7056 }, 7057 /** 7058 * @todo The internal comment mentions this might have been a stop-gap 7059 * before Backbone 0.9.8 came out. Figure out if Backbone core takes 7060 * care of this in Backbone.View now. 7061 * 7062 * @returns {wp.media.View} Returns itself to allow chaining 7063 */ 7064 dispose: function() { 7065 // Undelegating events, removing events from the model, and 7066 // removing events from the controller mirror the code for 7067 // `Backbone.View.dispose` in Backbone 0.9.8 development. 7068 this.undelegateEvents(); 7069 7070 if ( this.model && this.model.off ) { 7071 this.model.off( null, null, this ); 7072 } 7073 7074 if ( this.collection && this.collection.off ) { 7075 this.collection.off( null, null, this ); 7076 } 7077 7078 // Unbind controller events. 7079 if ( this.controller && this.controller.off ) { 7080 this.controller.off( null, null, this ); 7081 } 7082 7083 return this; 7084 }, 7085 /** 7086 * @returns {wp.media.View} Returns itself to allow chaining 7087 */ 7088 remove: function() { 7089 this.dispose(); 7090 /** 7091 * call 'remove' directly on the parent class 7092 */ 7093 return wp.Backbone.View.prototype.remove.apply( this, arguments ); 7094 } 7095 }); 7096 7097 module.exports = View; 7098 },{}]},{},[1]); -
src/wp-includes/js/media/audio-video.manifest.js
1 /* global _wpMediaViewsL10n, _wpmejsSettings, MediaElementPlayer */ 2 3 (function(_) { 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 122 frame = wp.media({ 123 frame: 'audio', 124 state: 'audio-details', 125 metadata: _.defaults( shortcode.attrs.named, this.defaults ) 126 }); 127 128 return frame; 129 }, 130 131 shortcode : function( model ) { 132 var self = this, content; 133 134 _.each( this.defaults, function( value, key ) { 135 model[ key ] = self.coerce( model, key ); 136 137 if ( value === model[ key ] ) { 138 delete model[ key ]; 139 } 140 }); 141 142 content = model.content; 143 delete model.content; 144 145 return new wp.shortcode({ 146 tag: 'audio', 147 attrs: model, 148 content: content 149 }); 150 } 151 }; 152 153 /** 154 * Shortcode modeling for video 155 * `edit()` prepares the shortcode for the media modal 156 * `shortcode()` builds the new shortcode after update 157 * 158 * @namespace 159 */ 160 wp.media.video = { 161 coerce : wp.media.coerce, 162 163 defaults : { 164 id : wp.media.view.settings.post.id, 165 src : '', 166 poster : '', 167 loop : false, 168 autoplay : false, 169 preload : 'metadata', 170 content : '', 171 width : 640, 172 height : 360 173 }, 174 175 edit : function( data ) { 176 var frame, 177 shortcode = wp.shortcode.next( 'video', data ).shortcode, 178 attrs; 179 180 attrs = shortcode.attrs.named; 181 attrs.content = shortcode.content; 182 183 frame = wp.media({ 184 frame: 'video', 185 state: 'video-details', 186 metadata: _.defaults( attrs, this.defaults ) 187 }); 188 189 return frame; 190 }, 191 192 shortcode : function( model ) { 193 var self = this, content; 194 195 _.each( this.defaults, function( value, key ) { 196 model[ key ] = self.coerce( model, key ); 197 198 if ( value === model[ key ] ) { 199 delete model[ key ]; 200 } 201 }); 202 203 content = model.content; 204 delete model.content; 205 206 return new wp.shortcode({ 207 tag: 'video', 208 attrs: model, 209 content: content 210 }); 211 } 212 }; 213 214 media.model.PostMedia = require( './models/post-media.js' ); 215 media.controller.AudioDetails = require( './controllers/audio-details.js' ); 216 media.controller.VideoDetails = require( './controllers/video-details.js' ); 217 media.view.MediaFrame.MediaDetails = require( './views/frame/media-details.js' ); 218 media.view.MediaFrame.AudioDetails = require( './views/frame/audio-details.js' ); 219 media.view.MediaFrame.VideoDetails = require( './views/frame/video-details.js' ); 220 media.view.MediaDetails = require( './views/media-details.js' ); 221 media.view.AudioDetails = require( './views/audio-details.js' ); 222 media.view.VideoDetails = require( './views/video-details.js' ); 223 224 }(_)); 225 No newline at end of file -
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 Selection = 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 $ = jQuery, 41 CollectionEdit; 42 43 CollectionEdit = Library.extend({ 44 defaults: { 45 multiple: false, 46 sortable: true, 47 searchable: false, 48 content: 'browse', 49 describe: true, 50 dragInfo: true, 51 idealColumnWidth: 170, 52 editing: false, 53 priority: 60, 54 SettingsView: false, 55 syncSelection: false 56 }, 57 58 /** 59 * @since 3.9.0 60 */ 61 initialize: function() { 62 var collectionType = this.get('collectionType'); 63 64 if ( 'video' === this.get( 'type' ) ) { 65 collectionType = 'video-' + collectionType; 66 } 67 68 this.set( 'id', collectionType + '-edit' ); 69 this.set( 'toolbar', collectionType + '-edit' ); 70 71 // If we haven't been provided a `library`, create a `Selection`. 72 if ( ! this.get('library') ) { 73 this.set( 'library', new Selection() ); 74 } 75 // The single `Attachment` view to be used in the `Attachments` view. 76 if ( ! this.get('AttachmentView') ) { 77 this.set( 'AttachmentView', EditLibraryView ); 78 } 79 Library.prototype.initialize.apply( this, arguments ); 80 }, 81 82 /** 83 * @since 3.9.0 84 */ 85 activate: function() { 86 var library = this.get('library'); 87 88 // Limit the library to images only. 89 library.props.set( 'type', this.get( 'type' ) ); 90 91 // Watch for uploaded attachments. 92 this.get('library').observe( wp.Uploader.queue ); 93 94 this.frame.on( 'content:render:browse', this.renderSettings, this ); 95 96 Library.prototype.activate.apply( this, arguments ); 97 }, 98 99 /** 100 * @since 3.9.0 101 */ 102 deactivate: function() { 103 // Stop watching for uploaded attachments. 104 this.get('library').unobserve( wp.Uploader.queue ); 105 106 this.frame.off( 'content:render:browse', this.renderSettings, this ); 107 108 Library.prototype.deactivate.apply( this, arguments ); 109 }, 110 111 /** 112 * Render the collection embed settings view in the browser sidebar. 113 * 114 * @todo This is against the pattern elsewhere in media. Typically the frame 115 * is responsible for adding region mode callbacks. Explain. 116 * 117 * @since 3.9.0 118 * 119 * @param {wp.media.view.attachmentsBrowser} The attachments browser view. 120 */ 121 renderSettings: function( attachmentsBrowserView ) { 122 var library = this.get('library'), 123 collectionType = this.get('collectionType'), 124 dragInfoText = this.get('dragInfoText'), 125 SettingsView = this.get('SettingsView'), 126 obj = {}; 127 128 if ( ! library || ! attachmentsBrowserView ) { 129 return; 130 } 131 132 library[ collectionType ] = library[ collectionType ] || new Backbone.Model(); 133 134 obj[ collectionType ] = new SettingsView({ 135 controller: this, 136 model: library[ collectionType ], 137 priority: 40 138 }); 139 140 attachmentsBrowserView.sidebar.set( obj ); 141 142 if ( dragInfoText ) { 143 attachmentsBrowserView.toolbar.set( 'dragInfo', new View({ 144 el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0], 145 priority: -40 146 }) ); 147 } 148 149 // Add the 'Reverse order' button to the toolbar. 150 attachmentsBrowserView.toolbar.set( 'reverse', { 151 text: l10n.reverseOrder, 152 priority: 80, 153 154 click: function() { 155 library.reset( library.toArray().reverse() ); 156 } 157 }); 158 } 159 }); 160 161 module.exports = CollectionEdit; 162 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 $ = jQuery, 25 Embed; 26 27 Embed = State.extend({ 28 defaults: { 29 id: 'embed', 30 title: l10n.insertFromUrlTitle, 31 content: 'embed', 32 menu: 'default', 33 toolbar: 'main-embed', 34 priority: 120, 35 type: 'link', 36 url: '', 37 metadata: {} 38 }, 39 40 // The amount of time used when debouncing the scan. 41 sensitivity: 200, 42 43 initialize: function(options) { 44 this.metadata = options.metadata; 45 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity ); 46 this.props = new Backbone.Model( this.metadata || { url: '' }); 47 this.props.on( 'change:url', this.debouncedScan, this ); 48 this.props.on( 'change:url', this.refresh, this ); 49 this.on( 'scan', this.scanImage, this ); 50 }, 51 52 /** 53 * Trigger a scan of the embedded URL's content for metadata required to embed. 54 * 55 * @fires wp.media.controller.Embed#scan 56 */ 57 scan: function() { 58 var scanners, 59 embed = this, 60 attributes = { 61 type: 'link', 62 scanners: [] 63 }; 64 65 // Scan is triggered with the list of `attributes` to set on the 66 // state, useful for the 'type' attribute and 'scanners' attribute, 67 // an array of promise objects for asynchronous scan operations. 68 if ( this.props.get('url') ) { 69 this.trigger( 'scan', attributes ); 70 } 71 72 if ( attributes.scanners.length ) { 73 scanners = attributes.scanners = $.when.apply( $, attributes.scanners ); 74 scanners.always( function() { 75 if ( embed.get('scanners') === scanners ) { 76 embed.set( 'loading', false ); 77 } 78 }); 79 } else { 80 attributes.scanners = null; 81 } 82 83 attributes.loading = !! attributes.scanners; 84 this.set( attributes ); 85 }, 86 /** 87 * Try scanning the embed as an image to discover its dimensions. 88 * 89 * @param {Object} attributes 90 */ 91 scanImage: function( attributes ) { 92 var frame = this.frame, 93 state = this, 94 url = this.props.get('url'), 95 image = new Image(), 96 deferred = $.Deferred(); 97 98 attributes.scanners.push( deferred.promise() ); 99 100 // Try to load the image and find its width/height. 101 image.onload = function() { 102 deferred.resolve(); 103 104 if ( state !== frame.state() || url !== state.props.get('url') ) { 105 return; 106 } 107 108 state.set({ 109 type: 'image' 110 }); 111 112 state.props.set({ 113 width: image.width, 114 height: image.height 115 }); 116 }; 117 118 image.onerror = deferred.reject; 119 image.src = url; 120 }, 121 122 refresh: function() { 123 this.frame.toolbar.get().refresh(); 124 }, 125 126 reset: function() { 127 this.props.clear().set({ url: '' }); 128 129 if ( this.active ) { 130 this.refresh(); 131 } 132 } 133 }); 134 135 module.exports = Embed; 136 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 Selection = 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 Selection) ) { 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 Selection( 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(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 }(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 * 1273 * @class 1274 * @augments wp.media.controller.Library 1275 * @augments wp.media.controller.State 1276 * @augments Backbone.Model 1277 */