Skip to content

Commit 1d24671

Browse files
authored
Merge branch 'trunk' into fix/rich-text-get-text-content
2 parents d8aaa5b + 150cd6d commit 1d24671

29 files changed

Lines changed: 802 additions & 234 deletions

File tree

‎lib/experimental/experiments/load.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function gutenberg_initialize_experiments_settings() {
4848
array(
4949
'id' => 'gutenberg-media-editor',
5050
'label' => __( 'Media Editor (Route)', 'gutenberg' ),
51-
'description' => __( 'Enables a dedicated route-based media editor screen for editing media items (metadata and content).', 'gutenberg' ),
51+
'description' => __( 'Replaces the core Edit Media screen with a dedicated route-based media editor for editing media items (metadata and content).', 'gutenberg' ),
5252
),
5353
array(
5454
'id' => 'gutenberg-dataviews-media-modal',

‎lib/experimental/media-editor/load.php‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,99 @@ function gutenberg_media_editor_wp_admin_set_title() {
3939

4040
$title = __( 'Edit media', 'gutenberg' );
4141
}
42+
43+
/**
44+
* Builds the media editor URL for an attachment.
45+
*
46+
* The router reads its internal path from the `p` query argument, so the
47+
* attachment id travels as `p=/media-editor/<id>` rather than as a query
48+
* argument of its own.
49+
*
50+
* @param int $post_id Attachment ID.
51+
* @param string $separator Argument separator. `&amp;` when the URL is
52+
* destined for HTML output, `&` otherwise.
53+
* @return string The media editor URL.
54+
*/
55+
function gutenberg_media_editor_get_url( $post_id, $separator = '&' ) {
56+
return admin_url(
57+
'admin.php?page=media-editor-wp-admin' . $separator . 'p=' . rawurlencode( '/media-editor/' . (int) $post_id )
58+
);
59+
}
60+
61+
/**
62+
* Points every "edit this attachment" link at the media editor.
63+
*
64+
* Filtering here rather than redirecting means the Media Library list table,
65+
* the media modal's "Edit more details" and "Edit Image" links, and the admin
66+
* bar all navigate straight to the media editor with no redirect hop.
67+
*
68+
* @param string|null $link The edit link, or null when the user cannot edit the post.
69+
* @param int $post_id Post ID.
70+
* @param string $context The link context. `display` expects an HTML-escaped separator.
71+
* @return string|null The filtered edit link.
72+
*/
73+
function gutenberg_media_editor_filter_edit_post_link( $link, $post_id, $context ) {
74+
// A null link means the user lacks the capability. Leave that alone.
75+
if ( ! $link || 'attachment' !== get_post_type( $post_id ) ) {
76+
return $link;
77+
}
78+
79+
// `edit_post` is enough for the classic screen, but the media editor page
80+
// requires `upload_files`. A contributor can own an attachment and edit it
81+
// without being able to upload, so pointing them at the media editor would
82+
// send them to a screen they cannot open.
83+
if ( ! current_user_can( 'upload_files' ) ) {
84+
return $link;
85+
}
86+
87+
return gutenberg_media_editor_get_url( $post_id, 'display' === $context ? '&amp;' : '&' );
88+
}
89+
90+
add_filter( 'get_edit_post_link', 'gutenberg_media_editor_filter_edit_post_link', 10, 3 );
91+
92+
/**
93+
* Redirects the classic Edit Media screen to the media editor.
94+
*
95+
* The `get_edit_post_link` filter above covers links rendered by WordPress,
96+
* but bookmarks, hand-typed URLs, and hard-coded links still reach post.php.
97+
* This catches those. It deliberately bails on the failure paths so that
98+
* WordPress keeps rendering its own error messages instead of sending people
99+
* to a screen that would only fail again.
100+
*/
101+
function gutenberg_media_editor_redirect_classic_screen() {
102+
// GET renders the form. POST is the editattachment/editpost save handler,
103+
// which must run untouched.
104+
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) ) {
105+
return;
106+
}
107+
108+
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reading the same unauthenticated query args post.php itself reads to decide what to render.
109+
if ( ! isset( $_GET['action'] ) || 'edit' !== $_GET['action'] ) {
110+
return;
111+
}
112+
113+
// Guard against `?post[]=1`, where an array casts to int 1 without warning.
114+
$post_id = isset( $_GET['post'] ) && is_scalar( $_GET['post'] ) ? (int) $_GET['post'] : 0;
115+
// phpcs:enable WordPress.Security.NonceVerification.Recommended
116+
117+
if ( ! $post_id || 'attachment' !== get_post_type( $post_id ) ) {
118+
return;
119+
}
120+
121+
// Let post.php own these: it already renders the appropriate wp_die().
122+
if ( ! current_user_can( 'edit_post', $post_id ) || 'trash' === get_post_status( $post_id ) ) {
123+
return;
124+
}
125+
126+
// The media editor page requires `upload_files`, which `edit_post` does not
127+
// imply. Without this, a contributor who owns an attachment would be
128+
// redirected off a screen that works onto one that refuses them.
129+
if ( ! current_user_can( 'upload_files' ) ) {
130+
return;
131+
}
132+
133+
wp_safe_redirect( gutenberg_media_editor_get_url( $post_id ) );
134+
exit;
135+
}
136+
137+
add_action( 'load-post.php', 'gutenberg_media_editor_redirect_classic_screen' );

