Skip to content
28 changes: 28 additions & 0 deletions docs/reference-guides/data/data-core-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,22 @@ _Returns_

- `Array`: Block list.

### getEditorIntent

Returns the current editor intent. The intent represents the user's editing purpose — directly editing content (`edit`), suggesting changes that the author can apply or reject (`suggest`), or viewing the post in a read-only mode (`view`).

The intent is orthogonal to the `editorMode` preference (visual vs. code).

Storage: the value lives in the preferences store under (`core`, `editorIntent`). The per-app default is registered in `packages/edit-post/src/index.js` and `packages/edit-site/src/index.js`. If no value is set we fall back to `EDITOR_INTENT_EDIT` so callers can rely on a non-null result.

_Parameters_

- _state_ `Object`: Global application state.

_Returns_

- `string`: The current editor intent. One of `edit`, `suggest`, `view`.

### getEditorMode

Returns the current editing mode.
Expand Down Expand Up @@ -1495,6 +1511,18 @@ _Returns_

- `Object`: Action object.

### setEditorIntent

Sets the current editor intent.

The intent represents the user's editing purpose: directly editing content (`edit`), suggesting changes that the author can apply or reject (`suggest`), or viewing the post in a read-only mode (`view`). It is orthogonal to the `editorMode` preference (visual vs. code).

The value persists via the preferences store so the intent survives reloads. Unknown intents are silently rejected (no dispatch, no announcement) to keep typos or stale values from corrupting the preference; valid values are listed in `EDITOR_INTENTS`.

_Parameters_

- _intent_ `'edit'|'suggest'|'view'`: The editor intent to set.

### setIsInserterOpened

