UNPKG

@wordpress/block-editor

Version:
566 lines (554 loc) 21.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.coordsToBackgroundPosition = exports.backgroundPositionToCoords = void 0; exports.default = BackgroundImagePanel; var _clsx = _interopRequireDefault(require("clsx")); var _components = require("@wordpress/components"); var _i18n = require("@wordpress/i18n"); var _notices = require("@wordpress/notices"); var _url = require("@wordpress/url"); var _element = require("@wordpress/element"); var _data = require("@wordpress/data"); var _dom = require("@wordpress/dom"); var _blob = require("@wordpress/blob"); var _utils = require("../global-styles/utils"); var _backgroundPanel = require("../global-styles/background-panel"); var _object = require("../../utils/object"); var _mediaReplaceFlow = _interopRequireDefault(require("../media-replace-flow")); var _store = require("../../store"); var _privateKeys = require("../../store/private-keys"); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const IMAGE_BACKGROUND_TYPE = 'image'; const BACKGROUND_POPOVER_PROPS = { placement: 'left-start', offset: 36, shift: true, className: 'block-editor-global-styles-background-panel__popover' }; const noop = () => {}; /** * Get the help text for the background size control. * * @param {string} value backgroundSize value. * @return {string} Translated help text. */ function backgroundSizeHelpText(value) { if (value === 'cover' || value === undefined) { return (0, _i18n.__)('Image covers the space evenly.'); } if (value === 'contain') { return (0, _i18n.__)('Image is contained without distortion.'); } return (0, _i18n.__)('Image has a fixed width.'); } /** * Converts decimal x and y coords from FocalPointPicker to percentage-based values * to use as backgroundPosition value. * * @param {{x?:number, y?:number}} value FocalPointPicker coords. * @return {string} backgroundPosition value. */ const coordsToBackgroundPosition = value => { if (!value || isNaN(value.x) && isNaN(value.y)) { return undefined; } const x = isNaN(value.x) ? 0.5 : value.x; const y = isNaN(value.y) ? 0.5 : value.y; return `${x * 100}% ${y * 100}%`; }; /** * Converts backgroundPosition value to x and y coords for FocalPointPicker. * * @param {string} value backgroundPosition value. * @return {{x?:number, y?:number}} FocalPointPicker coords. */ exports.coordsToBackgroundPosition = coordsToBackgroundPosition; const backgroundPositionToCoords = value => { if (!value) { return { x: undefined, y: undefined }; } let [x, y] = value.split(' ').map(v => parseFloat(v) / 100); x = isNaN(x) ? undefined : x; y = isNaN(y) ? x : y; return { x, y }; }; exports.backgroundPositionToCoords = backgroundPositionToCoords; function InspectorImagePreviewItem({ as = 'span', imgUrl, toggleProps = {}, filename, label, className, onToggleCallback = noop }) { (0, _element.useEffect)(() => { if (typeof toggleProps?.isOpen !== 'undefined') { onToggleCallback(toggleProps?.isOpen); } }, [toggleProps?.isOpen, onToggleCallback]); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalItemGroup, { as: as, className: className, ...toggleProps, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalHStack, { justify: "flex-start", as: "span", className: "block-editor-global-styles-background-panel__inspector-preview-inner", children: [imgUrl && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: "block-editor-global-styles-background-panel__inspector-image-indicator-wrapper", "aria-hidden": true, children: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: "block-editor-global-styles-background-panel__inspector-image-indicator", style: { backgroundImage: `url(${imgUrl})` } }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.FlexItem, { as: "span", style: imgUrl ? {} : { flexGrow: 1 }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalTruncate, { numberOfLines: 1, className: "block-editor-global-styles-background-panel__inspector-media-replace-title", children: label }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.VisuallyHidden, { as: "span", children: imgUrl ? (0, _i18n.sprintf)(/* translators: %s: file name */ (0, _i18n.__)('Background image: %s'), filename || label) : (0, _i18n.__)('No background image selected') })] })] }) }); } function BackgroundControlsPanel({ label, filename, url: imgUrl, children, onToggle: onToggleCallback = noop, hasImageValue }) { if (!hasImageValue) { return; } const imgLabel = label || (0, _url.getFilename)(imgUrl) || (0, _i18n.__)('Add background image'); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Dropdown, { popoverProps: BACKGROUND_POPOVER_PROPS, renderToggle: ({ onToggle, isOpen }) => { const toggleProps = { onClick: onToggle, className: 'block-editor-global-styles-background-panel__dropdown-toggle', 'aria-expanded': isOpen, 'aria-label': (0, _i18n.__)('Background size, position and repeat options.'), isOpen }; return /*#__PURE__*/(0, _jsxRuntime.jsx)(InspectorImagePreviewItem, { imgUrl: imgUrl, filename: filename, label: imgLabel, toggleProps: toggleProps, as: "button", onToggleCallback: onToggleCallback }); }, renderContent: () => /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalDropdownContentWrapper, { className: "block-editor-global-styles-background-panel__dropdown-content-wrapper", paddingSize: "medium", children: children }) }); } function LoadingSpinner() { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Placeholder, { className: "block-editor-global-styles-background-panel__loading", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Spinner, {}) }); } function BackgroundImageControls({ onChange, style, inheritedValue, onRemoveImage = noop, onResetImage = noop, displayInPanel, defaultValues }) { const [isUploading, setIsUploading] = (0, _element.useState)(false); const { getSettings } = (0, _data.useSelect)(_store.store); const { id, title, url } = style?.background?.backgroundImage || { ...inheritedValue?.background?.backgroundImage }; const replaceContainerRef = (0, _element.useRef)(); const { createErrorNotice } = (0, _data.useDispatch)(_notices.store); const onUploadError = message => { createErrorNotice(message, { type: 'snackbar' }); setIsUploading(false); }; const resetBackgroundImage = () => onChange((0, _object.setImmutably)(style, ['background', 'backgroundImage'], undefined)); const onSelectMedia = media => { if (!media || !media.url) { resetBackgroundImage(); setIsUploading(false); return; } if ((0, _blob.isBlobURL)(media.url)) { setIsUploading(true); return; } // For media selections originated from a file upload. if (media.media_type && media.media_type !== IMAGE_BACKGROUND_TYPE || !media.media_type && media.type && media.type !== IMAGE_BACKGROUND_TYPE) { onUploadError((0, _i18n.__)('Only images can be used as a background image.')); return; } const sizeValue = style?.background?.backgroundSize || defaultValues?.backgroundSize; const positionValue = style?.background?.backgroundPosition; onChange((0, _object.setImmutably)(style, ['background'], { ...style?.background, backgroundImage: { url: media.url, id: media.id, source: 'file', title: media.title || undefined }, backgroundPosition: /* * A background image uploaded and set in the editor receives a default background position of '50% 0', * when the background image size is the equivalent of "Tile". * This is to increase the chance that the image's focus point is visible. * This is in-editor only to assist with the user experience. */ !positionValue && ('auto' === sizeValue || !sizeValue) ? '50% 0' : positionValue, backgroundSize: sizeValue })); setIsUploading(false); }; // Drag and drop callback, restricting image to one. const onFilesDrop = filesList => { getSettings().mediaUpload({ allowedTypes: [IMAGE_BACKGROUND_TYPE], filesList, onFileChange([image]) { onSelectMedia(image); }, onError: onUploadError, multiple: false }); }; const hasValue = (0, _backgroundPanel.hasBackgroundImageValue)(style); const closeAndFocus = () => { const [toggleButton] = _dom.focus.tabbable.find(replaceContainerRef.current); // Focus the toggle button and close the dropdown menu. // This ensures similar behaviour as to selecting an image, where the dropdown is // closed and focus is redirected to the dropdown toggle button. toggleButton?.focus(); toggleButton?.click(); }; const onRemove = () => onChange((0, _object.setImmutably)(style, ['background'], { backgroundImage: 'none' })); const canRemove = !hasValue && (0, _backgroundPanel.hasBackgroundImageValue)(inheritedValue); const imgLabel = title || (0, _url.getFilename)(url) || (0, _i18n.__)('Add background image'); return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { ref: replaceContainerRef, className: "block-editor-global-styles-background-panel__image-tools-panel-item", children: [isUploading && /*#__PURE__*/(0, _jsxRuntime.jsx)(LoadingSpinner, {}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_mediaReplaceFlow.default, { mediaId: id, mediaURL: url, allowedTypes: [IMAGE_BACKGROUND_TYPE], accept: "image/*", onSelect: onSelectMedia, popoverProps: { className: (0, _clsx.default)({ 'block-editor-global-styles-background-panel__media-replace-popover': displayInPanel }) }, name: /*#__PURE__*/(0, _jsxRuntime.jsx)(InspectorImagePreviewItem, { className: "block-editor-global-styles-background-panel__image-preview", imgUrl: url, filename: title, label: imgLabel }), renderToggle: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Button, { ...props, __next40pxDefaultSize: true }), onError: onUploadError, onReset: () => { closeAndFocus(); onResetImage(); }, children: canRemove && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.MenuItem, { onClick: () => { closeAndFocus(); onRemove(); onRemoveImage(); }, children: (0, _i18n.__)('Remove') }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.DropZone, { onFilesDrop: onFilesDrop, label: (0, _i18n.__)('Drop to upload') })] }); } function BackgroundSizeControls({ onChange, style, inheritedValue, defaultValues }) { const sizeValue = style?.background?.backgroundSize || inheritedValue?.background?.backgroundSize; const repeatValue = style?.background?.backgroundRepeat || inheritedValue?.background?.backgroundRepeat; const imageValue = style?.background?.backgroundImage?.url || inheritedValue?.background?.backgroundImage?.url; const isUploadedImage = style?.background?.backgroundImage?.id; const positionValue = style?.background?.backgroundPosition || inheritedValue?.background?.backgroundPosition; const attachmentValue = style?.background?.backgroundAttachment || inheritedValue?.background?.backgroundAttachment; /* * Set default values for uploaded images. * The default values are passed by the consumer. * Block-level controls may have different defaults to root-level controls. * A falsy value is treated by default as `auto` (Tile). */ let currentValueForToggle = !sizeValue && isUploadedImage ? defaultValues?.backgroundSize : sizeValue || 'auto'; /* * The incoming value could be a value + unit, e.g. '20px'. * In this case set the value to 'tile'. */ currentValueForToggle = !['cover', 'contain', 'auto'].includes(currentValueForToggle) ? 'auto' : currentValueForToggle; /* * If the current value is `cover` and the repeat value is `undefined`, then * the toggle should be unchecked as the default state. Otherwise, the toggle * should reflect the current repeat value. */ const repeatCheckedValue = !(repeatValue === 'no-repeat' || currentValueForToggle === 'cover' && repeatValue === undefined); const updateBackgroundSize = next => { // When switching to 'contain' toggle the repeat off. let nextRepeat = repeatValue; let nextPosition = positionValue; if (next === 'contain') { nextRepeat = 'no-repeat'; nextPosition = undefined; } if (next === 'cover') { nextRepeat = undefined; nextPosition = undefined; } if ((currentValueForToggle === 'cover' || currentValueForToggle === 'contain') && next === 'auto') { nextRepeat = undefined; /* * A background image uploaded and set in the editor (an image with a record id), * receives a default background position of '50% 0', * when the toggle switches to "Tile". This is to increase the chance that * the image's focus point is visible. * This is in-editor only to assist with the user experience. */ if (!!style?.background?.backgroundImage?.id) { nextPosition = '50% 0'; } } /* * Next will be null when the input is cleared, * in which case the value should be 'auto'. */ if (!next && currentValueForToggle === 'auto') { next = 'auto'; } onChange((0, _object.setImmutably)(style, ['background'], { ...style?.background, backgroundPosition: nextPosition, backgroundRepeat: nextRepeat, backgroundSize: next })); }; const updateBackgroundPosition = next => { onChange((0, _object.setImmutably)(style, ['background', 'backgroundPosition'], coordsToBackgroundPosition(next))); }; const toggleIsRepeated = () => onChange((0, _object.setImmutably)(style, ['background', 'backgroundRepeat'], repeatCheckedValue === true ? 'no-repeat' : 'repeat')); const toggleScrollWithPage = () => onChange((0, _object.setImmutably)(style, ['background', 'backgroundAttachment'], attachmentValue === 'fixed' ? 'scroll' : 'fixed')); // Set a default background position for non-site-wide, uploaded images with a size of 'contain'. const backgroundPositionValue = !positionValue && isUploadedImage && 'contain' === sizeValue ? defaultValues?.backgroundPosition : positionValue; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalVStack, { spacing: 3, className: "single-column", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.FocalPointPicker, { __nextHasNoMarginBottom: true, label: (0, _i18n.__)('Focal point'), url: imageValue, value: backgroundPositionToCoords(backgroundPositionValue), onChange: updateBackgroundPosition }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToggleControl, { __nextHasNoMarginBottom: true, label: (0, _i18n.__)('Fixed background'), checked: attachmentValue === 'fixed', onChange: toggleScrollWithPage }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalToggleGroupControl, { __nextHasNoMarginBottom: true, size: "__unstable-large", label: (0, _i18n.__)('Size'), value: currentValueForToggle, onChange: updateBackgroundSize, isBlock: true, help: backgroundSizeHelpText(sizeValue || defaultValues?.backgroundSize), children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToggleGroupControlOption, { value: "cover", label: (0, _i18n._x)('Cover', 'Size option for background image control') }, "cover"), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToggleGroupControlOption, { value: "contain", label: (0, _i18n._x)('Contain', 'Size option for background image control') }, "contain"), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToggleGroupControlOption, { value: "auto", label: (0, _i18n._x)('Tile', 'Size option for background image control') }, "tile")] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalHStack, { justify: "flex-start", spacing: 2, as: "span", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalUnitControl, { "aria-label": (0, _i18n.__)('Background image width'), onChange: updateBackgroundSize, value: sizeValue, size: "__unstable-large", __unstableInputWidth: "100px", min: 0, placeholder: (0, _i18n.__)('Auto'), disabled: currentValueForToggle !== 'auto' || currentValueForToggle === undefined }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToggleControl, { __nextHasNoMarginBottom: true, label: (0, _i18n.__)('Repeat'), checked: repeatCheckedValue, onChange: toggleIsRepeated, disabled: currentValueForToggle === 'cover' })] })] }); } function BackgroundImagePanel({ value, onChange, inheritedValue = value, settings, defaultValues = {} }) { /* * Resolve any inherited "ref" pointers. * Should the block editor need resolved, inherited values * across all controls, this could be abstracted into a hook, * e.g., useResolveGlobalStyle */ const { globalStyles, _links } = (0, _data.useSelect)(select => { const { getSettings } = select(_store.store); const _settings = getSettings(); return { globalStyles: _settings[_privateKeys.globalStylesDataKey], _links: _settings[_privateKeys.globalStylesLinksDataKey] }; }, []); const resolvedInheritedValue = (0, _element.useMemo)(() => { const resolvedValues = { background: {} }; if (!inheritedValue?.background) { return inheritedValue; } Object.entries(inheritedValue?.background).forEach(([key, backgroundValue]) => { resolvedValues.background[key] = (0, _utils.getResolvedValue)(backgroundValue, { styles: globalStyles, _links }); }); return resolvedValues; }, [globalStyles, _links, inheritedValue]); const resetBackground = () => onChange((0, _object.setImmutably)(value, ['background'], {})); const { title, url } = value?.background?.backgroundImage || { ...resolvedInheritedValue?.background?.backgroundImage }; const hasImageValue = (0, _backgroundPanel.hasBackgroundImageValue)(value) || (0, _backgroundPanel.hasBackgroundImageValue)(resolvedInheritedValue); const imageValue = value?.background?.backgroundImage || inheritedValue?.background?.backgroundImage; const shouldShowBackgroundImageControls = hasImageValue && 'none' !== imageValue && (settings?.background?.backgroundSize || settings?.background?.backgroundPosition || settings?.background?.backgroundRepeat); const [isDropDownOpen, setIsDropDownOpen] = (0, _element.useState)(false); return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: (0, _clsx.default)('block-editor-global-styles-background-panel__inspector-media-replace-container', { 'is-open': isDropDownOpen }), children: shouldShowBackgroundImageControls ? /*#__PURE__*/(0, _jsxRuntime.jsx)(BackgroundControlsPanel, { label: title, filename: title, url: url, onToggle: setIsDropDownOpen, hasImageValue: hasImageValue, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalVStack, { spacing: 3, className: "single-column", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(BackgroundImageControls, { onChange: onChange, style: value, inheritedValue: resolvedInheritedValue, displayInPanel: true, onResetImage: () => { setIsDropDownOpen(false); resetBackground(); }, onRemoveImage: () => setIsDropDownOpen(false), defaultValues: defaultValues }), /*#__PURE__*/(0, _jsxRuntime.jsx)(BackgroundSizeControls, { onChange: onChange, style: value, defaultValues: defaultValues, inheritedValue: resolvedInheritedValue })] }) }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(BackgroundImageControls, { onChange: onChange, style: value, inheritedValue: resolvedInheritedValue, defaultValues: defaultValues, onResetImage: () => { setIsDropDownOpen(false); resetBackground(); }, onRemoveImage: () => setIsDropDownOpen(false) }) }); } //# sourceMappingURL=index.js.map