@wordpress/editor 14.21.0 → 14.23.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 (67) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/build/components/post-actions/set-as-homepage.js +1 -0
  3. package/build/components/post-actions/set-as-homepage.js.map +1 -1
  4. package/build/components/post-actions/set-as-posts-page.js +1 -0
  5. package/build/components/post-actions/set-as-posts-page.js.map +1 -1
  6. package/build/components/post-author/check.js +3 -7
  7. package/build/components/post-author/check.js.map +1 -1
  8. package/build/components/post-author/panel.js +10 -2
  9. package/build/components/post-author/panel.js.map +1 -1
  10. package/build/components/post-template/create-new-template-modal.js +6 -2
  11. package/build/components/post-template/create-new-template-modal.js.map +1 -1
  12. package/build/components/post-template/swap-template-button.js +18 -4
  13. package/build/components/post-template/swap-template-button.js.map +1 -1
  14. package/build/components/start-page-options/index.js +2 -1
  15. package/build/components/start-page-options/index.js.map +1 -1
  16. package/build/components/visual-editor/index.js +1 -2
  17. package/build/components/visual-editor/index.js.map +1 -1
  18. package/build/utils/search-templates.js +75 -0
  19. package/build/utils/search-templates.js.map +1 -0
  20. package/build-module/components/post-actions/set-as-homepage.js +1 -0
  21. package/build-module/components/post-actions/set-as-homepage.js.map +1 -1
  22. package/build-module/components/post-actions/set-as-posts-page.js +1 -0
  23. package/build-module/components/post-actions/set-as-posts-page.js.map +1 -1
  24. package/build-module/components/post-author/check.js +3 -7
  25. package/build-module/components/post-author/check.js.map +1 -1
  26. package/build-module/components/post-author/panel.js +10 -2
  27. package/build-module/components/post-author/panel.js.map +1 -1
  28. package/build-module/components/post-template/create-new-template-modal.js +6 -2
  29. package/build-module/components/post-template/create-new-template-modal.js.map +1 -1
  30. package/build-module/components/post-template/swap-template-button.js +19 -5
  31. package/build-module/components/post-template/swap-template-button.js.map +1 -1
  32. package/build-module/components/start-page-options/index.js +2 -1
  33. package/build-module/components/start-page-options/index.js.map +1 -1
  34. package/build-module/components/visual-editor/index.js +1 -2
  35. package/build-module/components/visual-editor/index.js.map +1 -1
  36. package/build-module/utils/search-templates.js +68 -0
  37. package/build-module/utils/search-templates.js.map +1 -0
  38. package/build-style/style-rtl.css +14 -18
  39. package/build-style/style.css +14 -18
  40. package/build-types/components/post-actions/set-as-homepage.d.ts +1 -0
  41. package/build-types/components/post-actions/set-as-homepage.d.ts.map +1 -1
  42. package/build-types/components/post-actions/set-as-posts-page.d.ts +1 -0
  43. package/build-types/components/post-actions/set-as-posts-page.d.ts.map +1 -1
  44. package/build-types/components/post-author/check.d.ts.map +1 -1
  45. package/build-types/components/post-author/panel.d.ts.map +1 -1
  46. package/build-types/components/post-template/create-new-template-modal.d.ts.map +1 -1
  47. package/build-types/components/post-template/swap-template-button.d.ts.map +1 -1
  48. package/build-types/components/start-page-options/index.d.ts.map +1 -1
  49. package/build-types/components/visual-editor/index.d.ts.map +1 -1
  50. package/build-types/utils/search-templates.d.ts +10 -0
  51. package/build-types/utils/search-templates.d.ts.map +1 -0
  52. package/package.json +37 -37
  53. package/src/components/editor-interface/style.scss +1 -2
  54. package/src/components/post-actions/set-as-homepage.js +1 -0
  55. package/src/components/post-actions/set-as-posts-page.js +1 -0
  56. package/src/components/post-author/check.js +2 -7
  57. package/src/components/post-author/panel.js +10 -2
  58. package/src/components/post-author/test/check.js +3 -11
  59. package/src/components/post-publish-panel/style.scss +1 -0
  60. package/src/components/post-template/create-new-template-modal.js +6 -2
  61. package/src/components/post-template/style.scss +10 -0
  62. package/src/components/post-template/swap-template-button.js +23 -6
  63. package/src/components/start-page-options/index.js +3 -1
  64. package/src/components/visual-editor/index.js +0 -1
  65. package/src/components/visual-editor/style.scss +6 -29
  66. package/src/utils/search-templates.js +77 -0
  67. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Filters a template list given a search term.
