@wordpress/block-library
Version:
Block library for the WordPress editor.
510 lines (461 loc) • 15.5 kB
JavaScript
import { createElement, Fragment } from "@wordpress/element";
/**
* External dependencies
*/
import classnames from 'classnames';
import { includes, pick } from 'lodash';
/**
* WordPress dependencies
*/
import { isBlobURL } from '@wordpress/blob';
import { createInterpolateElement, useEffect, useState, useRef } from '@wordpress/element';
import { __, isRTL } from '@wordpress/i18n';
import { MenuItem, PanelBody, RangeControl, ResizableBox, Spinner, ToggleControl, ToolbarButton, Placeholder, Button } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { BlockControls, InspectorControls, MediaPlaceholder, MediaReplaceFlow, useBlockProps, store as blockEditorStore, __experimentalImageEditor as ImageEditor, __experimentalImageEditingProvider as ImageEditingProvider } from '@wordpress/block-editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { crop, upload } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
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 = _ref => {
let {
alt,
attributes: {
align,
width,
height,
isLink,
linkTarget,
shouldSyncIcon
},
containerRef,
isSelected,
setAttributes,
setLogo,
logoUrl,
siteUrl,
logoId,
iconId,
setIcon,
canUserEdit
} = _ref;
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 [isEditingImage, setIsEditingImage] = useState(false);
const {
toggleSelection
} = useDispatch(blockEditorStore);
const classes = classnames('custom-logo-link', {
'is-transient': isBlobURL(logoUrl)
});
const {
imageEditing,
maxWidth,
title
} = useSelect(select => {
const {
getSettings
} = select(blockEditorStore);
const siteEntities = select(coreStore).getEditedEntityRecord('root', 'site');
return {
title: siteEntities.title,
...pick(getSettings(), ['imageEditing', 'maxWidth'])
};
}, []);
useEffect(() => {
// Turn the `Use as site icon` toggle off if it is on but the logo and icon have
// fallen out of sync. This can happen if the toggle is saved in the `on` position,
// but changes are later made to the site icon in the Customizer.
if (shouldSyncIcon && logoId !== iconId) {
setAttributes({
shouldSyncIcon: false
});
}
}, []);
useEffect(() => {
if (!isSelected) {
setIsEditingImage(false);
}
}, [isSelected]);
function onResizeStart() {
toggleSelection(false);
}
function onResizeStop() {
toggleSelection(true);
}
const img = createElement("img", {
className: "custom-logo",
src: logoUrl,
alt: alt,
onLoad: event => {
setNaturalSize(pick(event.target, ['naturalWidth', 'naturalHeight']));
}
});
let imgWrapper = img; // Disable reason: Image itself is not meant to be interactive, but
// should direct focus to block.
if (isLink) {
imgWrapper =
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
createElement("a", {
href: siteUrl,
className: classes,
rel: "home",
title: title,
onClick: event => event.preventDefault()
}, img)
/* 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 createElement("div", {
style: {
width,
height
}
}, imgWrapper);
} // Set the default width to a responsible size.
// Note that this width is also set in the attached frontend CSS file.
const defaultWidth = 120;
const currentWidth = width || defaultWidth;
const ratio = naturalWidth / naturalHeight;
const currentHeight = currentWidth / ratio;
const minWidth = naturalWidth < naturalHeight ? MIN_SIZE : Math.ceil(MIN_SIZE * ratio);
const minHeight = naturalHeight < naturalWidth ? MIN_SIZE : Math.ceil(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 */
const canEditImage = logoId && naturalWidth && naturalHeight && imageEditing;
const imgEdit = canEditImage && isEditingImage ? createElement(ImageEditingProvider, {
id: logoId,
url: logoUrl,
naturalWidth: naturalWidth,
naturalHeight: naturalHeight,
clientWidth: clientWidth,
onSaveImage: imageAttributes => {
setLogo(imageAttributes.id);
},
isEditing: isEditingImage,
onFinishEditing: () => setIsEditingImage(false)
}, createElement(ImageEditor, {
url: logoUrl,
width: currentWidth,
height: currentHeight,
clientWidth: clientWidth,
naturalHeight: naturalHeight,
naturalWidth: naturalWidth
})) : createElement(ResizableBox, {
size: {
width: currentWidth,
height: currentHeight
},
showHandle: isSelected,
minWidth: minWidth,
maxWidth: maxWidthBuffer,
minHeight: minHeight,
maxHeight: maxWidthBuffer / ratio,
lockAspectRatio: true,
enable: {
top: false,
right: showRightHandle,
bottom: true,
left: showLeftHandle
},
onResizeStart: onResizeStart,
onResizeStop: (event, direction, elt, delta) => {
onResizeStop();
setAttributes({
width: parseInt(currentWidth + delta.width, 10),
height: parseInt(currentHeight + delta.height, 10)
});
}
}, imgWrapper);
const syncSiteIconHelpText = createInterpolateElement(__('Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. To use a custom icon that is different from your site logo, use the <a>Site Icon settings</a>.'), {
a: // eslint-disable-next-line jsx-a11y/anchor-has-content
createElement("a", {
href: siteUrl + '/wp-admin/customize.php?autofocus[section]=title_tagline',
target: "_blank",
rel: "noopener noreferrer"
})
});
return createElement(Fragment, null, createElement(InspectorControls, null, createElement(PanelBody, {
title: __('Settings')
}, createElement(RangeControl, {
label: __('Image width'),
onChange: newWidth => setAttributes({
width: newWidth
}),
min: minWidth,
max: maxWidthBuffer,
initialPosition: Math.min(defaultWidth, maxWidthBuffer),
value: width || '',
disabled: !isResizable
}), createElement(ToggleControl, {
label: __('Link image to home'),
onChange: () => setAttributes({
isLink: !isLink
}),
checked: isLink
}), isLink && createElement(Fragment, null, createElement(ToggleControl, {
label: __('Open in new tab'),
onChange: value => setAttributes({
linkTarget: value ? '_blank' : '_self'
}),
checked: linkTarget === '_blank'
})), canUserEdit && createElement(Fragment, null, createElement(ToggleControl, {
label: __('Use as site icon'),
onChange: value => {
setAttributes({
shouldSyncIcon: value
});
setIcon(value ? logoId : undefined);
},
checked: !!shouldSyncIcon,
help: syncSiteIconHelpText
})))), createElement(BlockControls, {
group: "block"
}, canEditImage && !isEditingImage && createElement(ToolbarButton, {
onClick: () => setIsEditingImage(true),
icon: crop,
label: __('Crop')
})), imgEdit);
};
export default function LogoEdit(_ref2) {
let {
attributes,
className,
setAttributes,
isSelected
} = _ref2;
const {
width,
shouldSyncIcon
} = attributes;
const ref = useRef();
const {
siteLogoId,
canUserEdit,
url,
siteIconId,
mediaItemData,
isRequestingMediaItem
} = useSelect(select => {
const {
canUser,
getEntityRecord,
getEditedEntityRecord
} = select(coreStore);
const siteSettings = getEditedEntityRecord('root', 'site');
const siteData = getEntityRecord('root', '__unstableBase');
const _siteLogo = siteSettings === null || siteSettings === void 0 ? void 0 : siteSettings.site_logo;
const _readOnlyLogo = siteData === null || siteData === void 0 ? void 0 : siteData.site_logo;
const _canUserEdit = canUser('update', 'settings');
const _siteLogoId = _canUserEdit ? _siteLogo : _readOnlyLogo;
const _siteIconId = siteSettings === null || siteSettings === void 0 ? void 0 : siteSettings.site_icon;
const mediaItem = _siteLogoId && select(coreStore).getMedia(_siteLogoId, {
context: 'view'
});
const _isRequestingMediaItem = _siteLogoId && !select(coreStore).hasFinishedResolution('getMedia', [_siteLogoId, {
context: 'view'
}]);
return {
siteLogoId: _siteLogoId,
canUserEdit: _canUserEdit,
url: siteData === null || siteData === void 0 ? void 0 : siteData.url,
mediaItemData: mediaItem,
isRequestingMediaItem: _isRequestingMediaItem,
siteIconId: _siteIconId
};
}, []);
const {
editEntityRecord
} = useDispatch(coreStore);
const setLogo = function (newValue) {
let shouldForceSync = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
// `shouldForceSync` is used to force syncing when the attribute
// may not have updated yet.
if (shouldSyncIcon || shouldForceSync) {
setIcon(newValue);
}
editEntityRecord('root', 'site', undefined, {
site_logo: newValue
});
};
const setIcon = newValue => // The new value needs to be `null` to reset the Site Icon.
editEntityRecord('root', 'site', undefined, {
site_icon: newValue !== null && newValue !== void 0 ? newValue : null
});
const {
alt_text: alt,
source_url: logoUrl
} = mediaItemData !== null && mediaItemData !== void 0 ? mediaItemData : {};
const onInitialSelectLogo = media => {
// Initialize the syncSiteIcon toggle. If we currently have no Site logo and no
// site icon, automatically sync the logo to the icon.
if (shouldSyncIcon === undefined) {
const shouldForceSync = !siteIconId;
setAttributes({
shouldSyncIcon: shouldForceSync
}); // Because we cannot rely on the `shouldSyncIcon` attribute to have updated by
// the time `setLogo` is called, pass an argument to force the syncing.
onSelectLogo(media, shouldForceSync);
return;
}
onSelectLogo(media);
};
const onSelectLogo = function (media) {
let shouldForceSync = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!media) {
return;
}
if (!media.id && media.url) {
// This is a temporary blob image.
setLogo(undefined);
return;
}
setLogo(media.id, shouldForceSync);
};
const onRemoveLogo = () => {
setLogo(null);
setAttributes({
width: undefined
});
};
const {
createErrorNotice
} = useDispatch(noticesStore);
const onUploadError = message => {
createErrorNotice(message, {
type: 'snackbar'
});
};
const controls = canUserEdit && logoUrl && createElement(BlockControls, {
group: "other"
}, createElement(MediaReplaceFlow, {
mediaURL: logoUrl,
allowedTypes: ALLOWED_MEDIA_TYPES,
accept: ACCEPT_MEDIA_STRING,
onSelect: onSelectLogo,
onError: onUploadError
}, createElement(MenuItem, {
onClick: onRemoveLogo
}, __('Reset'))));
let logoImage;
const isLoading = siteLogoId === undefined || isRequestingMediaItem;
if (isLoading) {
logoImage = createElement(Spinner, null);
}
if (!!logoUrl) {
logoImage = createElement(SiteLogo, {
alt: alt,
attributes: attributes,
className: className,
containerRef: ref,
isSelected: isSelected,
setAttributes: setAttributes,
logoUrl: logoUrl,
setLogo: setLogo,
logoId: (mediaItemData === null || mediaItemData === void 0 ? void 0 : mediaItemData.id) || siteLogoId,
siteUrl: url,
setIcon: setIcon,
iconId: siteIconId,
canUserEdit: canUserEdit
});
}
const placeholder = content => {
const placeholderClassName = classnames('block-editor-media-placeholder', className);
return createElement(Placeholder, {
className: placeholderClassName,
preview: logoImage,
withIllustration: true
}, content);
};
const classes = classnames(className, {
'is-default-size': !width
});
const blockProps = useBlockProps({
ref,
className: classes
});
const label = __('Add a site logo');
return createElement("div", blockProps, controls, !!logoUrl && logoImage, !logoUrl && !canUserEdit && createElement(Placeholder, {
className: "site-logo_placeholder"
}, !!isLoading && createElement("span", {
className: "components-placeholder__preview"
}, createElement(Spinner, null))), !logoUrl && canUserEdit && createElement(MediaPlaceholder, {
onSelect: onInitialSelectLogo,
accept: ACCEPT_MEDIA_STRING,
allowedTypes: ALLOWED_MEDIA_TYPES,
onError: onUploadError,
placeholder: placeholder,
mediaLibraryButton: _ref3 => {
let {
open
} = _ref3;
return createElement(Button, {
icon: upload,
variant: "primary",
label: label,
showTooltip: true,
tooltipPosition: "top center",
onClick: () => {
open();
}
});
}
}));
}
//# sourceMappingURL=edit.js.map