diff --git a/lib/blocks.php b/lib/blocks.php index 80522bda95367..aa37d373bba5a 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -85,6 +85,7 @@ function gutenberg_reregister_core_block_types() { 'query.php' => 'core/query', 'query-loop.php' => 'core/query-loop', 'query-pagination.php' => 'core/query-pagination', + 'site-logo.php' => 'core/site-logo', 'site-title.php' => 'core/site-title', 'template-part.php' => 'core/template-part', ) diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 8f232cb0a35e6..491295a4af55a 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -37,6 +37,7 @@ @import "./search/editor.scss"; @import "./separator/editor.scss"; @import "./shortcode/editor.scss"; +@import "./site-logo/editor.scss"; @import "./social-link/editor.scss"; @import "./social-links/editor.scss"; @import "./spacer/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index cf54d74dab512..e4e553926eb14 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -64,6 +64,7 @@ import * as socialLink from './social-link'; import * as widgetArea from './widget-area'; // Full Site Editing Blocks +import * as siteLogo from './site-logo'; import * as siteTitle from './site-title'; import * as templatePart from './template-part'; import * as query from './query'; @@ -198,6 +199,7 @@ export const __experimentalRegisterExperimentalCoreBlocks = ...( __experimentalEnableFullSiteEditing ? [ siteTitle, + siteLogo, templatePart, query, queryLoop, diff --git a/packages/block-library/src/site-logo/block.json b/packages/block-library/src/site-logo/block.json new file mode 100644 index 0000000000000..f86b4a22ef363 --- /dev/null +++ b/packages/block-library/src/site-logo/block.json @@ -0,0 +1,16 @@ +{ + "name": "core/site-logo", + "category": "layout", + "attributes": { + "align": { + "type": "string" + }, + "width": { + "type": "number" + } + }, + "supports": { + "html": false, + "lightBlockWrapper": true + } +} diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js new file mode 100644 index 0000000000000..911189f311573 --- /dev/null +++ b/packages/block-library/src/site-logo/edit.js @@ -0,0 +1,373 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { includes, pick } from 'lodash'; + +/** + * WordPress dependencies + */ +import { isBlobURL } from '@wordpress/blob'; +import { useState, useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + Notice, + PanelBody, + RangeControl, + ResizableBox, + Spinner, + ToolbarButton, + ToolbarGroup, +} from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; +import { + BlockControls, + BlockIcon, + InspectorControls, + MediaPlaceholder, + MediaReplaceFlow, + __experimentalBlock as Block, +} from '@wordpress/block-editor'; +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import icon from './icon'; +import useClientWidth from '../image/use-client-width'; + +/** + * Module constants + */ +import { MIN_SIZE } from '../image/constants'; + +const ALLOWED_MEDIA_TYPES = [ 'image' ]; +const ACCEPT_MEDIA_STRING = 'image/*'; + +const SiteLogo = ( { + alt, + attributes: { align, width, height }, + containerRef, + isSelected, + setAttributes, + logoUrl, + siteUrl, +} ) => { + const clientWidth = useClientWidth( containerRef, [ align ] ); + const isLargeViewport = useViewportMatch( 'medium' ); + const isWideAligned = includes( [ 'wide', 'full' ], align ); + const isResizable = ! isWideAligned && isLargeViewport; + const [ { naturalWidth, naturalHeight }, setNaturalSize ] = useState( {} ); + const { toggleSelection } = useDispatch( 'core/block-editor' ); + const classes = classnames( { + 'is-transient': isBlobURL( logoUrl ), + } ); + const { maxWidth, isRTL, title } = useSelect( ( select ) => { + const { getSettings } = select( 'core/block-editor' ); + const siteEntities = select( 'core' ).getEditedEntityRecord( + 'root', + 'site' + ); + return { + title: siteEntities.title, + ...pick( getSettings(), [ 'imageSizes', 'isRTL', 'maxWidth' ] ), + }; + } ); + + function onResizeStart() { + toggleSelection( false ); + } + + function onResizeStop() { + toggleSelection( true ); + } + + const img = ( + // Disable reason: Image itself is not meant to be interactive, but + // should direct focus to block. + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ + event.preventDefault() } + > + + { { + setNaturalSize( + pick( event.target, [ + 'naturalWidth', + 'naturalHeight', + ] ) + ); + } } + /> + + + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ + ); + + let imageWidthWithinContainer; + + if ( clientWidth && naturalWidth && naturalHeight ) { + const exceedMaxWidth = naturalWidth > clientWidth; + imageWidthWithinContainer = exceedMaxWidth ? clientWidth : naturalWidth; + } + + if ( ! isResizable || ! imageWidthWithinContainer ) { + return
{ img }
; + } + + const currentWidth = width || imageWidthWithinContainer; + const ratio = naturalWidth / naturalHeight; + const currentHeight = currentWidth / ratio; + const minWidth = naturalWidth < naturalHeight ? MIN_SIZE : MIN_SIZE * ratio; + const minHeight = + naturalHeight < naturalWidth ? MIN_SIZE : MIN_SIZE / ratio; + + // With the current implementation of ResizableBox, an image needs an + // explicit pixel value for the max-width. In absence of being able to + // set the content-width, this max-width is currently dictated by the + // vanilla editor style. The following variable adds a buffer to this + // vanilla style, so 3rd party themes have some wiggleroom. This does, + // in most cases, allow you to scale the image beyond the width of the + // main column, though not infinitely. + // @todo It would be good to revisit this once a content-width variable + // becomes available. + const maxWidthBuffer = maxWidth * 2.5; + + let showRightHandle = false; + let showLeftHandle = false; + + /* eslint-disable no-lonely-if */ + // See https://github.com/WordPress/gutenberg/issues/7584. + if ( align === 'center' ) { + // When the image is centered, show both handles. + showRightHandle = true; + showLeftHandle = true; + } else if ( isRTL ) { + // In RTL mode the image is on the right by default. + // Show the right handle and hide the left handle only when it is + // aligned left. Otherwise always show the left handle. + if ( align === 'left' ) { + showRightHandle = true; + } else { + showLeftHandle = true; + } + } else { + // Show the left handle and hide the right handle only when the + // image is aligned right. Otherwise always show the right handle. + if ( align === 'right' ) { + showLeftHandle = true; + } else { + showRightHandle = true; + } + } + /* eslint-enable no-lonely-if */ + + return ( + <> + + + + setAttributes( { width: newWidth } ) + } + min={ minWidth } + max={ maxWidthBuffer } + initialPosition={ Math.min( + naturalWidth, + maxWidthBuffer + ) } + value={ width || '' } + disabled={ ! isResizable } + /> + + + { + onResizeStop(); + setAttributes( { + width: parseInt( currentWidth + delta.width, 10 ), + height: parseInt( currentHeight + delta.height, 10 ), + } ); + } } + > + { img } + + + ); +}; + +export default function LogoEdit( { + attributes, + className, + setAttributes, + isSelected, +} ) { + const { width } = attributes; + const [ logoUrl, setLogoUrl ] = useState(); + const [ error, setError ] = useState(); + const ref = useRef(); + const { mediaItemData, sitelogo, url } = useSelect( ( select ) => { + const siteSettings = select( 'core' ).getEditedEntityRecord( + 'root', + 'site' + ); + const mediaItem = select( 'core' ).getEntityRecord( + 'root', + 'media', + siteSettings.sitelogo + ); + return { + mediaItemData: mediaItem && { + url: mediaItem.source_url, + alt: mediaItem.alt_text, + }, + sitelogo: siteSettings.sitelogo, + url: siteSettings.url, + }; + }, [] ); + + const { editEntityRecord } = useDispatch( 'core' ); + const setLogo = ( newValue ) => + editEntityRecord( 'root', 'site', undefined, { + sitelogo: newValue, + } ); + + let alt = null; + if ( mediaItemData ) { + alt = mediaItemData.alt; + if ( logoUrl !== mediaItemData.url ) { + setLogoUrl( mediaItemData.url ); + } + } + + const onSelectLogo = ( media ) => { + if ( ! media ) { + return; + } + + if ( ! media.id && media.url ) { + // This is a temporary blob image + setLogo( '' ); + setError(); + setLogoUrl( media.url ); + return; + } + + setLogo( media.id.toString() ); + }; + + const deleteLogo = () => { + setLogo( '' ); + setLogoUrl( '' ); + }; + + const onUploadError = ( message ) => { + setError( message[ 2 ] ? message[ 2 ] : null ); + }; + + const controls = ( + + + { logoUrl && ( + + ) } + { !! logoUrl && ( + deleteLogo() } + label={ __( 'Delete Site Logo' ) } + /> + ) } + + + ); + + const label = __( 'Site Logo' ); + let logoImage; + if ( sitelogo === undefined ) { + logoImage = ; + } + + if ( !! logoUrl ) { + logoImage = ( + + ); + } + + const mediaPlaceholder = ( + } + labels={ { + title: label, + instructions: __( + 'Upload an image, or pick one from your media library, to be your site logo' + ), + } } + onSelect={ onSelectLogo } + accept={ ACCEPT_MEDIA_STRING } + allowedTypes={ ALLOWED_MEDIA_TYPES } + mediaPreview={ logoImage } + notices={ + error && ( + + { error } + + ) + } + onError={ onUploadError } + /> + ); + + const classes = classnames( className, { + 'is-resized': !! width, + 'is-focused': isSelected, + } ); + + const key = !! logoUrl; + + return ( + + { controls } + { logoUrl && logoImage } + { ! logoUrl && mediaPlaceholder } + + ); +} diff --git a/packages/block-library/src/site-logo/editor.scss b/packages/block-library/src/site-logo/editor.scss new file mode 100644 index 0000000000000..8cb4592480eca --- /dev/null +++ b/packages/block-library/src/site-logo/editor.scss @@ -0,0 +1,28 @@ +.wp-block[data-align="center"] > .wp-block-site-logo { + margin-left: auto; + margin-right: auto; + text-align: center; +} + +.wp-block-site-logo { + &.is-resized { + display: table; + } + + .custom-logo-link { + cursor: inherit; + + &:focus { + box-shadow: none; + } + + &.is-transient img { + opacity: 0.3; + } + } + + img { + display: block; + max-width: 100%; + } +} diff --git a/packages/block-library/src/site-logo/icon.js b/packages/block-library/src/site-logo/icon.js new file mode 100644 index 0000000000000..a7118a72de038 --- /dev/null +++ b/packages/block-library/src/site-logo/icon.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/components'; + +export default ( + + + + +); diff --git a/packages/block-library/src/site-logo/index.js b/packages/block-library/src/site-logo/index.js new file mode 100644 index 0000000000000..b021f2b08bbec --- /dev/null +++ b/packages/block-library/src/site-logo/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import icon from './icon'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Site Logo' ), + description: __( 'Show a site logo' ), + icon, + supports: { + align: true, + alignWide: false, + }, + edit, +}; diff --git a/packages/block-library/src/site-logo/index.php b/packages/block-library/src/site-logo/index.php new file mode 100644 index 0000000000000..66b3a84023af2 --- /dev/null +++ b/packages/block-library/src/site-logo/index.php @@ -0,0 +1,101 @@ +%s', $class_name, $custom_logo ); + remove_filter( 'wp_get_attachment_image_src', $adjust_width_height_filter ); + return $html; +} + + +/** + * Registers the `core/site-logo` block on the server. + */ +function register_block_core_site_logo() { + if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ) ) { + register_block_type( + 'core/site-logo', + array( + 'render_callback' => 'render_block_core_site_logo', + ) + ); + add_filter( 'pre_set_theme_mod_custom_logo', 'sync_site_logo_to_theme_mod' ); + add_filter( 'theme_mod_custom_logo', 'override_custom_logo_theme_mod' ); + } +} +add_action( 'init', 'register_block_core_site_logo' ); + +/** + * Overrides the custom logo with a site logo, if the option is set. + * + * @param string $custom_logo The custom logo set by a theme. + * + * @return string The site logo if set. + */ +function override_custom_logo_theme_mod( $custom_logo ) { + $sitelogo = get_option( 'sitelogo' ); + return false === $sitelogo ? $custom_logo : $sitelogo; +} + +/** + * Syncs the site logo with the theme modified logo. + * + * @param string $custom_logo The custom logo set by a theme. + * + * @return string The custom logo. + */ +function sync_site_logo_to_theme_mod( $custom_logo ) { + if ( $custom_logo ) { + update_option( 'sitelogo', $custom_logo ); + } + return $custom_logo; +} + +/** + * Register a core site setting for a site logo + */ +function register_block_core_site_logo_setting() { + register_setting( + 'general', + 'sitelogo', + array( + 'show_in_rest' => array( + 'name' => 'sitelogo', + ), + 'type' => 'string', + 'description' => __( 'Site logo.' ), + ) + ); +} + +add_action( 'rest_api_init', 'register_block_core_site_logo_setting', 10 ); diff --git a/packages/block-library/src/site-logo/style.scss b/packages/block-library/src/site-logo/style.scss new file mode 100644 index 0000000000000..6e186d32296df --- /dev/null +++ b/packages/block-library/src/site-logo/style.scss @@ -0,0 +1,5 @@ +.wp-block-custom-logo { + .aligncenter { + display: table; + } +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 2ef1a87bba417..31abfed6e3eb7 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -26,6 +26,7 @@ @import "./rss/style.scss"; @import "./search/style.scss"; @import "./separator/style.scss"; +@import "./site-logo/style.scss"; @import "./social-links/style.scss"; @import "./spacer/style.scss"; @import "./subhead/style.scss"; diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.html b/packages/e2e-tests/fixtures/blocks/core__site-logo.html new file mode 100644 index 0000000000000..cfd4cd6a3b8d6 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.html @@ -0,0 +1 @@ + diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.json b/packages/e2e-tests/fixtures/blocks/core__site-logo.json new file mode 100644 index 0000000000000..a84c7c57737e3 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.json @@ -0,0 +1,10 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/site-logo", + "isValid": true, + "attributes": {}, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.parsed.json b/packages/e2e-tests/fixtures/blocks/core__site-logo.parsed.json new file mode 100644 index 0000000000000..3db3836e10d1a --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/site-logo", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.serialized.html b/packages/e2e-tests/fixtures/blocks/core__site-logo.serialized.html new file mode 100644 index 0000000000000..cfd4cd6a3b8d6 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.serialized.html @@ -0,0 +1 @@ +