Make WordPress Core

Changeset 20988


Ignore:
Timestamp:
06/04/2012 03:51:46 PM (13 years ago)
Author:
ryan
Message:

Theme Customizer: Fix race condition in previewer and use message channels. Props koopersmith. fixes #20811

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/wp-admin/js/customize-controls.dev.js

    r20960 r20988  
    282282    api.control = new api.Values({ defaultConstructor: api.Control });
    283283
     284    api.PreviewFrame = api.Messenger.extend({
     285        sensitivity: 2000,
     286
     287        initialize: function( params, options ) {
     288            var loaded   = false,
     289                ready    = false,
     290                deferred = $.Deferred(),
     291                self     = this;
     292
     293            // This is the promise object.
     294            deferred.promise( this );
     295
     296            this.previewer = params.previewer;
     297
     298            $.extend( params, { channel: api.PreviewFrame.uuid() });
     299
     300            api.Messenger.prototype.initialize.call( this, params, options );
     301
     302            this.bind( 'ready', function() {
     303                ready = true;
     304
     305                if ( loaded )
     306                    deferred.resolveWith( self );
     307            });
     308
     309            params.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
     310
     311            this.request = $.ajax( this.url(), {
     312                type: 'POST',
     313                data: params.query,
     314                xhrFields: {
     315                    withCredentials: true
     316                }
     317            } );
     318
     319            this.request.fail( function() {
     320                deferred.rejectWith( self, [ 'request failure' ] );
     321            });
     322
     323            this.request.done( function( response ) {
     324                var location = self.request.getResponseHeader('Location'),
     325                    signature = 'WP_CUSTOMIZER_SIGNATURE',
     326                    index;
     327
     328                // Check if the location response header differs from the current URL.
     329                // If so, the request was redirected; try loading the requested page.
     330                if ( location && location != self.url() ) {
     331                    deferred.rejectWith( self, [ 'redirect', location ] );
     332                    return;
     333                }
     334
     335                // Check for a signature in the request.
     336                index = response.lastIndexOf( signature );
     337                if ( -1 === index || index < response.lastIndexOf('</html>') ) {
     338                    deferred.rejectWith( self, [ 'unsigned' ] );
     339                    return;
     340                }
     341
     342                // Strip the signature from the request.
     343                response = response.slice( 0, index ) + response.slice( index + signature.length );
     344
     345                // Create the iframe and inject the html content.
     346                // Strip the signature from the request.
     347                response = response.slice( 0, index ) + response.slice( index + signature.length );
     348
     349                // Create the iframe and inject the html content.
     350                self.iframe = $('<iframe />').appendTo( self.previewer.container );
     351
     352                // Bind load event after the iframe has been added to the page;
     353                // otherwise it will fire when injected into the DOM.
     354                self.iframe.one( 'load', function() {
     355                    loaded = true;
     356
     357                    if ( ready ) {
     358                        deferred.resolveWith( self );
     359                    } else {
     360                        setTimeout( function() {
     361                            deferred.rejectWith( self, [ 'ready timeout' ] );
     362                        }, self.sensitivity );
     363                    }
     364                });
     365
     366                self.targetWindow( self.iframe[0].contentWindow );
     367
     368                self.targetWindow().document.open();
     369                self.targetWindow().document.write( response );
     370                self.targetWindow().document.close();
     371            });
     372        },
     373
     374        destroy: function() {
     375            api.Messenger.prototype.destroy.call( this );
     376            this.request.abort();
     377
     378            if ( this.iframe )
     379                this.iframe.remove();
     380
     381            delete this.request;
     382            delete this.iframe;
     383            delete this.targetWindow;
     384        }
     385    });
     386
     387    (function(){
     388        var uuid = 0;
     389        api.PreviewFrame.uuid = function() {
     390            return 'preview-' + uuid++;
     391        };
     392    }());
     393
    284394    api.Previewer = api.Messenger.extend({
    285395        refreshBuffer: 250,
     
    295405
    296406            $.extend( this, options || {} );
    297 
    298             this.loaded = $.proxy( this.loaded, this );
    299407
    300408            /*
     
    321429                    if ( typeof timeout !== 'number' ) {
    322430                        if ( self.loading ) {
    323                             self.loading.remove();
    324                             delete self.loading;
    325                             self.loader();
     431                            self.abort();
    326432                        } else {
    327433                            return callback();
     
    337443            this.allowedUrls = params.allowedUrls;
    338444
    339             api.Messenger.prototype.initialize.call( this, params.url );
     445            api.Messenger.prototype.initialize.call( this, params );
    340446
    341447            // We're dynamically generating the iframe, so the origin is set
     
    392498            this.bind( 'url', this.url );
    393499        },
    394         loader: function() {
    395             if ( this.loading )
    396                 return this.loading;
    397 
    398             this.loading = $('<iframe />').appendTo( this.container );
    399 
    400             return this.loading;
    401         },
    402         loaded: function() {
    403             if ( this.iframe )
    404                 this.iframe.remove();
    405 
    406             this.iframe = this.loading;
    407             delete this.loading;
    408 
    409             this.targetWindow( this.iframe[0].contentWindow );
    410             this.send( 'scroll', this.scroll );
    411         },
     500
    412501        query: function() {},
     502
     503        abort: function() {
     504            if ( this.loading ) {
     505                this.loading.destroy();
     506                delete this.loading;
     507            }
     508        },
     509
    413510        refresh: function() {
    414511            var self = this;
    415512
    416             if ( this.request )
    417                 this.request.abort();
    418 
    419             this.request = $.ajax( this.url(), {
    420                 type: 'POST',
    421                 data: this.query() || {},
    422                 success: function( response ) {
    423                     var iframe = self.loader()[0].contentWindow,
    424                         location = self.request.getResponseHeader('Location'),
    425                         signature = 'WP_CUSTOMIZER_SIGNATURE',
    426                         index;
    427 
    428                     // Check if the location response header differs from the current URL.
    429                     // If so, the request was redirected; try loading the requested page.
    430                     if ( location && location != self.url() ) {
    431                         self.url( location );
    432                         return;
    433                     }
    434 
    435                     // Check for a signature in the request.
    436                     index = response.lastIndexOf( signature );
    437                     if ( -1 === index || index < response.lastIndexOf('</html>') )
    438                         return;
    439 
    440                     // Strip the signature from the request.
    441                     response = response.slice( 0, index ) + response.slice( index + signature.length );
    442 
    443                     self.loader().one( 'load', self.loaded );
    444 
    445                     iframe.document.open();
    446                     iframe.document.write( response );
    447                     iframe.document.close();
    448                 },
    449                 xhrFields: {
    450                     withCredentials: true
    451                 }
    452             } );
     513            this.abort();
     514
     515            this.loading = new api.PreviewFrame({
     516                url:       this.url(),
     517                query:     this.query() || {},
     518                previewer: this
     519            });
     520
     521            this.loading.done( function() {
     522                // 'this' is the loading frame
     523                this.bind( 'synced', function() {
     524                    if ( self.iframe )
     525                        self.iframe.destroy();
     526                    self.iframe = this;
     527                    delete self.loading;
     528
     529                    self.targetWindow( this.targetWindow() );
     530                    self.channel( this.channel() );
     531                });
     532
     533                this.send( 'sync', {
     534                    scroll:   self.scroll,
     535                    settings: api.get()
     536                });
     537            });
     538
     539            this.loading.fail( function( reason, location ) {
     540                if ( 'redirect' === reason && location )
     541                    self.url( location );
     542            });
    453543        }
    454544    });
     
    618708
    619709        // Create a potential postMessage connection with the parent frame.
    620         parent = new api.Messenger( api.settings.url.parent );
     710        parent = new api.Messenger({
     711            url: api.settings.url.parent,
     712            channel: 'loader'
     713        });
    621714
    622715        // If we receive a 'back' event, we're inside an iframe.
  • trunk/wp-includes/class-wp-customize-manager.php

    r20936 r20988  
    291291    public function customize_preview_settings() {
    292292        $settings = array(
    293             'values' => array(),
     293            'values'  => array(),
     294            'channel' => esc_js( $_POST['customize_messenger_channel'] ),
    294295        );
    295296
  • trunk/wp-includes/js/customize-base.dev.js

    r20897 r20988  
    303303        },
    304304
    305         get: function() {
    306             var result = {};
     305        each: function( callback, context ) {
     306            context = typeof context === 'undefined' ? this : context;
    307307
    308308            $.each( this._value, function( key, obj ) {
    309                 result[ key ] = obj.get();
    310             } );
    311             return result;
     309                callback.call( context, obj, key );
     310            });
    312311        },
    313312
     
    482481        },
    483482
    484         initialize: function( url, targetWindow, options ) {
     483        /**
     484         * Initialize Messenger.
     485         *
     486         * @param  {object} params        Parameters to configure the messenger.
     487         *         {string} .url          The URL to communicate with.
     488         *         {window} .targetWindow The window instance to communicate with. Default window.parent.
     489         *         {string} .channel      If provided, will send the channel with each message and only accept messages a matching channel.
     490         * @param  {object} options       Extend any instance parameter or method with this object.
     491         */
     492        initialize: function( params, options ) {
    485493            // Target the parent frame by default, but only if a parent frame exists.
    486494            var defaultTarget = window.parent == window ? null : window.parent;
     
    488496            $.extend( this, options || {} );
    489497
    490             url = this.add( 'url', url );
    491             this.add( 'targetWindow', targetWindow || defaultTarget );
    492             this.add( 'origin', url() ).link( url ).setter( function( to ) {
     498            this.add( 'channel', params.channel );
     499            this.add( 'url', params.url );
     500            this.add( 'targetWindow', params.targetWindow || defaultTarget );
     501            this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) {
    493502                return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' );
    494503            });
    495504
     505            // Since we want jQuery to treat the receive function as unique
     506            // to this instance, we give the function a new guid.
     507            //
     508            // This will prevent every Messenger's receive function from being
     509            // unbound when calling $.off( 'message', this.receive );
    496510            this.receive = $.proxy( this.receive, this );
     511            this.receive.guid = $.guid++;
     512
    497513            $( window ).on( 'message', this.receive );
    498514        },
     
    516532            message = JSON.parse( event.data );
    517533
    518             if ( message && message.id && typeof message.data !== 'undefined' )
    519                 this.trigger( message.id, message.data );
     534            // Check required message properties.
     535            if ( ! message || ! message.id || typeof message.data === 'undefined' )
     536                return;
     537
     538            // Check if channel names match.
     539            if ( ( message.channel || this.channel() ) && this.channel() !== message.channel )
     540                return;
     541
     542            this.trigger( message.id, message.data );
    520543        },
    521544
     
    528551                return;
    529552
    530             message = JSON.stringify({ id: id, data: data });
    531             this.targetWindow().postMessage( message, this.origin() );
     553            message = { id: id, data: data };
     554            if ( this.channel() )
     555                message.channel = this.channel();
     556
     557            this.targetWindow().postMessage( JSON.stringify( message ), this.origin() );
    532558        }
    533559    });
     
    541567
    542568    api = $.extend( new api.Values(), api );
     569    api.get = function() {
     570        var result = {};
     571
     572        this.each( function( obj, key ) {
     573            result[ key ] = obj.get();
     574        });
     575
     576        return result;
     577    };
    543578
    544579    // Expose the API to the world.
  • trunk/wp-includes/js/customize-loader.dev.js

    r20969 r20988  
    7878
    7979            // Create a postMessage connection with the iframe.
    80             this.messenger = new api.Messenger( src, this.iframe[0].contentWindow );
     80            this.messenger = new api.Messenger({
     81                url: src,
     82                channel: 'loader',
     83                targetWindow: this.iframe[0].contentWindow
     84            });
    8185
    8286            // Wait for the connection from the iframe before sending any postMessage events.
  • trunk/wp-includes/js/customize-preview.dev.js

    r20936 r20988  
    2222         * Requires params:
    2323         *  - url    - the URL of preview frame
    24          *
    25          * @todo: Perhaps add a window.onbeforeunload dialog in case the theme
    26          *        somehow attempts to leave the page and we don't catch it
    27          *        (which really shouldn't happen).
    2824         */
    29         initialize: function( url, options ) {
     25        initialize: function( params, options ) {
    3026            var self = this;
    3127
    32             api.Messenger.prototype.initialize.call( this, url, null, options );
     28            api.Messenger.prototype.initialize.call( this, params, options );
    3329
    3430            this.body = $( document.body );
     
    4036
    4137            // You cannot submit forms.
    42             // @todo: Namespace customizer settings so that we can mix the
    43             //        $_POST data with the customize setting $_POST data.
     38            // @todo: Allow form submissions by mixing $_POST data with the customize setting $_POST data.
    4439            this.body.on( 'submit.preview', 'form', function( event ) {
    4540                event.preventDefault();
     
    6459        var preview, bg;
    6560
    66         preview = new api.Preview( window.location.href );
    67 
    68         $.each( api.settings.values, function( id, value ) {
    69             api.create( id, value );
     61        preview = new api.Preview({
     62            url: window.location.href,
     63            channel: api.settings.channel
    7064        });
    7165
     66        preview.bind( 'settings', function( values ) {
     67            $.each( values, function( id, value ) {
     68                if ( api.has( id ) )
     69                    api( id ).set( value );
     70                else
     71                    api.create( id, value );
     72            });
     73        });
     74
     75        preview.trigger( 'settings', api.settings.values );
     76
    7277        preview.bind( 'setting', function( args ) {
    73             var value = api( args.shift() );
    74             if ( value )
     78            var value;
     79
     80            args = args.slice();
     81
     82            if ( value = api( args.shift() ) )
    7583                value.set.apply( value, args );
    7684        });
     85
     86        preview.bind( 'sync', function( events ) {
     87            $.each( events, function( event, args ) {
     88                preview.trigger( event, args );
     89            });
     90            preview.send( 'synced' );
     91        })
     92
     93        preview.send( 'ready' );
    7794
    7895        /* Custom Backgrounds */
Note: See TracChangeset for help on using the changeset viewer.