@wordpress/block-library 9.26.0 → 9.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/button/edit.js +1 -1
  3. package/build/button/edit.js.map +1 -1
  4. package/build/cover/edit/block-controls.js +4 -2
  5. package/build/cover/edit/block-controls.js.map +1 -1
  6. package/build/cover/edit/index.js +6 -3
  7. package/build/cover/edit/index.js.map +1 -1
  8. package/build/cover/edit/inspector-controls.js +11 -4
  9. package/build/cover/edit/inspector-controls.js.map +1 -1
  10. package/build/cover/edit/poster-image.js +81 -0
  11. package/build/cover/edit/poster-image.js.map +1 -0
  12. package/build/cover/index.js +6 -0
  13. package/build/cover/index.js.map +1 -1
  14. package/build/cover/save.js +3 -1
  15. package/build/cover/save.js.map +1 -1
  16. package/build/details/index.js +1 -1
  17. package/build/details/index.js.map +1 -1
  18. package/build/gallery/constants.js +2 -1
  19. package/build/gallery/constants.js.map +1 -1
  20. package/build/gallery/edit.js +93 -15
  21. package/build/gallery/edit.js.map +1 -1
  22. package/build/image/edit.js +6 -0
  23. package/build/image/edit.js.map +1 -1
  24. package/build/list/index.js +0 -1
  25. package/build/list/index.js.map +1 -1
  26. package/build/media-text/edit.js +2 -2
  27. package/build/media-text/edit.js.map +1 -1
  28. package/build/more/edit.native.js +17 -32
  29. package/build/more/edit.native.js.map +1 -1
  30. package/build/navigation-link/edit.js +49 -16
  31. package/build/navigation-link/edit.js.map +1 -1
  32. package/build/navigation-link/update-attributes.js +112 -14
  33. package/build/navigation-link/update-attributes.js.map +1 -1
  34. package/build/navigation-submenu/edit.js +19 -2
  35. package/build/navigation-submenu/edit.js.map +1 -1
  36. package/build/post-author/edit.js +78 -35
  37. package/build/post-author/edit.js.map +1 -1
  38. package/build/post-comments-form/form.js +1 -1
  39. package/build/post-comments-form/form.js.map +1 -1
  40. package/build/post-content/edit.js +78 -16
  41. package/build/post-content/edit.js.map +1 -1
  42. package/build/post-content/index.js +6 -0
  43. package/build/post-content/index.js.map +1 -1
  44. package/build/post-featured-image/edit.js +2 -1
  45. package/build/post-featured-image/edit.js.map +1 -1
  46. package/build/search/edit.js +1 -1
  47. package/build/search/edit.js.map +1 -1
  48. package/build/separator/edit.js +5 -30
  49. package/build/separator/edit.js.map +1 -1
  50. package/build/site-logo/edit.js +16 -5
  51. package/build/site-logo/edit.js.map +1 -1
  52. package/build/site-tagline/index.js +1 -1
  53. package/build/video/edit.js +2 -5
  54. package/build/video/edit.js.map +1 -1
  55. package/build/video/poster-image.js +25 -25
  56. package/build/video/poster-image.js.map +1 -1
  57. package/build/video/tracks-editor.js +95 -104
  58. package/build/video/tracks-editor.js.map +1 -1
  59. package/build/video/tracks.js +6 -2
  60. package/build/video/tracks.js.map +1 -1
  61. package/build-module/button/edit.js +1 -1
  62. package/build-module/button/edit.js.map +1 -1
  63. package/build-module/cover/edit/block-controls.js +4 -2
  64. package/build-module/cover/edit/block-controls.js.map +1 -1
  65. package/build-module/cover/edit/index.js +6 -3
  66. package/build-module/cover/edit/index.js.map +1 -1
  67. package/build-module/cover/edit/inspector-controls.js +10 -4
  68. package/build-module/cover/edit/inspector-controls.js.map +1 -1
  69. package/build-module/cover/edit/poster-image.js +74 -0
  70. package/build-module/cover/edit/poster-image.js.map +1 -0
  71. package/build-module/cover/index.js +6 -0
  72. package/build-module/cover/index.js.map +1 -1
  73. package/build-module/cover/save.js +3 -1
  74. package/build-module/cover/save.js.map +1 -1
  75. package/build-module/details/index.js +1 -1
  76. package/build-module/details/index.js.map +1 -1
  77. package/build-module/gallery/constants.js +1 -0
  78. package/build-module/gallery/constants.js.map +1 -1
  79. package/build-module/gallery/edit.js +95 -17
  80. package/build-module/gallery/edit.js.map +1 -1
  81. package/build-module/image/edit.js +6 -0
  82. package/build-module/image/edit.js.map +1 -1
  83. package/build-module/list/index.js +0 -1
  84. package/build-module/list/index.js.map +1 -1
  85. package/build-module/media-text/edit.js +2 -2
  86. package/build-module/media-text/edit.js.map +1 -1
  87. package/build-module/more/edit.native.js +16 -30
  88. package/build-module/more/edit.native.js.map +1 -1
  89. package/build-module/navigation-link/edit.js +50 -17
  90. package/build-module/navigation-link/edit.js.map +1 -1
  91. package/build-module/navigation-link/update-attributes.js +113 -15
  92. package/build-module/navigation-link/update-attributes.js.map +1 -1
  93. package/build-module/navigation-submenu/edit.js +20 -3
  94. package/build-module/navigation-submenu/edit.js.map +1 -1
  95. package/build-module/post-author/edit.js +78 -35
  96. package/build-module/post-author/edit.js.map +1 -1
  97. package/build-module/post-comments-form/form.js +1 -1
  98. package/build-module/post-comments-form/form.js.map +1 -1
  99. package/build-module/post-content/edit.js +80 -18
  100. package/build-module/post-content/edit.js.map +1 -1
  101. package/build-module/post-content/index.js +6 -0
  102. package/build-module/post-content/index.js.map +1 -1
  103. package/build-module/post-featured-image/edit.js +2 -1
  104. package/build-module/post-featured-image/edit.js.map +1 -1
  105. package/build-module/search/edit.js +1 -1
  106. package/build-module/search/edit.js.map +1 -1
  107. package/build-module/separator/edit.js +6 -31
  108. package/build-module/separator/edit.js.map +1 -1
  109. package/build-module/site-logo/edit.js +17 -6
  110. package/build-module/site-logo/edit.js.map +1 -1
  111. package/build-module/site-tagline/index.js +1 -1
  112. package/build-module/video/edit.js +2 -5
  113. package/build-module/video/edit.js.map +1 -1
  114. package/build-module/video/poster-image.js +26 -26
  115. package/build-module/video/poster-image.js.map +1 -1
  116. package/build-module/video/tracks-editor.js +96 -105
  117. package/build-module/video/tracks-editor.js.map +1 -1
  118. package/build-module/video/tracks.js +6 -2
  119. package/build-module/video/tracks.js.map +1 -1
  120. package/build-style/archives/editor-rtl.css +0 -4
  121. package/build-style/archives/editor.css +0 -4
  122. package/build-style/editor-rtl.css +0 -21
  123. package/build-style/editor.css +0 -21
  124. package/build-style/file/style-rtl.css +1 -1
  125. package/build-style/file/style.css +1 -1
  126. package/build-style/gallery/editor-rtl.css +0 -13
  127. package/build-style/gallery/editor.css +0 -13
  128. package/build-style/navigation/style-rtl.css +2 -0
  129. package/build-style/navigation/style.css +2 -0
  130. package/build-style/style-rtl.css +3 -1
  131. package/build-style/style.css +3 -1
  132. package/build-style/video/editor-rtl.css +0 -4
  133. package/build-style/video/editor.css +0 -4
  134. package/package.json +35 -35
  135. package/src/archives/editor.scss +0 -4
  136. package/src/button/edit.js +1 -1
  137. package/src/comments-pagination/index.php +7 -2
  138. package/src/cover/block.json +6 -0
  139. package/src/cover/edit/block-controls.js +22 -17
  140. package/src/cover/edit/index.js +4 -1
  141. package/src/cover/edit/inspector-controls.js +12 -3
  142. package/src/cover/edit/poster-image.js +91 -0
  143. package/src/cover/save.js +2 -0
  144. package/src/details/index.js +1 -1
  145. package/src/file/style.scss +1 -1
  146. package/src/gallery/constants.js +1 -0
  147. package/src/gallery/edit.js +182 -68
  148. package/src/gallery/editor.scss +0 -17
  149. package/src/image/edit.js +12 -0
  150. package/src/list/block.json +0 -1
  151. package/src/media-text/edit.js +1 -1
  152. package/src/more/edit.native.js +19 -33
  153. package/src/navigation/style.scss +2 -0
  154. package/src/navigation-link/edit.js +46 -17
  155. package/src/navigation-link/test/edit.js +738 -6
  156. package/src/navigation-link/update-attributes.js +125 -12
  157. package/src/navigation-submenu/edit.js +21 -1
  158. package/src/post-author/edit.js +91 -40
  159. package/src/post-comments-form/form.js +1 -1
  160. package/src/post-content/block.json +6 -0
  161. package/src/post-content/edit.js +71 -19
  162. package/src/post-content/index.php +11 -4
  163. package/src/post-featured-image/edit.js +1 -0
  164. package/src/post-featured-image/index.php +3 -2
  165. package/src/rss/index.php +2 -1
  166. package/src/search/edit.js +1 -1
  167. package/src/separator/edit.js +8 -43
  168. package/src/site-logo/edit.js +22 -10
  169. package/src/site-tagline/block.json +1 -1
  170. package/src/video/edit.js +1 -4
  171. package/src/video/editor.scss +0 -6
  172. package/src/video/poster-image.js +29 -24
  173. package/src/video/tracks-editor.js +93 -103
  174. package/src/video/tracks.js +2 -1
