Make WordPress Core

Ticket #28510: browserify.2.diff

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

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

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

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

     
     1/**
     2 * wp.media.controller.FeaturedImage
     3 *
     4 * A state for selecting a featured image for a post.
     5 *
     6 * @class
     7 * @augments wp.media.controller.Library
     8 * @augments wp.media.controller.State
     9 * @augments Backbone.Model
     10 *
     11 * @param {object}                     [attributes]                          The attributes hash passed to the state.
     12 * @param {string}                     [attributes.id=featured-image]        Unique identifier.
     13 * @param {string}                     [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region.
     14 * @param {wp.media.model.Attachments} [attributes.library]                  The attachments collection to browse.
     15 *                                                                           If one is not supplied, a collection of all images will be created.
     16 * @param {boolean}                    [attributes.multiple=false]           Whether multi-select is enabled.
     17 * @param {string}                     [attributes.content=upload]           Initial mode for the content region.
     18 *                                                                           Overridden by persistent user setting if 'contentUserSetting' is true.
     19 * @param {string}                     [attributes.menu=default]             Initial mode for the menu region.
     20 * @param {string}                     [attributes.router=browse]            Initial mode for the router region.
     21 * @param {string}                     [attributes.toolbar=featured-image]   Initial mode for the toolbar region.
     22 * @param {int}                        [attributes.priority=60]              The priority for the state link in the media menu.
     23 * @param {boolean}                    [attributes.searchable=true]          Whether the library is searchable.
     24 * @param {boolean|string}             [attributes.filterable=false]         Whether the library is filterable, and if so what filters should be shown.
     25 *                                                                           Accepts 'all', 'uploaded', or 'unattached'.
     26 * @param {boolean}                    [attributes.sortable=true]            Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
     27 * @param {boolean}                    [attributes.autoSelect=true]          Whether an uploaded attachment should be automatically added to the selection.
     28 * @param {boolean}                    [attributes.describe=false]           Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
     29 * @param {boolean}                    [attributes.contentUserSetting=true]  Whether the content region's mode should be set and persisted per user.
     30 * @param {boolean}                    [attributes.syncSelection=true]       Whether the Attachments selection should be persisted from the last state.
     31 */
     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        Selection = 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 Selection) ) {
     74                        props = selection;
     75
     76                        if ( ! props ) {
     77                                props = this.get('library').props.toJSON();
     78                                props = _.omit( props, 'orderby', 'query' );
     79                        }
     80
     81                        this.set( 'selection', new Selection( null, {
     82                                multiple: this.get('multiple'),
     83                                props: props
     84                        }) );
     85                }
     86
     87                this.resetDisplays();
     88        },
     89
     90        /**
     91         * @since 3.5.0
     92         */
     93        activate: function() {
     94                this.syncSelection();
     95
     96                wp.Uploader.queue.on( 'add', this.uploading, this );
     97
     98                this.get('selection').on( 'add remove reset', this.refreshContent, this );
     99
     100                if ( this.get( 'router' ) && this.get('contentUserSetting') ) {
     101                        this.frame.on( 'content:activate', this.saveContentMode, this );
     102                        this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
     103                }
     104        },
     105
     106        /**
     107         * @since 3.5.0
     108         */
     109        deactivate: function() {
     110                this.recordSelection();
     111
     112                this.frame.off( 'content:activate', this.saveContentMode, this );
     113
     114                // Unbind all event handlers that use this state as the context
     115                // from the selection.
     116                this.get('selection').off( null, null, this );
     117
     118                wp.Uploader.queue.off( null, null, this );
     119        },
     120
     121        /**
     122         * Reset the library to its initial state.
     123         *
     124         * @since 3.5.0
     125         */
     126        reset: function() {
     127                this.get('selection').reset();
     128                this.resetDisplays();
     129                this.refreshContent();
     130        },
     131
     132        /**
     133         * Reset the attachment display settings defaults to the site options.
     134         *
     135         * If site options don't define them, fall back to a persistent user setting.
     136         *
     137         * @since 3.5.0
     138         */
     139        resetDisplays: function() {
     140                var defaultProps = wp.media.view.settings.defaultProps;
     141                this._displays = [];
     142                this._defaultDisplaySettings = {
     143                        align: defaultProps.align || getUserSetting( 'align', 'none' ),
     144                        size:  defaultProps.size  || getUserSetting( 'imgsize', 'medium' ),
     145                        link:  defaultProps.link  || getUserSetting( 'urlbutton', 'file' )
     146                };
     147        },
     148
     149        /**
     150         * Create a model to represent display settings (alignment, etc.) for an attachment.
     151         *
     152         * @since 3.5.0
     153         *
     154         * @param {wp.media.model.Attachment} attachment
     155         * @returns {Backbone.Model}
     156         */
     157        display: function( attachment ) {
     158                var displays = this._displays;
     159
     160                if ( ! displays[ attachment.cid ] ) {
     161                        displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) );
     162                }
     163                return displays[ attachment.cid ];
     164        },
     165
     166        /**
     167         * Given an attachment, create attachment display settings properties.
     168         *
     169         * @since 3.6.0
     170         *
     171         * @param {wp.media.model.Attachment} attachment
     172         * @returns {Object}
     173         */
     174        defaultDisplaySettings: function( attachment ) {
     175                var settings = this._defaultDisplaySettings;
     176                if ( settings.canEmbed = this.canEmbed( attachment ) ) {
     177                        settings.link = 'embed';
     178                }
     179                return settings;
     180        },
     181
     182        /**
     183         * Whether an attachment can be embedded (audio or video).
     184         *
     185         * @since 3.6.0
     186         *
     187         * @param {wp.media.model.Attachment} attachment
     188         * @returns {Boolean}
     189         */
     190        canEmbed: function( attachment ) {
     191                // If uploading, we know the filename but not the mime type.
     192                if ( ! attachment.get('uploading') ) {
     193                        var type = attachment.get('type');
     194                        if ( type !== 'audio' && type !== 'video' ) {
     195                                return false;
     196                        }
     197                }
     198
     199                return _.contains( wp.media.view.settings.embedExts, attachment.get('filename').split('.').pop() );
     200        },
     201
     202
     203        /**
     204         * If the state is active, no items are selected, and the current
     205         * content mode is not an option in the state's router (provided
     206         * the state has a router), reset the content mode to the default.
     207         *
     208         * @since 3.5.0
     209         */
     210        refreshContent: function() {
     211                var selection = this.get('selection'),
     212                        frame = this.frame,
     213                        router = frame.router.get(),
     214                        mode = frame.content.mode();
     215
     216                if ( this.active && ! selection.length && router && ! router.get( mode ) ) {
     217                        this.frame.content.render( this.get('content') );
     218                }
     219        },
     220
     221        /**
     222         * Callback handler when an attachment is uploaded.
     223         *
     224         * Switch to the Media Library if uploaded from the 'Upload Files' tab.
     225         *
     226         * Adds any uploading attachments to the selection.
     227         *
     228         * If the state only supports one attachment to be selected and multiple
     229         * attachments are uploaded, the last attachment in the upload queue will
     230         * be selected.
     231         *
     232         * @since 3.5.0
     233         *
     234         * @param {wp.media.model.Attachment} attachment
     235         */
     236        uploading: function( attachment ) {
     237                var content = this.frame.content;
     238
     239                if ( 'upload' === content.mode() ) {
     240                        this.frame.content.mode('browse');
     241                }
     242
     243                if ( this.get( 'autoSelect' ) ) {
     244                        this.get('selection').add( attachment );
     245                        this.frame.trigger( 'library:selection:add' );
     246                }
     247        },
     248
     249        /**
     250         * Persist the mode of the content region as a user setting.
     251         *
     252         * @since 3.5.0
     253         */
     254        saveContentMode: function() {
     255                if ( 'browse' !== this.get('router') ) {
     256                        return;
     257                }
     258
     259                var mode = this.frame.content.mode(),
     260                        view = this.frame.router.get();
     261
     262                if ( view && view.get( mode ) ) {
     263                        setUserSetting( 'libraryContent', mode );
     264                }
     265        }
     266});
     267
     268// Make selectionSync available on any Media Library state.
     269_.extend( Library.prototype, selectionSync );
     270
     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(wp) {
     3        var media = wp.media;
     4
     5        media.controller.EditAttachmentMetadata = require( './controllers/edit-attachment-metadata.js' );
     6        media.view.MediaFrame.Manage = require( './views/frame/manage.js' );
     7        media.view.Attachment.Details.TwoColumn = require( './views/attachment/details-two-column.js' );
     8        media.view.MediaFrame.Manage.Router = require( './router/manage.js' );
     9        media.view.EditImage.Details = require( './views/edit-image-details.js' );
     10        media.view.MediaFrame.EditAttachments = require( './views/frame/edit-attachments.js' );
     11        media.view.SelectModeToggleButton = require( './views/button/select-mode-toggle.js' );
     12        media.view.DeleteSelectedButton = require( './views/button/delete-selected.js' );
     13        media.view.DeleteSelectedPermanentlyButton = require( './views/button/delete-selected-permanently.js' );
     14
     15}(wp));
  • src/wp-includes/js/media/models/attachment.js

     
     1/**
     2 * wp.media.model.Attachment
     3 *
     4 * @class
     5 * @augments Backbone.Model
     6 */
     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 *
     1273 * @class
     1274 * @augments wp.media.controller.Library
     1275 * @augments wp.media.controller.State
     1276 * @augments Backbone.Model
     1277 */