UNPKG

@wordpress/block-editor

Version:
637 lines (636 loc) 20.1 kB
// packages/block-editor/src/components/background-image-control/index.js import clsx from "clsx"; import { ToggleControl, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalUnitControl as UnitControl, __experimentalVStack as VStack, DropZone, FlexItem, FocalPointPicker, MenuItem, VisuallyHidden, __experimentalHStack as HStack, __experimentalTruncate as Truncate, Dropdown, Placeholder, Spinner, __experimentalDropdownContentWrapper as DropdownContentWrapper, Button } from "@wordpress/components"; import { reset as resetIcon } from "@wordpress/icons"; import { __, _x, sprintf } from "@wordpress/i18n"; import { store as noticesStore } from "@wordpress/notices"; import { getFilename } from "@wordpress/url"; import { useRef, useState, useEffect, useMemo } from "@wordpress/element"; import { useDispatch, useSelect } from "@wordpress/data"; import { focus } from "@wordpress/dom"; import { isBlobURL } from "@wordpress/blob"; import { getResolvedValue } from "@wordpress/global-styles-engine"; import { hasBackgroundImageValue } from "../global-styles/background-panel"; import { setImmutably } from "../../utils/object"; import MediaReplaceFlow from "../media-replace-flow"; import { store as blockEditorStore } from "../../store"; import { globalStylesDataKey, globalStylesLinksDataKey } from "../../store/private-keys"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; var IMAGE_BACKGROUND_TYPE = "image"; var BACKGROUND_POPOVER_PROPS = { placement: "left-start", offset: 36, shift: true, className: "block-editor-global-styles-background-panel__popover" }; var noop = () => { }; var focusToggleButton = (containerRef) => { window.requestAnimationFrame(() => { const [toggleButton] = focus.tabbable.find(containerRef?.current); if (!toggleButton) { return; } toggleButton.focus(); }); }; function backgroundSizeHelpText(value) { if (value === "cover" || value === void 0) { return __("Image covers the space evenly."); } if (value === "contain") { return __("Image is contained without distortion."); } return __("Image has a fixed width."); } var coordsToBackgroundPosition = (value) => { if (!value || isNaN(value.x) && isNaN(value.y)) { return void 0; } const x = isNaN(value.x) ? 0.5 : value.x; const y = isNaN(value.y) ? 0.5 : value.y; return `${x * 100}% ${y * 100}%`; }; var backgroundPositionToCoords = (value) => { if (!value) { return { x: void 0, y: void 0 }; } let [x, y] = value.split(" ").map((v) => parseFloat(v) / 100); x = isNaN(x) ? void 0 : x; y = isNaN(y) ? x : y; return { x, y }; }; function InspectorImagePreviewItem({ as = "span", imgUrl, toggleProps = {}, filename, label, onToggleCallback = noop }) { const { isOpen, ...restToggleProps } = toggleProps; useEffect(() => { if (typeof isOpen !== "undefined") { onToggleCallback(isOpen); } }, [isOpen, onToggleCallback]); const renderPreviewContent = () => { return /* @__PURE__ */ jsxs( HStack, { justify: "flex-start", as: "span", className: "block-editor-global-styles-background-panel__inspector-preview-inner", children: [ imgUrl && /* @__PURE__ */ jsx( "span", { className: "block-editor-global-styles-background-panel__inspector-image-indicator-wrapper", "aria-hidden": true, children: /* @__PURE__ */ jsx( "span", { className: "block-editor-global-styles-background-panel__inspector-image-indicator", style: { backgroundImage: `url(${imgUrl})` } } ) } ), /* @__PURE__ */ jsxs(FlexItem, { as: "span", style: imgUrl ? {} : { flexGrow: 1 }, children: [ /* @__PURE__ */ jsx( Truncate, { numberOfLines: 1, className: "block-editor-global-styles-background-panel__inspector-media-replace-title", children: label } ), /* @__PURE__ */ jsx(VisuallyHidden, { as: "span", children: imgUrl ? sprintf( /* translators: %s: file name */ __("Background image: %s"), filename || label ) : __("No background image selected") }) ] }) ] } ); }; return as === "button" ? /* @__PURE__ */ jsx(Button, { __next40pxDefaultSize: true, ...restToggleProps, children: renderPreviewContent() }) : renderPreviewContent(); } function BackgroundControlsPanel({ label, filename, url: imgUrl, children, onToggle: onToggleCallback = noop, hasImageValue, onReset, containerRef }) { if (!hasImageValue) { return; } const imgLabel = label || getFilename(imgUrl) || __("Add background image"); return /* @__PURE__ */ jsx( 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": __( "Background size, position and repeat options." ), isOpen }; return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( InspectorImagePreviewItem, { imgUrl, filename, label: imgLabel, toggleProps, as: "button", onToggleCallback } ), onReset && /* @__PURE__ */ jsx( Button, { __next40pxDefaultSize: true, label: __("Reset"), className: "block-editor-global-styles-background-panel__reset", size: "small", icon: resetIcon, onClick: () => { onReset(); if (isOpen) { onToggle(); } focusToggleButton(containerRef); } } ) ] }); }, renderContent: () => /* @__PURE__ */ jsx( DropdownContentWrapper, { className: "block-editor-global-styles-background-panel__dropdown-content-wrapper", paddingSize: "medium", children } ) } ); } function LoadingSpinner() { return /* @__PURE__ */ jsx(Placeholder, { className: "block-editor-global-styles-background-panel__loading", children: /* @__PURE__ */ jsx(Spinner, {}) }); } function BackgroundImageControls({ onChange, style, inheritedValue, onRemoveImage = noop, onResetImage = noop, displayInPanel, defaultValues, containerRef }) { const [isUploading, setIsUploading] = useState(false); const { getSettings } = useSelect(blockEditorStore); const { id, title, url } = style?.background?.backgroundImage || { ...inheritedValue?.background?.backgroundImage }; const { createErrorNotice } = useDispatch(noticesStore); const onUploadError = (message) => { createErrorNotice(message, { type: "snackbar" }); setIsUploading(false); }; const resetBackgroundImage = () => onChange( setImmutably( style, ["background", "backgroundImage"], void 0 ) ); const onSelectMedia = (media) => { if (!media || !media.url) { resetBackgroundImage(); setIsUploading(false); return; } if (isBlobURL(media.url)) { setIsUploading(true); return; } if (media.media_type && media.media_type !== IMAGE_BACKGROUND_TYPE || !media.media_type && media.type && media.type !== IMAGE_BACKGROUND_TYPE) { onUploadError( __("Only images can be used as a background image.") ); return; } const sizeValue = style?.background?.backgroundSize || defaultValues?.backgroundSize; const positionValue = style?.background?.backgroundPosition; onChange( setImmutably(style, ["background"], { ...style?.background, backgroundImage: { url: media.url, id: media.id, source: "file", title: media.title || void 0 }, 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); focusToggleButton(containerRef); }; const onFilesDrop = (filesList) => { getSettings().mediaUpload({ allowedTypes: [IMAGE_BACKGROUND_TYPE], filesList, onFileChange([image]) { onSelectMedia(image); }, onError: onUploadError, multiple: false }); }; const hasValue = hasBackgroundImageValue(style); const onRemove = () => onChange( setImmutably(style, ["background"], { backgroundImage: "none" }) ); const canRemove = !hasValue && hasBackgroundImageValue(inheritedValue); const imgLabel = title || getFilename(url) || __("Add background image"); return /* @__PURE__ */ jsxs("div", { className: "block-editor-global-styles-background-panel__image-tools-panel-item", children: [ isUploading && /* @__PURE__ */ jsx(LoadingSpinner, {}), /* @__PURE__ */ jsx( MediaReplaceFlow, { mediaId: id, mediaURL: url, allowedTypes: [IMAGE_BACKGROUND_TYPE], accept: "image/*", onSelect: onSelectMedia, popoverProps: { className: clsx({ "block-editor-global-styles-background-panel__media-replace-popover": displayInPanel }) }, name: /* @__PURE__ */ jsx( InspectorImagePreviewItem, { imgUrl: url, filename: title, label: imgLabel } ), renderToggle: (props) => /* @__PURE__ */ jsx(Button, { ...props, __next40pxDefaultSize: true }), onError: onUploadError, onReset: () => { focusToggleButton(containerRef); onResetImage(); }, children: canRemove && /* @__PURE__ */ jsx( MenuItem, { onClick: () => { focusToggleButton(containerRef); onRemove(); onRemoveImage(); }, children: __("Remove") } ) } ), /* @__PURE__ */ jsx( DropZone, { onFilesDrop, label: __("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; let currentValueForToggle = !sizeValue && isUploadedImage ? defaultValues?.backgroundSize : sizeValue || "auto"; currentValueForToggle = !["cover", "contain", "auto"].includes( currentValueForToggle ) ? "auto" : currentValueForToggle; const repeatCheckedValue = !(repeatValue === "no-repeat" || currentValueForToggle === "cover" && repeatValue === void 0); const updateBackgroundSize = (next) => { let nextRepeat = repeatValue; let nextPosition = positionValue; if (next === "contain") { nextRepeat = "no-repeat"; nextPosition = void 0; } if (next === "cover") { nextRepeat = void 0; nextPosition = void 0; } if ((currentValueForToggle === "cover" || currentValueForToggle === "contain") && next === "auto") { nextRepeat = void 0; if (!!style?.background?.backgroundImage?.id) { nextPosition = "50% 0"; } } if (!next && currentValueForToggle === "auto") { next = "auto"; } onChange( setImmutably(style, ["background"], { ...style?.background, backgroundPosition: nextPosition, backgroundRepeat: nextRepeat, backgroundSize: next }) ); }; const updateBackgroundPosition = (next) => { onChange( setImmutably( style, ["background", "backgroundPosition"], coordsToBackgroundPosition(next) ) ); }; const toggleIsRepeated = () => onChange( setImmutably( style, ["background", "backgroundRepeat"], repeatCheckedValue === true ? "no-repeat" : "repeat" ) ); const toggleScrollWithPage = () => onChange( setImmutably( style, ["background", "backgroundAttachment"], attachmentValue === "fixed" ? "scroll" : "fixed" ) ); const backgroundPositionValue = !positionValue && isUploadedImage && "contain" === sizeValue ? defaultValues?.backgroundPosition : positionValue; return /* @__PURE__ */ jsxs(VStack, { spacing: 3, className: "single-column", children: [ /* @__PURE__ */ jsx( FocalPointPicker, { __nextHasNoMarginBottom: true, label: __("Focal point"), url: imageValue, value: backgroundPositionToCoords(backgroundPositionValue), onChange: updateBackgroundPosition } ), /* @__PURE__ */ jsx( ToggleControl, { __nextHasNoMarginBottom: true, label: __("Fixed background"), checked: attachmentValue === "fixed", onChange: toggleScrollWithPage } ), /* @__PURE__ */ jsxs( ToggleGroupControl, { __nextHasNoMarginBottom: true, size: "__unstable-large", label: __("Size"), value: currentValueForToggle, onChange: updateBackgroundSize, isBlock: true, help: backgroundSizeHelpText( sizeValue || defaultValues?.backgroundSize ), children: [ /* @__PURE__ */ jsx( ToggleGroupControlOption, { value: "cover", label: _x( "Cover", "Size option for background image control" ) }, "cover" ), /* @__PURE__ */ jsx( ToggleGroupControlOption, { value: "contain", label: _x( "Contain", "Size option for background image control" ) }, "contain" ), /* @__PURE__ */ jsx( ToggleGroupControlOption, { value: "auto", label: _x( "Tile", "Size option for background image control" ) }, "tile" ) ] } ), /* @__PURE__ */ jsxs(HStack, { justify: "flex-start", spacing: 2, as: "span", children: [ /* @__PURE__ */ jsx( UnitControl, { "aria-label": __("Background image width"), onChange: updateBackgroundSize, value: sizeValue, size: "__unstable-large", __unstableInputWidth: "100px", min: 0, placeholder: __("Auto"), disabled: currentValueForToggle !== "auto" || currentValueForToggle === void 0 } ), /* @__PURE__ */ jsx( ToggleControl, { __nextHasNoMarginBottom: true, label: __("Repeat"), checked: repeatCheckedValue, onChange: toggleIsRepeated, disabled: currentValueForToggle === "cover" } ) ] }) ] }); } function BackgroundImagePanel({ value, onChange, inheritedValue = value, settings, defaultValues = {} }) { const { globalStyles, _links } = useSelect((select) => { const { getSettings } = select(blockEditorStore); const _settings = getSettings(); return { globalStyles: _settings[globalStylesDataKey], _links: _settings[globalStylesLinksDataKey] }; }, []); const resolvedInheritedValue = useMemo(() => { const resolvedValues = { background: {} }; if (!inheritedValue?.background) { return inheritedValue; } Object.entries(inheritedValue?.background).forEach( ([key, backgroundValue]) => { resolvedValues.background[key] = getResolvedValue( backgroundValue, { styles: globalStyles, _links } ); } ); return resolvedValues; }, [globalStyles, _links, inheritedValue]); const resetBackground = () => onChange(setImmutably(value, ["background"], {})); const { title, url } = value?.background?.backgroundImage || { ...resolvedInheritedValue?.background?.backgroundImage }; const hasImageValue = hasBackgroundImageValue(value) || 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] = useState(false); const containerRef = useRef(); return /* @__PURE__ */ jsx( "div", { ref: containerRef, className: clsx( "block-editor-global-styles-background-panel__inspector-media-replace-container", { "is-open": isDropDownOpen } ), children: shouldShowBackgroundImageControls ? /* @__PURE__ */ jsx( BackgroundControlsPanel, { label: title, filename: title, url, onToggle: setIsDropDownOpen, hasImageValue, onReset: resetBackground, containerRef, children: /* @__PURE__ */ jsxs(VStack, { spacing: 3, className: "single-column", children: [ /* @__PURE__ */ jsx( BackgroundImageControls, { onChange, style: value, inheritedValue: resolvedInheritedValue, displayInPanel: true, onResetImage: () => { setIsDropDownOpen(false); resetBackground(); }, onRemoveImage: () => setIsDropDownOpen(false), defaultValues, containerRef } ), /* @__PURE__ */ jsx( BackgroundSizeControls, { onChange, style: value, defaultValues, inheritedValue: resolvedInheritedValue } ) ] }) } ) : /* @__PURE__ */ jsx( BackgroundImageControls, { onChange, style: value, inheritedValue: resolvedInheritedValue, defaultValues, onResetImage: () => { setIsDropDownOpen(false); resetBackground(); }, onRemoveImage: () => setIsDropDownOpen(false), containerRef } ) } ); } export { backgroundPositionToCoords, coordsToBackgroundPosition, BackgroundImagePanel as default }; //# sourceMappingURL=index.js.map