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 {

- +
<?php esc_attr_e( 'Browser Chrome' ); ?> @@ -284,7 +284,7 @@ class WP_Site_Icon {
- +
<?php esc_attr_e( 'Preview Home 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