3
+ *
4
+ * @param {Array} templates Item list
5
+ * @param {string} searchValue Search input.
6
+ *
7
+ * @return {Array} Filtered template list.
8
+ */
9
+ export function searchTemplates(templates?: any[], searchValue?: string): any[];
10
+ //# sourceMappingURL=search-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-templates.d.ts","sourceRoot":"","sources":["../../src/utils/search-templates.js"],"names":[],"mappings":"AAuDA;;;;;;;GAOG;AACH,iEAJW,MAAM,SAiBhB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/editor",
3
- "version": "14.21.0",
3
+ "version": "14.23.0",
4
4
  "description": "Enhanced block editor for WordPress posts.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -34,41 +34,41 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@babel/runtime": "7.25.7",
37
- "@wordpress/a11y": "^4.21.0",
38
- "@wordpress/api-fetch": "^7.21.0",
39
- "@wordpress/blob": "^4.21.0",
40
- "@wordpress/block-editor": "^14.16.0",
41
- "@wordpress/blocks": "^14.10.0",
42
- "@wordpress/commands": "^1.21.0",
43
- "@wordpress/components": "^29.7.0",
44
- "@wordpress/compose": "^7.21.0",
45
- "@wordpress/core-data": "^7.21.0",
46
- "@wordpress/data": "^10.21.0",
47
- "@wordpress/dataviews": "^4.17.0",
48
- "@wordpress/date": "^5.21.0",
49
- "@wordpress/deprecated": "^4.21.0",
50
- "@wordpress/dom": "^4.21.0",
51
- "@wordpress/element": "^6.21.0",
52
- "@wordpress/fields": "^0.13.0",
53
- "@wordpress/hooks": "^4.21.0",
54
- "@wordpress/html-entities": "^4.21.0",
55
- "@wordpress/i18n": "^5.21.0",
56
- "@wordpress/icons": "^10.21.0",
57
- "@wordpress/interface": "^9.6.0",
58
- "@wordpress/keyboard-shortcuts": "^5.21.0",
59
- "@wordpress/keycodes": "^4.21.0",
60
- "@wordpress/media-utils": "^5.21.0",
61
- "@wordpress/notices": "^5.21.0",
62
- "@wordpress/patterns": "^2.21.0",
63
- "@wordpress/plugins": "^7.21.0",
64
- "@wordpress/preferences": "^4.21.0",
65
- "@wordpress/private-apis": "^1.21.0",
66
- "@wordpress/reusable-blocks": "^5.21.0",
67
- "@wordpress/rich-text": "^7.21.0",
68
- "@wordpress/server-side-render": "^5.21.0",
69
- "@wordpress/url": "^4.21.0",
70
- "@wordpress/warning": "^3.21.0",
71
- "@wordpress/wordcount": "^4.21.0",
37
+ "@wordpress/a11y": "^4.23.0",
38
+ "@wordpress/api-fetch": "^7.23.0",
39
+ "@wordpress/blob": "^4.23.0",
40
+ "@wordpress/block-editor": "^14.18.0",
41
+ "@wordpress/blocks": "^14.12.0",
42
+ "@wordpress/commands": "^1.23.0",
43
+ "@wordpress/components": "^29.9.0",
44
+ "@wordpress/compose": "^7.23.0",
45
+ "@wordpress/core-data": "^7.23.0",
46
+ "@wordpress/data": "^10.23.0",
47
+ "@wordpress/dataviews": "^4.19.0",
48
+ "@wordpress/date": "^5.23.0",
49
+ "@wordpress/deprecated": "^4.23.0",
50
+ "@wordpress/dom": "^4.23.0",
51
+ "@wordpress/element": "^6.23.0",
52
+ "@wordpress/fields": "^0.15.0",
53
+ "@wordpress/hooks": "^4.23.0",
54
+ "@wordpress/html-entities": "^4.23.0",
55
+ "@wordpress/i18n": "^5.23.0",
56
+ "@wordpress/icons": "^10.23.0",
57
+ "@wordpress/interface": "^9.8.0",
58
+ "@wordpress/keyboard-shortcuts": "^5.23.0",
59
+ "@wordpress/keycodes": "^4.23.0",
60
+ "@wordpress/media-utils": "^5.23.0",
61
+ "@wordpress/notices": "^5.23.0",
62
+ "@wordpress/patterns": "^2.23.0",
63
+ "@wordpress/plugins": "^7.23.0",
64
+ "@wordpress/preferences": "^4.23.0",
65
+ "@wordpress/private-apis": "^1.23.0",
66
+ "@wordpress/reusable-blocks": "^5.23.0",
67
+ "@wordpress/rich-text": "^7.23.0",
68
+ "@wordpress/server-side-render": "^5.23.0",
69
+ "@wordpress/url": "^4.23.0",
70
+ "@wordpress/warning": "^3.23.0",
71
+ "@wordpress/wordcount": "^4.23.0",
72
72
  "change-case": "^4.1.2",
