@wordpress/block-editor
Version:
566 lines (554 loc) • 21.3 kB
JavaScript
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
;