@wordpress/block-library 8.3.3 → 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 (161) 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/file/index.js +10 -1
  7. package/build/file/index.js.map +1 -1
  8. package/build/image/image.js +12 -11
  9. package/build/image/image.js.map +1 -1
  10. package/build/latest-comments/edit.js +6 -2
  11. package/build/latest-comments/edit.js.map +1 -1
  12. package/build/latest-comments/index.js +13 -0
  13. package/build/latest-comments/index.js.map +1 -1
  14. package/build/navigation/edit/menu-inspector-controls.js +3 -4
  15. package/build/navigation/edit/menu-inspector-controls.js.map +1 -1
  16. package/build/navigation/edit/navigation-menu-selector.js +14 -11
  17. package/build/navigation/edit/navigation-menu-selector.js.map +1 -1
  18. package/build/navigation/use-navigation-menu.js +1 -1
  19. package/build/navigation/use-navigation-menu.js.map +1 -1
  20. package/build/navigation-link/edit.js +4 -0
  21. package/build/navigation-link/edit.js.map +1 -1
  22. package/build/navigation-link/link-ui.js +1 -0
  23. package/build/navigation-link/link-ui.js.map +1 -1
  24. package/build/navigation-submenu/edit.js +4 -0
  25. package/build/navigation-submenu/edit.js.map +1 -1
  26. package/build/page-list/edit.js +5 -4
  27. package/build/page-list/edit.js.map +1 -1
  28. package/build/page-list/use-convert-to-navigation-links.js +61 -5
  29. package/build/page-list/use-convert-to-navigation-links.js.map +1 -1
  30. package/build/post-excerpt/edit.js +49 -3
  31. package/build/post-excerpt/edit.js.map +1 -1
  32. package/build/post-excerpt/index.js +4 -0
  33. package/build/post-excerpt/index.js.map +1 -1
  34. package/build/post-featured-image/dimension-controls.js +52 -1
  35. package/build/post-featured-image/dimension-controls.js.map +1 -1
  36. package/build/post-featured-image/edit.js +9 -4
  37. package/build/post-featured-image/edit.js.map +1 -1
  38. package/build/post-featured-image/index.js +3 -0
  39. package/build/post-featured-image/index.js.map +1 -1
  40. package/build/site-logo/edit.js +7 -11
  41. package/build/site-logo/edit.js.map +1 -1
  42. package/build/table/edit.js +3 -3
  43. package/build/table/edit.js.map +1 -1
  44. package/build/table-of-contents/utils.js +1 -1
  45. package/build/table-of-contents/utils.js.map +1 -1
  46. package/build/verse/index.js +6 -0
  47. package/build/verse/index.js.map +1 -1
  48. package/build-module/button/edit.js +2 -1
  49. package/build-module/button/edit.js.map +1 -1
  50. package/build-module/button/index.js +17 -6
  51. package/build-module/button/index.js.map +1 -1
  52. package/build-module/file/index.js +10 -1
  53. package/build-module/file/index.js.map +1 -1
  54. package/build-module/image/image.js +12 -11
  55. package/build-module/image/image.js.map +1 -1
  56. package/build-module/latest-comments/edit.js +6 -2
  57. package/build-module/latest-comments/edit.js.map +1 -1
  58. package/build-module/latest-comments/index.js +13 -0
  59. package/build-module/latest-comments/index.js.map +1 -1
  60. package/build-module/navigation/edit/menu-inspector-controls.js +2 -2
  61. package/build-module/navigation/edit/menu-inspector-controls.js.map +1 -1
  62. package/build-module/navigation/edit/navigation-menu-selector.js +14 -10
  63. package/build-module/navigation/edit/navigation-menu-selector.js.map +1 -1
  64. package/build-module/navigation/use-navigation-menu.js +1 -1
  65. package/build-module/navigation/use-navigation-menu.js.map +1 -1
  66. package/build-module/navigation-link/edit.js +4 -0
  67. package/build-module/navigation-link/edit.js.map +1 -1
  68. package/build-module/navigation-link/link-ui.js +1 -0
  69. package/build-module/navigation-link/link-ui.js.map +1 -1
  70. package/build-module/navigation-submenu/edit.js +4 -0
  71. package/build-module/navigation-submenu/edit.js.map +1 -1
  72. package/build-module/page-list/edit.js +5 -4
  73. package/build-module/page-list/edit.js.map +1 -1
  74. package/build-module/page-list/use-convert-to-navigation-links.js +61 -5
  75. package/build-module/page-list/use-convert-to-navigation-links.js.map +1 -1
  76. package/build-module/post-excerpt/edit.js +52 -5
  77. package/build-module/post-excerpt/edit.js.map +1 -1
  78. package/build-module/post-excerpt/index.js +4 -0
  79. package/build-module/post-excerpt/index.js.map +1 -1
  80. package/build-module/post-featured-image/dimension-controls.js +52 -1
  81. package/build-module/post-featured-image/dimension-controls.js.map +1 -1
  82. package/build-module/post-featured-image/edit.js +9 -4
  83. package/build-module/post-featured-image/edit.js.map +1 -1
  84. package/build-module/post-featured-image/index.js +3 -0
  85. package/build-module/post-featured-image/index.js.map +1 -1
  86. package/build-module/site-logo/edit.js +7 -11
  87. package/build-module/site-logo/edit.js.map +1 -1
  88. package/build-module/table/edit.js +3 -3
  89. package/build-module/table/edit.js.map +1 -1
  90. package/build-module/table-of-contents/utils.js +1 -1
  91. package/build-module/table-of-contents/utils.js.map +1 -1
  92. package/build-module/verse/index.js +6 -0
  93. package/build-module/verse/index.js.map +1 -1
  94. package/build-style/avatar/style-rtl.css +3 -0
  95. package/build-style/avatar/style.css +3 -0
  96. package/build-style/button/editor-rtl.css +31 -0
  97. package/build-style/button/editor.css +31 -0
  98. package/build-style/button/style-rtl.css +31 -0
  99. package/build-style/button/style.css +31 -0
  100. package/build-style/editor-rtl.css +32 -0
  101. package/build-style/editor.css +32 -0
  102. package/build-style/file/style-rtl.css +1 -0
  103. package/build-style/file/style.css +1 -0
  104. package/build-style/image/editor-rtl.css +1 -0
  105. package/build-style/image/editor.css +1 -0
  106. package/build-style/image/style-rtl.css +6 -2
  107. package/build-style/image/style.css +6 -0
  108. package/build-style/latest-comments/style-rtl.css +18 -5
  109. package/build-style/latest-comments/style.css +18 -5
  110. package/build-style/quote/style-rtl.css +5 -5
  111. package/build-style/quote/style.css +5 -5
  112. package/build-style/style-rtl.css +64 -12
  113. package/build-style/style.css +64 -10
  114. package/build-types/table-of-contents/utils.d.ts +1 -1
  115. package/package.json +30 -30
  116. package/src/avatar/index.php +67 -63
  117. package/src/avatar/style.scss +3 -0
  118. package/src/button/block.json +17 -6
  119. package/src/button/edit.js +2 -1
  120. package/src/button/editor.scss +36 -0
  121. package/src/button/style.scss +37 -1
  122. package/src/file/block.json +10 -1
  123. package/src/file/style.scss +1 -0
  124. package/src/image/editor.scss +1 -0
  125. package/src/image/image.js +5 -11
  126. package/src/image/style.scss +13 -0
  127. package/src/latest-comments/block.json +13 -0
  128. package/src/latest-comments/edit.js +9 -2
  129. package/src/latest-comments/style.scss +25 -7
  130. package/src/navigation/edit/menu-inspector-controls.js +1 -3
  131. package/src/navigation/edit/navigation-menu-selector.js +12 -26
  132. package/src/navigation/edit/test/navigation-menu-selector.js +638 -0
  133. package/src/navigation/index.php +8 -6
  134. package/src/navigation/use-navigation-menu.js +1 -1
  135. package/src/navigation-link/edit.js +3 -0
  136. package/src/navigation-link/link-ui.js +1 -0
  137. package/src/navigation-submenu/edit.js +3 -0
  138. package/src/page-list/edit.js +6 -5
  139. package/src/page-list/index.php +4 -4
  140. package/src/page-list/test/convert-to-links-modal.js +134 -0
  141. package/src/page-list/use-convert-to-navigation-links.js +64 -4
  142. package/src/post-excerpt/block.json +4 -0
  143. package/src/post-excerpt/edit.js +72 -7
  144. package/src/post-excerpt/index.php +29 -5
  145. package/src/post-featured-image/block.json +3 -0
  146. package/src/post-featured-image/dimension-controls.js +64 -2
  147. package/src/post-featured-image/edit.js +18 -6
  148. package/src/post-featured-image/index.php +25 -9
  149. package/src/post-title/index.php +3 -3
  150. package/src/quote/style.scss +2 -2
  151. package/src/site-logo/edit.js +3 -6
  152. package/src/table/edit.js +3 -3
  153. package/src/table-of-contents/utils.ts +1 -1
  154. package/src/template-part/index.php +1 -1
  155. package/src/verse/block.json +6 -0
  156. package/tsconfig.tsbuildinfo +1 -1
  157. package/build/navigation/leaf-more-menu.js +0 -95
  158. package/build/navigation/leaf-more-menu.js.map +0 -1
  159. package/build-module/navigation/leaf-more-menu.js +0 -76
  160. package/build-module/navigation/leaf-more-menu.js.map +0 -1
  161. package/src/navigation/leaf-more-menu.js +0 -93