@@ -2,7 +2,89 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { escapeHTML } from '@wordpress/escape-html';
5
- import { safeDecodeURI } from '@wordpress/url';
5
+ import { safeDecodeURI, getPath } from '@wordpress/url';
6
+
7
+ /**
8
+ * Determines if an entity link should be severed based on URL changes.
9
+ *
10
+ * @param {string} originalUrl - The original URL
11
+ * @param {string} newUrl - The new URL
12
+ * @return {boolean} True if the entity link should be severed
13
+ */
14
+ const shouldSeverEntityLink = ( originalUrl, newUrl ) => {
15
+ if ( ! originalUrl || ! newUrl ) {
16
+ return false;
17
+ }
18
+
19
+ const normalizePath = ( path ) => {
20
+ if ( ! path ) {
21
+ return '';
22
+ }
23
+ return path.replace( /\/+$/, '' ); // Remove trailing slashes
24
+ };
25
+
26
+ // Helper function to create URL objects with proper base handling
27
+ const createUrlObject = ( url, baseUrl = null ) => {
28
+ try {
29
+ // Always provide a base URL - it will be ignored for absolute URLs
30
+ // Use window.location.origin in browser, fallback for Node/tests
31
+ const base =
32
+ baseUrl ||
33
+ ( typeof window !== 'undefined'
34
+ ? window.location.origin
35
+ : 'https://wordpress.org' );
36
+ return new URL( url, base );
37
+ } catch ( error ) {
38
+ // If URL construction still fails, it's likely an invalid URL
39
+ // and we should sever the entity link
40
+ return null;
41
+ }
42
+ };
43
+
44
+ const originalUrlObj = createUrlObject( originalUrl );
45
+ if ( ! originalUrlObj ) {
46
+ return true;
47
+ }
48
+
49
+ const newUrlObj = createUrlObject( newUrl, originalUrl );
50
+ if ( ! newUrlObj ) {
51
+ return true;
52
+ }
53
+
54
+ // Move these declarations here, after the null checks
55
+ const originalHostname = originalUrlObj.hostname;
56
+ const newHostname = newUrlObj.hostname;
57
+ const originalPath = normalizePath( getPath( originalUrlObj.toString() ) );
58
+ const newPath = normalizePath( getPath( newUrlObj.toString() ) );
59
+
60
+ // If hostname or path changed, sever the entity link
61
+ if ( originalHostname !== newHostname || originalPath !== newPath ) {
62
+ return true;
63
+ }
64
+
65
+ // Special handling for plain permalinks (query string post IDs)
66
+ const originalP = originalUrlObj.searchParams.get( 'p' );
67
+ const newP = newUrlObj.searchParams.get( 'p' );
68
+
69
+ // If both are plain permalinks (with ?p= or ?page_id=), compare the IDs
70
+ if ( originalP && newP && originalP !== newP ) {
71
+ return true;
72
+ }
73
+
74
+ const originalPageId = originalUrlObj.searchParams.get( 'page_id' );
75
+ const newPageId = newUrlObj.searchParams.get( 'page_id' );
76
+
77
+ if ( originalPageId && newPageId && originalPageId !== newPageId ) {
78
+ return true;
79
+ }
80
+ // If switching between ?p= and ?page_id=, or one is missing, sever
81
+ if ( ( originalP && newPageId ) || ( originalPageId && newP ) ) {
82
+ return true;
83
+ }
84
+
85
+ // If only query string or fragment changed, preserve the entity link
86
+ return false;
87
+ };
6
88
 
