Make WordPress Core

Ticket #28510: browserify.diff

File browserify.diff, 1014.4 KB (added by wonderboymusic, 11 years ago)
  • Gruntfile.js

     
    114114                                }
    115115                        }
    116116                },
     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                },
    117128                sass: {
    118129                        colors: {
    119130                                expand: true,
     
    358369                                        '!wp-includes/js/zxcvbn.min.js'
    359370                                ]
    360371                        },
     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                        },
    361384                        jqueryui: {
    362385                                options: {
    363386                                        preserveComments: 'some'
     
    435458                                        interval: 2000
    436459                                }
    437460                        },
     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                        },
    438471                        config: {
    439472                                files: 'Gruntfile.js'
    440473                        },
     
    483516
    484517        // Build task.
    485518        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']);
    487521
    488522        // Testing tasks.
    489523        grunt.registerMultiTask('phpunit', 'Runs PHPUnit tests, including the ajax, external-http, and multisite tests.', function() {
  • package.json

     
    1111  "devDependencies": {
    1212    "grunt": "~0.4.5",
    1313    "grunt-autoprefixer": "~1.0.1",
     14        "grunt-browserify": "~3.2.1",
    1415    "grunt-contrib-clean": "~0.6.0",
    1516    "grunt-contrib-compress": "~0.12.0",
    1617    "grunt-contrib-concat": "~0.5.0",
  • src/wp-includes/js/media/audio-video.js

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

     
     1/* global _wpMediaViewsL10n, _wpmejsSettings, MediaElementPlayer */
     2
     3(function($, _, Backbone) {
     4        var media = wp.media,
     5                baseSettings = {},
     6                l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
     7
     8        if ( ! _.isUndefined( window._wpmejsSettings ) ) {
     9                baseSettings = _wpmejsSettings;
     10        }
     11
     12        /**
     13         * @mixin
     14         */
     15        wp.media.mixin = {
     16                mejsSettings: baseSettings,
     17
     18                removeAllPlayers: function() {
     19                        var p;
     20
     21                        if ( window.mejs && window.mejs.players ) {
     22                                for ( p in window.mejs.players ) {
     23                                        window.mejs.players[p].pause();
     24                                        this.removePlayer( window.mejs.players[p] );
     25                                }
     26                        }
     27                },
     28
     29                /**
     30                 * Override the MediaElement method for removing a player.
     31                 *      MediaElement tries to pull the audio/video tag out of
     32                 *      its container and re-add it to the DOM.
     33                 */
     34                removePlayer: function(t) {
     35                        var featureIndex, feature;
     36
     37                        if ( ! t.options ) {
     38                                return;
     39                        }
     40
     41                        // invoke features cleanup
     42                        for ( featureIndex in t.options.features ) {
     43                                feature = t.options.features[featureIndex];
     44                                if ( t['clean' + feature] ) {
     45                                        try {
     46                                                t['clean' + feature](t);
     47                                        } catch (e) {}
     48                                }
     49                        }
     50
     51                        if ( ! t.isDynamic ) {
     52                                t.$node.remove();
     53                        }
     54
     55                        if ( 'native' !== t.media.pluginType ) {
     56                                t.media.remove();
     57                        }
     58
     59                        delete window.mejs.players[t.id];
     60
     61                        t.container.remove();
     62                        t.globalUnbind();
     63                        delete t.node.player;
     64                },
     65
     66                /**
     67                 * Allows any class that has set 'player' to a MediaElementPlayer
     68                 *  instance to remove the player when listening to events.
     69                 *
     70                 *  Examples: modal closes, shortcode properties are removed, etc.
     71                 */
     72                unsetPlayers : function() {
     73                        if ( this.players && this.players.length ) {
     74                                _.each( this.players, function (player) {
     75                                        player.pause();
     76                                        wp.media.mixin.removePlayer( player );
     77                                } );
     78                                this.players = [];
     79                        }
     80                }
     81        };
     82
     83        /**
     84         * Autowire "collection"-type shortcodes
     85         */
     86        wp.media.playlist = new wp.media.collection({
     87                tag: 'playlist',
     88                editTitle : l10n.editPlaylistTitle,
     89                defaults : {
     90                        id: wp.media.view.settings.post.id,
     91                        style: 'light',
     92                        tracklist: true,
     93                        tracknumbers: true,
     94                        images: true,
     95                        artists: true,
     96                        type: 'audio'
     97                }
     98        });
     99
     100        /**
     101         * Shortcode modeling for audio
     102         *  `edit()` prepares the shortcode for the media modal
     103         *  `shortcode()` builds the new shortcode after update
     104         *
     105         * @namespace
     106         */
     107        wp.media.audio = {
     108                coerce : wp.media.coerce,
     109
     110                defaults : {
     111                        id : wp.media.view.settings.post.id,
     112                        src : '',
     113                        loop : false,
     114                        autoplay : false,
     115                        preload : 'none',
     116                        width : 400
     117                },
     118
     119                edit : function( data ) {
     120                        var frame, shortcode = wp.shortcode.next( 'audio', data ).shortcode;
     121                        console.log( 'woo' );
     122
     123                        frame = wp.media({
     124                                frame: 'audio',
     125                                state: 'audio-details',
     126                                metadata: _.defaults( shortcode.attrs.named, this.defaults )
     127                        });
     128
     129                        console.log( frame );
     130
     131                        return frame;
     132                },
     133
     134                shortcode : function( model ) {
     135                        var self = this, content;
     136
     137                        _.each( this.defaults, function( value, key ) {
     138                                model[ key ] = self.coerce( model, key );
     139
     140                                if ( value === model[ key ] ) {
     141                                        delete model[ key ];
     142                                }
     143                        });
     144
     145                        content = model.content;
     146                        delete model.content;
     147
     148                        return new wp.shortcode({
     149                                tag: 'audio',
     150                                attrs: model,
     151                                content: content
     152                        });
     153                }
     154        };
     155
     156        /**
     157         * Shortcode modeling for video
     158         *  `edit()` prepares the shortcode for the media modal
     159         *  `shortcode()` builds the new shortcode after update
     160         *
     161         * @namespace
     162         */
     163        wp.media.video = {
     164                coerce : wp.media.coerce,
     165
     166                defaults : {
     167                        id : wp.media.view.settings.post.id,
     168                        src : '',
     169                        poster : '',
     170                        loop : false,
     171                        autoplay : false,
     172                        preload : 'metadata',
     173                        content : '',
     174                        width : 640,
     175                        height : 360
     176                },
     177
     178                edit : function( data ) {
     179                        var frame,
     180                                shortcode = wp.shortcode.next( 'video', data ).shortcode,
     181                                attrs;
     182
     183                        attrs = shortcode.attrs.named;
     184                        attrs.content = shortcode.content;
     185
     186                        frame = wp.media({
     187                                frame: 'video',
     188                                state: 'video-details',
     189                                metadata: _.defaults( attrs, this.defaults )
     190                        });
     191
     192                        return frame;
     193                },
     194
     195                shortcode : function( model ) {
     196                        var self = this, content;
     197
     198                        _.each( this.defaults, function( value, key ) {
     199                                model[ key ] = self.coerce( model, key );
     200
     201                                if ( value === model[ key ] ) {
     202                                        delete model[ key ];
     203                                }
     204                        });
     205
     206                        content = model.content;
     207                        delete model.content;
     208
     209                        return new wp.shortcode({
     210                                tag: 'video',
     211                                attrs: model,
     212                                content: content
     213                        });
     214                }
     215        };
     216
     217        media.model.PostMedia = require( './models/post-media.js' );
     218        media.controller.AudioDetails = require( './controllers/audio-details.js' );
     219        media.controller.VideoDetails = require( './controllers/video-details.js' );
     220        media.view.MediaFrame.MediaDetails = require( './views/frame/media-details.js' );
     221        media.view.MediaFrame.AudioDetails = require( './views/frame/audio-details.js' );
     222        media.view.MediaFrame.VideoDetails = require( './views/frame/video-details.js' );
     223        media.view.MediaDetails = require( './views/media-details.js' );
     224        media.view.AudioDetails = require( './views/audio-details.js' );
     225        media.view.VideoDetails = require( './views/video-details.js' );
     226
     227}(jQuery, _, Backbone));
  • src/wp-includes/js/media/controllers/audio-details.js

     
     1/**
     2 * The controller for the Audio Details state
     3 *
     4 * @constructor
     5 * @augments wp.media.controller.State
     6 * @augments Backbone.Model
     7 */
     8var State = require( './state.js' ),
     9        l10n = wp.media.view.l10n,
     10        AudioDetails;
     11
     12AudioDetails = 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
     29module.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 */
     34var Selection = require( '../models/selection.js' ),
     35        Library = require( './library.js' ),
     36        CollectionAdd;
     37
     38CollectionAdd = 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
     99module.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 */
     35var SelectionModel = require( '../models/selection.js' ),
     36        Library = require( './library.js' ),
     37        View = require( '../views/view.js' ),
     38        EditLibraryView = require( '../views/attachment/edit-library.js' ),
     39        l10n = wp.media.view.l10n,
     40        CollectionEdit;
     41
     42CollectionEdit = Library.extend({
     43        defaults: {
     44                multiple:         false,
     45                sortable:         true,
     46                searchable:       false,
     47                content:          'browse',
     48                describe:         true,
     49                dragInfo:         true,
     50                idealColumnWidth: 170,
     51                editing:          false,
     52                priority:         60,
     53                SettingsView:     false,
     54                syncSelection:    false
     55        },
     56
     57        /**
     58         * @since 3.9.0
     59         */
     60        initialize: function() {
     61                var collectionType = this.get('collectionType');
     62
     63                if ( 'video' === this.get( 'type' ) ) {
     64                        collectionType = 'video-' + collectionType;
     65                }
     66
     67                this.set( 'id', collectionType + '-edit' );
     68                this.set( 'toolbar', collectionType + '-edit' );
     69
     70                // If we haven't been provided a `library`, create a `Selection`.
     71                if ( ! this.get('library') ) {
     72                        this.set( 'library', new SelectionModel() );
     73                }
     74                // The single `Attachment` view to be used in the `Attachments` view.
     75                if ( ! this.get('AttachmentView') ) {
     76                        this.set( 'AttachmentView', EditLibraryView );
     77                }
     78                Library.prototype.initialize.apply( this, arguments );
     79        },
     80
     81        /**
     82         * @since 3.9.0
     83         */
     84        activate: function() {
     85                var library = this.get('library');
     86
     87                // Limit the library to images only.
     88                library.props.set( 'type', this.get( 'type' ) );
     89
     90                // Watch for uploaded attachments.
     91                this.get('library').observe( wp.Uploader.queue );
     92
     93                this.frame.on( 'content:render:browse', this.renderSettings, this );
     94
     95                Library.prototype.activate.apply( this, arguments );
     96        },
     97
     98        /**
     99         * @since 3.9.0
     100         */
     101        deactivate: function() {
     102                // Stop watching for uploaded attachments.
     103                this.get('library').unobserve( wp.Uploader.queue );
     104
     105                this.frame.off( 'content:render:browse', this.renderSettings, this );
     106
     107                Library.prototype.deactivate.apply( this, arguments );
     108        },
     109
     110        /**
     111         * Render the collection embed settings view in the browser sidebar.
     112         *
     113         * @todo This is against the pattern elsewhere in media. Typically the frame
     114         *       is responsible for adding region mode callbacks. Explain.
     115         *
     116         * @since 3.9.0
     117         *
     118         * @param {wp.media.view.attachmentsBrowser} The attachments browser view.
     119         */
     120        renderSettings: function( attachmentsBrowserView ) {
     121                var library = this.get('library'),
     122                        collectionType = this.get('collectionType'),
     123                        dragInfoText = this.get('dragInfoText'),
     124                        SettingsView = this.get('SettingsView'),
     125                        obj = {};
     126
     127                if ( ! library || ! attachmentsBrowserView ) {
     128                        return;
     129                }
     130
     131                library[ collectionType ] = library[ collectionType ] || new Backbone.Model();
     132
     133                obj[ collectionType ] = new SettingsView({
     134                        controller: this,
     135                        model:      library[ collectionType ],
     136                        priority:   40
     137                });
     138
     139                attachmentsBrowserView.sidebar.set( obj );
     140
     141                if ( dragInfoText ) {
     142                        attachmentsBrowserView.toolbar.set( 'dragInfo', new View({
     143                                el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0],
     144                                priority: -40
     145                        }) );
     146                }
     147
     148                // Add the 'Reverse order' button to the toolbar.
     149                attachmentsBrowserView.toolbar.set( 'reverse', {
     150                        text:     l10n.reverseOrder,
     151                        priority: 80,
     152
     153                        click: function() {
     154                                library.reset( library.toArray().reverse() );
     155                        }
     156                });
     157        }
     158});
     159
     160module.exports = CollectionEdit;
     161 No newline at end of file
  • src/wp-includes/js/media/controllers/cropper.js

     
     1/**
     2 * wp.media.controller.Cropper
     3 *
     4 * A state for cropping an image.
     5 *
     6 * @class
     7 * @augments wp.media.controller.State
     8 * @augments Backbone.Model
     9 */
     10var 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
     16Cropper = 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
     115module.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 */
     10var State = require( './state.js' ),
     11        l10n = wp.media.view.l10n,
     12        EditAttachmentMetadata;
     13
     14EditAttachmentMetadata = 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
     27module.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 */
     19var State = require( './state.js' ),
     20        ToolbarView = require( '../views/toolbar.js' ),
     21        l10n = wp.media.view.l10n,
     22        EditImage;
     23
     24EditImage = 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
     76module.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 */
     22var State = require( './state.js' ),
     23        l10n = wp.media.view.l10n,
     24        Embed;
     25
     26Embed = State.extend({
     27        defaults: {
     28                id:       'embed',
     29                title:    l10n.insertFromUrlTitle,
     30                content:  'embed',
     31                menu:     'default',
     32                toolbar:  'main-embed',
     33                priority: 120,
     34                type:     'link',
     35                url:      '',
     36                metadata: {}
     37        },
     38
     39        // The amount of time used when debouncing the scan.
     40        sensitivity: 200,
     41
     42        initialize: function(options) {
     43                this.metadata = options.metadata;
     44                this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
     45                this.props = new Backbone.Model( this.metadata || { url: '' });
     46                this.props.on( 'change:url', this.debouncedScan, this );
     47                this.props.on( 'change:url', this.refresh, this );
     48                this.on( 'scan', this.scanImage, this );
     49        },
     50
     51        /**
     52         * Trigger a scan of the embedded URL's content for metadata required to embed.
     53         *
     54         * @fires wp.media.controller.Embed#scan
     55         */
     56        scan: function() {
     57                var scanners,
     58                        embed = this,
     59                        attributes = {
     60                                type: 'link',
     61                                scanners: []
     62                        };
     63
     64                // Scan is triggered with the list of `attributes` to set on the
     65                // state, useful for the 'type' attribute and 'scanners' attribute,
     66                // an array of promise objects for asynchronous scan operations.
     67                if ( this.props.get('url') ) {
     68                        this.trigger( 'scan', attributes );
     69                }
     70
     71                if ( attributes.scanners.length ) {
     72                        scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
     73                        scanners.always( function() {
     74                                if ( embed.get('scanners') === scanners ) {
     75                                        embed.set( 'loading', false );
     76                                }
     77                        });
     78                } else {
     79                        attributes.scanners = null;
     80                }
     81
     82                attributes.loading = !! attributes.scanners;
     83                this.set( attributes );
     84        },
     85        /**
     86         * Try scanning the embed as an image to discover its dimensions.
     87         *
     88         * @param {Object} attributes
     89         */
     90        scanImage: function( attributes ) {
     91                var frame = this.frame,
     92                        state = this,
     93                        url = this.props.get('url'),
     94                        image = new Image(),
     95                        deferred = $.Deferred();
     96
     97                attributes.scanners.push( deferred.promise() );
     98
     99                // Try to load the image and find its width/height.
     100                image.onload = function() {
     101                        deferred.resolve();
     102
     103                        if ( state !== frame.state() || url !== state.props.get('url') ) {
     104                                return;
     105                        }
     106
     107                        state.set({
     108                                type: 'image'
     109                        });
     110
     111                        state.props.set({
     112                                width:  image.width,
     113                                height: image.height
     114                        });
     115                };
     116
     117                image.onerror = deferred.reject;
     118                image.src = url;
     119        },
     120
     121        refresh: function() {
     122                this.frame.toolbar.get().refresh();
     123        },
     124
     125        reset: function() {
     126                this.props.clear().set({ url: '' });
     127
     128                if ( this.active ) {
     129                        this.refresh();
     130                }
     131        }
     132});
     133
     134module.exports = Embed;
     135 No newline at end of file
  • src/wp-includes/js/media/controllers/featured-image.js

     
     1/**
     2 * wp.media.controller.FeaturedImage
     3 *
     4 * A state for selecting a featured image for a post.
     5 *
     6 * @class
     7 * @augments wp.media.controller.Library
     8 * @augments wp.media.controller.State
     9 * @augments Backbone.Model
     10 *
     11 * @param {object}                     [attributes]                          The attributes hash passed to the state.
     12 * @param {string}                     [attributes.id=featured-image]        Unique identifier.
     13 * @param {string}                     [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region.
     14 * @param {wp.media.model.Attachments} [attributes.library]                  The attachments collection to browse.
     15 *                                                                           If one is not supplied, a collection of all images will be created.
     16 * @param {boolean}                    [attributes.multiple=false]           Whether multi-select is enabled.
     17 * @param {string}                     [attributes.content=upload]           Initial mode for the content region.
     18 *                                                                           Overridden by persistent user setting if 'contentUserSetting' is true.
     19 * @param {string}                     [attributes.menu=default]             Initial mode for the menu region.
     20 * @param {string}                     [attributes.router=browse]            Initial mode for the router region.
     21 * @param {string}                     [attributes.toolbar=featured-image]   Initial mode for the toolbar region.
     22 * @param {int}                        [attributes.priority=60]              The priority for the state link in the media menu.
     23 * @param {boolean}                    [attributes.searchable=true]          Whether the library is searchable.
     24 * @param {boolean|string}             [attributes.filterable=false]         Whether the library is filterable, and if so what filters should be shown.
     25 *                                                                           Accepts 'all', 'uploaded', or 'unattached'.
     26 * @param {boolean}                    [attributes.sortable=true]            Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
     27 * @param {boolean}                    [attributes.autoSelect=true]          Whether an uploaded attachment should be automatically added to the selection.
     28 * @param {boolean}                    [attributes.describe=false]           Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
     29 * @param {boolean}                    [attributes.contentUserSetting=true]  Whether the content region's mode should be set and persisted per user.
     30 * @param {boolean}                    [attributes.syncSelection=true]       Whether the Attachments selection should be persisted from the last state.
     31 */
     32var Attachment = require( '../models/attachment.js' ),
     33        Library = require( './library.js' ),
     34        l10n = wp.media.view.l10n,
     35        FeaturedImage;
     36
     37FeaturedImage = 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
     120module.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 */
     30var Selection = require( '../models/selection.js' ),
     31        Library = require( './library.js' ),
     32        l10n = wp.media.view.l10n,
     33        GalleryAdd;
     34
     35GalleryAdd = 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
     85module.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 */
     32var 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
     39GalleryEdit = 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
     138module.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 */
     25var State = require( './state.js' ),
     26        Library = require( './library.js' ),
     27        l10n = wp.media.view.l10n,
     28        ImageDetails;
     29
     30ImageDetails = 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
     60module.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 */
     35var selectionSync = require( '../utils/selection-sync.js' ),
     36        SelectionModel = require( '../models/selection.js' ),
     37        State = require( './state.js' ),
     38        l10n = wp.media.view.l10n,
     39        Library;
     40
     41Library = State.extend({
     42        defaults: {
     43                id:                 'library',
     44                title:              l10n.mediaLibraryTitle,
     45                multiple:           false,
     46                content:            'upload',
     47                menu:               'default',
     48                router:             'browse',
     49                toolbar:            'select',
     50                searchable:         true,
     51                filterable:         false,
     52                sortable:           true,
     53                autoSelect:         true,
     54                describe:           false,
     55                contentUserSetting: true,
     56                syncSelection:      true
     57        },
     58
     59        /**
     60         * If a library isn't provided, query all media items.
     61         * If a selection instance isn't provided, create one.
     62         *
     63         * @since 3.5.0
     64         */
     65        initialize: function() {
     66                var selection = this.get('selection'),
     67                        props;
     68
     69                if ( ! this.get('library') ) {
     70                        this.set( 'library', wp.media.query() );
     71                }
     72
     73                if ( ! (selection instanceof SelectionModel) ) {
     74                        props = selection;
     75
     76                        if ( ! props ) {
     77                                props = this.get('library').props.toJSON();
     78                                props = _.omit( props, 'orderby', 'query' );
     79                        }
     80
     81                        this.set( 'selection', new SelectionModel( null, {
     82                                multiple: this.get('multiple'),
     83                                props: props
     84                        }) );
     85                }
     86
     87                this.resetDisplays();
     88        },
     89
     90        /**
     91         * @since 3.5.0
     92         */
     93        activate: function() {
     94                this.syncSelection();
     95
     96                wp.Uploader.queue.on( 'add', this.uploading, this );
     97
     98                this.get('selection').on( 'add remove reset', this.refreshContent, this );
     99
     100                if ( this.get( 'router' ) && this.get('contentUserSetting') ) {
     101                        this.frame.on( 'content:activate', this.saveContentMode, this );
     102                        this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
     103                }
     104        },
     105
     106        /**
     107         * @since 3.5.0
     108         */
     109        deactivate: function() {
     110                this.recordSelection();
     111
     112                this.frame.off( 'content:activate', this.saveContentMode, this );
     113
     114                // Unbind all event handlers that use this state as the context
     115                // from the selection.
     116                this.get('selection').off( null, null, this );
     117
     118                wp.Uploader.queue.off( null, null, this );
     119        },
     120
     121        /**
     122         * Reset the library to its initial state.
     123         *
     124         * @since 3.5.0
     125         */
     126        reset: function() {
     127                this.get('selection').reset();
     128                this.resetDisplays();
     129                this.refreshContent();
     130        },
     131
     132        /**
     133         * Reset the attachment display settings defaults to the site options.
     134         *
     135         * If site options don't define them, fall back to a persistent user setting.
     136         *
     137         * @since 3.5.0
     138         */
     139        resetDisplays: function() {
     140                var defaultProps = wp.media.view.settings.defaultProps;
     141                this._displays = [];
     142                this._defaultDisplaySettings = {
     143                        align: defaultProps.align || getUserSetting( 'align', 'none' ),
     144                        size:  defaultProps.size  || getUserSetting( 'imgsize', 'medium' ),
     145                        link:  defaultProps.link  || getUserSetting( 'urlbutton', 'file' )
     146                };
     147        },
     148
     149        /**
     150         * Create a model to represent display settings (alignment, etc.) for an attachment.
     151         *
     152         * @since 3.5.0
     153         *
     154         * @param {wp.media.model.Attachment} attachment
     155         * @returns {Backbone.Model}
     156         */
     157        display: function( attachment ) {
     158                var displays = this._displays;
     159
     160                if ( ! displays[ attachment.cid ] ) {
     161                        displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) );
     162                }
     163                return displays[ attachment.cid ];
     164        },
     165
     166        /**
     167         * Given an attachment, create attachment display settings properties.
     168         *
     169         * @since 3.6.0
     170         *
     171         * @param {wp.media.model.Attachment} attachment
     172         * @returns {Object}
     173         */
     174        defaultDisplaySettings: function( attachment ) {
     175                var settings = this._defaultDisplaySettings;
     176                if ( settings.canEmbed = this.canEmbed( attachment ) ) {
     177                        settings.link = 'embed';
     178                }
     179                return settings;
     180        },
     181
     182        /**
     183         * Whether an attachment can be embedded (audio or video).
     184         *
     185         * @since 3.6.0
     186         *
     187         * @param {wp.media.model.Attachment} attachment
     188         * @returns {Boolean}
     189         */
     190        canEmbed: function( attachment ) {
     191                // If uploading, we know the filename but not the mime type.
     192                if ( ! attachment.get('uploading') ) {
     193                        var type = attachment.get('type');
     194                        if ( type !== 'audio' && type !== 'video' ) {
     195                                return false;
     196                        }
     197                }
     198
     199                return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() );
     200        },
     201
     202
     203        /**
     204         * If the state is active, no items are selected, and the current
     205         * content mode is not an option in the state's router (provided
     206         * the state has a router), reset the content mode to the default.
     207         *
     208         * @since 3.5.0
     209         */
     210        refreshContent: function() {
     211                var selection = this.get('selection'),
     212                        frame = this.frame,
     213                        router = frame.router.get(),
     214                        mode = frame.content.mode();
     215
     216                if ( this.active && ! selection.length && router && ! router.get( mode ) ) {
     217                        this.frame.content.render( this.get('content') );
     218                }
     219        },
     220
     221        /**
     222         * Callback handler when an attachment is uploaded.
     223         *
     224         * Switch to the Media Library if uploaded from the 'Upload Files' tab.
     225         *
     226         * Adds any uploading attachments to the selection.
     227         *
     228         * If the state only supports one attachment to be selected and multiple
     229         * attachments are uploaded, the last attachment in the upload queue will
     230         * be selected.
     231         *
     232         * @since 3.5.0
     233         *
     234         * @param {wp.media.model.Attachment} attachment
     235         */
     236        uploading: function( attachment ) {
     237                var content = this.frame.content;
     238
     239                if ( 'upload' === content.mode() ) {
     240                        this.frame.content.mode('browse');
     241                }
     242
     243                if ( this.get( 'autoSelect' ) ) {
     244                        this.get('selection').add( attachment );
     245                        this.frame.trigger( 'library:selection:add' );
     246                }
     247        },
     248
     249        /**
     250         * Persist the mode of the content region as a user setting.
     251         *
     252         * @since 3.5.0
     253         */
     254        saveContentMode: function() {
     255                if ( 'browse' !== this.get('router') ) {
     256                        return;
     257                }
     258
     259                var mode = this.frame.content.mode(),
     260                        view = this.frame.router.get();
     261
     262                if ( view && view.get( mode ) ) {
     263                        setUserSetting( 'libraryContent', mode );
     264                }
     265        }
     266});
     267
     268// Make selectionSync available on any Media Library state.
     269_.extend( Library.prototype, selectionSync );
     270
     271module.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 */
     9var Library = require( './library.js' ),
     10        MediaLibrary;
     11
     12MediaLibrary = 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
     48module.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 */
     21var Region = function( options ) {
     22        _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
     23};
     24
     25// Use Backbone's self-propagating `extend` inheritance method.
     26Region.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
     177module.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 */
     32var Library = require( './library.js' ),
     33        l10n = wp.media.view.l10n,
     34        ReplaceImage;
     35
     36ReplaceImage = 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
     106module.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 */
     18var 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.
     24StateMachine.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
     122module.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 */
     18var 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
     239module.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 */
     8var State = require( './state.js' ),
     9        l10n = wp.media.view.l10n,
     10        VideoDetails;
     11
     12VideoDetails = 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
     29module.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 */
     11var State = require( './state.js' ),
     12        l10n = wp.media.view.l10n,
     13        EditAttachmentMetadata;
     14
     15EditAttachmentMetadata = 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
     28module.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 */
     48var State = require( './state.js' ),
     49        ToolbarView = require( '../views/toolbar.js' ),
     50        l10n = wp.media.view.l10n,
     51        EditImage;
     52
     53EditImage = 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
     105module.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 */
     141var 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
     147Library = 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
     377module.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 */
     399var Region = function( options ) {
     400        _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
     401};
     402
     403// Use Backbone's self-propagating `extend` inheritance method.
     404Region.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
     555module.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 */
     574var 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.
     580StateMachine.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
     678module.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 */
     697var 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
     918module.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 */
     943var $ = jQuery,
     944        Attachment;
     945
     946Attachment = 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
     1102module.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 */
     1126var Attachment = require( './attachment.js' ),
     1127        Attachments;
     1128
     1129Attachments = 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
     1636module.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 */
     1655var Attachments = require( './attachments.js' ),
     1656        Query;
     1657
     1658Query = 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
     1943module.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 */
     1954var Attachments = require( './attachments.js' ),
     1955        Selection;
     1956
     1957Selection = 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
     2039module.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 */
     2047var 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
     2084module.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 */
     2096var 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
     2149module.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 */
     2161var View = require( './view.js' ),
     2162        AttachmentCompat;
     2163
     2164AttachmentCompat = 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
     2233module.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 */
     2243var View = require( './view.js' ),
     2244        $ = jQuery,
     2245        AttachmentFilters;
     2246
     2247AttachmentFilters = 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
     2310module.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 */
     2321var AttachmentFilters = require( '../attachment-filters.js' ),
     2322        l10n = wp.media.view.l10n,
     2323        All;
     2324
     2325All = 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
     2400module.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 */
     2411var AttachmentFilters = require( '../attachment-filters.js' ),
     2412        l10n = wp.media.view.l10n,
     2413        DateFilter;
     2414
     2415DateFilter = 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
     2441module.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 */
     2452var AttachmentFilters = require( '../attachment-filters.js' ),
     2453        l10n = wp.media.view.l10n,
     2454        Uploaded;
     2455
     2456Uploaded = 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
     2500module.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 */
     2510var View = require( './view.js' ),
     2511        $ = jQuery,
     2512        Attachment;
     2513
     2514Attachment = 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
     3041module.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 */
     3054var Details = require( './details.js' ),
     3055        MediaDetails = require( '../media-details.js' ),
     3056        TwoColumn;
     3057
     3058TwoColumn = 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
     3082module.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 */
     3093var Attachment = require( '../attachment.js' ),
     3094        l10n = wp.media.view.l10n,
     3095        Details;
     3096
     3097Details = 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
     3221module.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 */
     3232var Attachment = require( '../attachment.js' ),
     3233        Library;
     3234
     3235Library = Attachment.extend({
     3236        buttons: {
     3237                check: true
     3238        }
     3239});
     3240
     3241module.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 */
     3251var View = require( './view.js' ),
     3252        Attachment = require( './attachment.js' ),
     3253        $ = jQuery,
     3254        Attachments;
     3255
     3256Attachments = 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
     3540module.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 */
     3560var 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
     3581AttachmentsBrowser = 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 + ' &times; ' + 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
     3995module.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 */
     4005var View = require( './view.js' ),
     4006        Button;
     4007
     4008Button = 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
     4083module.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 */
     4095var Button = require( '../button.js' ),
     4096        DeleteSelected = require( './delete-selected.js' ),
     4097        DeleteSelectedPermanently;
     4098
     4099DeleteSelectedPermanently = 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
     4127module.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 */
     4138var Button = require( '../button.js' ),
     4139        l10n = wp.media.view.l10n,
     4140        DeleteSelected;
     4141
     4142DeleteSelected = 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
     4177module.exports = DeleteSelected;
     4178},{"../button.js":25}],28:[function(require,module,exports){
     4179var Button = require( '../button.js' ),
     4180        l10n = wp.media.view.l10n,
     4181        SelectModeToggle;
     4182
     4183SelectModeToggle = 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
     4231module.exports = SelectModeToggle;
     4232},{"../button.js":25}],29:[function(require,module,exports){
     4233var View = require( './view.js' ),
     4234        EditImage = require( './edit-image.js' ),
     4235        Details;
     4236
     4237Details = 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
     4258module.exports = Details;
     4259},{"./edit-image.js":30,"./view.js":55}],30:[function(require,module,exports){
     4260var View = require( './view.js' ),
     4261        EditImage;
     4262
     4263EditImage = 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
     4311module.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 */
     4321var View = require( './view.js' ),
     4322        FocusManager;
     4323
     4324FocusManager = 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
     4358module.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 */
     4375var 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
     4381Frame = 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
     4529module.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 */
     4545var 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
     4556EditAttachments = 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
     4776module.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 */
     4793var 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
     4801Manage = 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
     5021module.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 */
     5031var View = require( './view.js' ),
     5032        Iframe;
     5033
     5034Iframe = 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
     5047module.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 */
     5055var View = require( './view.js' ),
     5056        Label;
     5057
     5058Label = 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
     5073module.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 */
     5085var AttachmentDisplay = require( './settings/attachment-display.js' ),
     5086        $ = jQuery,
     5087        MediaDetails;
     5088
     5089MediaDetails = 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
     5223module.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 */
     5237var 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
     5248MediaFrame = 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
     5476module.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 */
     5486var View = require( './view.js' ),
     5487        $ = jQuery,
     5488        MenuItem;
     5489
     5490MenuItem = 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
     5548module.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 */
     5559var MenuItem = require( './menu-item.js' ),
     5560        PriorityList = require( './priority-list.js' ),
     5561        Menu;
     5562
     5563Menu = 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
     5664module.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 */
     5676var View = require( './view.js' ),
     5677        FocusManager = require( './focus-manager.js' ),
     5678        $ = jQuery,
     5679        Modal;
     5680
     5681Modal = 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
     5878module.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 */
     5888var View = require( './view.js' ),
     5889        PriorityList;
     5890
     5891PriorityList = 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
     5977module.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 */
     5988var MenuItem = require( './menu-item.js' ),
     5989        RouterItem;
     5990
     5991RouterItem = 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
     6003module.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 */
     6015var Menu = require( './menu.js' ),
     6016        RouterItem = require( './router-item.js' ),
     6017        Router;
     6018
     6019Router = 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
     6040module.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 */
     6050var View = require( './view.js' ),
     6051        l10n = wp.media.view.l10n,
     6052        Search;
     6053
     6054Search = 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
     6088module.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 */
     6098var View = require( './view.js' ),
     6099        $ = jQuery,
     6100        Settings;
     6101
     6102Settings = 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
     6208module.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 */
     6219var Settings = require( '../settings.js' ),
     6220        AttachmentDisplay;
     6221
     6222AttachmentDisplay = 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
     6301module.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 */
     6312var PriorityList = require( './priority-list.js' ),
     6313        Sidebar;
     6314
     6315Sidebar = PriorityList.extend({
     6316        className: 'media-sidebar'
     6317});
     6318
     6319module.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 */
     6329var View = require( './view.js' ),
     6330        Spinner;
     6331
     6332Spinner = 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
     6356module.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 */
     6369var View = require( './view.js' ),
     6370        Button = require( './button.js' ),
     6371        PriorityList = require( './priority-list.js' ),
     6372        Toolbar;
     6373
     6374Toolbar = 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
     6517module.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 */
     6529var View = require( '../view.js' ),
     6530        UploaderStatus = require( './status.js' ),
     6531        UploaderInline;
     6532
     6533UploaderInline = 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
     6648module.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 */
     6658var View = require( '../view.js' ),
     6659        UploaderStatusError;
     6660
     6661UploaderStatusError = View.extend({
     6662        className: 'upload-error',
     6663        template:  wp.template('uploader-status-error')
     6664});
     6665
     6666module.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 */
     6678var View = require( '../view.js' ),
     6679        UploaderStatusError = require( './status-error.js' ),
     6680        UploaderStatus;
     6681
     6682UploaderStatus = 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
     6804module.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 */
     6822var View = require( '../view.js' ),
     6823        $ = jQuery,
     6824        UploaderWindow;
     6825
     6826UploaderWindow = 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
     6915module.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 */
     6933var 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
     6980module.exports = View;
     6981},{}]},{},[7]);
  • src/wp-includes/js/media/grid.manifest.js

     
     1/* global _wpMediaViewsL10n, MediaElementPlayer, _wpMediaGridSettings */
     2(function($, _, Backbone, wp) {
     3        var media = wp.media;
     4
     5        media.controller.EditAttachmentMetadata = require( './controllers/edit-attachment-metadata.js' );
     6        media.view.MediaFrame.Manage = require( './views/frame/manage.js' );
     7        media.view.Attachment.Details.TwoColumn = require( './views/attachment/details-two-column.js' );
     8        media.view.MediaFrame.Manage.Router = require( './router/manage.js' );
     9        media.view.EditImage.Details = require( './views/edit-image-details.js' );
     10        media.view.MediaFrame.EditAttachments = require( './views/frame/edit-attachments.js' );
     11        media.view.SelectModeToggleButton = require( './views/button/select-mode-toggle.js' );
     12        media.view.DeleteSelectedButton = require( './views/button/delete-selected.js' );
     13        media.view.DeleteSelectedPermanentlyButton = require( './views/button/delete-selected-permanently.js' );
     14
     15}(jQuery, _, Backbone, wp));
  • src/wp-includes/js/media/models/attachment.js

     
     1/**
     2 * wp.media.model.Attachment
     3 *
     4 * @class
     5 * @augments Backbone.Model
     6 */
     7var $ = jQuery,
     8        Attachment;
     9
     10Attachment = 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
     166module.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 */
     23var Attachment = require( './attachment.js' ),
     24        Attachments;
     25
     26Attachments = 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
     533module.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 **/
     14var Attachment = require( './attachment' ),
     15        PostImage;
     16
     17PostImage = 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
     154module.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 */
     8var 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
     40module.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 */
     18var Attachments = require( './attachments.js' ),
     19        Query;
     20
     21Query = 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
     306module.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 */
     10var Attachments = require( './attachments.js' ),
     11        Selection;
     12
     13Selection = 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
     95module.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 */
     3window.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=&hellip;]
     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 || '&hellip;';
     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 */
     242var $ = jQuery,
     243        Attachment;
     244
     245Attachment = 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
     401module.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 */
     425var Attachment = require( './attachment.js' ),
     426        Attachments;
     427
     428Attachments = 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
     935module.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 **/
     950var Attachment = require( './attachment' ),
     951        PostImage;
     952
     953PostImage = 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
     1090module.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 */
     1109var Attachments = require( './attachments.js' ),
     1110        Query;
     1111
     1112Query = 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
     1397module.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 */
     1408var Attachments = require( './attachments.js' ),
     1409        Selection;
     1410
     1411Selection = 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
     1493module.exports = Selection;
     1494},{"./attachments.js":3}]},{},[1]);
  • src/wp-includes/js/media/models.manifest.js

     
     1/* global _wpMediaModelsL10n:false */
     2window.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=&hellip;]
     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 || '&hellip;';
     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 */
     7var 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
     44module.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 */
     11var 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
     64module.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 */
     12var Details = require( './details.js' ),
     13        MediaDetails = require( '../media-details.js' ),
     14        TwoColumn;
     15
     16TwoColumn = 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
     40module.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 */
     10var Attachment = require( '../attachment.js' ),
     11        l10n = wp.media.view.l10n,
     12        Details;
     13
     14Details = 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
     138module.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 */
     10var Attachment = require( '../attachment.js' ),
     11        EditLibrary;
     12
     13EditLibrary = Attachment.extend({
     14        buttons: {
     15                close: true
     16        }
     17});
     18
     19module.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 */
     11var Selection = require( './selection.js' ),
     12        EditSelection;
     13
     14EditSelection = Selection.extend({
     15        buttons: {
     16                close: true
     17        }
     18});
     19
     20module.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 */
     10var Attachment = require( '../attachment.js' ),
     11        Library;
     12
     13Library = Attachment.extend({
     14        buttons: {
     15                check: true
     16        }
     17});
     18
     19module.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 */
     10var Attachment = require( '../attachment.js' ),
     11        Selection;
     12
     13Selection = 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
     23module.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 */
     11var View = require( './view.js' ),
     12        AttachmentCompat;
     13
     14AttachmentCompat = 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
     83module.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 */
     10var AttachmentFilters = require( '../attachment-filters.js' ),
     11        l10n = wp.media.view.l10n,
     12        All;
     13
     14All = 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
     89module.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 */
     10var AttachmentFilters = require( '../attachment-filters.js' ),
     11        l10n = wp.media.view.l10n,
     12        DateFilter;
     13
     14DateFilter = 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
     40module.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 */
     10var AttachmentFilters = require( '../attachment-filters.js' ),
     11        l10n = wp.media.view.l10n,
     12        Uploaded;
     13
     14Uploaded = 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
     58module.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 */
     9var View = require( './view.js' ),
     10        $ = jQuery,
     11        AttachmentFilters;
     12
     13AttachmentFilters = 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
     76module.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 */
     9var View = require( './view.js' ),
     10        $ = jQuery,
     11        Attachment;
     12
     13Attachment = 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
     540module.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 */
     19var 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
     40AttachmentsBrowser = 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 + ' &times; ' + 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
     454module.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 */
     10var Attachments = require( '../attachments.js' ),
     11        AttachmentSelection = require( '../attachment/selection.js' ),
     12        Selection;
     13
     14Selection = 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
     29module.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 */
     9var View = require( './view.js' ),
     10        Attachment = require( './attachment.js' ),
     11        $ = jQuery,
     12        Attachments;
     13
     14Attachments = 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
     298module.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 */
     12var MediaDetails = require( './media-details' ),
     13        AudioDetails;
     14
     15AudioDetails = 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
     36module.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 */
     11var Button = require( '../button.js' ),
     12        DeleteSelected = require( './delete-selected.js' ),
     13        DeleteSelectedPermanently;
     14
     15DeleteSelectedPermanently = 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
     43module.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 */
     10var Button = require( '../button.js' ),
     11        l10n = wp.media.view.l10n,
     12        DeleteSelected;
     13
     14DeleteSelected = 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
     49module.exports = DeleteSelected;
     50 No newline at end of file
  • src/wp-includes/js/media/views/button/select-mode-toggle.js

     
     1var Button = require( '../button.js' ),
     2        l10n = wp.media.view.l10n,
     3        SelectModeToggle;
     4
     5SelectModeToggle = 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
     53module.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 */
     9var View = require( './view.js' ),
     10        Button = require( './button.js' ),
     11        $ = jQuery,
     12        ButtonGroup;
     13
     14ButtonGroup = 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
     46module.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 */
     9var View = require( './view.js' ),
     10        Button;
     11
     12Button = 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
     87module.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 */
     15var 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
     22Cropper = 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
     66module.exports = Cropper;
     67 No newline at end of file
  • src/wp-includes/js/media/views/edit-image-details.js

     
     1var View = require( './view.js' ),
     2        EditImage = require( './edit-image.js' ),
     3        Details;
     4
     5Details = 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
     26module.exports = Details;
     27 No newline at end of file
  • src/wp-includes/js/media/views/edit-image.js

     
     1var View = require( './view.js' ),
     2        EditImage;
     3
     4EditImage = 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
     52module.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 */
     11var AttachmentDisplay = require( '../settings/attachment-display.js' ),
     12        EmbedImage;
     13
     14EmbedImage = 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
     31module.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 */
     10var Settings = require( '../settings.js' ),
     11        $ = jQuery,
     12        EmbedLink;
     13
     14EmbedLink = 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
     65module.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 */
     9var View = require( '../view.js' ),
     10        $ = jQuery,
     11        EmbedUrl;
     12
     13EmbedUrl = 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
     79module.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 */
     9var 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
     15Embed = 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
     68module.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 */
     9var View = require( './view.js' ),
     10        FocusManager;
     11
     12FocusManager = 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
     46module.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 */
     14var 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
     21AudioDetails = 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
     75module.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 */
     15var 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
     26EditAttachments = 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
     246module.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 */
     16var 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
     28ImageDetails = 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
     183module.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 */
     16var 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
     24Manage = 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
     244module.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 */
     13var 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
     21MediaDetails = 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
     132module.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 */
     15var 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
     37Post = 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
     749module.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
     15var 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
     25Select = 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
     174module.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 */
     14var 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
     21VideoDetails = 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
     135module.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 */
     16var 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
     22Frame = 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
     170module.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 */
     9var View = require( './view.js' ),
     10        Iframe;
     11
     12Iframe = 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
     25module.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 */
     11var AttachmentDisplay = require( './settings/attachment-display.js' ),
     12        $ = jQuery,
     13        ImageDetails;
     14
     15ImageDetails = 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
     165module.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 */
     7var View = require( './view.js' ),
     8        Label;
     9
     10Label = 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
     25module.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 */
     11var AttachmentDisplay = require( './settings/attachment-display.js' ),
     12        $ = jQuery,
     13        MediaDetails;
     14
     15MediaDetails = 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
     149module.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 */
     13var 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
     24MediaFrame = 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
     252module.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 */
     9var View = require( './view.js' ),
     10        $ = jQuery,
     11        MenuItem;
     12
     13MenuItem = 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
     71module.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 */
     10var MenuItem = require( './menu-item.js' ),
     11        PriorityList = require( './priority-list.js' ),
     12        Menu;
     13
     14Menu = 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
     115module.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 */
     11var View = require( './view.js' ),
     12        FocusManager = require( './focus-manager.js' ),
     13        $ = jQuery,
     14        Modal;
     15
     16Modal = 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
     213module.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 */
     9var View = require( './view.js' ),
     10        PriorityList;
     11
     12PriorityList = 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
     98module.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 */
     10var MenuItem = require( './menu-item.js' ),
     11        RouterItem;
     12
     13RouterItem = 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
     25module.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 */
     11var Menu = require( './menu.js' ),
     12        RouterItem = require( './router-item.js' ),
     13        Router;
     14
     15Router = 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
     36module.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 */
     9var View = require( './view.js' ),
     10        l10n = wp.media.view.l10n,
     11        Search;
     12
     13Search = 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
     47module.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 */
     9var View = require( './view.js' ),
     10        AttachmentsSelection = require( './attachments/selection.js' ),
     11        l10n = wp.media.view.l10n,
     12        Selection;
     13
     14Selection = 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
     83module.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 */
     10var Settings = require( '../settings.js' ),
     11        AttachmentDisplay;
     12
     13AttachmentDisplay = 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
     92module.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 */
     10var Settings = require( '../settings.js' ),
     11        Gallery;
     12
     13Gallery = Settings.extend({
     14        className: 'collection-settings gallery-settings',
     15        template:  wp.template('gallery-settings')
     16});
     17
     18module.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 */
     10var Settings = require( '../settings.js' ),
     11        Playlist;
     12
     13Playlist = Settings.extend({
     14        className: 'collection-settings playlist-settings',
     15        template:  wp.template('playlist-settings')
     16});
     17
     18module.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 */
     9var View = require( './view.js' ),
     10        $ = jQuery,
     11        Settings;
     12
     13Settings = 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
     119module.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 */
     10var PriorityList = require( './priority-list.js' ),
     11        Sidebar;
     12
     13Sidebar = PriorityList.extend({
     14        className: 'media-sidebar'
     15});
     16
     17module.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 */
     9var View = require( './view.js' ),
     10        Spinner;
     11
     12Spinner = 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
     36module.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 */
     11var Select = require( './select.js' ),
     12        l10n = wp.media.view.l10n,
     13        Embed;
     14
     15Embed = 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
     35module.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 */
     10var Toolbar = require( '../toolbar.js' ),
     11        l10n = wp.media.view.l10n,
     12        Select;
     13
     14Select = 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
     68module.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 */
     12var View = require( './view.js' ),
     13        Button = require( './button.js' ),
     14        PriorityList = require( './priority-list.js' ),
     15        Toolbar;
     16
     17Toolbar = 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
     160module.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 */
     12var View = require( '../view.js' ),
     13        l10n = wp.media.view.l10n,
     14        $ = jQuery,
     15        EditorUploader;
     16
     17EditorUploader = 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
     219module.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 */
     11var View = require( '../view.js' ),
     12        UploaderStatus = require( './status.js' ),
     13        UploaderInline;
     14
     15UploaderInline = 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
     130module.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 */
     9var View = require( '../view.js' ),
     10        UploaderStatusError;
     11
     12UploaderStatusError = View.extend({
     13        className: 'upload-error',
     14        template:  wp.template('uploader-status-error')
     15});
     16
     17module.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 */
     11var View = require( '../view.js' ),
     12        UploaderStatusError = require( './status-error.js' ),
     13        UploaderStatus;
     14
     15UploaderStatus = 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
     137module.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 */
     17var View = require( '../view.js' ),
     18        $ = jQuery,
     19        UploaderWindow;
     20
     21UploaderWindow = 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
     110module.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 */
     12var MediaDetails = require( './media-details' ),
     13        VideoDetails;
     14
     15VideoDetails = 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
     41module.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 */
     17var 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
     64module.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 */
     35var Selection = require( '../models/selection.js' ),
     36        Library = require( './library.js' ),
     37        CollectionAdd;
     38
     39CollectionAdd = 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
     100module.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 */
     136var 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
     143CollectionEdit = 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
     261module.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 */
     272var 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
     278Cropper = 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
     377module.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 */
     397var State = require( './state.js' ),
     398        ToolbarView = require( '../views/toolbar.js' ),
     399        l10n = wp.media.view.l10n,
     400        EditImage;
     401
     402EditImage = 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
     454module.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 */
     477var State = require( './state.js' ),
     478        l10n = wp.media.view.l10n,
     479        Embed;
     480
     481Embed = 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
     589module.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 */
     622var Attachment = require( '../models/attachment.js' ),
     623        Library = require( './library.js' ),
     624        l10n = wp.media.view.l10n,
     625        FeaturedImage;
     626
     627FeaturedImage = 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
     710module.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 */
     741var Selection = require( '../models/selection.js' ),
     742        Library = require( './library.js' ),
     743        l10n = wp.media.view.l10n,
     744        GalleryAdd;
     745
     746GalleryAdd = 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
     796module.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 */
     829var 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
     836GalleryEdit = 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
     935module.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 */
     961var State = require( './state.js' ),
     962        Library = require( './library.js' ),
     963        l10n = wp.media.view.l10n,
     964        ImageDetails;
     965
     966ImageDetails = 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
     996module.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 */
     1032var 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
     1038Library = 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
     1268module.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 *