UNPKG

@wordpress/block-editor

Version:
285 lines (258 loc) 8.83 kB
/** * WordPress dependencies */ import { useDispatch } from '@wordpress/data'; import { useEffect, useMemo, useRef } from '@wordpress/element'; import { SlotFillProvider } from '@wordpress/components'; import { MediaUploadProvider, store as uploadStore, detectClientSideMediaSupport, } from '@wordpress/upload-media'; /** * Internal dependencies */ import withRegistryProvider from './with-registry-provider'; import useBlockSync from './use-block-sync'; import { store as blockEditorStore } from '../../store'; import { BlockRefsProvider } from './block-refs-provider'; import { unlock } from '../../lock-unlock'; import KeyboardShortcuts from '../keyboard-shortcuts'; import useMediaUploadSettings from './use-media-upload-settings'; import { mediaUploadOnSuccessKey } from '../../store/private-keys'; import { SelectionContext } from './selection-context'; /** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ const noop = () => {}; /** * Flag to track if we've already logged the fallback message. */ let hasLoggedFallback = false; /** * Cached result of whether client-side media processing should be enabled. * This is computed once per session for efficiency and stability. */ let isClientSideMediaEnabledCache = null; /** * Checks if client-side media processing should be enabled. * * Returns true only if: * 1. The client-side media processing flag is enabled * 2. The browser supports WebAssembly, SharedArrayBuffer, cross-origin isolation, and CSP allows blob workers * * The result is cached for the session to ensure stability during React renders. * * @return {boolean} Whether client-side media processing should be enabled. */ function shouldEnableClientSideMediaProcessing() { // Return cached result if available. if ( isClientSideMediaEnabledCache !== null ) { return isClientSideMediaEnabledCache; } // Check if the client-side media processing flag is enabled first. if ( ! window.__clientSideMediaProcessing ) { isClientSideMediaEnabledCache = false; return false; } // Safety check in case the import is unavailable. if ( typeof detectClientSideMediaSupport !== 'function' ) { isClientSideMediaEnabledCache = false; return false; } const detection = detectClientSideMediaSupport(); if ( ! detection || ! detection.supported ) { // Only log once per session to avoid console spam. if ( ! hasLoggedFallback ) { // eslint-disable-next-line no-console console.info( `Client-side media processing unavailable: ${ detection.reason }. Using server-side processing.` ); hasLoggedFallback = true; } isClientSideMediaEnabledCache = false; return false; } isClientSideMediaEnabledCache = true; return true; } /** * Upload a media file when the file upload button is activated * or when adding a file to the editor via drag & drop. * * @param {WPDataRegistry} registry * @param {Object} settings Block editor settings. * @param {Object} $3 Parameters object passed to the function. * @param {Array} $3.allowedTypes Array with the types of media that can be uploaded, if unset all types are allowed. * @param {Object} $3.additionalData Additional data to include in the request. * @param {Array<File>} $3.filesList List of files. * @param {Function} $3.onError Function called when an error happens. * @param {Function} $3.onFileChange Function called each time a file or a temporary representation of the file is available. * @param {Function} $3.onSuccess Function called once a file has completely finished uploading, including thumbnails. * @param {Function} $3.onBatchSuccess Function called once all files in a group have completely finished uploading, including thumbnails. */ function mediaUpload( registry, settings, { allowedTypes, additionalData = {}, filesList, onError = noop, onFileChange, onSuccess, onBatchSuccess, } ) { void registry.dispatch( uploadStore ).addItems( { files: Array.from( filesList ), onChange: onFileChange, onSuccess: ( attachments ) => { settings?.[ mediaUploadOnSuccessKey ]?.( attachments ); onSuccess?.( attachments ); }, onBatchSuccess, onError: ( error ) => onError( typeof error === 'string' ? error : error?.message ?? '' ), additionalData, allowedTypes, } ); } /** * Calls useBlockSync as a child of SelectionContext.Provider so that the * hook can read selection state from the context provided by this tree * rather than from a parent provider (which may not exist for the root). * * @param {Object} props Props forwarded to useBlockSync. */ function BlockSyncEffect( props ) { useBlockSync( props ); return null; } export const ExperimentalBlockEditorProvider = withRegistryProvider( ( props ) => { const { settings: _settings, registry, stripExperimentalSettings = false, } = props; const mediaUploadSettings = useMediaUploadSettings( _settings ); const isClientSideMediaEnabled = shouldEnableClientSideMediaProcessing(); // Nested providers (e.g. from useBlockPreview) inherit settings // where mediaUpload has already been replaced with the // interceptor. Detect this so we skip the replacement and // MediaUploadProvider for them — see the longer comment below. const isMediaUploadIntercepted = !! _settings?.mediaUpload?.__isMediaUploadInterceptor; const settings = useMemo( () => { if ( isClientSideMediaEnabled && _settings?.mediaUpload && ! isMediaUploadIntercepted ) { // Create a new object so that the original props.settings.mediaUpload is not modified. const interceptor = mediaUpload.bind( null, registry, _settings ); interceptor.__isMediaUploadInterceptor = true; return { ..._settings, mediaUpload: interceptor, }; } return _settings; }, [ _settings, registry, isClientSideMediaEnabled, isMediaUploadIntercepted, ] ); const { __experimentalUpdateSettings } = unlock( useDispatch( blockEditorStore ) ); useEffect( () => { __experimentalUpdateSettings( { ...settings, __internalIsInitialized: true, }, { stripExperimentalSettings, reset: true, } ); }, [ settings, stripExperimentalSettings, __experimentalUpdateSettings, ] ); // Store selection and onChangeSelection in refs and expose // stable getters/callers so that the context value is a // complete constant. This prevents re-rendering the entire // block tree (including async-rendered off-screen blocks) // when either value changes. const selectionRef = useRef( props.selection ); selectionRef.current = props.selection; const onChangeSelectionRef = useRef( props.onChangeSelection ?? noop ); onChangeSelectionRef.current = props.onChangeSelection ?? noop; const selectionContextValue = useMemo( () => ( { getSelection: () => selectionRef.current, onChangeSelection: ( ...args ) => onChangeSelectionRef.current( ...args ), } ), [] ); const children = ( <SlotFillProvider passthrough> { ! settings?.isPreviewMode && <KeyboardShortcuts.Register /> } <BlockRefsProvider>{ props.children }</BlockRefsProvider> </SlotFillProvider> ); const content = ( <SelectionContext.Provider value={ selectionContextValue }> <BlockSyncEffect clientId={ props.clientId } value={ props.value } onChange={ props.onChange } onInput={ props.onInput } /> { children } </SelectionContext.Provider> ); // MediaUploadProvider writes the mediaUpload function from // _settings into the shared upload-media store so the store can // hand files off to the server. useMediaUploadSettings extracts // mediaUpload from the original _settings prop — *before* the // interceptor replacement above — so the store receives the // real server-side upload function. // // Only the first (outermost) provider should do this. // Nested providers (e.g. from useBlockPreview in // core/post-template) inherit settings that already contain // the interceptor, so their MediaUploadProvider would // overwrite the store's server-side function with the // interceptor, causing uploads to loop instead of reaching // the server. if ( isClientSideMediaEnabled && ! isMediaUploadIntercepted ) { return ( <MediaUploadProvider settings={ mediaUploadSettings } useSubRegistry={ false } > { content } </MediaUploadProvider> ); } return content; } ); export const BlockEditorProvider = ( props ) => { return ( <ExperimentalBlockEditorProvider { ...props } stripExperimentalSettings> { props.children } </ExperimentalBlockEditorProvider> ); }; export default BlockEditorProvider;