+/* globals _wpCustomizeHeader, _wpMediaViewsL10n */
(function( exports, $ ){
var api = wp.customize;
- /*
+ /**
+ * @constructor
+ * @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'.
*/
api.Setting = api.Value.extend({
initialize: function( id, value, options ) {
- var element;
-
api.Value.prototype.initialize.call( this, value, options );
this.id = id;
}
});
+ /**
+ * @constructor
+ * @augments wp.customize.Class
+ */
api.Control = api.Class.extend({
initialize: function( id, options ) {
var control = this,
this.id = id;
this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
this.container = $( this.selector );
+ this.active = new api.Value( this.params.active );
settings = $.map( this.params.settings, function( value ) {
return value;
element.set( setting() );
});
});
+
+ control.active.bind( function ( active ) {
+ control.toggle( active );
+ } );
+ control.toggle( control.active() );
},
+ /**
+ * @abstract
+ */
ready: function() {},
+ /**
+ * Callback for change to the control's active state.
+ *
+ * Override function for custom behavior for the control being active/inactive.
+ *
+ * @param {Boolean} active
+ */
+ toggle: function ( active ) {
+ if ( active ) {
+ this.container.slideDown();
+ } else {
+ this.container.slideUp();
+ }
+ },
+
dropdownInit: function() {
- var control = this,
- statuses = this.container.find('.dropdown-status'),
- params = this.params,
- update = function( to ) {
- if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
+ var control = this,
+ statuses = this.container.find('.dropdown-status'),
+ params = this.params,
+ toggleFreeze = false,
+ update = function( to ) {
+ if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
statuses.html( params.statuses[ to ] ).show();
else
statuses.hide();
};
- var toggleFreeze = false;
-
// Support the .dropdown class to open/close complex elements
this.container.on( 'click keydown', '.dropdown', function( event ) {
if ( event.type === 'keydown' && 13 !== event.which ) // enter
}
});
+ /**
+ * @constructor
+ * @augments wp.customize.Control
+ * @augments wp.customize.Class
+ */
api.ColorControl = api.Control.extend({
ready: function() {
var control = this,
picker = this.container.find('.color-picker-hex');
picker.val( control.setting() ).wpColorPicker({
- change: function( event, options ) {
+ change: function() {
control.setting.set( picker.wpColorPicker('color') );
- },
- clear: function() {
- control.setting.set( false );
- }
+ },
+ clear: function() {
+ control.setting.set( false );
+ }
});
}
});
+ /**
+ * @constructor
+ * @augments wp.customize.Control
+ * @augments wp.customize.Class
+ */
api.UploadControl = api.Control.extend({
ready: function() {
var control = this;
}
});
+ /**
+ * @constructor
+ * @augments wp.customize.UploadControl
+ * @augments wp.customize.Control
+ * @augments wp.customize.Class
+ */
api.ImageControl = api.UploadControl.extend({
ready: function() {
var control = this,
panels;
this.uploader = {
- init: function( up ) {
+ init: function() {
var fallback, button;
if ( this.supports.dragdrop )
}
});
+ /**
+ * @constructor
+ * @augments wp.customize.Control
+ * @augments wp.customize.Class
+ */
+ api.HeaderControl = api.Control.extend({
+ ready: function() {
+ this.btnRemove = $('#customize-control-header_image .actions .remove');
+ this.btnNew = $('#customize-control-header_image .actions .new');
+
+ _.bindAll(this, 'openMedia', 'removeImage');
+
+ this.btnNew.on( 'click', this.openMedia );
+ this.btnRemove.on( 'click', this.removeImage );
+
+ api.HeaderTool.currentHeader = new api.HeaderTool.ImageModel();
+
+ new api.HeaderTool.CurrentView({
+ model: api.HeaderTool.currentHeader,
+ el: '.current .container'
+ });
+
+ new api.HeaderTool.ChoiceListView({
+ collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
+ el: '.choices .uploaded .list'
+ });
+
+ new api.HeaderTool.ChoiceListView({
+ collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
+ el: '.choices .default .list'
+ });
+
+ api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
+ api.HeaderTool.UploadsList,
+ api.HeaderTool.DefaultsList
+ ]);
+ },
+
+ /**
+ * Returns a set of options, computed from the attached image data and
+ * theme-specific data, to be fed to the imgAreaSelect plugin in
+ * wp.media.view.Cropper.
+ *
+ * @param {wp.media.model.Attachment} attachment
+ * @param {wp.media.controller.Cropper} controller
+ * @returns {Object} Options
+ */
+ calculateImageSelectOptions: function(attachment, controller) {
+ var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
+ yInit = parseInt(_wpCustomizeHeader.data.height, 10),
+ flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
+ flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
+ ratio, xImg, yImg, realHeight, realWidth,
+ imgSelectOptions;
+
+ realWidth = attachment.get('width');
+ realHeight = attachment.get('height');
+
+ this.headerImage = new api.HeaderTool.ImageModel();
+ this.headerImage.set({
+ themeWidth: xInit,
+ themeHeight: yInit,
+ themeFlexWidth: flexWidth,
+ themeFlexHeight: flexHeight,
+ imageWidth: realWidth,
+ imageHeight: realHeight
+ });
+
+ controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
+
+ ratio = xInit / yInit;
+ xImg = realWidth;
+ yImg = realHeight;
+
+ if ( xImg / yImg > ratio ) {
+ yInit = yImg;
+ xInit = yInit * ratio;
+ } else {
+ xInit = xImg;
+ yInit = xInit / ratio;
+ }
+
+ imgSelectOptions = {
+ handles: true,
+ keys: true,
+ instance: true,
+ persistent: true,
+ imageWidth: realWidth,
+ imageHeight: realHeight,
+ x1: 0,
+ y1: 0,
+ x2: xInit,
+ y2: yInit
+ };
+
+ if (flexHeight === false && flexWidth === false) {
+ imgSelectOptions.aspectRatio = xInit + ':' + yInit;
+ }
+ if (flexHeight === false ) {
+ imgSelectOptions.maxHeight = yInit;
+ }
+ if (flexWidth === false ) {
+ imgSelectOptions.maxWidth = xInit;
+ }
+
+ return imgSelectOptions;
+ },
+
+ /**
+ * Sets up and opens the Media Manager in order to select an image.
+ * Depending on both the size of the image and the properties of the
+ * current theme, a cropping step after selection may be required or
+ * skippable.
+ *
+ * @param {event} event
+ */
+ openMedia: function(event) {
+ var l10n = _wpMediaViewsL10n;
+
+ event.preventDefault();
+
+ this.frame = wp.media({
+ button: {
+ text: l10n.selectAndCrop,
+ close: false
+ },
+ states: [
+ new wp.media.controller.Library({
+ title: l10n.chooseImage,
+ library: wp.media.query({ type: 'image' }),
+ multiple: false,
+ priority: 20,
+ suggestedWidth: _wpCustomizeHeader.data.width,
+ suggestedHeight: _wpCustomizeHeader.data.height
+ }),
+ new wp.media.controller.Cropper({
+ imgSelectOptions: this.calculateImageSelectOptions
+ })
+ ]
+ });
+
+ this.frame.on('select', this.onSelect, this);
+ this.frame.on('cropped', this.onCropped, this);
+ this.frame.on('skippedcrop', this.onSkippedCrop, this);
+
+ this.frame.open();
+ },
+
+ onSelect: function() {
+ this.frame.setState('cropper');
+ },
+ onCropped: function(croppedImage) {
+ var url = croppedImage.post_content,
+ attachmentId = croppedImage.attachment_id,
+ w = croppedImage.width,
+ h = croppedImage.height;
+ this.setImageFromURL(url, attachmentId, w, h);
+ },
+ onSkippedCrop: function(selection) {
+ var url = selection.get('url'),
+ w = selection.get('width'),
+ h = selection.get('height');
+ this.setImageFromURL(url, selection.id, w, h);
+ },
+
+ /**
+ * Creates a new wp.customize.HeaderTool.ImageModel from provided
+ * header image data and inserts it into the user-uploaded headers
+ * collection.
+ *
+ * @param {String} url
+ * @param {Number} attachmentId
+ * @param {Number} width
+ * @param {Number} height
+ */
+ setImageFromURL: function(url, attachmentId, width, height) {
+ var choice, data = {};
+
+ data.url = url;
+ data.thumbnail_url = url;
+ data.timestamp = _.now();
+
+ if (attachmentId) {
+ data.attachment_id = attachmentId;
+ }
+
+ if (width) {
+ data.width = width;
+ }
+
+ if (height) {
+ data.height = height;
+ }
+
+ choice = new api.HeaderTool.ImageModel({
+ header: data,
+ choice: url.split('/').pop()
+ });
+ api.HeaderTool.UploadsList.add(choice);
+ api.HeaderTool.currentHeader.set(choice.toJSON());
+ choice.save();
+ choice.importImage();
+ },
+
+ /**
+ * Triggers the necessary events to deselect an image which was set as
+ * the currently selected one.
+ */
+ removeImage: function() {
+ api.HeaderTool.currentHeader.trigger('hide');
+ api.HeaderTool.CombinedList.trigger('control:removeImage');
+ }
+
+ });
+
// Change objects contained within the main customize object to Settings.
api.defaultConstructor = api.Setting;
// Create the collection of Control objects.
api.control = new api.Values({ defaultConstructor: api.Control });
+ /**
+ * @constructor
+ * @augments wp.customize.Messenger
+ * @augments wp.customize.Class
+ * @mixes wp.customize.Events
+ */
api.PreviewFrame = api.Messenger.extend({
sensitivity: 2000,
initialize: function( params, options ) {
- var deferred = $.Deferred(),
- self = this;
+ var deferred = $.Deferred();
// This is the promise object.
deferred.promise( this );
this.bind( 'ready', this._ready );
+ this.bind( 'ready', function ( data ) {
+ if ( ! data || ! data.activeControls ) {
+ return;
+ }
+
+ $.each( data.activeControls, function ( id, active ) {
+ var control = api.control( id );
+ if ( control ) {
+ control.active( active );
+ }
+ } );
+ } );
+
this.request = $.ajax( this.previewUrl(), {
type: 'POST',
data: this.query,
(function(){
var uuid = 0;
+ /**
+ * Create a universally unique identifier.
+ *
+ * @return {int}
+ */
api.PreviewFrame.uuid = function() {
return 'preview-' + uuid++;
};
}());
+ /**
+ * @constructor
+ * @augments wp.customize.Messenger
+ * @augments wp.customize.Class
+ * @mixes wp.customize.Events
+ */
api.Previewer = api.Messenger.extend({
refreshBuffer: 250,
*/
initialize: function( params, options ) {
var self = this,
- rscheme = /^https?/,
- url;
+ rscheme = /^https?/;
$.extend( this, options || {} );
}
});
- /* =====================================================================
- * Ready.
- * ===================================================================== */
-
api.controlConstructor = {
color: api.ColorControl,
upload: api.UploadControl,
- image: api.ImageControl
+ image: api.ImageControl,
+ header: api.HeaderControl
};
$( function() {
if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
return window.location = api.settings.url.fallback;
- var body = $( document.body ),
- overlay = body.children('.wp-full-overlay'),
- query, previewer, parent;
+ var parent, topFocus,
+ body = $( document.body ),
+ overlay = body.children( '.wp-full-overlay' ),
+ title = $( '#customize-info .theme-name.site-title' ),
+ closeBtn = $( '.customize-controls-close' ),
+ saveBtn = $( '#save' );
- // Prevent the form from saving when enter is pressed.
+ // Prevent the form from saving when enter is pressed on an input or select element.
$('#customize-controls').on( 'keydown', function( e ) {
- if ( $( e.target ).is('textarea') )
- return;
+ var isEnter = ( 13 === e.which ),
+ $el = $( e.target );
- if ( 13 === e.which ) // Enter
+ if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
e.preventDefault();
+ }
});
// Initialize Previewer
- previewer = new api.Previewer({
+ api.previewer = new api.Previewer({
container: '#customize-preview',
form: '#customize-controls',
previewUrl: api.settings.url.preview,
query: function() {
return {
wp_customize: 'on',
- theme: api.settings.theme.stylesheet,
- customized: JSON.stringify( api.get() ),
- nonce: this.nonce.preview
+ theme: api.settings.theme.stylesheet,
+ customized: JSON.stringify( api.get() ),
+ nonce: this.nonce.preview
};
},
query = $.extend( this.query(), {
action: 'customize_save',
nonce: this.nonce.save
- }),
- request = $.post( api.settings.url.ajax, query );
-
- api.trigger( 'save', request );
-
- body.addClass('saving');
+ } ),
+ processing = api.state( 'processing' ),
+ submitWhenDoneProcessing,
+ submit;
+
+ body.addClass( 'saving' );
+
+ submit = function () {
+ var request = $.post( api.settings.url.ajax, query );
+
+ api.trigger( 'save', request );
+
+ request.always( function () {
+ body.removeClass( 'saving' );
+ } );
+
+ request.done( function( response ) {
+ // Check if the user is logged out.
+ if ( '0' === response ) {
+ self.preview.iframe.hide();
+ self.login().done( function() {
+ self.save();
+ self.preview.iframe.show();
+ } );
+ return;
+ }
- request.always( function() {
- body.removeClass('saving');
- });
+ // Check for cheaters.
+ if ( '-1' === response ) {
+ self.cheatin();
+ return;
+ }
- request.done( function( response ) {
- // Check if the user is logged out.
- if ( '0' === response ) {
- self.preview.iframe.hide();
- self.login().done( function() {
- self.save();
- self.preview.iframe.show();
- });
- return;
- }
+ api.trigger( 'saved' );
+ } );
+ };
- // Check for cheaters.
- if ( '-1' === response ) {
- self.cheatin();
- return;
- }
+ if ( 0 === processing() ) {
+ submit();
+ } else {
+ submitWhenDoneProcessing = function () {
+ if ( 0 === processing() ) {
+ api.state.unbind( 'change', submitWhenDoneProcessing );
+ submit();
+ }
+ };
+ api.state.bind( 'change', submitWhenDoneProcessing );
+ }
- api.trigger( 'saved' );
- });
}
});
// Refresh the nonces if the preview sends updated nonces over.
- previewer.bind( 'nonce', function( nonce ) {
- $.extend( this.nonce, nonce );
- });
+ api.previewer.bind( 'nonce', function( nonce ) {
+ $.extend( this.nonce, nonce );
+ });
$.each( api.settings.settings, function( id, data ) {
api.create( id, id, data.value, {
transport: data.transport,
- previewer: previewer
+ previewer: api.previewer
} );
});
control = api.control.add( id, new constructor( id, {
params: data,
- previewer: previewer
+ previewer: api.previewer
} ) );
});
// Check if preview url is valid and load the preview frame.
- if ( previewer.previewUrl() )
- previewer.refresh();
- else
- previewer.previewUrl( api.settings.url.home );
+ if ( api.previewer.previewUrl() ) {
+ api.previewer.refresh();
+ } else {
+ api.previewer.previewUrl( api.settings.url.home );
+ }
// Save and activated states
(function() {
var state = new api.Values(),
- saved = state.create('saved'),
- activated = state.create('activated');
+ saved = state.create( 'saved' ),
+ activated = state.create( 'activated' ),
+ processing = state.create( 'processing' );
state.bind( 'change', function() {
- var save = $('#save'),
- back = $('.back');
-
if ( ! activated() ) {
- save.val( api.l10n.activate ).prop( 'disabled', false );
- back.text( api.l10n.cancel );
+ saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
+ closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
} else if ( saved() ) {
- save.val( api.l10n.saved ).prop( 'disabled', true );
- back.text( api.l10n.close );
+ saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
+ closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
} else {
- save.val( api.l10n.save ).prop( 'disabled', false );
- back.text( api.l10n.cancel );
+ saveBtn.val( api.l10n.save ).prop( 'disabled', false );
+ closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
}
});
// Set default states.
saved( true );
activated( api.settings.theme.active );
+ processing( 0 );
api.bind( 'change', function() {
state('saved').set( false );
}());
// Button bindings.
- $('#save').click( function( event ) {
- previewer.save();
+ saveBtn.click( function( event ) {
+ api.previewer.save();
event.preventDefault();
}).keydown( function( event ) {
if ( 9 === event.which ) // tab
return;
if ( 13 === event.which ) // enter
- previewer.save();
+ api.previewer.save();
event.preventDefault();
});
- $('.back').keydown( function( event ) {
+ closeBtn.keydown( function( event ) {
if ( 9 === event.which ) // tab
return;
if ( 13 === event.which ) // enter
event.preventDefault();
});
+ // Bind site title display to the corresponding field.
+ if ( title.length ) {
+ $( '#customize-control-blogname input' ).on( 'input', function() {
+ title.text( this.value );
+ } );
+ }
+
// Create a potential postMessage connection with the parent frame.
parent = new api.Messenger({
url: api.settings.url.parent,
// 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() {
- $('.back').on( 'click.back', function( event ) {
+ closeBtn.on( 'click.customize-controls-close', function( event ) {
event.preventDefault();
parent.send( 'close' );
});
});
+ // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
+ $( window ).on( 'beforeunload', function () {
+ if ( ! api.state( 'saved' )() ) {
+ return api.l10n.saveAlert;
+ }
+ } );
+
// Pass events through to the parent.
- api.bind( 'saved', function() {
- parent.send( 'saved' );
- });
+ $.each( [ 'saved', 'change' ], function ( i, event ) {
+ api.bind( event, function() {
+ parent.send( event );
+ });
+ } );
// When activated, let the loader handle redirecting the page.
// If no loader exists, redirect the page ourselves (if a url exists).
$.each({
'background_image': {
controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
- callback: function( to ) { return !! to }
+ callback: function( to ) { return !! to; }
},
'show_on_front': {
controls: [ 'page_on_front', 'page_for_posts' ],
- callback: function( to ) { return 'page' === to }
+ callback: function( to ) { return 'page' === to; }
},
'header_textcolor': {
controls: [ 'header_textcolor' ],
- callback: function( to ) { return 'blank' !== to }
+ callback: function( to ) { return 'blank' !== to; }
}
}, function( settingId, o ) {
api( settingId, function( setting ) {
});
});
- // Handle header image data
- api.control( 'header_image', function( control ) {
- control.setting.bind( function( to ) {
- if ( to === control.params.removed )
- control.settings.data.set( false );
- });
-
- control.library.on( 'click', 'a', function( event ) {
- control.settings.data.set( $(this).data('customizeHeaderImageData') );
- });
-
- control.uploader.success = function( attachment ) {
- var data;
-
- api.ImageControl.prototype.success.call( control, attachment );
-
- data = {
- attachment_id: attachment.get('id'),
- url: attachment.get('url'),
- thumbnail_url: attachment.get('url'),
- height: attachment.get('height'),
- width: attachment.get('width')
- };
-
- attachment.element.data( 'customizeHeaderImageData', data );
- control.settings.data.set( data );
- };
- });
-
api.trigger( 'ready' );
// Make sure left column gets focus
- var topFocus = $('.back');
+ topFocus = closeBtn;
topFocus.focus();
setTimeout(function () {
topFocus.focus();