Make WordPress Core

Changeset 22320


Ignore:
Timestamp:
10/28/2012 11:29:17 PM (12 years ago)
Author:
koopersmith
Message:

Give media a heart transplant.

Revises the concept of the media controller and workspace views (i.e. two central points of control) to be more granular. The main media object is now the Frame, which is a hybrid view and state machine.

The state machine is a collection of states, which are just generic instances of Backbone.Model. This circumvents the problem of juggling global parameters when changing states. Each state contains its own event loop. All events are also forwarded to the frame itself (as is the case in all model/collection relationships).

The frame view contains several regions, each of which can be overridden without harming or re-rendering the other regions. These work well when used in conjunction with the state machine events.

This removes the upload sidebar (don't worry, visible upload UI will return). Drag and drop uploading still works. The ability to upload has been abstracted into its own view (instead of being attached to the central workspace view).

Editing galleries is temporarily broken — the gallery creation and editing experiences will be unified in a future patch.

Adds events to detect dragging changes in wp.Uploader and adds methods to detect and leverage browser support for CSS3 transitions.

see #21390, #21809.

Location:
trunk
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/wp-admin/includes/meta-boxes.php

    r22044 r22320  
    10191019            $thumbnailId = $element.find('input[name="thumbnail_id"]'),
    10201020            title        = '<?php _e( "Choose a Featured Image" ); ?>',
    1021             workflow, setFeaturedImage;
     1021            workflow, selection, setFeaturedImage;
    10221022
    10231023        setFeaturedImage = function( thumbnailId ) {
     
    10381038                });
    10391039
    1040                 workflow.selection.on( 'add', function( model ) {
     1040                selection = workflow.state().get('selection');
     1041
     1042                selection.on( 'add', function( model ) {
    10411043                    var sizes = model.get('sizes'),
    10421044                        size;
     
    10521054                    size = size || model.toJSON();
    10531055
    1054                     workflow.modal.close();
    1055                     workflow.selection.clear();
     1056                    workflow.close();
     1057                    selection.clear();
    10561058
    10571059                    $( '<img />', {
     
    10621064            }
    10631065
    1064             workflow.modal.open();
     1066            workflow.open();
    10651067        });
    10661068
  • trunk/wp-admin/js/media-upload.js

    r22220 r22320  
    105105            workflow = workflows[ id ] = wp.media( _.defaults( options || {}, {
    106106                title:    wp.media.view.l10n.insertMedia,
    107                 multiple: true,
    108                 describe: true
     107                multiple: true
    109108            } ) );
    110109
    111             workflow.on( 'update:insert', function( selection ) {
     110            workflow.get('library').on( 'insert', function( selection ) {
    112111                this.insert( selection.map( function( attachment ) {
    113112                    if ( 'image' === attachment.get('type') )
     
    118117            }, this );
    119118
    120             workflow.on( 'update:gallery', function( selection ) {
     119            workflow.get('gallery').on( 'update', function( selection ) {
    121120                var view = wp.mce.view.get('gallery'),
    122121                    shortcode;
  • trunk/wp-includes/css/media-views.css

    r22247 r22320  
    7676    z-index: 50;
    7777    height: 60px;
     78    padding: 0 10px;
    7879    border-bottom: 1px solid #dfdfdf;
    7980}
     
    102103
    103104/**
    104  * Workspace
    105  */
    106 .media-workspace {
    107     position: relative;
    108     width: 100%;
    109     height: 100%;
    110 }
    111 
    112 .upload-attachments {
    113     position: absolute;
    114     top: 0;
    115     left: 0;
    116     bottom: 0;
    117     width: 180px;
    118     margin: 10px;
    119     text-align: center;
    120     border: 3px dashed #dfdfdf;
    121     background: #fff;
    122     z-index: 100;
    123 }
    124 
    125 .upload-attachments h3 {
    126     font-size: 18px;
    127     font-weight: 200;
    128     color: #777;
    129     padding: 40px 0 0;
    130     margin: 0;
    131 }
    132 
    133 .upload-attachments span {
    134     display: block;
    135     color: #777;
    136     margin: 10px 0;
    137 }
    138 
    139 .upload-attachments a {
    140     display: inline-block;
    141     margin: 0 auto;
    142 }
    143 
    144 .drag-over .upload-attachments {
    145     width: auto;
    146     right: 0;
    147     border-color: #83B4D8;
    148     box-shadow: 0 0 0 10px #fff;
    149 }
    150 
    151 .existing-attachments {
    152     position: absolute;
    153     top: 0;
    154     left: 200px;
    155     right: 0;
    156     bottom: 0;
    157     margin: 0 20px;
    158 }
    159 
    160 .media-workspace .attachments,
    161 .media-workspace .media-toolbar {
     105 * Frame
     106 */
     107
     108.media-frame .attachments,
     109.media-frame .media-toolbar {
    162110    -webkit-transition-property: left, right, top, bottom, margin;
    163111    -moz-transition-property:    left, right, top, bottom, margin;
     
    173121}
    174122
    175 .media-workspace .attachments {
    176     position: absolute;
    177     top: 0;
     123.media-frame .attachments {
     124    position: absolute;
     125    top: 61px;
    178126    left: 0;
    179127    right: 0;
     
    183131}
    184132
    185 .media-workspace.with-toolbar .attachments {
    186     top: 61px;
    187 }
    188 
    189 .media-workspace .media-toolbar {
     133.media-frame.hide-toolbar .attachments {
     134    top: 0;
     135}
     136
     137.media-frame .media-toolbar {
     138    margin-top: 0;
     139}
     140
     141.media-frame.hide-toolbar .media-toolbar {
    190142    margin-top: -61px;
    191143}
    192144
    193 .media-workspace.with-toolbar .media-toolbar {
    194     margin-top: 0;
    195 }
    196 
    197 .media-workspace .media-toolbar .add-to-gallery {
     145.media-frame .media-toolbar .add-to-gallery {
    198146    display: none;
    199147}
    200 
    201148/**
    202149 * Attachments
     
    214161    right: 0;
    215162    height: 50px;
     163    padding: 0 10px;
    216164    background: #fff;
    217165}
     
    243191    bottom: 0;
    244192    overflow: auto;
    245     margin: 0 -10px 20px;
     193    margin: 0 0 20px;
    246194}
    247195
     
    446394}
    447395
    448 .upload-attachments .media-progress-bar {
    449     margin-top: 80px;
     396.uploader-window {
     397    position: fixed;
     398    top: 0;
     399    left: 0;
     400    right: 0;
     401    bottom: 0;
     402    background: rgba( 0, 86, 132, 0.9 );
     403
     404    /*z-index: -200;*/
     405    z-index: 250000;
    450406    display: none;
    451 }
    452 
    453 .uploading .upload-attachments .media-progress-bar {
     407    text-align: center;
     408    opacity: 0;
     409
     410    -webkit-transition: opacity 250ms;
     411    -moz-transition:    opacity 250ms;
     412    -ms-transition:     opacity 250ms;
     413    -o-transition:      opacity 250ms;
     414    transition:         opacity 250ms;
     415}
     416
     417/*.drag-over .uploader-window {
     418    z-index: 250000;
     419}*/
     420
     421.uploader-window-content {
     422    position: absolute;
     423    top: 30px;
     424    left: 30px;
     425    right: 30px;
     426    bottom: 30px;
     427    border: 1px dashed #fff;
     428}
     429
     430.uploader-window h3 {
     431    position: absolute;
     432    top: 50%;
     433    left: 0;
     434    right: 0;
     435    -webkit-transform: translateY( -50% );
     436    -moz-transform:    translateY( -50% );
     437    -ms-transform:     translateY( -50% );
     438    -o-transform:      translateY( -50% );
     439    transform:         translateY( -50% );
     440
     441    font-size: 18px;
     442    font-weight: 200;
     443    color: #fff;
     444    padding: 0;
     445}
     446
     447.uploader-window .media-progress-bar {
     448    margin-top: 20px;
     449    max-width: 300px;
     450    background: transparent;
     451    border-color: #fff;
     452    /*display: none;*/
     453}
     454
     455.uploader-window .media-progress-bar div {
     456    background: #fff;
     457}
     458
     459.uploading .uploader-window .media-progress-bar {
    454460    display: block;
    455461}
  • trunk/wp-includes/js/mce-view.js

    r22220 r22320  
    693693
    694694                this.workflow = wp.media({
    695                     view:      'gallery',
     695                    state:     'gallery',
    696696                    selection: this.attachments.models,
    697697                    title:     mceview.l10n.editGallery,
    698698                    editing:   true,
    699                     multiple:  true,
    700                     describe:  true
     699                    multiple:  true
    701700                });
    702701
  • trunk/wp-includes/js/media-models.js

    r22212 r22320  
    1515     */
    1616    media = wp.media = function( attributes ) {
    17         if ( media.controller.Workflow )
    18             return new media.controller.Workflow( attributes ).attach().render();
     17        if ( media.view.Frame )
     18            return new media.view.Frame( attributes ).render().attach().open();
    1919    };
    2020
  • trunk/wp-includes/js/media-views.js

    r22267 r22320  
    99    l10n = media.view.l10n = _.isUndefined( _wpMediaViewsL10n ) ? {} : _wpMediaViewsL10n;
    1010
     11    // Check if the browser supports CSS 3.0 transitions
     12    $.support.transition = (function(){
     13        var style = document.documentElement.style,
     14            transitions = {
     15                WebkitTransition: 'webkitTransitionEnd',
     16                MozTransition:    'transitionend',
     17                OTransition:      'oTransitionEnd otransitionend',
     18                transition:       'transitionend'
     19            }, transition;
     20
     21        transition = _.find( _.keys( transitions ), function( transition ) {
     22            return ! _.isUndefined( style[ transition ] );
     23        });
     24
     25        return transition && {
     26            end: transitions[ transition ]
     27        };
     28    }());
     29
     30    // Makes it easier to bind events using transitions.
     31    media.transition = function( selector ) {
     32        var deferred = $.Deferred();
     33
     34        if ( $.support.transition ) {
     35            if ( ! (selector instanceof $) )
     36                selector = $( selector );
     37
     38            // Resolve the deferred when the first element finishes animating.
     39            selector.first().one( $.support.transition.end, deferred.resolve );
     40
     41        // Otherwise, execute on the spot.
     42        } else {
     43            deferred.resolve();
     44        }
     45
     46        return deferred.promise();
     47    };
     48
    1149    /**
    1250     * ========================================================================
     
    1654
    1755    /**
    18      * wp.media.controller.Workflow
     56     * wp.media.controller.StateMachine
    1957     */
    20     media.controller.Workflow = Backbone.Model.extend({
     58    media.controller.StateMachine = function( states ) {
     59        this.states = new Backbone.Collection( states );
     60    };
     61
     62    // Use Backbone's self-propagating `extend` inheritance method.
     63    media.controller.StateMachine.extend = Backbone.Model.extend;
     64
     65    _.extend( media.controller.StateMachine.prototype, {
     66        // Fetch a state model.
     67        //
     68        // Implicitly creates states.
     69        get: function( id ) {
     70            // Ensure that the `states` collection exists so the `StateMachine`
     71            // can be used as a mixin.
     72            this.states = this.states || new Backbone.Collection();
     73
     74            if ( ! this.states.get( id ) )
     75                this.states.add({ id: id });
     76            return this.states.get( id );
     77        },
     78
     79        // Selects or returns the active state.
     80        //
     81        // If a `id` is provided, sets that as the current state.
     82        // If no parameters are provided, returns the current state object.
     83        state: function( id ) {
     84            var previous;
     85
     86            if ( id ) {
     87                if ( previous = this.state() )
     88                    previous.trigger('deactivate');
     89                this._state = id;
     90                return this.state().trigger('activate');
     91            }
     92
     93            if ( this._state )
     94                return this.get( this._state );
     95        }
     96    });
     97
     98    // Map methods from the `states` collection to the `StateMachine` itself.
     99    _.each([ 'on', 'off', 'trigger' ], function( method ) {
     100        media.controller.StateMachine.prototype[ method ] = function() {
     101            // Ensure that the `states` collection exists so the `StateMachine`
     102            // can be used as a mixin.
     103            this.states = this.states || new Backbone.Collection();
     104            // Forward the method to the `states` collection.
     105            this.states[ method ].apply( this.states, arguments );
     106            return this;
     107        };
     108    });
     109
     110    // wp.media.controller.Library
     111    // ---------------------------
     112    media.controller.Library = Backbone.Model.extend({
    21113        defaults: {
    22             title:     '',
    23             multiple:  false,
    24             view:      'library',
    25             library:   {},
    26             selection: []
     114            id:       'library',
     115            multiple: false,
     116            describe: false
    27117        },
    28118
    29119        initialize: function() {
     120            if ( ! this.get('selection') )
     121                this.set( 'selection', new Attachments() );
     122
     123            if ( ! this.get('library') )
     124                this.set( 'library', media.query() );
     125
     126            this.on( 'activate', this.activate, this );
     127        },
     128
     129        activate: function() {
     130            var frame = this.frame,
     131                toolbar;
     132
     133            toolbar = this._postLibraryToolbar = new media.view.Toolbar.PostLibrary({
     134                controller: frame,
     135                selection:  this.get('selection')
     136            });
     137
     138            frame.toolbar( toolbar );
     139            this.get('selection').on( 'add remove', toolbar.visibility, toolbar );
     140
     141            frame.content( new media.view.Attachments({
     142                directions: this.get('multiple') ? l10n.selectMediaMultiple : l10n.selectMediaSingular,
     143                controller: frame,
     144                collection: this.get('library'),
     145                // The single `Attachment` view to be used in the `Attachments` view.
     146                AttachmentView: media.view.Attachment.Library
     147            }).render() );
     148
     149            if ( ! this.get('selection').length )
     150                frame.$el.addClass('hide-toolbar');
     151
     152            // If we're in a workflow that supports multiple attachments,
     153            // automatically select any uploading attachments.
     154            if ( this.get('multiple') )
     155                wp.Uploader.queue.on( 'add', this.selectUpload, this );
     156        },
     157
     158        deactivate: function() {
     159            var toolbar = this._postLibraryToolbar;
     160
     161            wp.Uploader.queue.off( 'add', this.selectUpload, this );
     162            this.get('selection').off( 'add remove', toolbar.visibility, toolbar );
     163        },
     164
     165        selectUpload: function( attachment ) {
     166            this.get('selection').add( attachment );
     167        }
     168    });
     169
     170    // wp.media.controller.Gallery
     171    // ---------------------------
     172    media.controller.Gallery = Backbone.Model.extend({
     173        defaults: {
     174            id:         'gallery',
     175            multiple:   true,
     176            describe:   true
     177        },
     178
     179        initialize: function() {
     180            if ( ! this.get('selection') )
     181                this.set( 'selection', new Attachments() );
     182
     183            this.on( 'activate', this.activate, this );
     184        },
     185
     186        activate: function() {
     187            var frame = this.frame;
     188
     189            frame.toolbar( new media.view.Toolbar.Gallery({
     190                controller: frame,
     191                editing:    this.get('editing'),
     192                selection:  this.get('selection')
     193            }) );
     194
     195            frame.content( new media.view.Attachments({
     196                directions: 'Gallery time!',
     197                controller: frame,
     198                collection: this.get('selection'),
     199                sortable:   true,
     200                // The single `Attachment` view to be used in the `Attachments` view.
     201                AttachmentView: media.view.Attachment.Gallery
     202            }).render() );
     203
     204            // Automatically select any uploading attachments.
     205            wp.Uploader.queue.on( 'add', this.selectUpload, this );
     206        },
     207
     208        deactivate: function() {
     209            wp.Uploader.queue.off( 'add', this.selectUpload, this );
     210        },
     211
     212        selectUpload: function( attachment ) {
     213            this.get('selection').add( attachment );
     214        }
     215    });
     216
     217    /**
     218     * ========================================================================
     219     * VIEWS
     220     * ========================================================================
     221     */
     222
     223    /**
     224     * wp.media.view.Frame
     225     */
     226    media.view.Frame = Backbone.View.extend({
     227        tagName:   'div',
     228        className: 'media-frame',
     229        template:  media.template('media-frame'),
     230
     231        initialize: function() {
     232            _.defaults( this.options, {
     233                state:     'library',
     234                title:     '',
     235                selection: [],
     236                library:   {},
     237                modal:     true,
     238                multiple:  false,
     239                uploader:  true
     240            });
     241
    30242            this.createSelection();
    31 
    32             // Initialize view storage.
    33             this._views        = {};
    34             this._pendingViews = {};
    35 
    36             // Initialize modal container view.
    37             this.modal = new media.view.Modal({ controller: this });
    38 
    39             // Add default views.
    40             //
    41             // Use the `library` property to initialize the corresponding view,
    42             // then unset the property.
    43             this.add( 'library', media.view.Workspace.Library, {
    44                 collection: media.query( this.get('library') )
    45             });
    46             this.unset('library');
    47 
    48             // Add the gallery view.
    49             this.add( 'gallery', media.view.Workspace.Gallery, {
    50                 collection: this.selection
    51             });
    52             this.add( 'gallery-library', media.view.Workspace.Library.Gallery, {
    53                 collection: media.query({ type: 'image' })
    54             });
    55         },
    56 
    57 
    58         // Registers a view.
    59         //
    60         // `id` is a unique ID for the view relative to the workflow instance.
    61         // `constructor` is a `Backbone.View` constructor. `options` are the
    62         // options to be passed when the view is initialized.
    63         //
    64         // Triggers the `add` and `add:VIEW_ID` events.
    65         add: function( id, constructor, options ) {
    66             this.remove( id );
    67             this._pendingViews[ id ] = {
    68                 view:    constructor,
    69                 options: options
    70             };
    71             this.trigger( 'add add:' + id, constructor, options );
    72             return this;
    73         },
    74 
    75         // Returns a registered view instance. If an `id` is not provided,
    76         // it will return the active view.
    77         //
    78         // Lazily instantiates a registered view.
    79         //
    80         // Triggers the `init` and `init:VIEW_ID` events.
    81         view: function( id ) {
    82             var pending;
    83 
    84             id = id || this.get('view');
    85             pending = this._pendingViews[ id ];
    86 
    87             if ( ! this._views[ id ] && pending ) {
    88                 this._views[ id ] = new pending.view( _.extend({ controller: this }, pending.options || {} ) );
    89                 delete this._pendingViews[ id ];
    90                 this.trigger( 'init init:' + id, this._views[ id ] );
    91             }
    92 
    93             return this._views[ id ];
    94         },
    95 
    96         // Unregisters a view from the workflow.
    97         //
    98         // Triggers the `remove` and `remove:VIEW_ID` events.
    99         remove: function( id ) {
    100             delete this._views[ id ];
    101             delete this._pendingViews[ id ];
    102             this.trigger( 'remove remove:' + id );
    103             return this;
    104         },
    105 
    106         // Renders a view and places it within the modal window.
    107         // Automatically adds a view if `constructor` is provided.
    108         render: function( id, constructor, options ) {
    109             var view;
    110             id = id || this.get('view');
    111 
    112             if ( constructor )
    113                 this.add( id, constructor, options );
    114 
    115             view = this.view( id );
    116 
    117             if ( ! view )
    118                 return this;
    119 
    120             view.render();
    121             this.modal.content( view );
    122             return this;
    123         },
    124 
    125         update: function( event ) {
    126             this.close();
    127             this.trigger( 'update', this.selection, this );
    128             this.trigger( 'update:' + event, this.selection, this );
    129             this.selection.clear();
     243            this.createSubviews();
     244            this.createStates();
     245        },
     246
     247        render: function() {
     248            var els = [ this.sidebar().el, this.toolbar().el, this.content().el ];
     249
     250            if ( this.modal )
     251                this.modal.render();
     252
     253            // Detach any views that will be rebound to maintain event bidnings.
     254            this.$el.children().filter( els ).detach();
     255            this.$el.empty().append( els );
     256
     257            // Render the window uploader if it exists.
     258            if ( this.uploader )
     259                this.uploader.render().$el.appendTo( this.$el );
     260
     261            return this;
    130262        },
    131263
    132264        createSelection: function() {
    133             var controller = this;
    134 
    135             // Initialize workflow-specific models.
    136             // Use the `selection` property to initialize the Attachments
    137             // collection, then unset the property.
    138             this.selection = new Attachments( this.get('selection') );
    139             this.unset('selection');
    140 
    141             _.extend( this.selection, {
     265            var controller = this,
     266                selection = this.options.selection;
     267
     268            if ( ! (selection instanceof Attachments) )
     269                selection = this.options.selection = new Attachments( selection );
     270
     271            _.extend( selection, {
    142272                // Override the selection's add method.
    143273                // If the workflow does not support multiple
    144274                // selected attachments, reset the selection.
    145275                add: function( models, options ) {
    146                     if ( ! controller.get('multiple') ) {
     276                    if ( ! controller.state().get('multiple') ) {
    147277                        models = _.isArray( models ) ? _.first( models ) : models;
    148278                        this.clear( options );
     
    171301                }
    172302            });
    173         }
    174     });
    175 
    176     // Map modal methods to the workflow.
    177     _.each(['attach','detach','open','close'], function( method ) {
    178         media.controller.Workflow.prototype[ method ] = function() {
    179             this.modal[ method ].apply( this.modal, arguments );
    180             return this;
     303        },
     304
     305        createStates: function() {
     306            var options = this.options;
     307
     308            // Create the default `states` collection.
     309            this.states = new Backbone.Collection();
     310
     311            // Ensure states have a reference to the frame.
     312            this.states.on( 'add', function( model ) {
     313                model.frame = this;
     314            }, this );
     315
     316            // Add the default states.
     317            this.states.add([
     318                new media.controller.Library({
     319                    selection:  options.selection,
     320                    collection: media.query( options.library ),
     321                    multiple:   this.options.multiple
     322                }),
     323                new media.controller.Gallery({
     324                    selection:  options.selection
     325                })
     326            ]);
     327
     328            // Set the default state.
     329            this.state( options.state );
     330        },
     331
     332        createSubviews: function() {
     333            // Initialize a stub view for each subview region.
     334            _.each(['toolbar','sidebar','content'], function( subview ) {
     335                this[ '_' + subview ] = new Backbone.View({
     336                    tagName:   'div',
     337                    className: 'media-' + subview
     338                });
     339            }, this );
     340
     341            // Initialize modal container view.
     342            if ( this.options.modal ) {
     343                this.modal = new media.view.Modal({
     344                    controller: this,
     345                    $content:   this.$el,
     346                    title:      this.options.title
     347                });
     348            }
     349
     350            // Initialize window-wide uploader.
     351            if ( this.options.uploader ) {
     352                this.uploader = new media.view.UploaderWindow({
     353                    uploader: {
     354                        dropzone: this.modal ? this.modal.$el : this.$el
     355                    }
     356                });
     357            }
     358        }
     359    });
     360
     361    // Make the `Frame` a `StateMachine`.
     362    _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
     363
     364    // Create methods to fetch and replace individual subviews.
     365    _.each(['toolbar','sidebar','content'], function( subview ) {
     366        media.view.Frame.prototype[ subview ] = function( view ) {
     367            var previous = this[ '_' + subview ];
     368
     369            if ( ! view )
     370                return previous;
     371
     372            view.$el.addClass( 'media-' + subview );
     373
     374            if ( previous.destroy )
     375                previous.destroy();
     376            previous.undelegateEvents();
     377            previous.$el.replaceWith( view.$el );
     378            this[ '_' + subview ] = view;
    181379        };
    182380    });
    183381
    184     /**
    185      * ========================================================================
    186      * VIEWS
    187      * ========================================================================
    188      */
     382    // Map some of the modal's methods to the frame.
     383    _.each(['open','close','attach','detach'], function( method ) {
     384        media.view.Frame.prototype[ method ] = function( view ) {
     385            if ( this.modal )
     386                this.modal[ method ].apply( this.modal, arguments );
     387            return this;
     388        };
     389    });
    189390
    190391    /**
     
    201402        initialize: function() {
    202403            this.controller = this.options.controller;
    203             this.controller.on( 'change:title', this.render, this );
    204404
    205405            _.defaults( this.options, {
    206                 container: document.body
     406                container: document.body,
     407                title:     ''
    207408            });
    208409        },
     
    216417            this.options.$content.detach();
    217418
    218             this.$el.html( this.template( this.controller.toJSON() ) );
    219             this.$('.media-modal-content').append( this.options.$content );
     419            this.$el.html( this.template({
     420                title: this.options.title
     421            }) );
     422
     423            this.options.$content.addClass('media-modal-content');
     424            this.$('.media-modal').append( this.options.$content );
    220425            return this;
    221426        },
     
    224429            this.$el.appendTo( this.options.container );
    225430            this.controller.trigger( 'attach', this.controller );
     431            return this;
    226432        },
    227433
     
    229435            this.$el.detach();
    230436            this.controller.trigger( 'detach', this.controller );
     437            return this;
    231438        },
    232439
     
    234441            this.$el.show();
    235442            this.controller.trigger( 'open', this.controller );
     443            return this;
    236444        },
    237445
     
    239447            this.$el.hide();
    240448            this.controller.trigger( 'close', this.controller );
     449            return this;
    241450        },
    242451
     
    256465        }
    257466    });
     467
     468    // wp.media.view.UploaderWindow
     469    // ----------------------------
     470    media.view.UploaderWindow = Backbone.View.extend({
     471        tagName:   'div',
     472        className: 'uploader-window',
     473        template:  media.template('uploader-window'),
     474
     475        initialize: function() {
     476            var uploader;
     477
     478            this.controller = this.options.controller;
     479
     480            uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
     481                container: this.$el,
     482                dropzone:  this.$el,
     483                browser:   this.$('.upload-attachments a'),
     484                params:    {}
     485            });
     486
     487            // Track uploading attachments.
     488            wp.Uploader.queue.on( 'add remove reset change:percent', this.renderUploadProgress, this );
     489
     490            if ( uploader.dropzone ) {
     491                // Ensure the dropzone is a jQuery collection.
     492                if ( ! (uploader.dropzone instanceof $) )
     493                    uploader.dropzone = $( uploader.dropzone );
     494
     495                // Attempt to initialize the uploader whenever the dropzone is hovered.
     496                uploader.dropzone.one( 'mouseenter dragenter', _.bind( this.maybeInitUploader, this ) );
     497            }
     498        },
     499
     500        render: function() {
     501            this.maybeInitUploader();
     502            this.renderUploadProgress();
     503            this.$el.html( this.template( this.options ) );
     504            this.$bar = this.$('.upload-attachments .media-progress-bar div');
     505            return this;
     506        },
     507
     508        maybeInitUploader: function() {
     509            var $id, dropzone;
     510
     511            // If the uploader already exists or the body isn't in the DOM, bail.
     512            if ( this.uploader || ! this.$el.closest('body').length )
     513                return;
     514
     515            $id = $('#post_ID');
     516            if ( $id.length )
     517                this.options.uploader.params.post_id = $id.val();
     518
     519            this.uploader = new wp.Uploader( this.options.uploader );
     520
     521            dropzone = this.uploader.dropzone;
     522            dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
     523            dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
     524        },
     525
     526        show: function() {
     527            var $el = this.$el.show();
     528
     529            // Ensure that the animation is triggered by waiting until
     530            // the transparent element is painted into the DOM.
     531            _.defer( function() {
     532                $el.css({ opacity: 1 });
     533            });
     534        },
     535
     536        hide: function() {
     537            var $el = this.$el.css({ opacity: 0 });
     538
     539            media.transition( $el ).done( function() {
     540                // Transition end events are subject to race conditions.
     541                // Make sure that the value is set as intended.
     542                if ( '0' === $el.css('opacity') )
     543                    $el.hide();
     544            });
     545        },
     546
     547        renderUploadProgress: function() {
     548            var queue = wp.Uploader.queue;
     549
     550            this.$el.toggleClass( 'uploading', !! queue.length );
     551
     552            if ( ! this.$bar || ! queue.length )
     553                return;
     554
     555            this.$bar.width( ( queue.reduce( function( memo, attachment ) {
     556                if ( attachment.get('uploading') )
     557                    return memo + ( attachment.get('percent') || 0 );
     558                else
     559                    return memo + 100;
     560            }, 0 ) / queue.length ) + '%' );
     561        }
     562    });
     563
    258564
    259565    /**
     
    265571
    266572        initialize: function() {
    267             this._views    = {};
     573            this.controller = this.options.controller;
     574
     575            this._views     = {};
    268576            this.$primary   = $('<div class="media-toolbar-primary" />').prependTo( this.$el );
    269577            this.$secondary = $('<div class="media-toolbar-secondary" />').prependTo( this.$el );
    270578
    271             if ( this.options.items ) {
    272                 _.each( this.options.items, function( view, id ) {
    273                     this.add( id, view, { silent: true } );
    274                 }, this );
    275                 this.render();
    276             }
     579            if ( this.options.items )
     580                this.add( this.options.items, { silent: true }).render();
    277581        },
    278582
     
    294598
    295599        add: function( id, view, options ) {
     600            // Accept an object with an `id` : `view` mapping.
     601            if ( _.isObject( id ) ) {
     602                _.each( id, function( view, id ) {
     603                    this.add( id, view, options );
     604                }, this );
     605                return this;
     606            }
     607
    296608            if ( ! ( view instanceof Backbone.View ) ) {
    297609                view.classes = [ id ].concat( view.classes || [] );
     
    299611            }
    300612
     613            view.controller = view.controller || this.controller;
     614
    301615            this._views[ id ] = view;
    302616            if ( ! options || ! options.silent )
     
    317631    });
    318632
     633    // wp.media.view.Toolbar.PostLibrary
     634    // ---------------------------------
     635    media.view.Toolbar.PostLibrary = media.view.Toolbar.extend({
     636        initialize: function() {
     637            var selection = this.options.selection,
     638                controller = this.options.controller;
     639
     640            this.options.items = {
     641                'selection-preview': new media.view.SelectionPreview({
     642                    controller: controller,
     643                    collection: selection,
     644                    priority: -40
     645                }),
     646
     647                'create-new-gallery': {
     648                    style:    'primary',
     649                    text:     l10n.createNewGallery,
     650                    priority: 40,
     651
     652                    click: function() {
     653                        this.controller.state('gallery');
     654                    }
     655                },
     656
     657                'insert-into-post': new media.view.ButtonGroup({
     658                    priority: 30,
     659                    classes:  'dropdown-flip-x',
     660                    buttons:  [
     661                        {
     662                            text:  l10n.insertIntoPost,
     663                            click: function() {
     664                                controller.close();
     665                                controller.state().trigger( 'insert', selection );
     666                                selection.clear();
     667                            }
     668                        },
     669                        {
     670                            classes:  ['down-arrow'],
     671                            dropdown: new media.view.AttachmentDisplaySettings().render().$el,
     672
     673                            click: function( event ) {
     674                                var $el = this.$el;
     675
     676                                if ( ! $( event.target ).closest('.dropdown').length )
     677                                    $el.toggleClass('active');
     678
     679                                // Stop the event from propagating further so we can bind
     680                                // a one-time event to the body (and ensure that a click
     681                                // on the dropdown won't trigger said event).
     682                                event.stopPropagation();
     683
     684                                if ( $el.is(':visible') ) {
     685                                    $(document.body).one( 'click', function() {
     686                                        $el.removeClass('active');
     687                                    });
     688                                }
     689                            }
     690                        }
     691                    ]
     692                }).render(),
     693
     694                'add-to-gallery': {
     695                    text:     l10n.addToGallery,
     696                    priority: 20
     697                }
     698            };
     699
     700            media.view.Toolbar.prototype.initialize.apply( this, arguments );
     701        },
     702
     703        visibility: function() {
     704            var selection = this.options.selection,
     705                controller = this.options.controller,
     706                count = selection.length,
     707                showGallery;
     708
     709            controller.$el.toggleClass( 'hide-toolbar', ! count );
     710
     711            // Check if every attachment in the selection is an image.
     712            showGallery = count > 1 && selection.all( function( attachment ) {
     713                return 'image' === attachment.get('type');
     714            });
     715
     716            this.get('create-new-gallery').$el.toggle( showGallery );
     717            insert = this.get('insert-into-post');
     718            _.each( insert.buttons, function( button ) {
     719                button.model.set( 'style', showGallery ? '' : 'primary' );
     720            });
     721        }
     722    });
     723
     724    // wp.media.view.Toolbar.Gallery
     725    // -----------------------------
     726    media.view.Toolbar.Gallery = media.view.Toolbar.extend({
     727        initialize: function() {
     728            var editing = this.options.editing,
     729                selection = this.options.selection,
     730                controller = this.options.controller;
     731
     732            this.options.items = {
     733                'update-gallery': {
     734                    style:    'primary',
     735                    text:     editing ? l10n.updateGallery : l10n.insertGalleryIntoPost,
     736                    priority: 40,
     737                    click:    function() {
     738                        controller.close();
     739                        controller.state().trigger( 'update', selection );
     740                        selection.clear();
     741                        controller.state('library');
     742                    }
     743                },
     744
     745                'return-to-library': {
     746                    text:     editing ? l10n.addImagesFromLibrary : l10n.returnToLibrary,
     747                    priority: -40,
     748
     749                    click: function() {
     750                        this.controller.state('library');
     751                    }
     752                }
     753            };
     754
     755            media.view.Toolbar.prototype.initialize.apply( this, arguments );
     756        }
     757    });
    319758
    320759    /**
     
    460899
    461900            options.buttons  = this.buttons;
    462             options.describe = this.controller.get('describe');
     901            options.describe = this.controller.state().get('describe');
    463902
    464903            if ( 'image' === options.type )
     
    473912
    474913            // Check if the model is selected.
    475             if ( this.controller.selection.has( this.model ) )
     914            if ( this.selected() )
    476915                this.select();
    477916
     
    485924
    486925        toggleSelection: function( event ) {
    487             var selection = this.controller.selection;
     926            var selection = this.controller.state().get('selection');
     927
     928            if ( ! selection )
     929                return;
     930
    488931            selection[ selection.has( this.model ) ? 'remove' : 'add' ]( this.model );
    489932        },
    490933
     934        selected: function() {
     935            var selection = this.controller.state().get('selection');
     936            if ( selection )
     937                return selection.has( this.model );
     938        },
     939
    491940        select: function( model, collection ) {
    492             // If a collection is provided, check if it's the selection.
    493             // If it's not, bail; we're in another selection's event loop.
    494             if ( collection && collection !== this.controller.selection )
     941            var selection = this.controller.state().get('selection');
     942
     943            // Check if a selection exists and if it's the collection provided.
     944            // If they're not the same collection, bail; we're in another
     945            // selection's event loop.
     946            if ( ! selection || ( collection && collection !== selection ) )
    495947                return;
    496948
     
    499951
    500952        deselect: function( model, collection ) {
    501             // If a collection is provided, check if it's the selection.
    502             // If it's not, bail; we're in another selection's event loop.
    503             if ( collection && collection !== this.controller.selection )
     953            var selection = this.controller.state().get('selection');
     954
     955            // Check if a selection exists and if it's the collection provided.
     956            // If they're not the same collection, bail; we're in another
     957            // selection's event loop.
     958            if ( ! selection || ( collection && collection !== selection ) )
    504959                return;
    505960
     
    6091064        }())
    6101065    });
    611 
    612     /**
    613      * wp.media.view.Workspace
    614      */
    615     media.view.Workspace = Backbone.View.extend({
    616         tagName:   'div',
    617         className: 'media-workspace',
    618         template:  media.template('media-workspace'),
    619 
    620         // The `options` to be passed to `Attachments` view.
    621         attachmentsView: {},
    622 
    623         events: {
    624             'dragenter':  'maybeInitUploader',
    625             'mouseenter': 'maybeInitUploader'
    626         },
    627 
    628         initialize: function() {
    629             this.controller = this.options.controller;
    630 
    631             _.defaults( this.options, {
    632                 selectOne:       false,
    633                 uploader:        {},
    634                 attachmentsView: {}
    635             });
    636 
    637             this.$content = $('<div class="existing-attachments" />');
    638 
    639             // Generate the `options` passed to the `Attachments` view.
    640             // Order of priority from lowest to highest: the provided defaults,
    641             // the prototypal `attachmentsView` property, the `attachmentsView`
    642             // option for the current instance, and then the `controller` and
    643             // `collection` keys, to ensure they're correctly set.
    644             this.attachmentsView = _.extend( {
    645                 directions: this.controller.get('multiple') ? l10n.selectMediaMultiple : l10n.selectMediaSingular
    646             }, this.attachmentsView, this.options.attachmentsView, {
    647                 controller: this.controller,
    648                 collection: this.collection
    649             });
    650 
    651             // Initialize the `Attachments` view.
    652             this.attachmentsView = new media.view.Attachments( this.attachmentsView );
    653             this.$content.append( this.attachmentsView.$el );
    654 
    655             // Track uploading attachments.
    656             wp.Uploader.queue.on( 'add remove reset change:percent', this.renderUploadProgress, this );
    657 
    658             // If we're in a workflow that supports multiple attachments,
    659             // automatically select any uploading attachments.
    660             if ( this.controller.get('multiple') )
    661                 wp.Uploader.queue.on( 'add', this.selectUpload, this );
    662         },
    663 
    664         render: function() {
    665             this.$content.detach();
    666 
    667             this.attachmentsView.render();
    668             this.renderUploadProgress();
    669             this.$el.html( this.template( this.options ) ).append( this.$content );
    670             this.$bar = this.$('.upload-attachments .media-progress-bar div');
    671             return this;
    672         },
    673 
    674         maybeInitUploader: function() {
    675             var workspace = this,
    676                 params = {},
    677                 $id;
    678 
    679             // If the uploader already exists or the body isn't in the DOM, bail.
    680             if ( this.uploader || ! this.$el.closest('body').length )
    681                 return;
    682 
    683             $id = $('#post_ID');
    684             if ( $id.length )
    685                 params.post_id = $id.val();
    686 
    687             this.uploader = new wp.Uploader( _.extend({
    688                 container: this.$el,
    689                 dropzone:  this.$el,
    690                 browser:   this.$('.upload-attachments a'),
    691                 params:    params
    692             }, this.options.uploader ) );
    693         },
    694 
    695         selectUpload: function( attachment ) {
    696             this.controller.selection.add( attachment );
    697         },
    698 
    699         renderUploadProgress: function() {
    700             var queue = wp.Uploader.queue;
    701 
    702             this.$el.toggleClass( 'uploading', !! queue.length );
    703 
    704             if ( ! this.$bar || ! queue.length )
    705                 return;
    706 
    707             this.$bar.width( ( queue.reduce( function( memo, attachment ) {
    708                 if ( attachment.get('uploading') )
    709                     return memo + ( attachment.get('percent') || 0 );
    710                 else
    711                     return memo + 100;
    712             }, 0 ) / queue.length ) + '%' );
    713         }
    714     });
    715 
    716     /**
    717      * wp.media.view.Workspace.Library
    718      */
    719     media.view.Workspace.Library = media.view.Workspace.extend({
    720 
    721         attachmentsView: {
    722             // The single `Attachment` view to be used in the `Attachments` view.
    723             AttachmentView: media.view.Attachment.Library
    724         },
    725 
    726         initialize: function() {
    727             media.view.Workspace.prototype.initialize.apply( this, arguments );
    728 
    729             // If this supports multiple attachments, initialize the sample toolbar view.
    730             if ( this.controller.get('multiple') )
    731                 this.initToolbarView();
    732         },
    733 
    734         // Initializes the toolbar view. Currently uses defaults set for
    735         // inserting media into a post. This should be pulled out into the
    736         // appropriate workflow when the time comes, but is currently here
    737         // to test multiple selections.
    738         initToolbarView: function() {
    739             var controller = this.controller;
    740 
    741             this.toolbarView = new media.view.Toolbar({
    742                 items: {
    743                     'selection-preview': new media.view.SelectionPreview({
    744                         controller: this.controller,
    745                         collection: this.controller.selection,
    746                         priority: -40
    747                     }),
    748 
    749                     'create-new-gallery': {
    750                         style:    'primary',
    751                         text:     l10n.createNewGallery,
    752                         priority: 40,
    753 
    754                         click: function() {
    755                             controller.render('gallery');
    756                         }
    757                     },
    758 
    759                     'insert-into-post': new media.view.ButtonGroup({
    760                         priority: 30,
    761                         classes:  'dropdown-flip-x',
    762                         buttons:  [
    763                             {
    764                                 text:  l10n.insertIntoPost,
    765                                 click: _.bind( controller.update, controller, 'insert' )
    766                             },
    767                             {
    768                                 classes:  ['down-arrow'],
    769                                 dropdown: new media.view.AttachmentDisplaySettings().render().$el,
    770 
    771                                 click: function( event ) {
    772                                     var $el = this.$el;
    773 
    774                                     if ( ! $( event.target ).closest('.dropdown').length )
    775                                         $el.toggleClass('active');
    776 
    777                                     // Stop the event from propagating further so we can bind
    778                                     // a one-time event to the body (and ensure that a click
    779                                     // on the dropdown won't trigger said event).
    780                                     event.stopPropagation();
    781 
    782                                     if ( $el.is(':visible') ) {
    783                                         $(document.body).one( 'click', function() {
    784                                             $el.removeClass('active');
    785                                         });
    786                                     }
    787                                 }
    788                             }
    789                         ]
    790                     }).render(),
    791 
    792                     'add-to-gallery': {
    793                         text:     l10n.addToGallery,
    794                         priority: 20
    795                     }
    796                 }
    797             });
    798 
    799             this.controller.selection.on( 'add remove', function() {
    800                 var count = this.controller.selection.length,
    801                     showGallery;
    802 
    803                 this.$el.toggleClass( 'with-toolbar', !! count );
    804 
    805                 // Check if every attachment in the selection is an image.
    806                 showGallery = count > 1 && this.controller.selection.all( function( attachment ) {
    807                     return 'image' === attachment.get('type');
    808                 });
    809 
    810                 this.toolbarView.get('create-new-gallery').$el.toggle( showGallery );
    811                 insert = this.toolbarView.get('insert-into-post');
    812                 _.each( insert.buttons, function( button ) {
    813                     button.model.set( 'style', showGallery ? '' : 'primary' );
    814                 });
    815             }, this );
    816 
    817             this.$content.append( this.toolbarView.$el );
    818         }
    819     });
    820 
    821     media.view.Workspace.Library.Gallery = media.view.Workspace.Library.extend({
    822         initToolbarView: function() {
    823             var controller = this.controller,
    824                 editing = controller.get('editing'),
    825                 items = {
    826                     'selection-preview': new media.view.SelectionPreview({
    827                         controller: this.controller,
    828                         collection: this.controller.selection,
    829                         priority:   -40,
    830                         clearable:  false
    831                     }),
    832 
    833                     'continue-editing-gallery': {
    834                         style:    'primary',
    835                         text:     l10n.continueEditingGallery,
    836                         priority: 40,
    837 
    838                         click: function() {
    839                             controller.render( 'gallery' );
    840                         }
    841                     }
    842                 };
    843 
    844             this.toolbarView = new media.view.Toolbar({
    845                 items: items
    846             });
    847 
    848             this.$el.addClass('with-toolbar');
    849             this.$content.append( this.toolbarView.$el );
    850         }
    851     });
    852 
    853     /**
    854      * wp.media.view.Workspace.Gallery
    855      */
    856     media.view.Workspace.Gallery = media.view.Workspace.extend({
    857 
    858         attachmentsView: {
    859             // The single `Attachment` view to be used in the `Attachments` view.
    860             AttachmentView: media.view.Attachment.Gallery,
    861             sortable:       true
    862         },
    863 
    864         initialize: function() {
    865             media.view.Workspace.prototype.initialize.apply( this, arguments );
    866             this.initToolbarView();
    867         },
    868 
    869         // Initializes the toolbar view. Currently uses defaults set for
    870         // inserting media into a post. This should be pulled out into the
    871         // appropriate workflow when the time comes, but is currently here
    872         // to test multiple selections.
    873         initToolbarView: function() {
    874             var controller = this.controller,
    875                 editing = controller.get('editing'),
    876                 items = {
    877                     'update-gallery': {
    878                         style:    'primary',
    879                         text:     editing ? l10n.updateGallery : l10n.insertGalleryIntoPost,
    880                         priority: 40,
    881                         click:    _.bind( controller.update, controller, 'gallery' )
    882                     },
    883 
    884                     'return-to-library': {
    885                         text:     editing ? l10n.addImagesFromLibrary : l10n.returnToLibrary,
    886                         priority: -40,
    887 
    888                         click: function() {
    889                             controller.render( editing ? 'gallery-library' : 'library' );
    890                         }
    891                     }
    892                 };
    893 
    894             this.toolbarView = new media.view.Toolbar({
    895                 items: items
    896             });
    897 
    898             this.$el.addClass('with-toolbar');
    899             this.$content.append( this.toolbarView.$el );
    900         }
    901     });
    902 
    9031066
    9041067    /**
  • trunk/wp-includes/js/plupload/wp-plupload.js

    r22126 r22320  
    115115                    return;
    116116
    117                 dropzone.addClass('drag-over');
     117                dropzone.trigger('dropzone:enter').addClass('drag-over');
    118118                active = true;
    119119            });
     
    127127                timer = setTimeout( function() {
    128128                    active = false;
    129                     dropzone.removeClass('drag-over');
     129                    dropzone.trigger('dropzone:leave').removeClass('drag-over');
    130130                }, 0 );
    131131            });
  • trunk/wp-includes/media.php

    r22247 r22320  
    13001300            <h3 class="media-modal-title"><%- title %></h3>
    13011301            <a class="media-modal-close" href="" title="<?php esc_attr_e('Close'); ?>">&times;</a>
    1302             <div class="media-modal-content"></div>
    13031302        </div>
    13041303        <div class="media-modal-backdrop"></div>
    13051304    </script>
    13061305
    1307     <script type="text/html" id="tmpl-media-workspace">
    1308         <div class="upload-attachments">
    1309             <% if ( selectOne ) { %>
    1310                 <h3><?php _e( 'Drop a file here' ); ?></h3>
    1311                 <span><?php _ex( 'or', 'Uploader: Drop a file here - or - Select a File' ); ?></span>
    1312                 <a href="#" class="button-secondary"><?php _e( 'Select a File' ); ?></a>
    1313             <% } else { %>
    1314                 <h3><?php _e( 'Drop files here' ); ?></h3>
    1315                 <span><?php _ex( 'or', 'Uploader: Drop files here - or - Select Files' ); ?></span>
    1316                 <a href="#" class="button-secondary"><?php _e( 'Select Files' ); ?></a>
    1317             <% } %>
    1318 
    1319             <div class="media-progress-bar"><div></div></div>
     1306    <script type="text/html" id="tmpl-uploader-window">
     1307        <div class="uploader-window-content">
     1308            <h3><?php _e( 'Drop files here to upload' ); ?></h3>
    13201309        </div>
    13211310    </script>
Note: See TracChangeset for help on using the changeset viewer.