7
89
  /**
8
90
  * @typedef {'post-type'|'custom'|'taxonomy'|'post-type-archive'} WPNavigationLinkKind
@@ -42,19 +124,23 @@ export const updateAttributes = (
42
124
 
43
125
  const {
44
126
  title: newLabel = '', // the title of any provided Post.
45
- url: newUrl = '',
127
+ label: newLabelFromLabel = '', // alternative to title
128
+ url: newUrl,
46
129
  opensInNewTab,
47
- id,
130
+ id: newID,
48
131
  kind: newKind = originalKind,
49
132
  type: newType = originalType,
50
133
  } = updatedValue;
51
134
 
52
- const newLabelWithoutHttp = newLabel.replace( /http(s?):\/\//gi, '' );
53
- const newUrlWithoutHttp = newUrl.replace( /http(s?):\/\//gi, '' );
135
+ // Use title if provided, otherwise fall back to label
136
+ const finalNewLabel = newLabel || newLabelFromLabel;
137
+
138
+ const newLabelWithoutHttp = finalNewLabel.replace( /http(s?):\/\//gi, '' );
139
+ const newUrlWithoutHttp = newUrl?.replace( /http(s?):\/\//gi, '' ) ?? '';
54
140
 
55
141
  const useNewLabel =
56
- newLabel &&
57
- newLabel !== originalLabel &&
142
+ finalNewLabel &&
143
+ finalNewLabel !== originalLabel &&
58
144
  // LinkControl without the title field relies
59
145
  // on the check below. Specifically, it assumes that
60
146
  // the URL is the same as a title.
@@ -73,7 +159,7 @@ export const updateAttributes = (
73
159
  // - https://github.com/WordPress/gutenberg/pull/41063
74
160
  // - https://github.com/WordPress/gutenberg/pull/18617.
75
161
  const label = useNewLabel
76
- ? escapeHTML( newLabel )
162
+ ? escapeHTML( finalNewLabel )
77
163
  : originalLabel || escapeHTML( newUrlWithoutHttp );
78
164
 
79
165
  // In https://github.com/WordPress/gutenberg/pull/24670 we decided to use "tag" in favor of "post_tag"
@@ -86,13 +172,40 @@ export const updateAttributes = (
86
172
  ( ! newKind && ! isBuiltInType ) || newKind === 'custom';
87
173
  const kind = isCustomLink ? 'custom' : newKind;
88
174
 
89
- setAttributes( {
175
+ const attributes = {
90
176
  // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string.
91
- ...( newUrl && { url: encodeURI( safeDecodeURI( newUrl ) ) } ),
177
+ ...( newUrl !== undefined
178
+ ? { url: newUrl ? encodeURI( safeDecodeURI( newUrl ) ) : newUrl }
179
+ : {} ),
92
180
  ...( label && { label } ),
93
181
  ...( undefined !== opensInNewTab && { opensInNewTab } ),
94
- ...( id && Number.isInteger( id ) && { id } ),
95
182
  ...( kind && { kind } ),
96
183
  ...( type && type !== 'URL' && { type } ),
97
- } );
184
+ };
185
+
186
+ // If the block's id is set then the menu item is linking to an entity.
187
+ // Therefore, if the URL is set but a new ID is not provided, check if
188
+ // the entity link should be severed based on URL changes.
189
+ if ( newUrl && ! newID && blockAttributes.id ) {
190
+ const shouldSever = shouldSeverEntityLink(
191
+ blockAttributes.url,
192
+ newUrl
193
+ );
194
+
195
+ if ( shouldSever ) {
196
+ attributes.id = undefined; // explicitly "unset" the ID.
197
+ // When URL is manually changed in a way that severs the entity link,
198
+ // update kind and type to "custom" to indicate this is now a custom link.
199
+ attributes.kind = 'custom';
200
+ attributes.type = 'custom';
201
+ }
202
+ } else if ( newID && Number.isInteger( newID ) ) {
203
+ attributes.id = newID;
204
+ } else if ( blockAttributes.id ) {
205
+ // If we have an existing ID and no URL change, ensure kind and type are preserved
206
+ attributes.kind = kind;
207
+ attributes.type = type;
208
+ }
209
+
210
+ setAttributes( attributes );
98
211
  };
@@ -8,6 +8,7 @@ import clsx from 'clsx';
8
8
  */