‎packages/block-editor/src/components/block-list/block.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,11 @@ function BlockListBlockProvider( props ) {
771771
blockVisibility,
772772
deviceType,
773773
viewportSettings,
774+
supportsSplitting: hasBlockSupport(
775+
blockName,
776+
'splitting',
777+
false
778+
),
774779
};
775780
},
776781
[ clientId, rootClientId, ghostBlock, ghostBlockWithoutAttributes ]
@@ -854,6 +859,7 @@ function BlockListBlockProvider( props ) {
854859
blockVisibility,
855860
deviceType,
856861
viewportSettings,
862+
supportsSplitting,
857863
} = selectedProps;
858864

859865
const privateContext = {
@@ -896,6 +902,7 @@ function BlockListBlockProvider( props ) {
896902
blockVisibility,
897903
deviceType,
898904
viewportSettings,
905+
supportsSplitting,
899906
};
900907

901908
if (

‎packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export function useFocusFirstElement( { clientId, initialPosition } ) {
5050
return;
5151
}
5252

53+
if ( initialPosition === true ) {
54+
ref.current.focus();
55+
return;
56+
}
57+
5358
// Find all tabbables within node.
5459
const textInputs = focus.tabbable
5560
.find( ref.current )

‎packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export function useEventHandlers( { clientId, isSelected } ) {
2828
getBlock,
2929
} = unlock( useSelect( blockEditorStore ) );
3030
const {
31-
insertAfterBlock,
3231
removeBlock,
3332
resetZoomLevel,
3433
startDraggingBlocks,
@@ -66,13 +65,13 @@ export function useEventHandlers( { clientId, isSelected } ) {
6665
return;
6766
}
6867

69-
event.preventDefault();
70-
71-
if ( keyCode === ENTER && isZoomOut() ) {
72-
resetZoomLevel();
73-
} else if ( keyCode === ENTER ) {
74-
insertAfterBlock( clientId );
68+
if ( keyCode === ENTER ) {
69+
if ( isZoomOut() ) {
70+
event.preventDefault();
71+
resetZoomLevel();
72+
}
7573
} else {
74+
event.preventDefault();
7675
removeBlock( clientId );
7776
}
7877
}
@@ -327,7 +326,6 @@ export function useEventHandlers( { clientId, isSelected } ) {
327326
getBlock,
328327
isReusableBlock,
329328
isTemplatePart,
330-
insertAfterBlock,
331329
removeBlock,
332330
isZoomOut,
333331
resetZoomLevel,

‎packages/block-editor/src/components/block-popover/inbetween.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,18 @@ function BlockPopoverInbetween( {
101101
} else if ( isVertical ) {
102102
// vertical
103103
top = previousRect ? previousRect.bottom : nextRect.top;
104-
width = previousRect ? previousRect.width : nextRect.width;
104+
width =
105+
nextRect && previousRect
106+
? ( previousRect.width + nextRect.width ) / 2
107+
: ( previousRect || nextRect ).width;
105108
height =
106109
nextRect && previousRect
107110
? nextRect.top - previousRect.bottom
108111
: 0;
109-
left = previousRect ? previousRect.left : nextRect.left;
112+
left =
113+
nextRect && previousRect
114+
? ( previousRect.left + nextRect.left ) / 2
115+
: ( previousRect || nextRect ).left;
110116
} else {
111117
top = previousRect ? previousRect.top : nextRect.top;
112118
height = previousRect

‎packages/block-editor/src/components/rich-text/event-listeners/enter.js‎

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,62 @@ const { subscribeOwnedListener, ownsSelection } = unlock( richTextPrivateApis );
1111
const { subscribeDelegatedListener } = unlock( composePrivateApis );
1212

1313
export default ( props ) => ( element ) => {
14-
function onKeyDownDeprecated( event ) {
14+
function onKeyDown( event ) {
1515
if ( event.keyCode !== ENTER ) {
1616
return;
1717
}
1818

19-
const { onReplace, onSplit } = props.current;
19+
const {
20+
onReplace,
21+
onSplit,
22+
supportsSplitting,
23+
disableLineBreaks,
24+
onChange,
25+
value,
26+
onSplitAtDoubleLineEnd,
27+
registry,
28+
onSplitAtEnd,
29+
} = props.current;
30+
const { text, start, end } = value;
2031

21-
if ( onReplace && onSplit ) {
32+
if ( event.shiftKey ) {
33+
if ( ! disableLineBreaks ) {
34+
event.preventDefault();
35+
onChange( insert( value, '\n' ) );
36+
}
37+
} else if ( onSplitAtEnd && start === end && end === text.length ) {
38+
event.preventDefault();
39+
onSplitAtEnd();
40+
} else if ( onReplace && onSplit ) {
2241
event.__deprecatedOnSplit = true;
42+
} else if (
43+
! supportsSplitting &&
44+
! disableLineBreaks &&
45+
! event.defaultPrevented
46+
) {
47+
event.preventDefault();
48+
if (
49+
// For some blocks it's desirable to split at the end of the
50+
// block when there are two line breaks at the end of the
51+
// block, so triple Enter exits the block.
52+
onSplitAtDoubleLineEnd &&
53+
start === end &&
54+
end === text.length &&
55+
text.slice( -2 ) === '\n\n'
56+
) {
57+
registry.batch( () => {
58+
const _value = { ...value };
59+
_value.start = _value.end - 2;
60+
onChange( remove( _value ) );
61+
onSplitAtDoubleLineEnd();
62+
} );
63+
} else {
64+
onChange( insert( value, '\n' ) );
65+
}
2366
}
2467
}
2568

26-
function onKeyDown( event ) {
69+
function onDefaultKeyDown( event ) {
2770
if ( event.defaultPrevented ) {
2871
return;
2972
}
@@ -39,64 +82,30 @@ export default ( props ) => ( element ) => {
3982
return;
4083
}
4184

42-
const {
43-
value,
44-
onChange,
45-
disableLineBreaks,
46-
onSplitAtEnd,
47-
onSplitAtDoubleLineEnd,
48-
registry,
49-
} = props.current;
50-
85+
// On ENTER, we ALWAYS want to prevent the default browser behaviour
86+
// at this last interception point.
5187
event.preventDefault();
52-
53-
const { text, start, end } = value;
54-
55-
if ( event.shiftKey ) {
56-
if ( ! disableLineBreaks ) {
57-
onChange( insert( value, '\n' ) );
58-
}
59-
} else if ( onSplitAtEnd && start === end && end === text.length ) {
60-
onSplitAtEnd();
61-
} else if (
62-
// For some blocks it's desirable to split at the end of the
63-
// block when there are two line breaks at the end of the
64-
// block, so triple Enter exits the block.
65-
onSplitAtDoubleLineEnd &&
66-
start === end &&
67-
end === text.length &&
68-
text.slice( -2 ) === '\n\n'
69-
) {
70-
registry.batch( () => {
71-
const _value = { ...value };
72-
_value.start = _value.end - 2;
73-
onChange( remove( _value ) );
74-
onSplitAtDoubleLineEnd();
75-
} );
76-
} else if ( ! disableLineBreaks ) {
77-
onChange( insert( value, '\n' ) );
78-
}
7988
}
8089

8190
const { defaultView } = element.ownerDocument;
8291

8392
// Attach the listener to the window so parent elements have the chance to
8493
// prevent the default behavior.
85-
const unsubscribeKeyDown = subscribeDelegatedListener(
94+
const unsubscribeDefaultKeyDown = subscribeDelegatedListener(
8695
defaultView,
8796
'keydown',
88-
onKeyDown
97+
onDefaultKeyDown
8998
);
9099
// Capture phase so this runs before ancestor (writing flow) bubble
91100
// handlers, matching the timing of the previous raw element listener.
92-
const unsubscribeKeyDownDeprecated = subscribeOwnedListener(
101+
const unsubscribeKeyDown = subscribeOwnedListener(
93102
element,
94103
'keydown',
95-
onKeyDownDeprecated,
104+
onKeyDown,
96105
true
97106
);
98107
return () => {
108+
unsubscribeDefaultKeyDown();
99109
unsubscribeKeyDown();
100-
unsubscribeKeyDownDeprecated();
101110
};
102111
};

‎packages/block-editor/src/components/rich-text/index.js‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { Content, valueToHTMLString } from './content';
3333
import { withDeprecations } from './with-deprecations';
3434
import BlockContext from '../block-context';
3535
import { unlock } from '../../lock-unlock';
36+
import { PrivateBlockContext } from '../block-list/private-block-context';
3637

3738
// `RichTextShortcut` and `RichTextInputEvent` now live in
3839
// `@wordpress/rich-text` so they share the shortcut and input-event contexts
@@ -48,7 +49,7 @@ const {
4849

4950
const instanceIdKey = Symbol( 'instanceId' );
5051

51-
export function RichTextWrapper(
52+
function RichTextWrapper(
5253
{
5354
children,
5455
tagName = 'div',
@@ -87,6 +88,7 @@ export function RichTextWrapper(
8788
} );
8889
}
8990

91+
const { supportsSplitting } = useContext( PrivateBlockContext );
9092
const instanceId = useInstanceId( RichTextWrapper );
9193
const anchorRef = useRef();
9294
const [ anchorElement, setAnchorElement ] = useState( null );
@@ -511,6 +513,7 @@ export function RichTextWrapper(
511513
onSplitAtDoubleLineEnd,
512514
keyboardShortcuts,
513515
inputEvents,
516+
supportsSplitting,
514517
} ),
515518
anchorRef,
516519
setAnchorElement,
@@ -529,11 +532,13 @@ export function RichTextWrapper(
529532
);
530533
}
531534

535+
const ForwardedRichTextWrapper = forwardRef( RichTextWrapper );
536+
537+
export { ForwardedRichTextWrapper as RichTextWrapper };
538+
532539
// This is the private API for the RichText component.
533540
// It allows access to all props, not just the public ones.
534-
export const PrivateRichText = withDeprecations(
535-
forwardRef( RichTextWrapper )
536-
);
541+
export const PrivateRichText = withDeprecations( ForwardedRichTextWrapper );
537542

538543
PrivateRichText.Content = Content;
539544
PrivateRichText.isEmpty = ( value ) => {

0 commit comments

Comments
 (0)