Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/media-editor/src/image-editor/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ The session also holds named edits other than crop: `session.edits`, set with `s

`session.reset()` puts every slice back to its starting state in one undo entry: default geometry and crop options, no edits, same image. The Reset button calls it; it does not compose per-slice resets itself.

Panels drive the editor through the session itself: `useMediaEditor()` gives them the cropper controller, crop options, edits, the source image, reset and history, so a plugin can offer its own crop tools or load new pixels and the canvas follows. What stays with Core is everything outside the session: the canvas component, the modal shell, Save and Cancel, keyboard shortcuts, notices, persistence, and the undo stacks themselves. `useMediaEditorEdit( name, defaultValue )` is a shortcut for the common case of a panel that owns one value: it returns `{ value, setValue, beginGesture, endGesture }`, applies the panel's default, and removes the edit when the value returns to it so the session reads as clean.

### Viewport

Viewport pan/zoom lets the user inspect or follow the crop area without changing the crop output. It is not part of export and should not create undo entries.
Expand Down
2 changes: 2 additions & 0 deletions packages/media-editor/src/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export {
MediaEditorStateProvider,
useMediaEditor,
} from './media-editor-state-provider';
export { useMediaEditorEdit } from './use-media-editor-edit';
export type { MediaEditorEdit } from './use-media-editor-edit';
export {
mediaEditorReducer,
areMediaEditorStatesEqual,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { act, renderHook } from '@testing-library/react';
import { MediaEditorStateProvider, useMediaEditor } from '..';
import { useMediaEditorEdit } from '../use-media-editor-edit';

const NAME = 'test/vignette';
const DEFAULT = { amount: 0 };

function setupHook() {
return renderHook(
() => ( {
edit: useMediaEditorEdit( NAME, DEFAULT ),
session: useMediaEditor(),
} ),
{ wrapper: MediaEditorStateProvider }
);
}

describe( 'useMediaEditorEdit', () => {
it( 'reports the default while the edit is not set', () => {
const { result } = setupHook();

expect( result.current.edit.value ).toEqual( DEFAULT );
expect( result.current.session.edits ).toEqual( {} );
} );

it( 'records the value in the session', () => {
const { result } = setupHook();

act( () => result.current.edit.setValue( { amount: 0.5 } ) );

expect( result.current.edit.value ).toEqual( { amount: 0.5 } );
expect( result.current.session.edits ).toEqual( {
[ NAME ]: { amount: 0.5 },
} );
expect( result.current.session.isDirty ).toBe( true );
} );

it( 'removes the edit when set back to a value equal to the default', () => {
const { result } = setupHook();

act( () => result.current.edit.setValue( { amount: 0.5 } ) );
act( () => result.current.edit.setValue( { amount: 0 } ) );

expect( result.current.edit.value ).toEqual( DEFAULT );
expect( result.current.session.edits ).toEqual( {} );
expect( result.current.session.isDirty ).toBe( false );
} );

it( 'groups the changes made during a gesture into one undo entry', () => {
const { result } = setupHook();

act( () => result.current.edit.beginGesture() );
act( () => result.current.edit.setValue( { amount: 0.2 } ) );
act( () => result.current.edit.setValue( { amount: 0.4 } ) );
act( () => result.current.edit.endGesture() );

expect( result.current.edit.value ).toEqual( { amount: 0.4 } );

act( () => result.current.session.undo() );

expect( result.current.edit.value ).toEqual( DEFAULT );
expect( result.current.session.hasUndo ).toBe( false );
} );
} );
66 changes: 66 additions & 0 deletions packages/media-editor/src/state/use-media-editor-edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useMemo } from '@wordpress/element';
import fastDeepEqual from 'fast-deep-equal/es6/index.js';
import { useMediaEditor } from './media-editor-state-provider';
import type { EditValue } from './types';

/**
* One named edit, with its default applied. A shortcut over
* `useMediaEditor()`, not a fence: a panel that also drives the
* cropper or loads new pixels uses the session directly.
*/
export interface MediaEditorEdit< V extends EditValue > {
/** The current value, or the default when the edit is not set. */
value: V;
/**
* Set the value through the session history. A value equal in
* content to the default removes the edit instead, so the session
* reads as clean and no undo entry is spent storing a default.
*/
setValue: ( value: V ) => void;
/** Open a gesture: changes until `endGesture` make one undo entry. */
beginGesture: () => void;
/** Close the gesture and commit it. */
endGesture: () => void;
}

/**
* Read and write one named edit in the media editor session.
*
* Panels drive the editor through the session (`useMediaEditor()`):
* the cropper, crop options, edits, the source image, reset and
* history are all theirs to call. This hook covers the common case of
* a panel that owns one value: it applies the panel's default and
* keeps the session clean when the value returns to it.
*
* The default lives here, with the panel, not in the session: the
* session only knows an edit that is set. Define the default once,
* outside the component, so `setValue` keeps a stable identity.
*
* @param name Edit name, for example `my-plugin/vignette`.
* @param defaultValue Value to report, and treat as "no edit", when
* the edit is not set.
* @return The edit's value and the calls to change it.
*/
export function useMediaEditorEdit< V extends EditValue >(
name: string,
defaultValue: V
): MediaEditorEdit< V > {
const { edits, setEdit, beginGesture, endGesture } = useMediaEditor();
// The slice holds any `EditValue`; the caller's default fixes the type.
const value = ( edits[ name ] as V | undefined ) ?? defaultValue;

const setValue = useCallback(
( next: V ) => {
setEdit(
name,
fastDeepEqual( next, defaultValue ) ? undefined : next
);
},
[ name, defaultValue, setEdit ]
);

return useMemo(
() => ( { value, setValue, beginGesture, endGesture } ),
[ value, setValue, beginGesture, endGesture ]
);
}
Loading