]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-admin/js/customize-controls.js
WordPress 4.5
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.js
index 1e5103a914e5512135b5c29c9a5fed7112b4fa97..5c21af6be4cf2a69b94cf3341d6a51fb0bd0fb82 100644 (file)
@@ -3,13 +3,22 @@
        var Container, focus, api = wp.customize;
 
        /**
+        * A Customizer Setting.
+        *
+        * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
+        * draft changes to in the Customizer.
+        *
+        * @see PHP class WP_Customize_Setting.
+        *
         * @class
         * @augments wp.customize.Value
         * @augments wp.customize.Class
         *
-        * @param options
-        * - previewer - The Previewer instance to sync with.
-        * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
+        * @param {object} id                The Setting ID.
+        * @param {object} value             The initial value of the setting.
+        * @param {object} options.previewer The Previewer instance to sync with.
+        * @param {object} options.transport The transport to use for previewing. Supports 'refresh' and 'postMessage'.
+        * @param {object} options.dirty
         */
        api.Setting = api.Value.extend({
                initialize: function( id, value, options ) {
                        this.transport = this.transport || 'refresh';
                        this._dirty = options.dirty || false;
 
+                       // Whenever the setting's value changes, refresh the preview.
                        this.bind( this.preview );
                },
+
+               /**
+                * Refresh the preview, respective of the setting's refresh policy.
+                */
                preview: function() {
                        switch ( this.transport ) {
                                case 'refresh':
         * @since 4.1.0
         *
         * @param {Object}   [params]
-        * @param {Callback} [params.completeCallback]
+        * @param {Function} [params.completeCallback]
         */
        focus = function ( params ) {
-               var construct, completeCallback, focus;
+               var construct, completeCallback, focus, focusElement;
                construct = this;
                params = params || {};
                focus = function () {
                        var focusContainer;
-                       if ( construct.expanded && construct.expanded() ) {
-                               focusContainer = construct.container.find( 'ul:first' );
+                       if ( construct.extended( api.Panel ) && construct.expanded && construct.expanded() ) {
+                               focusContainer = construct.container.find( 'ul.control-panel-content' );
+                       } else if ( construct.extended( api.Section ) && construct.expanded && construct.expanded() ) {
+                               focusContainer = construct.container.find( 'ul.accordion-section-content' );
                        } else {
                                focusContainer = construct.container;
                        }
 
-                       // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
-                       focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first().focus();
+                       focusElement = focusContainer.find( '.control-focus:first' );
+                       if ( 0 === focusElement.length ) {
+                               // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
+                               focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
+                       }
+                       focusElement.focus();
                };
                if ( params.completeCallback ) {
                        completeCallback = params.completeCallback;
                },
 
                /**
-                * Handle changes to the active state.
+                * Active state change handler.
                 *
-                * This does not change the active state, it merely handles the behavior
-                * for when it does change.
+                * Shows the container if it is active, hides it if not.
                 *
                 * To override by subclass, update the container's UI to reflect the provided active state.
                 *
                 * @param {Object}  args.duration
                 * @param {Object}  args.completeCallback
                 */
-               onChangeActive: function ( active, args ) {
-                       var duration, construct = this;
+               onChangeActive: function( active, args ) {
+                       var duration, construct = this, expandedOtherPanel;
+                       if ( args.unchanged ) {
+                               if ( args.completeCallback ) {
+                                       args.completeCallback();
+                               }
+                               return;
+                       }
+
                        duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
+
+                       if ( construct.extended( api.Panel ) ) {
+                               // If this is a panel is not currently expanded but another panel is expanded, do not animate.
+                               api.panel.each(function ( panel ) {
+                                       if ( panel !== construct && panel.expanded() ) {
+                                               expandedOtherPanel = panel;
+                                               duration = 0;
+                                       }
+                               });
+
+                               // Collapse any expanded sections inside of this panel first before deactivating.
+                               if ( ! active ) {
+                                       _.each( construct.sections(), function( section ) {
+                                               section.collapse( { duration: 0 } );
+                                       } );
+                               }
+                       }
+
                        if ( ! $.contains( document, construct.container[0] ) ) {
                                // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
                                construct.container.toggle( active );
                                        construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
                                }
                        }
+
+                       // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
+                       if ( expandedOtherPanel ) {
+                               expandedOtherPanel._recalculateTopMargin();
+                       }
                },
 
                /**
                },
 
                /**
-                * @param {Boolean} expanded
-                * @param {Object} [params]
-                * @returns {Boolean} false if state already applied
+                * Handle the toggle logic for expand/collapse.
+                *
+                * @param {Boolean}  expanded - The new state to apply.
+                * @param {Object}   [params] - Object containing options for expand/collapse.
+                * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
+                * @returns {Boolean} false if state already applied or active state is false
                 */
-               _toggleExpanded: function ( expanded, params ) {
-                       var self = this;
+               _toggleExpanded: function( expanded, params ) {
+                       var instance = this, previousCompleteCallback;
                        params = params || {};
-                       var section = this, previousCompleteCallback = params.completeCallback;
-                       params.completeCallback = function () {
+                       previousCompleteCallback = params.completeCallback;
+
+                       // Short-circuit expand() if the instance is not active.
+                       if ( expanded && ! instance.active() ) {
+                               return false;
+                       }
+
+                       params.completeCallback = function() {
                                if ( previousCompleteCallback ) {
-                                       previousCompleteCallback.apply( section, arguments );
+                                       previousCompleteCallback.apply( instance, arguments );
                                }
                                if ( expanded ) {
-                                       section.container.trigger( 'expanded' );
+                                       instance.container.trigger( 'expanded' );
                                } else {
-                                       section.container.trigger( 'collapsed' );
+                                       instance.container.trigger( 'collapsed' );
                                }
                        };
-                       if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
+                       if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
                                params.unchanged = true;
-                               self.onChangeExpanded( self.expanded.get(), params );
+                               instance.onChangeExpanded( instance.expanded.get(), params );
                                return false;
                        } else {
                                params.unchanged = false;
-                               this.expandedArgumentsQueue.push( params );
-                               this.expanded.set( expanded );
+                               instance.expandedArgumentsQueue.push( params );
+                               instance.expanded.set( expanded );
                                return true;
                        }
                },
 
                /**
                 * @param {Object} [params]
-                * @returns {Boolean} false if already expanded
+                * @returns {Boolean} false if already expanded or if inactive.
                 */
                expand: function ( params ) {
                        return this._toggleExpanded( true, params );
 
                /**
                 * @param {Object} [params]
-                * @returns {Boolean} false if already collapsed
+                * @returns {Boolean} false if already collapsed.
                 */
                collapse: function ( params ) {
                        return this._toggleExpanded( false, params );
                        };
                        section.panel.bind( inject );
                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
+
+                       section.deferred.embedded.done(function() {
+                               // Fix the top margin after reflow.
+                               api.bind( 'pane-contents-reflowed', _.debounce( function() {
+                                       section._recalculateTopMargin();
+                               }, 100 ) );
+                       });
                },
 
                /**
                                                // Fix the height after browser resize.
                                                $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
 
-                                               // Fix the top margin after reflow.
-                                               api.bind( 'pane-contents-reflowed', _.debounce( function() {
-                                                       var offset = ( content.offset().top - headerActionsHeight );
-                                                       if ( 0 < offset ) {
-                                                               content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
-                                                       }
-                                               }, 100 ) );
+                                               setTimeout( _.bind( section._recalculateTopMargin, section ), 0 );
                                        };
                                }
 
                                                completeCallback: expand
                                        });
                                } else {
+                                       api.panel.each( function( panel ) {
+                                               panel.collapse();
+                                       });
                                        expand();
                                }
 
                                        args.completeCallback();
                                }
                        }
+               },
+
+               /**
+                * Recalculate the top margin.
+                *
+                * @since 4.4.0
+                * @private
+                */
+               _recalculateTopMargin: function() {
+                       var section = this, content, offset, headerActionsHeight;
+                       content = section.container.find( '.accordion-section-content' );
+                       if ( 0 === content.length ) {
+                               return;
+                       }
+                       headerActionsHeight = $( '#customize-header-actions' ).height();
+                       offset = ( content.offset().top - headerActionsHeight );
+                       if ( 0 < offset ) {
+                               content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
+                       }
                }
        });
 
                        }
                },
 
+               /**
+                * Recalculate the top margin.
+                *
+                * @since 4.4.0
+                * @private
+                */
+               _recalculateTopMargin: function() {
+                       api.Panel.prototype._recalculateTopMargin.call( this );
+               },
+
                /**
                 * Render control's screenshot if the control comes into view.
                 *
                                parentContainer.append( panel.container );
                                panel.renderContent();
                        }
+
+                       api.bind( 'pane-contents-reflowed', _.debounce( function() {
+                               panel._recalculateTopMargin();
+                       }, 100 ) );
+
                        panel.deferred.embedded.resolve();
                },
 
                 * @param {Boolean}  expanded
                 * @param {Object}   args
                 * @param {Boolean}  args.unchanged
-                * @param {Callback} args.completeCallback
+                * @param {Function} args.completeCallback
                 */
                onChangeExpanded: function ( expanded, args ) {
 
                        // Note: there is a second argument 'args' passed
                        var position, scroll,
                                panel = this,
-                               section = panel.container.closest( '.accordion-section' ), // This is actually the panel.
-                               overlay = section.closest( '.wp-full-overlay' ),
-                               container = section.closest( '.wp-full-overlay-sidebar-content' ),
+                               accordionSection = panel.container.closest( '.accordion-section' ),
+                               overlay = accordionSection.closest( '.wp-full-overlay' ),
+                               container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
                                siblings = container.find( '.open' ),
                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
-                               backBtn = section.find( '.customize-panel-back' ),
-                               panelTitle = section.find( '.accordion-section-title' ).first(),
-                               content = section.find( '.control-panel-content' ),
+                               backBtn = accordionSection.find( '.customize-panel-back' ),
+                               panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
+                               content = accordionSection.find( '.control-panel-content' ),
                                headerActionsHeight = $( '#customize-header-actions' ).height();
 
                        if ( expanded ) {
 
                                // Collapse any sibling sections/panels
                                api.section.each( function ( section ) {
-                                       if ( ! section.panel() ) {
+                                       if ( panel.id !== section.panel() ) {
                                                section.collapse( { duration: 0 } );
                                        }
                                });
                                        position = content.offset().top;
                                        scroll = container.scrollTop();
                                        content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
-                                       section.addClass( 'current-panel' );
+                                       accordionSection.addClass( 'current-panel' );
                                        overlay.addClass( 'in-sub-panel' );
                                        container.scrollTop( 0 );
                                        if ( args.completeCallback ) {
                                topPanel.attr( 'tabindex', '-1' );
                                backBtn.attr( 'tabindex', '0' );
                                backBtn.focus();
-
-                               // Fix the top margin after reflow.
-                               api.bind( 'pane-contents-reflowed', _.debounce( function() {
-                                       content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
-                               }, 100 ) );
+                               panel._recalculateTopMargin();
                        } else {
                                siblings.removeClass( 'open' );
-                               section.removeClass( 'current-panel' );
+                               accordionSection.removeClass( 'current-panel' );
                                overlay.removeClass( 'in-sub-panel' );
                                content.delay( 180 ).hide( 0, function() {
                                        content.css( 'margin-top', 'inherit' ); // Reset
                        }
                },
 
+               /**
+                * Recalculate the top margin.
+                *
+                * @since 4.4.0
+                * @private
+                */
+               _recalculateTopMargin: function() {
+                       var panel = this, headerActionsHeight, content, accordionSection;
+                       headerActionsHeight = $( '#customize-header-actions' ).height();
+                       accordionSection = panel.container.closest( '.accordion-section' );
+                       content = accordionSection.find( '.control-panel-content' );
+                       content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
+               },
+
                /**
                 * Render the panel from its JS template, if it exists.
                 *
         * @class
         * @augments wp.customize.Class
         *
-        * @param {string} id                            Unique identifier for the control instance.
-        * @param {object} options                       Options hash for the control instance.
+        * @param {string} id                              Unique identifier for the control instance.
+        * @param {object} options                         Options hash for the control instance.
         * @param {object} options.params
-        * @param {object} options.params.type           Type of control (e.g. text, radio, dropdown-pages, etc.)
-        * @param {string} options.params.content        The HTML content for the control.
-        * @param {string} options.params.priority       Order of priority to show the control within the section.
+        * @param {object} options.params.type             Type of control (e.g. text, radio, dropdown-pages, etc.)
+        * @param {string} options.params.content          The HTML content for the control.
+        * @param {string} options.params.priority         Order of priority to show the control within the section.
         * @param {string} options.params.active
-        * @param {string} options.params.section
+        * @param {string} options.params.section          The ID of the section the control belongs to.
+        * @param {string} options.params.settings.default The ID of the setting the control relates to.
+        * @param {string} options.params.settings.data
         * @param {string} options.params.label
         * @param {string} options.params.description
         * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
 
                        api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
 
-                       // Associate this control with its settings when they are created
+                       /*
+                        * After all settings related to the control are available,
+                        * make them available on the control and embed the control into the page.
+                        */
                        settings = $.map( control.params.settings, function( value ) {
                                return value;
                        });
-                       api.apply( api, settings.concat( function () {
-                               var key;
 
+                       if ( 0 === settings.length ) {
+                               control.setting = null;
                                control.settings = {};
-                               for ( key in control.params.settings ) {
-                                       control.settings[ key ] = api( control.params.settings[ key ] );
-                               }
+                               control.embed();
+                       } else {
+                               api.apply( api, settings.concat( function() {
+                                       var key;
 
-                               control.setting = control.settings['default'] || null;
+                                       control.settings = {};
+                                       for ( key in control.params.settings ) {
+                                               control.settings[ key ] = api( control.params.settings[ key ] );
+                                       }
 
-                               control.embed();
-                       }) );
+                                       control.setting = control.settings['default'] || null;
 
+                                       control.embed();
+                               }) );
+                       }
+
+                       // After the control is embedded on the page, invoke the "ready" method.
                        control.deferred.embedded.done( function () {
                                control.ready();
                        });
                        // Watch for changes to the section state
                        inject = function ( sectionId ) {
                                var parentContainer;
-                               if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
+                               if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
                                        return;
                                }
                                // Wait for the section to be registered
                 * @param {Callback} args.completeCallback
                 */
                onChangeActive: function ( active, args ) {
-                       if ( ! $.contains( document, this.container ) ) {
+                       if ( args.unchanged ) {
+                               if ( args.completeCallback ) {
+                                       args.completeCallback();
+                               }
+                               return;
+                       }
+
+                       if ( ! $.contains( document, this.container[0] ) ) {
                                // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
                                this.container.toggle( active );
                                if ( args.completeCallback ) {
                                        control.setting.set( picker.wpColorPicker('color') );
                                },
                                clear: function() {
-                                       control.setting.set( false );
+                                       control.setting.set( '' );
                                }
                        });
 
                                        control.pausePlayer();
                                });
 
-                       // Re-render whenever the control's setting changes.
-                       control.setting.bind( function () { control.renderContent(); } );
+                       control.setting.bind( function( value ) {
+
+                               // Send attachment information to the preview for possible use in `postMessage` transport.
+                               wp.media.attachment( value ).fetch().done( function() {
+                                       wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
+                               } );
+
+                               // Re-render whenever the control's setting changes.
+                               control.renderContent();
+                       } );
                },
 
                pausePlayer: function () {
                                xInit = parseInt( control.params.width, 10 ),
                                yInit = parseInt( control.params.height, 10 ),
                                ratio = xInit / yInit,
-                               xImg  = realWidth,
-                               yImg  = realHeight,
+                               xImg  = xInit,
+                               yImg  = yInit,
                                x1, y1, imgSelectOptions;
 
                        controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
 
-                       if ( xImg / yImg > ratio ) {
-                               yInit = yImg;
+                       if ( realWidth / realHeight > ratio ) {
+                               yInit = realHeight;
                                xInit = yInit * ratio;
                        } else {
-                               xInit = xImg;
+                               xInit = realWidth;
                                yInit = xInit / ratio;
                        }
 
-                       x1 = ( xImg - xInit ) / 2;
-                       y1 = ( yImg - yInit ) / 2;
+                       x1 = ( realWidth - xInit ) / 2;
+                       y1 = ( realHeight - yInit ) / 2;
 
                        imgSelectOptions = {
                                handles: true,
                                persistent: true,
                                imageWidth: realWidth,
                                imageHeight: realHeight,
+                               minWidth: xImg > xInit ? xInit : xImg,
+                               minHeight: yImg > yInit ? yInit : yImg,
                                x1: x1,
                                y1: y1,
                                x2: xInit + x1,
                        if ( flexHeight === false && flexWidth === false ) {
                                imgSelectOptions.aspectRatio = xInit + ':' + yInit;
                        }
-                       if ( flexHeight === false ) {
-                               imgSelectOptions.maxHeight = yInit;
+
+                       if ( true === flexHeight ) {
+                               delete imgSelectOptions.minHeight;
+                               imgSelectOptions.maxWidth = realWidth;
                        }
-                       if ( flexWidth === false ) {
-                               imgSelectOptions.maxWidth = xInit;
+
+                       if ( true === flexWidth ) {
+                               delete imgSelectOptions.minWidth;
+                               imgSelectOptions.maxHeight = realHeight;
                        }
 
                        return imgSelectOptions;
                 * @param {object} attachment
                 */
                setImageFromAttachment: function( attachment ) {
-                       var icon = typeof attachment.sizes['site_icon-32'] !== 'undefined' ? attachment.sizes['site_icon-32'] : attachment.sizes.thumbnail;
+                       var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
+                               icon;
+
+                       _.each( sizes, function( size ) {
+                               if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
+                                       icon = attachment.sizes[ size ];
+                               }
+                       } );
 
                        this.params.attachment = attachment;
 
                        // Set the Customizer setting; the callback takes care of rendering.
                        this.setting( attachment.id );
 
-
                        // Update the icon in-browser.
                        $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
                },
                                api.HeaderTool.UploadsList,
                                api.HeaderTool.DefaultsList
                        ]);
+
+                       // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
+                       wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
+                       wp.media.controller.Cropper.prototype.defaults.doCropArgs.theme = api.settings.theme.stylesheet;
                },
 
                /**
                 * @param {object} croppedImage Cropped attachment data.
                 */
                onCropped: function(croppedImage) {
-                       var url = croppedImage.post_content,
+                       var url = croppedImage.url,
                                attachmentId = croppedImage.attachment_id,
                                w = croppedImage.width,
                                h = croppedImage.height;
        api.panel = new api.Values({ defaultConstructor: api.Panel });
 
        /**
+        * An object that fetches a preview in the background of the document, which
+        * allows for seamless replacement of an existing preview.
+        *
         * @class
         * @augments wp.customize.Messenger
         * @augments wp.customize.Class
        api.PreviewFrame = api.Messenger.extend({
                sensitivity: 2000,
 
+               /**
+                * Initialize the PreviewFrame.
+                *
+                * @param {object} params.container
+                * @param {object} params.signature
+                * @param {object} params.previewUrl
+                * @param {object} params.query
+                * @param {object} options
+                */
                initialize: function( params, options ) {
                        var deferred = $.Deferred();
 
-                       // This is the promise object.
+                       /*
+                        * Make the instance of the PreviewFrame the promise object
+                        * so other objects can easily interact with it.
+                        */
                        deferred.promise( this );
 
                        this.container = params.container;
                        this.run( deferred );
                },
 
+               /**
+                * Run the preview request.
+                *
+                * @param {object} deferred jQuery Deferred object to be resolved with
+                *                          the request.
+                */
                run: function( deferred ) {
                        var self   = this,
                                loaded = false,
 
                                iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
                                iframe.appendTo( self.container );
-                               iframe.load( function() {
+                               iframe.on( 'load', function() {
                                        self.triedLogin = true;
 
                                        iframe.remove();
                refreshBuffer: 250,
 
                /**
-                * Requires params:
-                *  - container  - a selector or jQuery element
-                *  - previewUrl - the URL of preview frame
+                * @param {array}  params.allowedUrls
+                * @param {string} params.container   A selector or jQuery element for the preview
+                *                                    frame to be placed.
+                * @param {string} params.form
+                * @param {string} params.previewUrl  The URL to preview.
+                * @param {string} params.signature
+                * @param {object} options
                 */
                initialize: function( params, options ) {
                        var self = this,
 
                        // Limit the URL to internal, front-end links.
                        //
-                       // If the frontend and the admin are served from the same domain, load the
+                       // If the front end and the admin are served from the same domain, load the
                        // preview over ssl if the Customizer is being loaded over ssl. This avoids
-                       // insecure content warnings. This is not attempted if the admin and frontend
-                       // are on different domains to avoid the case where the frontend doesn't have
+                       // insecure content warnings. This is not attempted if the admin and front end
+                       // are on different domains to avoid the case where the front end doesn't have
                        // ssl certs.
 
                        this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
                        } );
                },
 
+               /**
+                * Query string data sent with each preview request.
+                *
+                * @abstract
+                */
                query: function() {},
 
                abort: function() {
                        }
                },
 
+               /**
+                * Refresh the preview.
+                */
                refresh: function() {
                        var self = this;
 
                },
 
                cheatin: function() {
-                       $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
+                       $( document.body ).empty().addClass( 'cheatin' ).append(
+                               '<h1>' + api.l10n.cheatin + '</h1>' +
+                               '<p>' + api.l10n.notAllowed + '</p>'
+                       );
                },
 
                refreshNonces: function() {
                        overlay = body.children( '.wp-full-overlay' ),
                        title = $( '#customize-info .panel-title.site-title' ),
                        closeBtn = $( '.customize-controls-close' ),
-                       saveBtn = $( '#save' );
+                       saveBtn = $( '#save' ),
+                       footerActions = $( '#customize-footer-actions' );
 
                // Prevent the form from saving when enter is pressed on an input or select element.
                $('#customize-controls').on( 'keydown', function( e ) {
 
                        nonce: api.settings.nonce,
 
+                       /**
+                        * Build the query to send along with the Preview request.
+                        *
+                        * @return {object}
+                        */
                        query: function() {
                                var dirtyCustomized = {};
                                api.each( function ( value, key ) {
                                                        value._dirty = false;
                                                } );
 
+                                               api.previewer.send( 'saved', response );
+
                                                api.trigger( 'saved', response );
                                        } );
                                };
                api.bind( 'nonce-refresh', function( nonce ) {
                        $.extend( api.settings.nonce, nonce );
                        $.extend( api.previewer.nonce, nonce );
+                       api.previewer.send( 'nonce-refresh', nonce );
                });
 
                // Create Settings
                });
 
                // Focus the autofocused element
-               _.each( [ 'panel', 'section', 'control' ], function ( type ) {
-                       var instance, id = api.settings.autofocus[ type ];
-                       if ( id && api[ type ]( id ) ) {
-                               instance = api[ type ]( id );
-                               // Wait until the element is embedded in the DOM
-                               instance.deferred.embedded.done( function () {
-                                       // Wait until the preview has activated and so active panels, sections, controls have been set
-                                       api.previewer.deferred.active.done( function () {
+               _.each( [ 'panel', 'section', 'control' ], function( type ) {
+                       var id = api.settings.autofocus[ type ];
+                       if ( ! id ) {
+                               return;
+                       }
+
+                       /*
+                        * Defer focus until:
+                        * 1. The panel, section, or control exists (especially for dynamically-created ones).
+                        * 2. The instance is embedded in the document (and so is focusable).
+                        * 3. The preview has finished loading so that the active states have been set.
+                        */
+                       api[ type ]( id, function( instance ) {
+                               instance.deferred.embedded.done( function() {
+                                       api.previewer.deferred.active.done( function() {
                                                instance.focus();
                                        });
                                });
-                       }
+                       });
                });
 
                /**
                        event.preventDefault();
                });
 
+               // Previewed device bindings.
+               api.previewedDevice = new api.Value();
+
+               // Set the default device.
+               api.bind( 'ready', function() {
+                       _.find( api.settings.previewableDevices, function( value, key ) {
+                               if ( true === value['default'] ) {
+                                       api.previewedDevice.set( key );
+                                       return true;
+                               }
+                       } );
+               } );
+
+               // Set the toggled device.
+               footerActions.find( '.devices button' ).on( 'click', function( event ) {
+                       api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
+               });
+
+               // Bind device changes.
+               api.previewedDevice.bind( function( newDevice ) {
+                       var overlay = $( '.wp-full-overlay' ),
+                               devices = '';
+
+                       footerActions.find( '.devices button' )
+                               .removeClass( 'active' )
+                               .attr( 'aria-pressed', false );
+
+                       footerActions.find( '.devices .preview-' + newDevice )
+                               .addClass( 'active' )
+                               .attr( 'aria-pressed', true );
+
+                       $.each( api.settings.previewableDevices, function( device ) {
+                               devices += ' preview-' + device;
+                       } );
+
+                       overlay
+                               .removeClass( devices )
+                               .addClass( 'preview-' + newDevice );
+               } );
+
                // Bind site title display to the corresponding field.
                if ( title.length ) {
-                       $( '#customize-control-blogname input' ).on( 'input', function() {
-                               title.text( this.value );
+                       api( 'blogname', function( setting ) {
+                               var updateTitle = function() {
+                                       title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
+                               };
+                               setting.bind( updateTitle );
+                               updateTitle();
                        } );
                }
 
-               // Create a potential postMessage connection with the parent frame.
+               /*
+                * Create a postMessage connection with a parent frame,
+                * in case the Customizer frame was opened with the Customize loader.
+                *
+                * @see wp.customize.Loader
+                */
                parent = new api.Messenger({
                        url: api.settings.url.parent,
                        channel: 'loader'
                });
 
-               // If we receive a 'back' event, we're inside an iframe.
-               // Send any clicks to the 'Return' link to the parent page.
+               /*
+                * If we receive a 'back' event, we're inside an iframe.
+                * Send any clicks to the 'Return' link to the parent page.
+                */
                parent.bind( 'back', function() {
                        closeBtn.on( 'click.customize-controls-close', function( event ) {
                                event.preventDefault();
                        });
                } );
 
-               // When activated, let the loader handle redirecting the page.
-               // If no loader exists, redirect the page ourselves (if a url exists).
+               /*
+                * When activated, let the loader handle redirecting the page.
+                * If no loader exists, redirect the page ourselves (if a url exists).
+                */
                api.bind( 'activated', function() {
                        if ( parent.targetWindow() )
                                parent.send( 'activated', api.settings.url.activated );
                        });
                });
 
+               // Focus on the control that is associated with the given setting.
+               api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
+                       var matchedControl;
+                       api.control.each( function( control ) {
+                               var settingIds = _.pluck( control.settings, 'id' );
+                               if ( -1 !== _.indexOf( settingIds, settingId ) ) {
+                                       matchedControl = control;
+                               }
+                       } );
+
+                       if ( matchedControl ) {
+                               matchedControl.focus();
+                       }
+               } );
+
+               // Refresh the preview when it requests.
+               api.previewer.bind( 'refresh', function() {
+                       api.previewer.refresh();
+               });
+
                api.trigger( 'ready' );
 
                // Make sure left column gets focus