73
73
  "client-zip": "^2.4.5",
74
74
  "clsx": "^2.1.1",
@@ -88,5 +88,5 @@
88
88
  "publishConfig": {
89
89
  "access": "public"
90
90
  },
91
- "gitHead": "104af00f9abcd7a4d36b87e648f148c72cc4ea5f"
91
+ "gitHead": "ab5c79cd40adbb68898536c50e035b0a734338ea"
92
92
  }
@@ -8,6 +8,5 @@
8
8
  }
9
9
 
10
10
  .editor-visual-editor {
11
- // Fits the height to the parent — flex-shrink ensures it doesn’t create overflow.
12
- flex: 1 1 0%;
11
+ flex: 1 0 auto;
13
12
  }
@@ -160,6 +160,7 @@ export const useSetAsHomepageAction = () => {
160
160
 
161
161
  return true;
162
162
  },
163
+ modalFocusOnMount: 'firstContentElement',
163
164
  RenderModal: SetAsHomepageModal,
164
165
  } ),
165
166
  [ pageForPosts, pageOnFront ]
@@ -157,6 +157,7 @@ export const useSetAsPostsPageAction = () => {
157
157
 
158
158
  return true;
159
159
  },
160
+ modalFocusOnMount: 'firstContentElement',
160
161
  RenderModal: SetAsPostsPageModal,
161
162
  } ),
162
163
  [ pageForPosts, pageOnFront ]
@@ -2,14 +2,12 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { useSelect } from '@wordpress/data';
5
- import { store as coreStore } from '@wordpress/core-data';
6
5
 
7
6
  /**
8
7
  * Internal dependencies
9
8
  */
10
9
  import PostTypeSupportCheck from '../post-type-support-check';
11
10
  import { store as editorStore } from '../../store';
12
- import { AUTHORS_QUERY } from './constants';
13
11
 
14
12
  /**
15
13
  * Wrapper component that renders its children only if the post type supports the author.
@@ -21,20 +19,17 @@ import { AUTHORS_QUERY } from './constants';
21
19
  * supports the author or if there are no authors available.
22
20
  */
