@wordpress/block-editor
Version:
285 lines (258 loc) • 8.83 kB
JavaScript
/**
* 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;