9
9
  import { useSelect, useDispatch } from '@wordpress/data';
10
10
  import {
11
+ CheckboxControl,
11
12
  TextControl,
12
13
  TextareaControl,
13
14
  ToolbarButton,
@@ -134,7 +135,7 @@ export default function NavigationSubmenuEdit( {
134
135
  context,
135
136
  clientId,
136
137
  } ) {
137
- const { label, url, description, rel } = attributes;
138
+ const { label, url, description, rel, opensInNewTab } = attributes;
138
139
 
139
140
  const { showSubmenuIcon, maxNestingLevel, openSubmenusOnClick } = context;
140
141
 
@@ -392,6 +393,7 @@ export default function NavigationSubmenuEdit( {
392
393
  url: '',
393
394
  description: '',
394
395
  rel: '',
396
+ opensInNewTab: false,
395
397
  } );
396
398
  } }
397
399
  dropdownMenuProps={ dropdownMenuProps }
@@ -433,6 +435,24 @@ export default function NavigationSubmenuEdit( {
433
435
  />
434
436
  </ToolsPanelItem>
435
437
 
438
+ <ToolsPanelItem
439
+ hasValue={ () => !! opensInNewTab }
440
+ label={ __( 'Open in new tab' ) }
441
+ onDeselect={ () =>
442
+ setAttributes( { opensInNewTab: false } )
443
+ }
444
+ isShownByDefault
445
+ >
446
+ <CheckboxControl
447
+ __nextHasNoMarginBottom
448
+ label={ __( 'Open in new tab' ) }
449
+ checked={ opensInNewTab }
450
+ onChange={ ( value ) =>
451
+ setAttributes( { opensInNewTab: value } )
452
+ }
453
+ />
454
+ </ToolsPanelItem>
455
+
436
456
  <ToolsPanelItem
437
457
  label={ __( 'Description' ) }
438
458
  isShownByDefault
@@ -20,8 +20,11 @@ import {
20
20
  __experimentalToolsPanel as ToolsPanel,
21
21
  __experimentalToolsPanelItem as ToolsPanelItem,
22
22
  } from '@wordpress/components';
23
+ import { debounce } from '@wordpress/compose';
24
+ import { useMemo, useState } from '@wordpress/element';
23
25
  import { useSelect, useDispatch } from '@wordpress/data';
24
26
  import { __, sprintf } from '@wordpress/i18n';
27
+ import { decodeEntities } from '@wordpress/html-entities';
25
28
  import { store as coreStore } from '@wordpress/core-data';
26
29
 
27
30
  /**
@@ -29,13 +32,81 @@ import { store as coreStore } from '@wordpress/core-data';
29
32
  */
30
33
  import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
31
34
 
32
- const minimumUsersForCombobox = 25;
33
-
34
35
  const AUTHORS_QUERY = {
35
36
  who: 'authors',
36
37
  per_page: 100,
38
+ _fields: 'id,name',
39
+ context: 'view',
37
40
  };
38
41
 
42
+ function AuthorCombobox( { value, onChange } ) {
43
+ const [ filterValue, setFilterValue ] = useState( '' );
44
+ const { authors, isLoading } = useSelect(
45
+ ( select ) => {
46
+ const { getUsers, isResolving } = select( coreStore );
47
+
48
+ const query = { ...AUTHORS_QUERY };
49
+ if ( filterValue ) {
50
+ query.search = filterValue;
51
+ query.search_columns = [ 'name' ];
52
+ }
53
+
54
+ return {
55
+ authors: getUsers( query ),
56
+ isLoading: isResolving( 'getUsers', [ query ] ),
57
+ };
58
+ },
59
+ [ filterValue ]
60
+ );
61
+
62
+ const authorOptions = useMemo( () => {
63
+ const fetchedAuthors = ( authors ?? [] ).map( ( author ) => {
64
+ return {
65
+ value: author.id,
66
+ label: decodeEntities( author.name ),
67
+ };
68
+ } );
69
+
70
+ // Ensure the current author is included in the list.
71
+ const foundAuthor = fetchedAuthors.findIndex(
72
+ ( fetchedAuthor ) => value?.id === fetchedAuthor.value
73
+ );
74
+
75
+ let currentAuthor = [];
76
+ if ( foundAuthor < 0 && value ) {
77
+ currentAuthor = [
78
+ {
79
+ value: value.id,
80
+ label: decodeEntities( value.name ),
81
+ },
82
+ ];
83
+ } else if ( foundAuthor < 0 && ! value ) {
84
+ currentAuthor = [
85
+ {
86
+ value: 0,
87
+ label: __( '(No author)' ),
88
+ },
89
+ ];
90
+ }
91
+
92
+ return [ ...currentAuthor, ...fetchedAuthors ];
93
+ }, [ authors, value ] );
94
+
95
+ return (
96
+ <ComboboxControl
97
+ __next40pxDefaultSize
98
+ __nextHasNoMarginBottom
99
+ label={ __( 'Author' ) }
100
+ options={ authorOptions }
101
+ value={ value?.id }
102
+ onFilterValueChange={ debounce( setFilterValue, 300 ) }
103
+ onChange={ onChange }
104
+ allowReset={ false }
105
+ isLoading={ isLoading }
106
+ />
107
+ );
108
+ }
109
+
39
110
  function PostAuthorEdit( {
40
111
  isSelected,
41
112
  context: { postType, postId, queryId },
@@ -45,22 +116,28 @@ function PostAuthorEdit( {
45
116
  const isDescendentOfQueryLoop = Number.isFinite( queryId );
46
117
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
47
118
 
48
- const { authorId, authorDetails, authors, supportsAuthor } = useSelect(
119
+ const { authorDetails, canAssignAuthor, supportsAuthor } = useSelect(
49
120
  ( select ) => {
50
- const { getEditedEntityRecord, getUser, getUsers, getPostType } =
121
+ const { getEditedEntityRecord, getUser, getPostType } =
51
122
  select( coreStore );
52
- const _authorId = getEditedEntityRecord(
123
+ const currentPost = getEditedEntityRecord(
53
124
  'postType',
54
125
  postType,
55
126
  postId
56
- )?.author;
127
+ );
128
+ const authorId = currentPost?.author;
57
129
 
58
130
  return {
59
- authorId: _authorId,
60
- authorDetails: _authorId ? getUser( _authorId ) : null,
61
- authors: getUsers( AUTHORS_QUERY ),
131
+ authorDetails: authorId
132
+ ? getUser( authorId, { context: 'view' } )
133
+ : null,
62
134
  supportsAuthor:
63
135
  getPostType( postType )?.supports?.author ?? false,
136
+ canAssignAuthor: currentPost?._links?.[
137
+ 'wp:action-assign-author'
138
+ ]
139
+ ? true
140
+ : false,
64
141
  };
65
142
  },
66
143
  [ postType, postId ]
@@ -94,24 +171,14 @@ function PostAuthorEdit( {
94
171
  } ),
95
172
  } );
96
173
 
97
- const authorOptions = authors?.length
98
- ? authors.map( ( { id, name } ) => {
99
- return {
100
- value: id,
101
- label: name,
102
- };
103
- } )
104
- : [];
105
-
106
174
  const handleSelect = ( nextAuthorId ) => {
107
175
  editEntityRecord( 'postType', postType, postId, {
108
176
  author: nextAuthorId,
109
177
  } );
110
178
  };
111
179
 
112
- const showCombobox = authorOptions.length >= minimumUsersForCombobox;
113
180
  const showAuthorControl =
114
- !! postId && ! isDescendentOfQueryLoop && authorOptions.length > 0;
181
+ !! postId && ! isDescendentOfQueryLoop && canAssignAuthor;
115
182
 
116
183
  if ( ! supportsAuthor && postType !== undefined ) {
117
184
  return (
@@ -142,26 +209,10 @@ function PostAuthorEdit( {
142
209
  >
143
210
  { showAuthorControl && (
144
211
  <div style={ { gridColumn: '1 / -1' } }>
145
- { ( showCombobox && (
146
- <ComboboxControl
147
- __next40pxDefaultSize
148
- __nextHasNoMarginBottom
149
- label={ __( 'Author' ) }
150
- options={ authorOptions }
151
- value={ authorId }
152
- onChange={ handleSelect }
153
- allowReset={ false }
154
- />
155
- ) ) || (
156
- <SelectControl
157
- __next40pxDefaultSize
158
- __nextHasNoMarginBottom
159
- label={ __( 'Author' ) }
160
- value={ authorId }
161
- options={ authorOptions }
162
- onChange={ handleSelect }
163
- />
164
- ) }
212
+ <AuthorCombobox
213
+ value={ authorDetails }
214
+ onChange={ handleSelect }
215
+ />
165
216
  </div>
166
217
  ) }
167
218
  <ToolsPanelItem
@@ -106,7 +106,7 @@ const CommentsForm = ( { postId, postType } ) => {
106
106
  return (
107
107
  <Warning>
108
108
  { sprintf(
109
- /* translators: 1: Post type (i.e. "post", "page") */
109
+ /* translators: %s: Post type (i.e. "post", "page") */
110
110
  __(
111
111
  'Post Comments Form block: Comments are not enabled for this post type (%s).'
112
112
  ),
@@ -7,6 +7,12 @@
7
7
  "description": "Displays the contents of a post or page.",
8
8
  "textdomain": "default",
9
9
  "usesContext": [ "postId", "postType", "queryId" ],
10
+ "attributes": {
11
+ "tagName": {
12
+ "type": "string",
13
+ "default": "div"
14
+ }
15
+ },
10
16
  "example": {
11
17
  "viewportWidth": 350
12
18
  },
@@ -3,11 +3,13 @@
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
5
  import {
6
+ InspectorControls,
6
7
  useBlockProps,
7
8
  useInnerBlocksProps,
8
9
  RecursionProvider,
9
10
  useHasRecursion,
10
11
  Warning,
12
+ privateApis as blockEditorPrivateApis,
11
13
  __experimentalUseBlockPreview as useBlockPreview,
12
14
  } from '@wordpress/block-editor';
13
15
  import { parse } from '@wordpress/blocks';
@@ -23,6 +25,9 @@ import { useMemo } from '@wordpress/element';
23
25
  * Internal dependencies
24
26
  */
25
27
  import { useCanEditEntity } from '../utils/hooks';
28
+ import { unlock } from '../lock-unlock';
29
+
30
+ const { HTMLElementControl } = unlock( blockEditorPrivateApis );
26
31
 
27
32
  function ReadOnlyContent( {
28
33
  parentLayout,
@@ -30,6 +35,7 @@ function ReadOnlyContent( {
30
35
  userCanEdit,
31
36
  postType,
32
37
  postId,
38
+ tagName: TagName = 'div',
33
39
  } ) {
34
40
  const [ , , content ] = useEntityProp(
35
41
  'postType',
@@ -60,18 +66,18 @@ function ReadOnlyContent( {
60
66
  }
61
67
 
62
68
  return content?.protected ? (
63
- <div { ...blockProps }>
69
+ <TagName { ...blockProps }>
64
70
  <Warning>{ __( 'This content is password protected.' ) }</Warning>
65
- </div>
71
+ </TagName>
66
72
  ) : (
67
- <div
73
+ <TagName
68
74
  { ...blockProps }
69
75
  dangerouslySetInnerHTML={ { __html: content?.rendered } }
70
- ></div>
76
+ ></TagName>
71
77
  );
72
78
  }
73
79
 
74
- function EditableContent( { context = {} } ) {
80
+ function EditableContent( { context = {}, tagName: TagName = 'div' } ) {
75
81
  const { postType, postId } = context;
76
82
 
77
83
  const [ blocks, onInput, onChange ] = useEntityBlockEditor(
@@ -104,12 +110,15 @@ function EditableContent( { context = {} } ) {
104
110
  template: ! hasInnerBlocks ? initialInnerBlocks : undefined,
105
111
  }
106
112
  );
107
- return <div { ...props } />;
113
+ return <TagName { ...props } />;
108
114
  }
109
115
 
110
116
  function Content( props ) {
111
- const { context: { queryId, postType, postId } = {}, layoutClassNames } =
112
- props;
117
+ const {
118
+ context: { queryId, postType, postId } = {},
119
+ layoutClassNames,
120
+ tagName,
121
+ } = props;
113
122
  const userCanEdit = useCanEditEntity( 'postType', postType, postId );
114
123
  if ( userCanEdit === undefined ) {
115
124
  return null;
@@ -127,6 +136,7 @@ function Content( props ) {
127
136
  userCanEdit={ userCanEdit }
128
137
  postType={ postType }
129
138
  postId={ postId }
139
+ tagName={ tagName }
130
140
  />
131
141
  );
132
142
  }
@@ -165,8 +175,39 @@ function RecursionError() {
165
175
  );
166
176
  }
167
177
 
178
+ /**
179
+ * Render inspector controls for the PostContent block.
180
+ *
181
+ * @param {Object} props Component props.
182
+ * @param {string} props.tagName The HTML tag name.
183
+ * @param {Function} props.onSelectTagName onChange function for the SelectControl.
184
+ * @param {string} props.clientId The client ID of the current block.
185
+ *
186
+ * @return {JSX.Element} The control group.
187
+ */
188
+ function PostContentEditControls( { tagName, onSelectTagName, clientId } ) {
189
+ return (
190
+ <InspectorControls group="advanced">
191
+ <HTMLElementControl
192
+ tagName={ tagName }
193
+ onChange={ onSelectTagName }
194
+ clientId={ clientId }
195
+ options={ [
196
+ { label: __( 'Default (<div>)' ), value: 'div' },
197
+ { label: '<main>', value: 'main' },
198
+ { label: '<section>', value: 'section' },
199
+ { label: '<article>', value: 'article' },
200
+ ] }
201
+ />
202
+ </InspectorControls>
203
+ );
204
+ }
205
+
168
206
  export default function PostContentEdit( {
169
207
  context,
208
+ attributes: { tagName = 'div' },
209
+ setAttributes,
210
+ clientId,
170
211
  __unstableLayoutClassNames: layoutClassNames,
171
212
  __unstableParentLayout: parentLayout,
172
213
  } ) {
@@ -177,17 +218,28 @@ export default function PostContentEdit( {
177
218
  return <RecursionError />;
178
219
  }
179
220
 
221
+ const handleSelectTagName = ( value ) => {
222
+ setAttributes( { tagName: value } );
223
+ };
224
+
180
225
  return (
181
- <RecursionProvider uniqueId={ contextPostId }>
182
- { contextPostId && contextPostType ? (
183
- <Content
184
- context={ context }
185
- parentLayout={ parentLayout }
186
- layoutClassNames={ layoutClassNames }
187
- />
188
- ) : (
189
- <Placeholder layoutClassNames={ layoutClassNames } />
190
- ) }
191
- </RecursionProvider>
226
+ <>
227
+ <PostContentEditControls
228
+ tagName={ tagName }
229
+ onSelectTagName={ handleSelectTagName }
230
+ clientId={ clientId }
231
+ />
232
+ <RecursionProvider uniqueId={ contextPostId }>
233
+ { contextPostId && contextPostType ? (
234
+ <Content
235
+ context={ context }
236
+ parentLayout={ parentLayout }
237
+ layoutClassNames={ layoutClassNames }
238
+ />
239
+ ) : (
240
+ <Placeholder layoutClassNames={ layoutClassNames } />
241
+ ) }
242
+ </RecursionProvider>
243
+ </>
192
244
  );
193
245
  }
@@ -54,12 +54,19 @@ function render_block_core_post_content( $attributes, $content, $block ) {
54
54
  return '';
55
55
  }
56
56
 
57
+ $tag_name = 'div';
58
+
59
+ if ( ! empty( $attributes['tagName'] ) && tag_escape( $attributes['tagName'] ) === $attributes['tagName'] ) {
60
+ $tag_name = $attributes['tagName'];
61
+ }
62
+
57
63
  $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => 'entry-content' ) );
58
64
 
59
- return (
60
- '<div ' . $wrapper_attributes . '>' .
61
- $content .
62
- '</div>'
65
+ return sprintf(
66
+ '<%1$s %2$s>%3$s</%1$s>',
67
+ $tag_name,
68
+ $wrapper_attributes,
69
+ $content
63
70
  );
64
71
  }
65
72
 
@@ -248,6 +248,7 @@ export default function PostFeaturedImageEdit( {
248
248
  isLink: false,
249
249
  linkTarget: '_self',
250
250
  rel: '',
251
+ sizeSlug: DEFAULT_MEDIA_SIZE_SLUG,
251
252
  } );
252
253
  } }
253
254
  dropdownMenuProps={ dropdownMenuProps }
@@ -27,8 +27,9 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
27
27
  $overlay_markup = get_block_core_post_featured_image_overlay_element_markup( $attributes );
28
28
 
29
29
  if ( $is_link ) {
30
- if ( get_the_title( $post_ID ) ) {
31
- $attr['alt'] = trim( strip_tags( get_the_title( $post_ID ) ) );
30
+ $title = get_the_title( $post_ID );
31
+ if ( $title ) {
32
+ $attr['alt'] = trim( strip_tags( $title ) );
32
33
  } else {
33
34
  $attr['alt'] = sprintf(
34
35
  // translators: %d is the post ID.
package/src/rss/index.php CHANGED
@@ -46,7 +46,8 @@ function render_block_core_rss( $attributes ) {
46
46
  }
47
47
 
48
48
  foreach ( $rss_items as $item ) {
49
- $title = esc_html( trim( strip_tags( $item->get_title() ) ) );
49
+ $title = esc_html( trim( strip_tags( html_entity_decode( $item->get_title() ) ) ) );
50
+
50
51
  if ( empty( $title ) ) {
51
52
  $title = __( '(no title)' );
52
53
  }
@@ -480,7 +480,7 @@ export default function SearchEdit( {
480
480
  key={ widthValue }
481
481
  value={ widthValue }
482
482
  label={ sprintf(
483
- /* translators: Percentage value. */
483
+ /* translators: %d: Percentage value. */
484
484
  __( '%d%%' ),
485
485
  widthValue
486
486
  ) }