23
21
  export default function PostAuthorCheck( { children } ) {
24
- const { hasAssignAuthorAction, hasAuthors } = useSelect( ( select ) => {
22
+ const { hasAssignAuthorAction } = useSelect( ( select ) => {
25
23
  const post = select( editorStore ).getCurrentPost();
26
24
  const canAssignAuthor = post?._links?.[ 'wp:action-assign-author' ]
27
25
  ? true
28
26
  : false;
29
27
  return {
30
28
  hasAssignAuthorAction: canAssignAuthor,
31
- hasAuthors: canAssignAuthor
32
- ? select( coreStore ).getUsers( AUTHORS_QUERY )?.length >= 1
33
- : false,
34
29
  };
35
30
  }, [] );
36
31
 
37
- if ( ! hasAssignAuthorAction || ! hasAuthors ) {
32
+ if ( ! hasAssignAuthorAction ) {
38
33
  return null;
39
34
  }
40
35
 
@@ -6,6 +6,8 @@ import { Button, Dropdown } from '@wordpress/components';
6
6
  import { useState, useMemo } from '@wordpress/element';
7
7
  import { decodeEntities } from '@wordpress/html-entities';
8
8
  import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor';
9
+ import { useSelect } from '@wordpress/data';
10
+ import { store as coreStore } from '@wordpress/core-data';
9
11
 
10
12
  /**
11
13
  * Internal dependencies
@@ -13,10 +15,16 @@ import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '
13
15
  import PostAuthorCheck from './check';
14
16
  import PostAuthorForm from './index';
15
17
  import PostPanelRow from '../post-panel-row';
16
- import { useAuthorsQuery } from './hook';
18
+ import { BASE_QUERY } from './constants';
19
+ import { store as editorStore } from '../../store';
17
20
 
18
21
  function PostAuthorToggle( { isOpen, onClick } ) {
19
- const { postAuthor } = useAuthorsQuery();
22
+ const { postAuthor } = useSelect( ( select ) => {
23
+ const id = select( editorStore ).getEditedPostAttribute( 'author' );
24
+ return {
25
+ postAuthor: select( coreStore ).getUser( id, BASE_QUERY ),
26
+ };
27
+ }, [] );
20
28
  const authorName =
21
29
  decodeEntities( postAuthor?.name ) || __( '(No author)' );
22
30
  return (
@@ -19,7 +19,7 @@ jest.mock( '@wordpress/data/src/components/use-select', () => {
19
19
  return mock;
20
20
  } );
21
21
 
22
- function setupUseSelectMock( hasAssignAuthorAction, hasAuthors ) {
22
+ function setupUseSelectMock( hasAssignAuthorAction ) {
23
23
  useSelect.mockImplementation( ( cb ) => {
24
24
  return cb( () => ( {
25
25
  getPostType: () => ( { supports: { author: true } } ),
@@ -29,28 +29,20 @@ function setupUseSelectMock( hasAssignAuthorAction, hasAuthors ) {
29
29
  'wp:action-assign-author': hasAssignAuthorAction,
30
30
  },
31
31
  } ),
32
- getUsers: () => Array( hasAuthors ? 1 : 0 ).fill( {} ),
33
32
  } ) );
34
33
  } );
35
34
  }
36
35
 
37
36
  describe( 'PostAuthorCheck', () => {
38
- it( 'should not render anything if has no authors', () => {
39
- setupUseSelectMock( false, true );
40
-
41
- render( <PostAuthorCheck>authors</PostAuthorCheck> );
42
- expect( screen.queryByText( 'authors' ) ).not.toBeInTheDocument();
43
- } );
44
-
45
37
  it( "should not render anything if doesn't have author action", () => {
46
- setupUseSelectMock( true, false );
38
+ setupUseSelectMock( false );
47
39
 
48
40
  render( <PostAuthorCheck>authors</PostAuthorCheck> );
49
41
  expect( screen.queryByText( 'authors' ) ).not.toBeInTheDocument();
50
42
  } );
51
43
 
52
44
  it( 'should render control', () => {
53
- setupUseSelectMock( true, true );
45
+ setupUseSelectMock( true );
54
46
 
55
47
  render( <PostAuthorCheck>authors</PostAuthorCheck> );
56
48
  expect( screen.getByText( 'authors' ) ).toBeVisible();
@@ -129,6 +129,7 @@
129
129
  .post-publish-panel__postpublish .components-panel__body {
130
130
  border-bottom: $border-width solid $gray-200;
131
131
  border-top: none;
132
+ word-break: break-word;
132
133
  }
133
134
 
134
135
  .post-publish-panel__postpublish-buttons {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { paramCase as kebabCase } from 'change-case';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
@@ -12,7 +17,6 @@ import {
12
17
  __experimentalVStack as VStack,
13
18
  } from '@wordpress/components';
14
19
  import { __ } from '@wordpress/i18n';
15
- import { cleanForSlug } from '@wordpress/url';
16
20
 
17
21
  /**
18
22
  * Internal dependencies
@@ -92,7 +96,7 @@ export default function CreateNewTemplateModal( { onClose } ) {
92
96
  ] );
93
97
 
94
98
  const newTemplate = await createTemplate( {
95
- slug: cleanForSlug( title || DEFAULT_TITLE ),
99
+ slug: kebabCase( title || DEFAULT_TITLE ) || 'wp-custom-template',
96
100
  content: newTemplateContent,
97
101
  title: title || DEFAULT_TITLE,
98
102
  } );
@@ -1,5 +1,15 @@
1
1
  .editor-post-template__swap-template-modal {
2
2
  z-index: z-index(".editor-post-template__swap-template-modal");
3
+
4
+ .editor-post-template__swap-template-search {
5
+ background: $white;
6
+ position: sticky;
7
+ top: 0;
8
+ padding: $grid-unit-20 0;
9
+ transform: translateY(- $grid-unit-05); // Offsets the top padding on the modal content container
10
+ margin-bottom: - $grid-unit-05;
11
+ z-index: z-index(".editor-post-template__swap-template-search");
12
+ }
3
13
  }
4
14
 
5
15
  .editor-post-template__create-template-modal {
@@ -4,7 +4,7 @@
4
4
  import { useMemo, useState } from '@wordpress/element';
5
5
  import { decodeEntities } from '@wordpress/html-entities';
6
6
  import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor';
7
- import { MenuItem, Modal } from '@wordpress/components';
7
+ import { MenuItem, Modal, SearchControl } from '@wordpress/components';
8
8
  import { __ } from '@wordpress/i18n';
9
9
  import { useDispatch } from '@wordpress/data';
10
10
  import { store as coreStore } from '@wordpress/core-data';
@@ -14,6 +14,7 @@ import { parse } from '@wordpress/blocks';
14
14
  * Internal dependencies
15
15
  */
16
16
  import { useAvailableTemplates, useEditedPostContext } from './hooks';
17
+ import { searchTemplates } from '../../utils/search-templates';
17
18
 
18
19
  export default function SwapTemplateButton( { onClick } ) {
19
20
  const [ showModal, setShowModal ] = useState( false );
@@ -61,6 +62,7 @@ export default function SwapTemplateButton( { onClick } ) {
61
62
  }
62
63
 
63
64
  function TemplatesList( { postType, onSelect } ) {
65
+ const [ searchValue, setSearchValue ] = useState( '' );
64
66
  const availableTemplates = useAvailableTemplates( postType );
65
67
  const templatesAsPatterns = useMemo(
66
68
  () =>
@@ -72,11 +74,26 @@ function TemplatesList( { postType, onSelect } ) {
72
74
  } ) ),
73
75
  [ availableTemplates ]
74
76
  );
77
+
78
+ const filteredBlockTemplates = useMemo( () => {
79
+ return searchTemplates( templatesAsPatterns, searchValue );
80
+ }, [ templatesAsPatterns, searchValue ] );
81
+
75
82
  return (
76
- <BlockPatternsList
77
- label={ __( 'Templates' ) }
78
- blockPatterns={ templatesAsPatterns }
79
- onClickPattern={ onSelect }
80
- />
83
+ <>
84
+ <SearchControl
85
+ __nextHasNoMarginBottom
86
+ onChange={ setSearchValue }
87
+ value={ searchValue }
88
+ label={ __( 'Search' ) }
89
+ placeholder={ __( 'Search' ) }
90
+ className="editor-post-template__swap-template-search"
91
+ />
92
+ <BlockPatternsList
93
+ label={ __( 'Templates' ) }
94
+ blockPatterns={ filteredBlockTemplates }
95
+ onClickPattern={ onSelect }
96
+ />
97
+ </>
81
98
  );
82
99
  }
@@ -17,6 +17,7 @@ import { store as interfaceStore } from '@wordpress/interface';
17
17
  /**
18
18
  * Internal dependencies
19
19
  */
20
+ import { TEMPLATE_POST_TYPE } from '../../store/constants';
20
21
  import { store as editorStore } from '../../store';
21
22
 
22
23
  export function useStartPatterns() {
@@ -152,7 +153,8 @@ export default function StartPageOptions() {
152
153
  return {
153
154
  postId: getCurrentPostId(),
154
155
  enabled:
155
- choosePatternModalEnabled && 'page' === getCurrentPostType(),
156
+ choosePatternModalEnabled &&
157
+ TEMPLATE_POST_TYPE !== getCurrentPostType(),
156
158
  };
157
159
  }, [] );
158
160
 
@@ -385,7 +385,6 @@ function VisualEditor( {
385
385
  'has-padding': isFocusedEntity || enableResizing,
386
386
  'is-resizable': enableResizing,
387
387
  'is-iframed': ! disableIframe,
388
- 'is-scrollable': disableIframe || deviceType !== 'Desktop',
389
388
  }
390
389
  ) }
391
390
  >
@@ -6,9 +6,6 @@
6
6
  // when the iframe doesn't cover the whole canvas
7
7
  // like the "focused entities".
8
8
  background-color: $gray-300;
9
- // Allows the height to fit the parent container and avoids parent scrolling contexts from
10
- // having overflow due to popovers of block tools.
11
- overflow: hidden;
12
9
 
13
10
  // This overrides the iframe background since it's applied again here
14
11
  // It also prevents some style glitches if `editor-visual-editor`
@@ -28,6 +25,12 @@
28
25
  padding: $grid-unit-30 $grid-unit-30 0;
29
26
  }
30
27
 
28
+ // In the iframed canvas this keeps extra scrollbars from appearing (when block toolbars overflow). In the
29
+ // legacy (non-iframed) canvas, overflow must not be hidden in order to maintain support for sticky positioning.
30
+ &.is-iframed {
31
+ overflow: hidden;
32
+ }
33
+
31
34
  // The button element easily inherits styles that are meant for the editor style.
32
35
  // These rules enhance the specificity to reduce that inheritance.
33
36
  // This is duplicated in edit-site.
@@ -41,30 +44,4 @@
41
44
  padding: 6px;
42
45
  }
