@wordpress/block-library 8.3.2 → 8.4.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 (193) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/button/edit.js +3 -1
  3. package/build/button/edit.js.map +1 -1
  4. package/build/button/index.js +17 -6
  5. package/build/button/index.js.map +1 -1
  6. package/build/cover/edit/index.js +3 -2
  7. package/build/cover/edit/index.js.map +1 -1
  8. package/build/file/index.js +10 -1
  9. package/build/file/index.js.map +1 -1
  10. package/build/image/image.js +21 -15
  11. package/build/image/image.js.map +1 -1
  12. package/build/latest-comments/edit.js +6 -2
  13. package/build/latest-comments/edit.js.map +1 -1
  14. package/build/latest-comments/index.js +13 -0
  15. package/build/latest-comments/index.js.map +1 -1
  16. package/build/navigation/edit/index.js +2 -28
  17. package/build/navigation/edit/index.js.map +1 -1
  18. package/build/navigation/edit/menu-inspector-controls.js +5 -6
  19. package/build/navigation/edit/menu-inspector-controls.js.map +1 -1
  20. package/build/navigation/edit/navigation-menu-selector.js +14 -11
  21. package/build/navigation/edit/navigation-menu-selector.js.map +1 -1
  22. package/build/navigation/edit/unsaved-inner-blocks.js +4 -5
  23. package/build/navigation/edit/unsaved-inner-blocks.js.map +1 -1
  24. package/build/navigation/edit/use-create-navigation-menu.js +11 -2
  25. package/build/navigation/edit/use-create-navigation-menu.js.map +1 -1
  26. package/build/navigation/use-navigation-menu.js +1 -1
  27. package/build/navigation/use-navigation-menu.js.map +1 -1
  28. package/build/navigation-link/edit.js +4 -0
  29. package/build/navigation-link/edit.js.map +1 -1
  30. package/build/navigation-link/link-ui.js +1 -0
  31. package/build/navigation-link/link-ui.js.map +1 -1
  32. package/build/navigation-submenu/edit.js +4 -0
  33. package/build/navigation-submenu/edit.js.map +1 -1
  34. package/build/page-list/edit.js +5 -4
  35. package/build/page-list/edit.js.map +1 -1
  36. package/build/page-list/use-convert-to-navigation-links.js +61 -5
  37. package/build/page-list/use-convert-to-navigation-links.js.map +1 -1
  38. package/build/post-excerpt/edit.js +49 -3
  39. package/build/post-excerpt/edit.js.map +1 -1
  40. package/build/post-excerpt/index.js +4 -0
  41. package/build/post-excerpt/index.js.map +1 -1
  42. package/build/post-featured-image/dimension-controls.js +52 -1
  43. package/build/post-featured-image/dimension-controls.js.map +1 -1
  44. package/build/post-featured-image/edit.js +9 -4
  45. package/build/post-featured-image/edit.js.map +1 -1
  46. package/build/post-featured-image/index.js +3 -0
  47. package/build/post-featured-image/index.js.map +1 -1
  48. package/build/{experiments.js → private-apis.js} +3 -3
  49. package/build/private-apis.js.map +1 -0
  50. package/build/site-logo/edit.js +7 -11
  51. package/build/site-logo/edit.js.map +1 -1
  52. package/build/table/edit.js +3 -3
  53. package/build/table/edit.js.map +1 -1
  54. package/build/table-of-contents/utils.js +1 -1
  55. package/build/table-of-contents/utils.js.map +1 -1
  56. package/build/verse/index.js +6 -0
  57. package/build/verse/index.js.map +1 -1
  58. package/build-module/button/edit.js +2 -1
  59. package/build-module/button/edit.js.map +1 -1
  60. package/build-module/button/index.js +17 -6
  61. package/build-module/button/index.js.map +1 -1
  62. package/build-module/cover/edit/index.js +3 -2
  63. package/build-module/cover/edit/index.js.map +1 -1
  64. package/build-module/file/index.js +10 -1
  65. package/build-module/file/index.js.map +1 -1
  66. package/build-module/image/image.js +21 -15
  67. package/build-module/image/image.js.map +1 -1
  68. package/build-module/latest-comments/edit.js +6 -2
  69. package/build-module/latest-comments/edit.js.map +1 -1
  70. package/build-module/latest-comments/index.js +13 -0
  71. package/build-module/latest-comments/index.js.map +1 -1
  72. package/build-module/navigation/edit/index.js +3 -29
  73. package/build-module/navigation/edit/index.js.map +1 -1
  74. package/build-module/navigation/edit/menu-inspector-controls.js +5 -5
  75. package/build-module/navigation/edit/menu-inspector-controls.js.map +1 -1
  76. package/build-module/navigation/edit/navigation-menu-selector.js +14 -10
  77. package/build-module/navigation/edit/navigation-menu-selector.js.map +1 -1
  78. package/build-module/navigation/edit/unsaved-inner-blocks.js +4 -5
  79. package/build-module/navigation/edit/unsaved-inner-blocks.js.map +1 -1
  80. package/build-module/navigation/edit/use-create-navigation-menu.js +11 -2
  81. package/build-module/navigation/edit/use-create-navigation-menu.js.map +1 -1
  82. package/build-module/navigation/use-navigation-menu.js +1 -1
  83. package/build-module/navigation/use-navigation-menu.js.map +1 -1
  84. package/build-module/navigation-link/edit.js +4 -0
  85. package/build-module/navigation-link/edit.js.map +1 -1
  86. package/build-module/navigation-link/link-ui.js +1 -0
  87. package/build-module/navigation-link/link-ui.js.map +1 -1
  88. package/build-module/navigation-submenu/edit.js +4 -0
  89. package/build-module/navigation-submenu/edit.js.map +1 -1
  90. package/build-module/page-list/edit.js +5 -4
  91. package/build-module/page-list/edit.js.map +1 -1
  92. package/build-module/page-list/use-convert-to-navigation-links.js +61 -5
  93. package/build-module/page-list/use-convert-to-navigation-links.js.map +1 -1
  94. package/build-module/post-excerpt/edit.js +52 -5
  95. package/build-module/post-excerpt/edit.js.map +1 -1
  96. package/build-module/post-excerpt/index.js +4 -0
  97. package/build-module/post-excerpt/index.js.map +1 -1
  98. package/build-module/post-featured-image/dimension-controls.js +52 -1
  99. package/build-module/post-featured-image/dimension-controls.js.map +1 -1
  100. package/build-module/post-featured-image/edit.js +9 -4
  101. package/build-module/post-featured-image/edit.js.map +1 -1
  102. package/build-module/post-featured-image/index.js +3 -0
  103. package/build-module/post-featured-image/index.js.map +1 -1
  104. package/build-module/{experiments.js → private-apis.js} +2 -2
  105. package/build-module/private-apis.js.map +1 -0
  106. package/build-module/site-logo/edit.js +7 -11
  107. package/build-module/site-logo/edit.js.map +1 -1
  108. package/build-module/table/edit.js +3 -3
  109. package/build-module/table/edit.js.map +1 -1
  110. package/build-module/table-of-contents/utils.js +1 -1
  111. package/build-module/table-of-contents/utils.js.map +1 -1
  112. package/build-module/verse/index.js +6 -0
  113. package/build-module/verse/index.js.map +1 -1
  114. package/build-style/avatar/style-rtl.css +3 -0
  115. package/build-style/avatar/style.css +3 -0
  116. package/build-style/button/editor-rtl.css +31 -0
  117. package/build-style/button/editor.css +31 -0
  118. package/build-style/button/style-rtl.css +31 -0
  119. package/build-style/button/style.css +31 -0
  120. package/build-style/classic-rtl.css +5 -0
  121. package/build-style/classic.css +5 -0
  122. package/build-style/editor-rtl.css +36 -1
  123. package/build-style/editor.css +36 -1
  124. package/build-style/file/style-rtl.css +1 -0
  125. package/build-style/file/style.css +1 -0
  126. package/build-style/image/editor-rtl.css +1 -0
  127. package/build-style/image/editor.css +1 -0
  128. package/build-style/image/style-rtl.css +6 -2
  129. package/build-style/image/style.css +6 -0
  130. package/build-style/latest-comments/style-rtl.css +18 -5
  131. package/build-style/latest-comments/style.css +18 -5
  132. package/build-style/quote/style-rtl.css +5 -5
  133. package/build-style/quote/style.css +5 -5
  134. package/build-style/style-rtl.css +64 -12
  135. package/build-style/style.css +64 -10
  136. package/build-types/table-of-contents/utils.d.ts +1 -1
  137. package/package.json +30 -30
  138. package/src/avatar/index.php +67 -63
  139. package/src/avatar/style.scss +3 -0
  140. package/src/button/block.json +17 -6
  141. package/src/button/edit.js +2 -1
  142. package/src/button/editor.scss +36 -0
  143. package/src/button/style.scss +37 -1
  144. package/src/classic.scss +5 -0
  145. package/src/cover/edit/index.js +4 -1
  146. package/src/editor.scss +5 -0
  147. package/src/file/block.json +10 -1
  148. package/src/file/style.scss +1 -0
  149. package/src/image/editor.scss +1 -0
  150. package/src/image/image.js +32 -27
  151. package/src/image/style.scss +13 -0
  152. package/src/latest-comments/block.json +13 -0
  153. package/src/latest-comments/edit.js +9 -2
  154. package/src/latest-comments/style.scss +25 -7
  155. package/src/navigation/edit/index.js +1 -30
  156. package/src/navigation/edit/menu-inspector-controls.js +3 -4
  157. package/src/navigation/edit/navigation-menu-selector.js +12 -26
  158. package/src/navigation/edit/test/navigation-menu-selector.js +638 -0
  159. package/src/navigation/edit/unsaved-inner-blocks.js +29 -36
  160. package/src/navigation/edit/use-create-navigation-menu.js +13 -1
  161. package/src/navigation/index.php +8 -6
  162. package/src/navigation/use-navigation-menu.js +1 -1
  163. package/src/navigation-link/edit.js +3 -0
  164. package/src/navigation-link/link-ui.js +1 -0
  165. package/src/navigation-submenu/edit.js +3 -0
  166. package/src/page-list/edit.js +6 -5
  167. package/src/page-list/index.php +4 -4
  168. package/src/page-list/test/convert-to-links-modal.js +134 -0
  169. package/src/page-list/use-convert-to-navigation-links.js +64 -4
  170. package/src/post-excerpt/block.json +4 -0
  171. package/src/post-excerpt/edit.js +72 -7
  172. package/src/post-excerpt/index.php +29 -5
  173. package/src/post-featured-image/block.json +3 -0
  174. package/src/post-featured-image/dimension-controls.js +64 -2
  175. package/src/post-featured-image/edit.js +18 -6
  176. package/src/post-featured-image/index.php +25 -9
  177. package/src/post-title/index.php +3 -3
  178. package/src/{experiments.js → private-apis.js} +1 -1
  179. package/src/quote/style.scss +2 -2
  180. package/src/site-logo/edit.js +3 -6
  181. package/src/table/edit.js +3 -3
  182. package/src/table-of-contents/utils.ts +1 -1
  183. package/src/template-part/index.php +1 -1
  184. package/src/verse/block.json +6 -0
  185. package/tsconfig.json +24 -1
  186. package/tsconfig.tsbuildinfo +1 -1
  187. package/build/experiments.js.map +0 -1
  188. package/build/navigation/leaf-more-menu.js +0 -95
  189. package/build/navigation/leaf-more-menu.js.map +0 -1
  190. package/build-module/experiments.js.map +0 -1
  191. package/build-module/navigation/leaf-more-menu.js +0 -76
  192. package/build-module/navigation/leaf-more-menu.js.map +0 -1
  193. package/src/navigation/leaf-more-menu.js +0 -93