Returns an action object used to open/close the inserter.
Expand Down
1 change: 1 addition & 0 deletions packages/edit-post/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function initializeEditor(

dispatch( preferencesStore ).setDefaults( 'core', {
allowRightClickOverrides: true,
editorIntent: 'edit',
editorMode: 'visual',
editorTool: 'edit',
fixedToolbar: false,
Expand Down
1 change: 1 addition & 0 deletions packages/edit-site/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function initializeEditor( id, settings ) {
dispatch( preferencesStore ).setDefaults( 'core', {
allowRightClickOverrides: true,
distractionFree: false,
editorIntent: 'edit',
editorMode: 'visual',
editorTool: 'edit',
fixedToolbar: false,
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features

- Added an `editorIntent` preference (`edit`, `suggest`, `view`) with a matching `setEditorIntent` action and `getEditorIntent` selector. Surfaced as an Edit / Suggest / View menu in the editor options for post types that support notes. The `view` intent puts the block editor into a read-only preview. Keyboard shortcuts follow the Google Docs convention: Ctrl+Alt+Shift+Z (Edit), +X (Suggest), +C (View) on Windows / ⌘⌥⇧Z/X/C on macOS.

### Bug Fixes

- `mediaFinalize` now returns the post-finalize attachment (transformed from the REST response), so the upload-media queue can refresh the in-flight attachment URL. Required for the front-end `srcset` to render on client-side-media uploads that exceeded the big-image threshold.
Expand Down
21 changes: 21 additions & 0 deletions packages/editor/src/components/global-keyboard-shortcuts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import {
EDITOR_INTENT_EDIT,
EDITOR_INTENT_SUGGEST,
EDITOR_INTENT_VIEW,
} from '../../store/constants';

/**
* Handles the keyboard shortcuts for the editor.
Expand All @@ -35,6 +40,7 @@ export default function EditorKeyboardShortcuts() {
setIsListViewOpened,
switchEditorMode,
toggleDistractionFree,
setEditorIntent,
} = useDispatch( editorStore );
const {
isEditedPostDirty,
Expand Down Expand Up @@ -99,6 +105,21 @@ export default function EditorKeyboardShortcuts() {
}
} );

useShortcut( 'core/editor/intent-edit', ( event ) => {
event.preventDefault();
setEditorIntent( EDITOR_INTENT_EDIT );
} );

useShortcut( 'core/editor/intent-suggest', ( event ) => {
event.preventDefault();
setEditorIntent( EDITOR_INTENT_SUGGEST );
} );

useShortcut( 'core/editor/intent-view', ( event ) => {
event.preventDefault();
setEditorIntent( EDITOR_INTENT_VIEW );
} );

useShortcut( 'core/editor/toggle-sidebar', ( event ) => {
// This shortcut has no known clashes, but use preventDefault to prevent any
// obscure shortcuts from triggering.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,36 @@ function EditorKeyboardShortcutsRegister() {
},
} );

registerShortcut( {
name: 'core/editor/intent-edit',
category: 'global',
description: __( 'Switch to Edit mode.' ),
keyCombination: {
modifier: 'secondary',
character: 'z',
},
} );

registerShortcut( {
name: 'core/editor/intent-suggest',
category: 'global',
description: __( 'Switch to Suggest mode.' ),
keyCombination: {
modifier: 'secondary',
character: 'x',
},
} );

registerShortcut( {
name: 'core/editor/intent-view',
category: 'global',
description: __( 'Switch to View mode.' ),
keyCombination: {
modifier: 'secondary',
character: 'c',
},
} );

registerShortcut( {
name: 'core/editor/next-region',
category: 'global',
Expand Down
83 changes: 83 additions & 0 deletions packages/editor/src/components/intent-switcher/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { MenuItemsChoice, MenuGroup } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { displayShortcut } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import {
EDITOR_INTENT_EDIT,
EDITOR_INTENT_SUGGEST,
EDITOR_INTENT_VIEW,
} from '../../store/constants';
import PostTypeSupportCheck from '../post-type-support-check';

/**
* Available editor intent options surfaced in the more-menu mode picker.
*
* Each option is mirrored across three files; keep them in sync when adding
* or renaming an intent:
* - This file: UI label, description, and shortcut hint.
* - `../global-keyboard-shortcuts/register-shortcuts.js`: registers the
* keyboard binding with `@wordpress/keyboard-shortcuts`.
* - `../global-keyboard-shortcuts/index.js`: wires `useShortcut` so the
* binding dispatches `setEditorIntent`.
*
* The `value` field must be one of the `EDITOR_INTENT_*` constants — the
* `setEditorIntent` action validates against `EDITOR_INTENTS` and silently
* ignores unknown values.
*
* @type {Array<{value: string, label: string, info: string, shortcut: string}>}
*/
const INTENTS = [
{
value: EDITOR_INTENT_EDIT,
label: __( 'Edit' ),
info: __( 'Edit content directly.' ),
shortcut: displayShortcut.secondary( 'z' ),
},
{
value: EDITOR_INTENT_SUGGEST,
label: __( 'Suggest' ),
info: __( 'Propose changes the author can apply or reject.' ),
shortcut: displayShortcut.secondary( 'x' ),
},
{
value: EDITOR_INTENT_VIEW,
label: __( 'View' ),
info: __( 'Read-only preview of the content.' ),
shortcut: displayShortcut.secondary( 'c' ),
},
];

/**
* Editor intent switcher. Lets the user pick between direct editing,
* suggesting changes, or viewing in read-only. Only rendered for post
* types that declare the `editor.notes` support.
*/
function IntentSwitcher() {
const intent = useSelect(
( select ) => select( editorStore ).getEditorIntent(),
[]
);
const { setEditorIntent } = useDispatch( editorStore );

return (
<PostTypeSupportCheck supportKeys="editor.notes">
<MenuGroup label={ __( 'Mode' ) }>
<MenuItemsChoice
choices={ INTENTS }
value={ intent }
onSelect={ setEditorIntent }
/>
</MenuGroup>
</PostTypeSupportCheck>
);
}

export default IntentSwitcher;
2 changes: 2 additions & 0 deletions packages/editor/src/components/more-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { VisuallyHidden } from '@wordpress/ui';
* Internal dependencies
*/
import CopyContentMenuItem from './copy-content-menu-item';
import IntentSwitcher from '../intent-switcher';
import ModeSwitcher from '../mode-switcher';
import ToolsMoreMenuGroup from './tools-more-menu-group';
import ViewMoreMenuGroup from './view-more-menu-group';
Expand Down Expand Up @@ -106,6 +107,7 @@ export default function MoreMenu( { disabled = false } ) {
/>
<ViewMoreMenuGroup.Slot fillProps={ { onClose } } />
</MenuGroup>
<IntentSwitcher />
<ModeSwitcher />
<ActionItem.Slot
name="core/plugin-more-menu"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
focusMode,
hasFixedToolbar,
isDistractionFree,
isViewIntent,
keepCaretInsideBlock,
hasUploadPermissions,
hiddenBlockTypes,
Expand Down Expand Up @@ -203,6 +204,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
get( 'core', 'fixedToolbar' ) || ! isLargeViewport,
hiddenBlockTypes: get( 'core', 'hiddenBlockTypes' ),
isDistractionFree: get( 'core', 'distractionFree' ),
isViewIntent: get( 'core', 'editorIntent' ) === 'view',
keepCaretInsideBlock: get( 'core', 'keepCaretInsideBlock' ),
hasUploadPermissions:
canUser( 'create', {
Expand Down Expand Up @@ -412,13 +414,14 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
[ isNavigationOverlayContextKey ]: isNavigationOverlayContext,
};

if ( isRevisionsMode ) {
if ( isRevisionsMode || isViewIntent ) {
blockEditorSettings.isPreviewMode = true;
}

return blockEditorSettings;
}, [
isRevisionsMode,
isViewIntent,
allowedBlockTypes,
allowRightClickOverrides,
focusMode,
Expand Down
44 changes: 44 additions & 0 deletions packages/editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import {
getNotificationArgumentsForTrashFail,
} from './utils/notice-builder';
import { unlock } from '../lock-unlock';
import {
EDITOR_INTENTS,
EDITOR_INTENT_EDIT,
EDITOR_INTENT_SUGGEST,
EDITOR_INTENT_VIEW,
} from './constants';
/**
* Returns an action generator used in signalling that editor has initialized with
* the specified post object and editor settings.
Expand Down Expand Up @@ -1064,6 +1070,44 @@ export const switchEditorMode =
}
};

/**
* Sets the current editor intent.
*
* The intent represents the user's editing purpose: directly editing content
* (`edit`), suggesting changes that the author can apply or reject
* (`suggest`), or viewing the post in a read-only mode (`view`). It is
* orthogonal to the `editorMode` preference (visual vs. code).
*
* The value persists via the preferences store so the intent survives
* reloads. Unknown intents are silently rejected (no dispatch, no
* announcement) to keep typos or stale values from corrupting the
* preference; valid values are listed in `EDITOR_INTENTS`.
*
* @param {'edit'|'suggest'|'view'} intent The editor intent to set.
*/
export const setEditorIntent =
( intent ) =>
( { registry } ) => {
// Reject unknown intents instead of throwing so a typo from a
// bookmarklet, browser extension, or third-party plugin can't poison
// the persisted preference.
if ( ! EDITOR_INTENTS.includes( intent ) ) {
return;
}

registry
.dispatch( preferencesStore )
.set( 'core', 'editorIntent', intent );

if ( intent === EDITOR_INTENT_EDIT ) {
speak( __( 'Edit mode selected' ), 'assertive' );
} else if ( intent === EDITOR_INTENT_SUGGEST ) {
speak( __( 'Suggest mode selected' ), 'assertive' );
} else if ( intent === EDITOR_INTENT_VIEW ) {
speak( __( 'View mode selected' ), 'assertive' );
}
};

/**
* Returns an action object used in signalling that the user opened the publish
* sidebar.
Expand Down
35 changes: 35 additions & 0 deletions packages/editor/src/store/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,38 @@ export const DESIGN_POST_TYPES = [
PATTERN_POST_TYPE,
NAVIGATION_POST_TYPE,
];

/**
* Editor intent values. The intent represents the user's current editing
* purpose (edit the post directly, suggest changes, or view in read-only).
*
* Orthogonal to the `editorMode` preference (visual vs. code): a user can
* be in `suggest` intent in either visual or code mode.
*
* Storage and defaults:
* - Persisted via `@wordpress/preferences` under (`core`, `editorIntent`),
* so the intent survives reloads.
* - The per-app default is registered in `packages/edit-post/src/index.js`
* and `packages/edit-site/src/index.js` (both default to `'edit'`).
* - `getEditorIntent` falls back to `EDITOR_INTENT_EDIT` when no value
* is set, so consumers can rely on a non-null result.
*
* Suggest Mode context:
* Phase 1 of the Suggest Mode feature only wires the intent state and the
* UI surface (menu + keyboard shortcuts). Subsequent phases use the
* `suggest` intent to capture edits as in-memory overlays, render them as
* suggestions, and let other users apply or reject them. Adding a new
* intent here also requires updates to:
* - packages/editor/src/components/intent-switcher/index.js (UI choices)
* - packages/editor/src/components/global-keyboard-shortcuts/* (shortcut
* registration and dispatch)
*/
export const EDITOR_INTENT_EDIT = 'edit';
export const EDITOR_INTENT_SUGGEST = 'suggest';
export const EDITOR_INTENT_VIEW = 'view';
export const EDITOR_INTENTS = [
EDITOR_INTENT_EDIT,
EDITOR_INTENT_SUGGEST,
EDITOR_INTENT_VIEW,
] as const;
export type EditorIntent = ( typeof EDITOR_INTENTS )[ number ];
Loading
Loading