43
46
  }
44
-
45
- // The cases for this are non-iframed editor canvas or previewing devices. The block canvas is
46
- // made the scrolling context.
47
- &.is-scrollable .block-editor-block-canvas {
48
- overflow: auto;
49
-
50
- // Applicable only when legacy (non-iframed).
51
- > .block-editor-writing-flow {
52
- display: flow-root;
53
- min-height: 100%;
54
- box-sizing: border-box; // Ensures that 100% min-height doesn’t create overflow.
55
- }
56
-
57
- // Applicable only when iframed. These styles ensure that if the the iframe is
58
- // given a fixed height and it’s taller than the viewport then scrolling is
59
- // allowed. This is needed for device previews.
60
- > .block-editor-iframe__container {
61
- display: flex;
62
- flex-direction: column;
63
-
64
- > .block-editor-iframe__scale-container {
65
- flex: 1 0 fit-content;
66
- display: flow-root;
67
- }
68
- }
69
- }
70
47
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import removeAccents from 'remove-accents';
5
+
6
+ /**
7
+ * Sanitizes the search input string.
8
+ *
9
+ * @param {string} input The search input to normalize.
10
+ *
11
+ * @return {string} The normalized search input.
12
+ */
13
+ function normalizeSearchInput( input = '' ) {
14
+ // Disregard diacritics.
15
+ input = removeAccents( input );
16
+
17
+ // Trim & Lowercase.
18
+ input = input.trim().toLowerCase();
19
+
20
+ return input;
21
+ }
22
+
23
+ /**
24
+ * Get the search rank for a given template and a specific search term.
25
+ *
26
+ * @param {Object} template Template to rank
27
+ * @param {string} searchValue Search term
28
+ *
29
+ * @return {number} A template search rank
30
+ */
31
+ function getTemplateSearchRank( template, searchValue ) {
32
+ const normalizedSearchValue = normalizeSearchInput( searchValue );
33
+ const normalizedTitle = normalizeSearchInput( template.title );
34
+
35
+ let rank = 0;
36
+
37
+ if ( normalizedSearchValue === normalizedTitle ) {
38
+ rank += 30;
39
+ } else if ( normalizedTitle.startsWith( normalizedSearchValue ) ) {
40
+ rank += 20;
41
+ } else {
42
+ const searchTerms = normalizedSearchValue.split( ' ' );
43
+ const hasMatchedTerms = searchTerms.every( ( searchTerm ) =>
44
+ normalizedTitle.includes( searchTerm )
45
+ );
46
+
47
+ // Prefer template with every search word in the title.
48
+ if ( hasMatchedTerms ) {
49
+ rank += 10;
50
+ }
51
+ }
52
+
53
+ return rank;
54
+ }
55
+
56
+ /**
57
+ * Filters a template list given a search term.
58
+ *
59
+ * @param {Array} templates Item list
60
+ * @param {string} searchValue Search input.
61
+ *
62
+ * @return {Array} Filtered template list.
63
+ */
64
+ export function searchTemplates( templates = [], searchValue = '' ) {
65
+ if ( ! searchValue ) {
66
+ return templates;
67
+ }
68
+
69
+ const rankedTemplates = templates
70
+ .map( ( template ) => {
71
+ return [ template, getTemplateSearchRank( template, searchValue ) ];
72
+ } )
73
+ .filter( ( [ , rank ] ) => rank > 0 );
74
+
75
+ rankedTemplates.sort( ( [ , rank1 ], [ , rank2 ] ) => rank2 - rank1 );
76
+ return rankedTemplates.map( ( [ template ] ) => template );
77
+ }