@@ -39,7 +39,6 @@ const ALLOWED_BLOCKS = [
39
39
  export default function UnsavedInnerBlocks( {
40
40
  blocks,
41
41
  createNavigationMenu,
42
-
43
42
  hasSelection,
44
43
  } ) {
45
44
  const originalBlocks = useRef();
@@ -91,37 +90,34 @@ export default function UnsavedInnerBlocks( {
91
90
  }
92
91
  );
93
92
 
94
- const { isSaving, draftNavigationMenus, hasResolvedDraftNavigationMenus } =
95
- useSelect(
96
- ( select ) => {
97
- if ( isDisabled ) {
98
- return EMPTY_OBJECT;
99
- }
100
-
101
- const {
102
- getEntityRecords,
103
- hasFinishedResolution,
104
- isSavingEntityRecord,
105
- } = select( coreStore );
106
-
107
- return {
108
- isSaving: isSavingEntityRecord(
109
- 'postType',
110
- 'wp_navigation'
111
- ),
112
- draftNavigationMenus: getEntityRecords(
113
- ...DRAFT_MENU_PARAMS
114
- ),
115
- hasResolvedDraftNavigationMenus: hasFinishedResolution(
116
- 'getEntityRecords',
117
- DRAFT_MENU_PARAMS
118
- ),
119
- };
120
- },
121
- [ isDisabled ]
122
- );
123
-
124
- const { hasResolvedNavigationMenus, navigationMenus } = useNavigationMenu();
93
+ const { isSaving, hasResolvedDraftNavigationMenus } = useSelect(
94
+ ( select ) => {
95
+ if ( isDisabled ) {
96
+ return EMPTY_OBJECT;
97
+ }
98
+
99
+ const {
100
+ getEntityRecords,
101
+ hasFinishedResolution,
102
+ isSavingEntityRecord,
103
+ } = select( coreStore );
104
+
105
+ return {
106
+ isSaving: isSavingEntityRecord( 'postType', 'wp_navigation' ),
107
+ draftNavigationMenus: getEntityRecords(
108
+ // This is needed so that hasResolvedDraftNavigationMenus gives the correct status.
109
+ ...DRAFT_MENU_PARAMS
110
+ ),
111
+ hasResolvedDraftNavigationMenus: hasFinishedResolution(
112
+ 'getEntityRecords',
113
+ DRAFT_MENU_PARAMS
114
+ ),
115
+ };
116
+ },
117
+ [ isDisabled ]
118
+ );
119
+
120
+ const { hasResolvedNavigationMenus } = useNavigationMenu();
125
121
 
126
122
  // Automatically save the uncontrolled blocks.
127
123
  useEffect( () => {
@@ -154,11 +150,8 @@ export default function UnsavedInnerBlocks( {
154
150
  isSaving,
155
151
  hasResolvedDraftNavigationMenus,
156
152
  hasResolvedNavigationMenus,
157
- draftNavigationMenus,
158
- navigationMenus,
153
+ innerBlocksAreDirty,
159
154
  hasSelection,
160
- createNavigationMenu,
161
- blocks,
162
155
  ] );
163
156
 
164
157
  const Wrapper = isSaving ? Disabled : 'div';
@@ -21,7 +21,7 @@ export default function useCreateNavigationMenu( clientId ) {
21
21
  const [ value, setValue ] = useState( null );
22
22
  const [ error, setError ] = useState( null );
23
23
 
24
- const { saveEntityRecord } = useDispatch( coreStore );
24
+ const { saveEntityRecord, editEntityRecord } = useDispatch( coreStore );
25
25
  const generateDefaultTitle = useGenerateDefaultNavigationTitle( clientId );
26
26
 
27
27
  // This callback uses data from the two placeholder steps and only creates
@@ -68,6 +68,18 @@ export default function useCreateNavigationMenu( clientId ) {
68
68
  .then( ( response ) => {
69
69
  setValue( response );
70
70
  setStatus( CREATE_NAVIGATION_MENU_SUCCESS );
71
+
72
+ // Set the status to publish so that the Navigation block
73
+ // shows up in the multi entity save flow.
74
+ if ( postStatus !== 'publish' ) {
75
+ editEntityRecord(
76
+ 'postType',
77
+ 'wp_navigation',
78
+ response.id,
79
+ { status: 'publish' }
80
+ );
81
+ }
82
+
71
83
  return response;
72
84
  } )
73
85
  .catch( ( err ) => {
@@ -373,12 +373,14 @@ function block_core_navigation_get_most_recently_published_navigation() {
373
373
 
374
374
  // Default to the most recently created menu.
375
375
  $parsed_args = array(
376
- 'post_type' => 'wp_navigation',
377
- 'no_found_rows' => true,
378
- 'order' => 'DESC',
379
- 'orderby' => 'date',
380
- 'post_status' => 'publish',
381
- 'posts_per_page' => 1, // get only the most recent.
376
+ 'post_type' => 'wp_navigation',
377
+ 'no_found_rows' => true,
378
+ 'update_post_meta_cache' => false,
379
+ 'update_post_term_cache' => false,
380
+ 'order' => 'DESC',
381
+ 'orderby' => 'date',
382
+ 'post_status' => 'publish',
383
+ 'posts_per_page' => 1, // get only the most recent.
382
384
  );
383
385
 
384
386
  $navigation_post = new WP_Query( $parsed_args );
@@ -24,7 +24,7 @@ export default function useNavigationMenu( ref ) {
24
24
  navigationMenus,
25
25
  isResolvingNavigationMenus,
26
26
  hasResolvedNavigationMenus,
27
- } = selectNavigationMenus( select, ref );
27
+ } = selectNavigationMenus( select );
28
28
 
29
29
  const {
30
30
  navigationMenu,
@@ -261,6 +261,9 @@ export default function NavigationLinkEdit( {
261
261
  useEffect( () => {
262
262
  // If block has inner blocks, transform to Submenu.
263
263
  if ( hasChildren ) {
264
+ // This side-effect should not create an undo level as those should
265
+ // only be created via user interactions.
266
+ __unstableMarkNextChangeAsNotPersistent();
264
267
  transformToSubmenu();
265
268
  }
266
269
  }, [ hasChildren ] );
@@ -198,6 +198,7 @@ export function LinkUI( props ) {
198
198
  suggestionsQuery={ getSuggestionsQuery( type, kind ) }
199
199
  onChange={ props.onChange }
200
200
  onRemove={ props.onRemove }
201
+ onCancel={ props.onCancel }
201
202
  renderControlBottom={
202
203
  ! url
203
204
  ? () => (
@@ -358,6 +358,9 @@ export default function NavigationSubmenuEdit( {
358
358
  useEffect( () => {
359
359
  // If block becomes empty, transform to Navigation Link.
360
360
  if ( ! hasChildren && prevHasChildren ) {
361
+ // This side-effect should not create an undo level as those should
362
+ // only be created via user interactions.
363
+ __unstableMarkNextChangeAsNotPersistent();
361
364
  transformToLink();
362
365
  }
363
366
  }, [ hasChildren, prevHasChildren ] );
@@ -182,11 +182,6 @@ export default function PageListEdit( {
182
182
  pages?.length > 0 &&
183
183
  pages?.length <= MAX_PAGE_COUNT;
184
184
 
185
- const convertToNavigationLinks = useConvertToNavigationLinks( {
186
- clientId,
187
- pages,
188
- } );
189
-
190
185
  const pagesByParentId = useMemo( () => {
191
186
  if ( pages === null ) {
192
187
  return new Map();
@@ -213,6 +208,12 @@ export default function PageListEdit( {
213
208
  }, new Map() );
214
209
  }, [ pages ] );
215
210
 
211
+ const convertToNavigationLinks = useConvertToNavigationLinks( {
212
+ clientId,
213
+ pages,
214
+ parentPageID,
215
+ } );
216
+
216
217
  const blockProps = useBlockProps( {
217
218
  className: classnames( 'wp-block-page-list', {
218
219
  'has-text-color': !! context.textColor,
@@ -150,7 +150,8 @@ function block_core_page_list_render_nested_page_list( $open_submenus_on_click,
150
150
  if ( empty( $nested_pages ) ) {
151
151
  return;
152
152
  }
153
- $markup = '';
153
+ $front_page_id = (int) get_option( 'page_on_front' );
154
+ $markup = '';
154
155
  foreach ( (array) $nested_pages as $page ) {
155
156
  $css_class = $page['is_active'] ? ' current-menu-item' : '';
156
157
  $aria_current = $page['is_active'] ? ' aria-current="page"' : '';
@@ -181,7 +182,6 @@ function block_core_page_list_render_nested_page_list( $open_submenus_on_click,
181
182
  }
182
183
  }
183
184
 
184
- $front_page_id = (int) get_option( 'page_on_front' );
185
185
  if ( (int) $page['page_id'] === $front_page_id ) {
186
186
  $css_class .= ' menu-item-home';
187
187
  }
@@ -282,14 +282,14 @@ function render_block_core_page_list( $attributes, $content, $block ) {
282
282
  $pages_with_children[ $page->post_parent ][ $page->ID ] = array(
283
283
  'page_id' => $page->ID,
284
284
  'title' => $page->post_title,
285
- 'link' => get_permalink( $page->ID ),
285
+ 'link' => get_permalink( $page ),
286
286
  'is_active' => $is_active,
287
287
  );
288
288
  } else {
289
289
  $top_level_pages[ $page->ID ] = array(
290
290
  'page_id' => $page->ID,
291
291
  'title' => $page->post_title,
292
- 'link' => get_permalink( $page->ID ),
292
+ 'link' => get_permalink( $page ),
293
293
  'is_active' => $is_active,
294
294
  );
295
295
 
@@ -383,5 +383,139 @@ describe( 'page list convert to links', () => {
383
383
  },
384
384
  ] );
385
385
  } );
386
+
387
+ it( 'Can use a different parent page', () => {
388
+ const pages = [
389
+ {
390
+ title: {
391
+ raw: 'Sample Page',
392
+ rendered: 'Sample Page',
393
+ },
394
+ id: 2,
395
+ parent: 0,
396
+ link: 'http://wordpress.local/sample-page/',
397
+ type: 'page',
398
+ },
399
+ {
400
+ title: {
401
+ raw: 'About',
402
+ rendered: 'About',
403
+ },
404
+ id: 34,
405
+ parent: 0,
406
+ link: 'http://wordpress.local/about/',
407
+ type: 'page',
408
+ },
409
+ {
410
+ title: {
411
+ raw: 'Contact Page',
412
+ rendered: 'Contact Page',
413
+ },
414
+ id: 37,
415
+ parent: 0,
416
+ link: 'http://wordpress.local/contact-page/',
417
+ type: 'page',
418
+ },
419
+ {
420
+ title: {
421
+ raw: 'Test',
422
+ rendered: 'Test',
423
+ },
424
+ id: 229,
425
+ parent: 0,
426
+ link: 'http://wordpress.local/test/',
427
+ type: 'page',
428
+ },
429
+ {
430
+ title: {
431
+ raw: 'About Sub 1',
432
+ rendered: 'About Sub 1',
433
+ },
434
+ id: 738,
435
+ parent: 34,
436
+ link: 'http://wordpress.local/about/about-sub-1/',
437
+ type: 'page',
438
+ },
439
+ {
440
+ title: {
441
+ raw: 'About Sub 2',
442
+ rendered: 'About Sub 2',
443
+ },
444
+ id: 740,
445
+ parent: 34,
446
+ link: 'http://wordpress.local/about/about-sub-2/',
447
+ type: 'page',
448
+ },
449
+ {
450
+ title: {
451
+ raw: 'Test Sub',
452
+ rendered: 'Test Sub',
453
+ },
454
+ id: 742,
455
+ parent: 229,
456
+ link: 'http://wordpress.local/test/test-sub/',
457
+ type: 'page',
458
+ },
459
+ {
460
+ title: {
461
+ raw: 'Test Sub Sub',
462
+ rendered: 'Test Sub Sub',
463
+ },
464
+ id: 744,
465
+ parent: 742,
466
+ link: 'http://wordpress.local/test/test-sub/test-sub-sub/',
467
+ type: 'page',
468
+ },
469
+ ];
470
+
471
+ const convertLinksWithParentOneLevel = convertToNavigationLinks(
472
+ pages,
473
+ 34
474
+ );
475
+
476
+ expect( convertLinksWithParentOneLevel ).toEqual( [
477
+ {
478
+ attributes: {
479
+ id: 738,
480
+ kind: 'post-type',
481
+ label: 'About Sub 1',
482
+ type: 'page',
483
+ url: 'http://wordpress.local/about/about-sub-1/',
484
+ },
485
+ innerBlocks: [],
486
+ name: 'core/navigation-link',
487
+ },
488
+ {
489
+ attributes: {
490
+ id: 740,
491
+ kind: 'post-type',
492
+ label: 'About Sub 2',
493
+ type: 'page',
494
+ url: 'http://wordpress.local/about/about-sub-2/',
495
+ },
496
+ innerBlocks: [],
497
+ name: 'core/navigation-link',
498
+ },
499
+ ] );
500
+
501
+ const convertLinksWithParentTwoLevels = convertToNavigationLinks(
502
+ pages,
503
+ 742
504
+ );
505
+
506
+ expect( convertLinksWithParentTwoLevels ).toEqual( [
507
+ {
508
+ attributes: {
509
+ id: 744,
510
+ kind: 'post-type',
511
+ label: 'Test Sub Sub',
512
+ type: 'page',
513
+ url: 'http://wordpress.local/test/test-sub/test-sub-sub/',
514
+ },
515
+ innerBlocks: [],
516
+ name: 'core/navigation-link',
517
+ },
518
+ ] );
519
+ } );
386
520
  } );
387
521
  } );
@@ -5,7 +5,14 @@ import { createBlock } from '@wordpress/blocks';
5
5
  import { useSelect, useDispatch } from '@wordpress/data';
6
6
  import { store as blockEditorStore } from '@wordpress/block-editor';
7
7
 
8
- export function convertToNavigationLinks( pages = [] ) {
8
+ /**
9
+ * Converts an array of pages into a nested array of navigation link blocks.
10
+ *
11
+ * @param {Array} pages An array of pages.
12
+ *
13
+ * @return {Array} A nested array of navigation link blocks.
14
+ */
15
+ function createNavigationLinks( pages = [] ) {
9
16
  const linkMap = {};
10
17
  const navigationLinks = [];
11
18
  pages.forEach( ( { id, title, link: url, type, parent } ) => {
@@ -30,11 +37,61 @@ export function convertToNavigationLinks( pages = [] ) {
30
37
  // Use a placeholder if the child appears before parent in list.
31
38
  linkMap[ parent ] = { innerBlocks: [] };
32
39
  }
40
+ // Although these variables are not referenced, they are needed to store the innerBlocks in memory.
33
41
  const parentLinkInnerBlocks = linkMap[ parent ].innerBlocks;
34
42
  parentLinkInnerBlocks.push( linkMap[ id ] );
35
43
  }
36
44
  } );
37
45
 
46
+ return navigationLinks;
47
+ }
48
+
49
+ /**
50
+ * Finds a navigation link block by id, recursively.
51
+ * It might be possible to make this a more generic helper function.
52
+ *
53
+ * @param {Array} navigationLinks An array of navigation link blocks.
54
+ * @param {number} id The id of the navigation link to find.
55
+ *
56
+ * @return {Object|null} The navigation link block with the given id.
57
+ */
58
+ function findNavigationLinkById( navigationLinks, id ) {
59
+ for ( const navigationLink of navigationLinks ) {
60
+ // Is this the link we're looking for?
61
+ if ( navigationLink.attributes.id === id ) {
62
+ return navigationLink;
63
+ }
64
+
65
+ // If not does it have innerBlocks?
66
+ if ( navigationLink.innerBlocks && navigationLink.innerBlocks.length ) {
67
+ const foundNavigationLink = findNavigationLinkById(
68
+ navigationLink.innerBlocks,
69
+ id
70
+ );
71
+
72
+ if ( foundNavigationLink ) {
73
+ return foundNavigationLink;
74
+ }
75
+ }
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ export function convertToNavigationLinks( pages = [], parentPageID = null ) {
82
+ let navigationLinks = createNavigationLinks( pages );
83
+
84
+ // If a parent page ID is provided, only return the children of that page.
85
+ if ( parentPageID ) {
86
+ const parentPage = findNavigationLinkById(
87
+ navigationLinks,
88
+ parentPageID
89
+ );
90
+ if ( parentPage && parentPage.innerBlocks ) {
91
+ navigationLinks = parentPage.innerBlocks;
92
+ }
93
+ }
94
+
38
95
  // Transform all links with innerBlocks into Submenus. This can't be done
39
96
  // sooner because page objects have no information on their children.
40
97
  const transformSubmenus = ( listOfLinks ) => {
@@ -53,11 +110,14 @@ export function convertToNavigationLinks( pages = [] ) {
53
110
  };
54
111
 
55
112
  transformSubmenus( navigationLinks );
56
-
57
113
  return navigationLinks;
58
114
  }
59
115
 
60
- export function useConvertToNavigationLinks( { clientId, pages } ) {
116
+ export function useConvertToNavigationLinks( {
117
+ clientId,
118
+ pages,
119
+ parentPageID,
120
+ } ) {
61
121
  const { replaceBlock, selectBlock } = useDispatch( blockEditorStore );
62
122
 
63
123
  const { parentNavBlockClientId } = useSelect(
@@ -79,7 +139,7 @@ export function useConvertToNavigationLinks( { clientId, pages } ) {
79
139
  );
80
140
 
81
141
  return () => {
82
- const navigationLinks = convertToNavigationLinks( pages );
142
+ const navigationLinks = convertToNavigationLinks( pages, parentPageID );
83
143
 
84
144
  // Replace the Page List block with the Navigation Links.
85
145
  replaceBlock( clientId, navigationLinks );
@@ -16,6 +16,10 @@
16
16
  "showMoreOnNewLine": {
17
17
  "type": "boolean",
18
18
  "default": true
19
+ },
20
+ "excerptLength": {
21
+ "type": "number",
22
+ "default": 55
19
23
  }
20
24
  },
21
25
  "usesContext": [ "postId", "postType", "queryId" ],
@@ -16,8 +16,8 @@ import {
16
16
  Warning,
17
17
  useBlockProps,
18
18
  } from '@wordpress/block-editor';
19
- import { PanelBody, ToggleControl } from '@wordpress/components';
20
- import { __ } from '@wordpress/i18n';
19
+ import { PanelBody, ToggleControl, RangeControl } from '@wordpress/components';
20
+ import { __, _x } from '@wordpress/i18n';
21
21
 
22
22
  /**
23
23
  * Internal dependencies
@@ -25,7 +25,7 @@ import { __ } from '@wordpress/i18n';
25
25
  import { useCanEditEntity } from '../utils/hooks';
26
26
 
27
27
  export default function PostExcerptEditor( {
28
- attributes: { textAlign, moreText, showMoreOnNewLine },
28
+ attributes: { textAlign, moreText, showMoreOnNewLine, excerptLength },
29
29
  setAttributes,
30
30
  isSelected,
31
31
  context: { postId, postType, queryId },
@@ -33,6 +33,7 @@ export default function PostExcerptEditor( {
33
33
  const isDescendentOfQueryLoop = Number.isFinite( queryId );
34
34
  const userCanEdit = useCanEditEntity( 'postType', postType, postId );
35
35
  const isEditable = userCanEdit && ! isDescendentOfQueryLoop;
36
+
36
37
  const [
37
38
  rawExcerpt,
38
39
  setExcerpt,
@@ -43,6 +44,14 @@ export default function PostExcerptEditor( {
43
44
  [ `has-text-align-${ textAlign }` ]: textAlign,
44
45
  } ),
45
46
  } );
47
+
48
+ /**
49
+ * translators: If your word count is based on single characters (e.g. East Asian characters),
50
+ * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
51
+ * Do not translate into your own language.
52
+ */
53
+ const wordCountType = _x( 'words', 'Word count type. Do not translate!' );
54
+
46
55
  /**
47
56
  * When excerpt is editable, strip the html tags from
48
57
  * rendered excerpt. This will be used if the entity's
@@ -109,21 +118,67 @@ export default function PostExcerptEditor( {
109
118
  const excerptClassName = classnames( 'wp-block-post-excerpt__excerpt', {
110
119
  'is-inline': ! showMoreOnNewLine,
111
120
  } );
121
+
122
+ /**
123
+ * The excerpt length setting needs to be applied to both
124
+ * the raw and the rendered excerpt depending on which is being used.
125
+ */
126
+ const rawOrRenderedExcerpt = !! renderedExcerpt
127
+ ? strippedRenderedExcerpt
128
+ : rawExcerpt;
129
+
130
+ let trimmedExcerpt = '';
131
+ if ( wordCountType === 'words' ) {
132
+ trimmedExcerpt = rawOrRenderedExcerpt
133
+ .trim()
134
+ .split( ' ', excerptLength )
135
+ .join( ' ' );
136
+ } else if ( wordCountType === 'characters_excluding_spaces' ) {
137
+ /*
138
+ * 1. Split the excerpt at the character limit,
139
+ * then join the substrings back into one string.
140
+ * 2. Count the number of spaces in the excerpt
141
+ * by comparing the lengths of the string with and without spaces.
142
+ * 3. Add the number to the length of the visible excerpt,
143
+ * so that the spaces are excluded from the word count.
144
+ */
145
+ const excerptWithSpaces = rawOrRenderedExcerpt
146
+ .trim()
147
+ .split( '', excerptLength )
148
+ .join( '' );
149
+
150
+ const numberOfSpaces =
151
+ excerptWithSpaces.length -
152
+ excerptWithSpaces.replaceAll( ' ', '' ).length;
153
+
154
+ trimmedExcerpt = rawOrRenderedExcerpt
155
+ .trim()
156
+ .split( '', excerptLength + numberOfSpaces )
157
+ .join( '' );
158
+ } else if ( wordCountType === 'characters_including_spaces' ) {
159
+ trimmedExcerpt = rawOrRenderedExcerpt.trim().split( '', excerptLength );
160
+ }
161
+
162
+ trimmedExcerpt = trimmedExcerpt + '...';
163
+
112
164
  const excerptContent = isEditable ? (
113
165
  <RichText
114
166
  className={ excerptClassName }
115
167
  aria-label={ __( 'Post excerpt text' ) }
116
168
  value={
117
- rawExcerpt ||
118
- strippedRenderedExcerpt ||
119
- ( isSelected ? '' : __( 'No post excerpt found' ) )
169
+ isSelected
170
+ ? rawOrRenderedExcerpt
171
+ : ( trimmedExcerpt !== '...' ? trimmedExcerpt : '' ) ||
172
+ __( 'No post excerpt found' )
120
173
  }
121
174
  onChange={ setExcerpt }
122
175
  tagName="p"
123
176
  />
124
177
  ) : (
125
178
  <p className={ excerptClassName }>
126
- { strippedRenderedExcerpt || __( 'No post excerpt found' ) }
179
+ { trimmedExcerpt !== '...'
180
+ ? trimmedExcerpt
181
+ : __( 'No post excerpt found' ) }
127
182
  </p>
128
183
  );
129
184
  return (
@@ -147,6 +202,16 @@ export default function PostExcerptEditor( {
147
202
  } )
148
203
  }
149
204
  />
205
+ <RangeControl
206
+ label={ __( 'Max number of words' ) }
207
+ value={ excerptLength }
208
+ onChange={ ( value ) => {
209
+ setAttributes( { excerptLength: value } );
210
+ setExcerpt();
211
+ } }
212
+ min="10"
213
+ max="100"
214
+ />
150
215
  </PanelBody>
151
216
  </InspectorControls>
152
217
  <div { ...blockProps }>
@@ -18,12 +18,18 @@ function render_block_core_post_excerpt( $attributes, $content, $block ) {
18
18
  return '';
19
19
  }
20
20
 
21
- $excerpt = get_the_excerpt();
22
-
23
- if ( empty( $excerpt ) ) {
24
- return '';
21
+ /*
22
+ * The purpose of the excerpt length setting is to limit the length of both
23
+ * automatically generated and user-created excerpts.
24
+ * Because the excerpt_length filter only applies to auto generated excerpts,
25
+ * wp_trim_words is used instead.
26
+ */
27
+ $excerpt_length = $attributes['excerptLength'];
28
+ if ( isset( $excerpt_length ) ) {
29
+ $excerpt = wp_trim_words( get_the_excerpt(), $excerpt_length );
30
+ } else {
31
+ $excerpt = get_the_excerpt();
25
32
  }
26
-
27
33
  $more_text = ! empty( $attributes['moreText'] ) ? '<a class="wp-block-post-excerpt__more-link" href="' . esc_url( get_the_permalink( $block->context['postId'] ) ) . '">' . wp_kses_post( $attributes['moreText'] ) . '</a>' : '';
28
34
  $filter_excerpt_more = function( $more ) use ( $more_text ) {
29
35
  return empty( $more_text ) ? $more : '';
@@ -70,3 +76,21 @@ function register_block_core_post_excerpt() {
70
76
  );
71
77
  }
72
78
  add_action( 'init', 'register_block_core_post_excerpt' );
79
+
80
+ /**
81
+ * If themes or plugins filter the excerpt_length, we need to
82
+ * override the filter in the editor, otherwise
83
+ * the excerpt length block setting has no effect.
84
+ * Returns 100 because 100 is the max length in the setting.
85
+ */
86
+ if ( is_admin() ||
87
+ defined( 'REST_REQUEST' ) ||
88
+ 'REST_REQUEST' ) {
89
+ add_filter(
90
+ 'excerpt_length',
91
+ function() {
92
+ return 100;
93
+ },
94
+ PHP_INT_MAX
95
+ );
96
+ }
@@ -11,6 +11,9 @@
11
11
  "type": "boolean",
12
12
  "default": false
13
13
  },
14
+ "aspectRatio": {
15
+ "type": "string"
16
+ },
14
17
  "width": {
15
18
  "type": "string"
16
19
  },