@@ -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
  },
@@ -49,7 +49,7 @@ const scaleHelp = {
49
49
 
50
50
  const DimensionControls = ( {
51
51
  clientId,
52
- attributes: { width, height, scale, sizeSlug },
52
+ attributes: { aspectRatio, width, height, scale, sizeSlug },
53
53
  setAttributes,
54
54
  imageSizeOptions = [],
55
55
  } ) => {
@@ -72,6 +72,68 @@ const DimensionControls = ( {
72
72
  const scaleLabel = _x( 'Scale', 'Image scaling options' );
73
73
  return (
74
74
  <InspectorControls group="dimensions">
75
+ <ToolsPanelItem
76
+ hasValue={ () => !! aspectRatio }
77
+ label={ __( 'Aspect ratio' ) }
78
+ onDeselect={ () => setAttributes( { aspectRatio: undefined } ) }
79
+ resetAllFilter={ () => ( {
80
+ aspectRatio: undefined,
81
+ } ) }
82
+ isShownByDefault={ true }
83
+ panelId={ clientId }
84
+ >
85
+ <SelectControl
86
+ __nextHasNoMarginBottom
87
+ label={ __( 'Aspect ratio' ) }
88
+ value={ aspectRatio }
89
+ options={ [
90
+ // These should use the same values as AspectRatioDropdown in @wordpress/block-editor
91
+ {
92
+ label: __( 'Original' ),
93
+ value: 'auto',
94
+ },
95
+ {
96
+ label: __( 'Square' ),
97
+ value: '1',
98
+ },
99
+ {
100
+ label: __( '16:10' ),
101
+ value: '16/10',
102
+ },
103
+ {
104
+ label: __( '16:9' ),
105
+ value: '16/9',
106
+ },
107
+ {
108
+ label: __( '4:3' ),
109
+ value: '4/3',
110
+ },
111
+ {
112
+ label: __( '3:2' ),
113
+ value: '3/2',
114
+ },
115
+ {
116
+ label: __( '10:16' ),
117
+ value: '10/16',
118
+ },
119
+ {
120
+ label: __( '9:16' ),
121
+ value: '9/16',
122
+ },
123
+ {
124
+ label: __( '3:4' ),
125
+ value: '3/4',
126
+ },
127
+ {
128
+ label: __( '2:3' ),
129
+ value: '2/3',
130
+ },
131
+ ] }
132
+ onChange={ ( nextAspectRatio ) =>
133
+ setAttributes( { aspectRatio: nextAspectRatio } )
134
+ }
135
+ />
136
+ </ToolsPanelItem>
75
137
  <ToolsPanelItem
76
138
  className="single-column"
77
139
  hasValue={ () => !! height }
@@ -116,7 +178,7 @@ const DimensionControls = ( {
116
178
  units={ units }
117
179
  />
118
180
  </ToolsPanelItem>
119
- { !! height && (
181
+ { ( height || aspectRatio ) && (
120
182
  <ToolsPanelItem
121
183
  hasValue={ () => !! scale && scale !== DEFAULT_SCALE }
122
184
  label={ scaleLabel }
@@ -50,8 +50,16 @@ export default function PostFeaturedImageEdit( {
50
50
  context: { postId, postType: postTypeSlug, queryId },
51
51
  } ) {
52
52
  const isDescendentOfQueryLoop = Number.isFinite( queryId );
53
- const { isLink, height, width, scale, sizeSlug, rel, linkTarget } =
54
- attributes;
53
+ const {
54
+ isLink,
55
+ aspectRatio,
56
+ height,
57
+ width,
58
+ scale,
59
+ sizeSlug,
60
+ rel,
61
+ linkTarget,
62
+ } = attributes;
55
63
  const [ featuredImage, setFeaturedImage ] = useEntityProp(
56
64
  'postType',
57
65
  postTypeSlug,
@@ -89,7 +97,7 @@ export default function PostFeaturedImageEdit( {
89
97
  } ) );
90
98
 
91
99
  const blockProps = useBlockProps( {
92
- style: { width, height },
100
+ style: { width, height, aspectRatio },
93
101
  } );
94
102
  const borderProps = useBorderProps( attributes );
95
103
 
@@ -101,7 +109,10 @@ export default function PostFeaturedImageEdit( {
101
109
  borderProps.className
102
110
  ) }
103
111
  withIllustration={ true }
104
- style={ borderProps.style }
112
+ style={ {
113
+ ...blockProps.style,
114
+ ...borderProps.style,
115
+ } }
105
116
  >
106
117
  { content }
107
118
  </Placeholder>
@@ -218,8 +229,9 @@ export default function PostFeaturedImageEdit( {
218
229
  const label = __( 'Add a featured image' );
219
230
  const imageStyles = {
220
231
  ...borderProps.style,
221
- height,
222
- objectFit: height && scale,
232
+ height: ( !! aspectRatio && '100%' ) || height,
233
+ width: !! aspectRatio && '100%',
234
+ objectFit: !! ( height || aspectRatio ) && scale,
223
235
  };
224
236
 
225
237
  /**
@@ -42,11 +42,20 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
42
42
  }
43
43
  }
44
44
 
45
- if ( ! empty( $attributes['height'] ) ) {
46
- $extra_styles = "height:{$attributes['height']};";
47
- if ( ! empty( $attributes['scale'] ) ) {
48
- $extra_styles .= "object-fit:{$attributes['scale']};";
49
- }
45
+ $extra_styles = '';
46
+
47
+ // Aspect ratio with a height set needs to override the default width/height.
48
+ if ( ! empty( $attributes['aspectRatio'] ) ) {
49
+ $extra_styles .= 'width:100%;height:100%;';
50
+ } elseif ( ! empty( $attributes['height'] ) ) {
51
+ $extra_styles .= "height:{$attributes['height']};";
52
+ }
53
+
54
+ if ( ! empty( $attributes['scale'] ) ) {
55
+ $extra_styles .= "object-fit:{$attributes['scale']};";
56
+ }
57
+
58
+ if ( ! empty( $extra_styles ) ) {
50
59
  $attr['style'] = empty( $attr['style'] ) ? $extra_styles : $attr['style'] . $extra_styles;
51
60
  }
52
61
 
@@ -71,12 +80,19 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
71
80
  $featured_image = $featured_image . $overlay_markup;
72
81
  }
73
82
 
74
- $width = ! empty( $attributes['width'] ) ? esc_attr( safecss_filter_attr( 'width:' . $attributes['width'] ) ) . ';' : '';
75
- $height = ! empty( $attributes['height'] ) ? esc_attr( safecss_filter_attr( 'height:' . $attributes['height'] ) ) . ';' : '';
76
- if ( ! $height && ! $width ) {
83
+ $aspect_ratio = ! empty( $attributes['aspectRatio'] )
84
+ ? esc_attr( safecss_filter_attr( 'aspect-ratio:' . $attributes['aspectRatio'] ) ) . ';'
85
+ : '';
86
+ $width = ! empty( $attributes['width'] )
87
+ ? esc_attr( safecss_filter_attr( 'width:' . $attributes['width'] ) ) . ';'
88
+ : '';
89
+ $height = ! empty( $attributes['height'] )
90
+ ? esc_attr( safecss_filter_attr( 'height:' . $attributes['height'] ) ) . ';'
91
+ : '';
92
+ if ( ! $height && ! $width && ! $aspect_ratio ) {
77
93
  $wrapper_attributes = get_block_wrapper_attributes();
78
94
  } else {
79
- $wrapper_attributes = get_block_wrapper_attributes( array( 'style' => $width . $height ) );
95
+ $wrapper_attributes = get_block_wrapper_attributes( array( 'style' => $aspect_ratio . $width . $height ) );
80
96
  }
81
97
  return "<figure {$wrapper_attributes}>{$featured_image}</figure>";
82
98
  }