/** * Background Image Control Component * WordPress Gutenberg-style background image control with media upload, * focal point picker, and fixed background toggle * * Follows WordPress patterns from: * packages/block-editor/src/hooks/background.js * packages/block-editor/src/components/background-image-control/index.js */ import React, { useState, useRef } from "react"; // @ts-ignore - WordPress i18n package import { __ } from "@wordpress/i18n"; // @ts-ignore - WordPress i18n package import { sprintf } from "@wordpress/i18n"; import { useDispatch, useSelect } from "@wordpress/data"; import { useBlockEditContext, store as blockEditorStore, } from "@wordpress/block-editor"; import { Button, FocalPointPicker, ToggleControl, DropZone, Spinner, VisuallyHidden, Dropdown, __experimentalVStack as VStack, } from "@wordpress/components"; import { reset as resetIcon } from "@wordpress/icons"; import { isBlobURL } from "@wordpress/blob"; import { store as noticesStore } from "@wordpress/notices"; import { focus } from "@wordpress/dom"; import { getFilename } from "@wordpress/url"; import type { BackgroundImageControlProps } from "./types"; interface FocalPoint { x: number; y: number; } interface MediaObject { id?: number; url: string; alt?: string; media_type?: string; type?: string; } // MediaReplaceFlow component - access from global wp object const MediaReplaceFlow = (window as any).wp.blockEditor?.MediaReplaceFlow || (() => null); // Default values matching WordPress BACKGROUND_BLOCK_DEFAULT_VALUES /** * Focuses the toggle button for accessibility after modal/dropdown closes * Uses requestAnimationFrame to ensure DOM updates are complete */ const focusToggleButton = (containerRef: React.RefObject) => { window.requestAnimationFrame(() => { if (containerRef.current) { const [toggleButton] = focus.tabbable.find(containerRef.current); toggleButton?.focus(); } }); }; /** * Background Image Control Component * Provides media upload, focal point picker, and fixed background toggle * following WordPress Gutenberg patterns from the Cover block */ const BackgroundImageControl: React.FC = ({ label = __("Background Image", "sliderberg"), }) => { const { clientId } = useBlockEditContext(); const { updateBlockAttributes } = useDispatch("core/block-editor") as any; const { createErrorNotice } = useDispatch(noticesStore) as any; // State for upload tracking and UI feedback const [isUploading, setIsUploading] = useState(false); const containerRef = useRef(null); // Popover props for consistent positioning const BACKGROUND_POPOVER_PROPS = { placement: "left-start" as const, offset: 36, shift: true, }; const { backgroundImage, focalPoint, isFixed, mediaUpload } = useSelect( (select: any) => { const blockEditor = select(blockEditorStore); const attributes = blockEditor.getBlockAttributes(clientId) as Record< string, any >; const settings = blockEditor.getSettings(); return { backgroundImage: attributes.backgroundImage as MediaObject | null, focalPoint: attributes.focalPoint as FocalPoint, isFixed: attributes.isFixed as boolean, mediaUpload: settings.mediaUpload, }; }, [clientId], ) as { backgroundImage: MediaObject | null; focalPoint: FocalPoint; isFixed: boolean; mediaUpload: any; }; const setAttributes = (newAttributes: Record) => updateBlockAttributes(clientId, newAttributes); /** * Handle upload errors with WordPress-native error notices */ const onUploadError = (message: string) => { createErrorNotice(message, { type: "snackbar" }); setIsUploading(false); }; /** * Handle media selection from media library * Validates file types, handles blob URLs, and sets default position */ const onSelectMedia = (media: MediaObject) => { if (!media || !media.url) { onRemoveImage(); setIsUploading(false); return; } // Check if upload is still in progress (blob URL) if (isBlobURL(media.url)) { setIsUploading(true); return; } // Validate file type - only allow images const mediaType = (media as any).media_type || (media as any).type; if (mediaType && mediaType !== "image") { onUploadError( __("Only images can be used as a background image.", "sliderberg"), ); return; } setAttributes({ backgroundImage: { id: media.id, url: media.url, alt: media.alt || "", }, // Set default position '50% 0' to increase chance focus point is visible focalPoint: focalPoint || { x: 0.5, y: 0 }, }); setIsUploading(false); // Return focus to toggle button after selection focusToggleButton(containerRef); }; /** * Clear/remove background image */ const onRemoveImage = () => { setAttributes({ backgroundImage: null, focalPoint: { x: 0.5, y: 0.5 }, isFixed: false, }); // Return focus to toggle button after removal focusToggleButton(containerRef); }; /** * Handle drag-and-drop file uploads * Restricts to single image file */ const onFilesDrop = (filesList: File[]) => { if (!mediaUpload) { return; } mediaUpload({ allowedTypes: ["image"], filesList, onFileChange([image]: MediaObject[]) { onSelectMedia(image); }, onError: onUploadError, multiple: false, }); }; /** * Update focal point - converts picker coords to CSS background-position */ const onChangeFocalPoint = (newFocalPoint: FocalPoint) => { setAttributes({ focalPoint: newFocalPoint, }); }; /** * Toggle fixed background */ const onToggleFixed = (value: boolean) => { setAttributes({ isFixed: value }); }; const imageLabel = backgroundImage?.alt || getFilename(backgroundImage?.url || "") || label; return (
{isUploading && (
)} {!backgroundImage ? ( <> ( )} /> ) : (
( <>
)} {backgroundImage ? sprintf(__("Background image: %s", "sliderberg"), imageLabel) : __("No background image selected", "sliderberg")}
); }; export default BackgroundImageControl;