diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php
index 7b2d5d6af1..15acd7cedb 100644
--- a/src/wp-admin/admin-ajax.php
+++ b/src/wp-admin/admin-ajax.php
@@ -62,7 +62,7 @@ $core_actions_post = array(
'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs',
'save-user-color-scheme', 'update-widget', 'query-themes', 'parse-embed', 'set-attachment-thumbnail',
'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post',
- 'press-this-add-category',
+ 'press-this-add-category', 'crop-image',
);
// Deprecated
diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css
index 31ca6e6a2d..5e0eb6802b 100644
--- a/src/wp-admin/css/customize-controls.css
+++ b/src/wp-admin/css/customize-controls.css
@@ -751,6 +751,8 @@ p.customize-section-description {
.customize-control-upload .current,
.customize-control-image .current,
.customize-control-background .current,
+.customize-control-cropped_image .current,
+.customize-control-site_icon .current,
.customize-control-header .current {
margin-bottom: 8px;
}
@@ -786,6 +788,12 @@ p.customize-section-description {
.customize-control-background .remove-button,
.customize-control-background .default-button,
.customize-control-background .upload-button,
+.customize-control-cropped_image .remove-button,
+.customize-control-cropped_image .default-button,
+.customize-control-cropped_image .upload-button,
+.customize-control-site_icon .remove-button,
+.customize-control-site_icon .default-button,
+.customize-control-site_icon .upload-button,
.customize-control-header button.new,
.customize-control-header button.remove {
white-space: normal;
@@ -797,6 +805,8 @@ p.customize-section-description {
.customize-control-upload .current .container,
.customize-control-image .current .container,
.customize-control-background .current .container,
+.customize-control-cropped_image .current .container,
+.customize-control-site_icon .current .container,
.customize-control-header .current .container {
overflow: hidden;
-webkit-border-radius: 2px;
@@ -808,6 +818,8 @@ p.customize-section-description {
.customize-control-media .current .container,
.customize-control-upload .current .container,
.customize-control-background .current .container,
+.customize-control-cropped_image .current .container,
+.customize-control-site_icon .current .container,
.customize-control-image .current .container {
min-height: 40px;
}
@@ -816,6 +828,8 @@ p.customize-section-description {
.customize-control-upload .placeholder,
.customize-control-image .placeholder,
.customize-control-background .placeholder,
+.customize-control-cropped_image .placeholder,
+.customize-control-site_icon .placeholder,
.customize-control-header .placeholder {
width: 100%;
position: relative;
@@ -827,6 +841,8 @@ p.customize-section-description {
.customize-control-upload .inner,
.customize-control-image .inner,
.customize-control-background .inner,
+.customize-control-cropped_image .inner,
+.customize-control-site_icon .inner,
.customize-control-header .inner {
display: none;
position: absolute;
@@ -840,6 +856,8 @@ p.customize-section-description {
.customize-control-media .inner,
.customize-control-upload .inner,
.customize-control-background .inner,
+.customize-control-cropped_image .inner,
+.customize-control-site_icon .inner,
.customize-control-image .inner {
display: block;
min-height: 40px;
@@ -849,6 +867,8 @@ p.customize-section-description {
.customize-control-upload .inner,
.customize-control-image .inner,
.customize-control-background .inner,
+.customize-control-cropped_image .inner,
+.customize-control-site_icon .inner,
.customize-control-header .inner,
.customize-control-header .inner .dashicons {
line-height: 20px;
@@ -952,6 +972,8 @@ p.customize-section-description {
.customize-control-upload .actions,
.customize-control-image .actions,
.customize-control-background .actions,
+.customize-control-cropped_image .actions,
+.customize-control-site_icon .actions,
.customize-control-header .actions {
margin-bottom: 32px;
}
@@ -970,6 +992,8 @@ p.customize-section-description {
.customize-control-upload img,
.customize-control-image img,
.customize-control-background img,
+.customize-control-cropped_image img,
+.customize-control-site_icon img,
.customize-control-header img {
width: 100%;
-webkit-border-radius: 2px;
@@ -984,6 +1008,10 @@ p.customize-section-description {
.customize-control-image .default-button,
.customize-control-background .remove-button,
.customize-control-background .default-button,
+.customize-control-cropped_image .remove-button,
+.customize-control-cropped_image .default-button,
+.customize-control-site_icon .remove-button,
+.customize-control-site_icon .default-button,
.customize-control-header .remove {
float: left;
margin-right: 3px;
@@ -993,6 +1021,8 @@ p.customize-section-description {
.customize-control-upload .upload-button,
.customize-control-image .upload-button,
.customize-control-background .upload-button,
+.customize-control-cropped_image .upload-button,
+.customize-control-site_icon .upload-button,
.customize-control-header .new {
float: right;
}
diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php
index ba62b66d4d..bdec8c1691 100644
--- a/src/wp-admin/includes/ajax-actions.php
+++ b/src/wp-admin/includes/ajax-actions.php
@@ -3052,3 +3052,65 @@ function wp_ajax_press_this_add_category() {
$GLOBALS['wp_press_this']->add_category();
}
+
+/**
+ * AJAX handler for cropping an image.
+ *
+ * @since 4.3.0
+ *
+ * @global WP_Site_Icon $wp_site_icon
+ */
+function wp_ajax_crop_image() {
+ $attachment_id = absint( $_POST['id'] );
+
+ check_ajax_referer( 'image_editor-' . $attachment_id, 'nonce' );
+ if ( ! current_user_can( 'customize' ) ) {
+ wp_send_json_error();
+ }
+
+ $context = str_replace( '_', '-', $_POST['context'] );
+ $data = array_map( 'absint', $_POST['cropDetails'] );
+ $cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );
+
+ if ( ! $cropped || is_wp_error( $cropped ) ) {
+ wp_send_json_error( array( 'message' => __( 'Image could not be processed.' ) ) );
+ }
+
+ switch ( $context ) {
+ case 'site-icon':
+ require_once ABSPATH . '/wp-admin/includes/class-wp-site-icon.php';
+ global $wp_site_icon;
+
+ /** This filter is documented in wp-admin/custom-header.php */
+ $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
+ $object = $wp_site_icon->create_attachment_object( $cropped, $attachment_id );
+ unset( $object['ID'] );
+
+ // Update the attachment.
+ add_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
+ $attachment_id = $wp_site_icon->insert_attachment( $object, $cropped );
+ remove_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
+
+ // Additional sizes in wp_prepare_attachment_for_js().
+ add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
+ break;
+
+ default:
+
+ /**
+ * Filters the attachment id for a cropped image.
+ *
+ * @since 4.3.0
+ *
+ * @param int $attachment_id The ID of the cropped image.
+ * @param string $context The feature requesting the cropped image.
+ */
+ $attachment_id = apply_filters( 'wp_ajax_cropped_attachment_id', 0, $context );
+
+ if ( ! $attachment_id ) {
+ wp_send_json_error();
+ }
+ }
+
+ wp_send_json_success( wp_prepare_attachment_for_js( $attachment_id ) );
+}
diff --git a/src/wp-admin/includes/class-wp-site-icon.php b/src/wp-admin/includes/class-wp-site-icon.php
index 1f7b32915f..fb35dbccf0 100644
--- a/src/wp-admin/includes/class-wp-site-icon.php
+++ b/src/wp-admin/includes/class-wp-site-icon.php
@@ -274,7 +274,7 @@ class WP_Site_Icon {
-
+
@@ -284,7 +284,7 @@ class WP_Site_Icon {
-
+
@@ -505,7 +505,7 @@ class WP_Site_Icon {
// ensure that we only resize the image into
foreach ( $sizes as $name => $size_array ) {
- if ( $size_array['crop'] ) {
+ if ( isset( $size_array['crop'] ) ) {
$only_crop_sizes[ $name ] = $size_array;
}
}
diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js
index e763e34a32..5ebd830f8d 100644
--- a/src/wp-admin/js/customize-controls.js
+++ b/src/wp-admin/js/customize-controls.js
@@ -1835,6 +1835,245 @@
}
});
+ /**
+ * A control for selecting and cropping an image.
+ *
+ * @class
+ * @augments wp.customize.MediaControl
+ * @augments wp.customize.Control
+ * @augments wp.customize.Class
+ */
+ api.CroppedImageControl = api.MediaControl.extend({
+
+ /**
+ * Open the media modal to the library state.
+ */
+ openFrame: function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+
+ this.initFrame();
+ this.frame.setState( 'library' ).open();
+ },
+
+ /**
+ * Create a media modal select frame, and store it so the instance can be reused when needed.
+ */
+ initFrame: function() {
+ var l10n = _wpMediaViewsL10n;
+
+ this.frame = wp.media({
+ button: {
+ text: l10n.select,
+ close: false
+ },
+ states: [
+ new wp.media.controller.Library({
+ title: this.params.button_labels.frame_title,
+ library: wp.media.query({ type: 'image' }),
+ multiple: false,
+ date: false,
+ priority: 20,
+ suggestedWidth: this.params.width,
+ suggestedHeight: this.params.height
+ }),
+ new wp.media.controller.customizeImageCropper({
+ imgSelectOptions: this.calculateImageSelectOptions,
+ control: this
+ })
+ ]
+ });
+
+ this.frame.on( 'select', this.onSelect, this );
+ this.frame.on( 'cropped', this.onCropped, this );
+ this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
+ },
+
+ /**
+ * After an image is selected in the media modal, switch to the cropper
+ * state if the image isn't the right size.
+ */
+ onSelect: function() {
+ var attachment = this.frame.state().get( 'selection' ).first().toJSON();
+
+ if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
+ this.setImageFromAttachment( attachment );
+ this.frame.close();
+ } else {
+ this.frame.setState( 'cropper' );
+ }
+ },
+
+ /**
+ * After the image has been cropped, apply the cropped image data to the setting.
+ *
+ * @param {object} croppedImage Cropped attachment data.
+ */
+ onCropped: function( croppedImage ) {
+ this.setImageFromAttachment( croppedImage );
+ },
+
+ /**
+ * Returns a set of options, computed from the attached image data and
+ * control-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 control = controller.get( 'control' ),
+ flexWidth = !! parseInt( control.params.flex_width, 10 ),
+ flexHeight = !! parseInt( control.params.flex_height, 10 ),
+ realWidth = attachment.get( 'width' ),
+ realHeight = attachment.get( 'height' ),
+ xInit = parseInt( control.params.width, 10 ),
+ yInit = parseInt( control.params.height, 10 ),
+ ratio = xInit / yInit,
+ xImg = realWidth,
+ yImg = realHeight,
+ imgSelectOptions;
+
+ controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, 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;
+ },
+
+ /**
+ * Return whether the image must be cropped, based on required dimensions.
+ *
+ * @param {bool} flexW
+ * @param {bool} flexH
+ * @param {int} dstW
+ * @param {int} dstH
+ * @param {int} imgW
+ * @param {int} imgH
+ * @return {bool}
+ */
+ mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
+ if ( true === flexW && true === flexH ) {
+ return false;
+ }
+
+ if ( true === flexW && dstH === imgH ) {
+ return false;
+ }
+
+ if ( true === flexH && dstW === imgW ) {
+ return false;
+ }
+
+ if ( dstW === imgW && dstH === imgH ) {
+ return false;
+ }
+
+ if ( imgW <= dstW ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * If cropping was skipped, apply the image data directly to the setting.
+ */
+ onSkippedCrop: function() {
+ var attachment = this.frame.state().get( 'selection' ).first().toJSON();
+ this.setImageFromAttachment( attachment );
+ },
+
+ /**
+ * Updates the setting and re-renders the control UI.
+ *
+ * @param {object} attachment
+ */
+ setImageFromAttachment: function( attachment ) {
+ this.params.attachment = attachment;
+
+ // Set the Customizer setting; the callback takes care of rendering.
+ this.setting( attachment.id );
+ }
+ });
+
+ /**
+ * A control for selecting and cropping Site Icons.
+ *
+ * @class
+ * @augments wp.customize.CroppedImageControl
+ * @augments wp.customize.MediaControl
+ * @augments wp.customize.Control
+ * @augments wp.customize.Class
+ */
+ api.SiteIconControl = api.CroppedImageControl.extend({
+ /**
+ * Updates the setting and re-renders the control UI.
+ *
+ * @param {object} attachment
+ */
+ setImageFromAttachment: function( attachment ) {
+ var icon = typeof attachment.sizes['site_icon-32'] !== 'undefined' ? attachment.sizes['site_icon-32'] : attachment.sizes.thumbnail;
+
+ this.params.attachment = attachment;
+
+ // Set the Customizer setting; the callback takes care of rendering.
+ this.setting( attachment.id );
+
+
+ // Update the icon in-browser.
+ $( 'link[rel="icon"]' ).attr( 'href', icon.url );
+ },
+
+ /**
+ * Called when the "Remove" link is clicked. Empties the setting.
+ *
+ * @param {object} event jQuery Event object
+ */
+ removeFile: function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+ event.preventDefault();
+
+ this.params.attachment = {};
+ this.setting( '' );
+ this.renderContent(); // Not bound to setting change when emptying.
+ $( 'link[rel="icon"]' ).attr( 'href', '' );
+ }
+ });
+
/**
* @class
* @augments wp.customize.Control
@@ -2695,13 +2934,15 @@
});
api.controlConstructor = {
- color: api.ColorControl,
- media: api.MediaControl,
- upload: api.UploadControl,
- image: api.ImageControl,
- header: api.HeaderControl,
- background: api.BackgroundControl,
- theme: api.ThemeControl
+ color: api.ColorControl,
+ media: api.MediaControl,
+ upload: api.UploadControl,
+ image: api.ImageControl,
+ cropped_image: api.CroppedImageControl,
+ site_icon: api.SiteIconControl,
+ header: api.HeaderControl,
+ background: api.BackgroundControl,
+ theme: api.ThemeControl
};
api.panelConstructor = {};
api.sectionConstructor = {
diff --git a/src/wp-admin/options-general.php b/src/wp-admin/options-general.php
index 21f568fc43..cb3b7e8229 100644
--- a/src/wp-admin/options-general.php
+++ b/src/wp-admin/options-general.php
@@ -163,7 +163,7 @@ include( ABSPATH . 'wp-admin/admin-header.php' );
-
+
diff --git a/src/wp-includes/class-wp-customize-control.php b/src/wp-includes/class-wp-customize-control.php
index b1be94f680..59f980ee73 100644
--- a/src/wp-includes/class-wp-customize-control.php
+++ b/src/wp-includes/class-wp-customize-control.php
@@ -1000,6 +1000,134 @@ class WP_Customize_Background_Image_Control extends WP_Customize_Image_Control {
}
}
+/**
+ * Customize Cropped Image Control class.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Image_Control
+ */
+class WP_Customize_Cropped_Image_Control extends WP_Customize_Image_Control {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'cropped_image';
+
+ /**
+ * Suggested width for cropped image.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var int
+ */
+ public $width = 150;
+
+ /**
+ * Suggested height for cropped image.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var int
+ */
+ public $height = 150;
+
+ /**
+ * Whether the width is flexible.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var bool
+ */
+ public $flex_width = false;
+
+ /**
+ * Whether the height is flexible.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var bool
+ */
+ public $flex_height = false;
+
+ /**
+ * Enqueue control related scripts/styles.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ */
+ public function enqueue() {
+ wp_enqueue_script( 'customize-views' );
+
+ parent::enqueue();
+ }
+
+ /**
+ * Refresh the parameters passed to the JavaScript via JSON.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @uses WP_Customize_Image_Control::to_json()
+ * @see WP_Customize_Control::to_json()
+ */
+ public function to_json() {
+ parent::to_json();
+
+ $this->json['width'] = absint( $this->width );
+ $this->json['height'] = absint( $this->height );
+ $this->json['flex_width'] = absint( $this->flex_width );
+ $this->json['flex_height'] = absint( $this->flex_height );
+ }
+
+}
+
+/**
+ * Customize Site Icon control class.
+ *
+ * Used only for custom functionality in JavaScript.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Cropped_Image_Control
+ */
+class WP_Customize_Site_Icon_Control extends WP_Customize_Cropped_Image_Control {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'site_icon';
+
+ /**
+ * Constructor.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_Customize_Manager $manager
+ * @param string $id
+ * @param array $args
+ */
+ public function __construct( $manager, $id, $args = array() ) {
+ parent::__construct( $manager, $id, $args );
+ add_action( 'customize_controls_print_styles', 'wp_site_icon', 99 );
+ }
+}
+
/**
* Customize Header Image Control class.
*
diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php
index eddf652137..dd33ba6531 100644
--- a/src/wp-includes/class-wp-customize-manager.php
+++ b/src/wp-includes/class-wp-customize-manager.php
@@ -1278,6 +1278,8 @@ final class WP_Customize_Manager {
$this->register_control_type( 'WP_Customize_Upload_Control' );
$this->register_control_type( 'WP_Customize_Image_Control' );
$this->register_control_type( 'WP_Customize_Background_Image_Control' );
+ $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
+ $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
$this->register_control_type( 'WP_Customize_Theme_Control' );
/* Themes */
@@ -1324,10 +1326,10 @@ final class WP_Customize_Manager {
) ) );
}
- /* Site Title & Tagline */
+ /* Site Identity */
$this->add_section( 'title_tagline', array(
- 'title' => __( 'Site Title & Tagline' ),
+ 'title' => __( 'Site Identity' ),
'priority' => 20,
) );
@@ -1353,6 +1355,23 @@ final class WP_Customize_Manager {
'section' => 'title_tagline',
) );
+ $icon = wp_get_attachment_image_src( absint( get_option( 'site_icon' ) ), 'full' );
+ $this->add_setting( 'site_icon', array(
+ 'default' => $icon[0] ? $icon[0] : '',
+ 'type' => 'option',
+ 'capability' => 'manage_options',
+ 'transport' => 'postMessage', // Previewed with JS in the Customizer controls window.
+ ) );
+
+ $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
+ 'label' => __( 'Site Icon' ),
+ 'description' => __( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least 512px wide and tall.' ),
+ 'section' => 'title_tagline',
+ 'priority' => 60,
+ 'height' => 512,
+ 'width' => 512,
+ ) ) );
+
/* Colors */
$this->add_section( 'colors', array(
@@ -1375,6 +1394,7 @@ final class WP_Customize_Manager {
'label' => __( 'Display Header Text' ),
'section' => 'title_tagline',
'type' => 'checkbox',
+ 'priority' => 40,
) );
$this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index a95da70f79..a653bd6f68 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -2445,7 +2445,7 @@ function wp_no_robots() {
* @link http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon HTML5 specification link icon.
*/
function wp_site_icon() {
- if ( ! has_site_icon() ) {
+ if ( ! has_site_icon() && ! is_customize_preview() ) {
return;
}
diff --git a/src/wp-includes/js/customize-views.js b/src/wp-includes/js/customize-views.js
index 142501b35b..59b8c97b6e 100644
--- a/src/wp-includes/js/customize-views.js
+++ b/src/wp-includes/js/customize-views.js
@@ -3,6 +3,26 @@
if ( ! wp || ! wp.customize ) { return; }
var api = wp.customize;
+ /**
+ * Use a custom ajax action for cropped image controls.
+ */
+ wp.media.controller.customizeImageCropper = wp.media.controller.Cropper.extend( {
+ doCrop: function( attachment ) {
+ var cropDetails = attachment.get( 'cropDetails' ),
+ control = this.get( 'control' );
+
+ cropDetails.dst_width = control.params.width;
+ cropDetails.dst_height = control.params.height;
+
+ return wp.ajax.post( 'crop-image', {
+ wp_customize: 'on',
+ nonce: attachment.get( 'nonces' ).edit,
+ id: attachment.get( 'id' ),
+ context: control.id,
+ cropDetails: cropDetails
+ } );
+ }
+ } );
/**
* wp.customize.HeaderTool.CurrentView