@wordpress/block-editor
Version:
637 lines (636 loc) • 20.1 kB
JavaScript
// 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