diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 73e4186fce9b6..628225f536e45 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -275,7 +275,7 @@ Add a link to a downloadable file. ([Source](https://github.com/WordPress/gutenb
- **Name:** core/footnotes
- **Category:** text
-- **Supports:** ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~
+- **Supports:** ~~html~~, ~~multiple~~, ~~reusable~~
- **Attributes:**
## Classic
diff --git a/lib/compat/wordpress-6.3/theme-previews.php b/lib/compat/wordpress-6.3/theme-previews.php
index eab05c5824b1f..f663e7a9783d6 100644
--- a/lib/compat/wordpress-6.3/theme-previews.php
+++ b/lib/compat/wordpress-6.3/theme-previews.php
@@ -107,7 +107,7 @@ function block_theme_activate_nonce() {
$nonce_handle = 'switch-theme_' . gutenberg_get_theme_preview_path();
?>
{
- const doc = document.implementation.createHTMLDocument( '' );
- doc.body.innerHTML = html;
- return Array.from( doc.body.children );
- }, [ html ] );
-}
-
-async function loadScript( head, { id, src } ) {
- return new Promise( ( resolve, reject ) => {
- const script = head.ownerDocument.createElement( 'script' );
- script.id = id;
- if ( src ) {
- script.src = src;
- script.onload = () => resolve();
- script.onerror = () => reject();
- } else {
- resolve();
- }
- head.appendChild( script );
- } );
-}
-
function Iframe( {
contentRef,
children,
@@ -112,21 +88,22 @@ function Iframe( {
forwardedRef: ref,
...props
} ) {
- const assets = useSelect(
+ const { styles = '', scripts = '' } = useSelect(
( select ) =>
select( blockEditorStore ).getSettings().__unstableResolvedAssets,
[]
);
- const [ , forceRender ] = useReducer( () => ( {} ) );
const [ iframeDocument, setIframeDocument ] = useState();
const [ bodyClasses, setBodyClasses ] = useState( [] );
const compatStyles = useCompatibilityStyles();
- const scripts = useParsedAssets( assets?.scripts );
const clearerRef = useBlockSelectionClearer();
const [ before, writingFlowRef, after ] = useWritingFlow();
const [ contentResizeListener, { height: contentHeight } ] =
useResizeObserver();
const setRef = useRefEffect( ( node ) => {
+ node._load = () => {
+ setIframeDocument( node.contentDocument );
+ };
let iFrameDocument;
// Prevent the default browser action for files dropped outside of dropzones.
function preventFileDropDefault( event ) {
@@ -138,7 +115,6 @@ function Iframe( {
iFrameDocument = contentDocument;
bubbleEvents( contentDocument );
- setIframeDocument( contentDocument );
clearerRef( documentElement );
// Ideally ALL classes that are added through get_body_class should
@@ -154,7 +130,6 @@ function Iframe( {
);
contentDocument.dir = ownerDocument.dir;
- documentElement.removeChild( contentDocument.body );
for ( const compatStyle of compatStyles ) {
if ( contentDocument.getElementById( compatStyle.id ) ) {
@@ -199,35 +174,29 @@ function Iframe( {
};
}, [] );
- const headRef = useRefEffect( ( element ) => {
- scripts
- .reduce(
- ( promise, script ) =>
- promise.then( () => loadScript( element, script ) ),
- Promise.resolve()
- )
- .finally( () => {
- // When script are loaded, re-render blocks to allow them
- // to initialise.
- forceRender();
- } );
- }, [] );
const disabledRef = useDisabled( { isDisabled: ! readonly } );
const bodyRef = useMergeRefs( [
contentRef,
clearerRef,
writingFlowRef,
disabledRef,
- headRef,
] );
// Correct doctype is required to enable rendering in standards
// mode. Also preload the styles to avoid a flash of unstyled
// content.
- const html =
- '' +
- '' +
- ( assets?.styles ?? '' );
+ const html = `
+
+
+
+
+ ${ styles }
+ ${ scripts }
+
+
+
+
+`;
const [ src, cleanup ] = useMemo( () => {
const _src = URL.createObjectURL(
diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js
index dfd206a1ddb7e..9762582f86f14 100644
--- a/packages/block-editor/src/components/rich-text/content.js
+++ b/packages/block-editor/src/components/rich-text/content.js
@@ -2,11 +2,7 @@
* WordPress dependencies
*/
import { RawHTML } from '@wordpress/element';
-import {
- children as childrenSource,
- getSaveElement,
- __unstableGetBlockProps as getBlockProps,
-} from '@wordpress/blocks';
+import { children as childrenSource } from '@wordpress/blocks';
import deprecated from '@wordpress/deprecated';
/**
@@ -42,44 +38,3 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => {
return content;
};
-
-Content.__unstableIsRichTextContent = {};
-
-function findContent( blocks, richTextValues = [] ) {
- if ( ! Array.isArray( blocks ) ) {
- blocks = [ blocks ];
- }
-
- for ( const block of blocks ) {
- if (
- block?.type?.__unstableIsRichTextContent ===
- Content.__unstableIsRichTextContent
- ) {
- richTextValues.push( block.props.value );
- continue;
- }
-
- if ( block?.props?.children ) {
- findContent( block.props.children, richTextValues );
- }
- }
-
- return richTextValues;
-}
-
-function _getSaveElement( { name, attributes, innerBlocks } ) {
- return getSaveElement(
- name,
- attributes,
- innerBlocks.map( _getSaveElement )
- );
-}
-
-export function getRichTextValues( blocks = [] ) {
- getBlockProps.skipFilters = true;
- const values = findContent(
- ( Array.isArray( blocks ) ? blocks : [ blocks ] ).map( _getSaveElement )
- );
- getBlockProps.skipFilters = false;
- return values;
-}
diff --git a/packages/block-editor/src/components/rich-text/get-rich-text-values.js b/packages/block-editor/src/components/rich-text/get-rich-text-values.js
new file mode 100644
index 0000000000000..4ecee9b76530e
--- /dev/null
+++ b/packages/block-editor/src/components/rich-text/get-rich-text-values.js
@@ -0,0 +1,95 @@
+/**
+ * WordPress dependencies
+ */
+import { RawHTML, StrictMode, Fragment } from '@wordpress/element';
+import {
+ getSaveElement,
+ __unstableGetBlockProps as getBlockProps,
+} from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import InnerBlocks from '../inner-blocks';
+import { Content } from './content';
+
+/*
+ * This function is similar to `@wordpress/element`'s `renderToString` function,
+ * except that it does not render the elements to a string, but instead collects
+ * the values of all rich text `Content` elements.
+ */
+function addValuesForElement( element, ...args ) {
+ if ( null === element || undefined === element || false === element ) {
+ return;
+ }
+
+ if ( Array.isArray( element ) ) {
+ return addValuesForElements( element, ...args );
+ }
+
+ switch ( typeof element ) {
+ case 'string':
+ case 'number':
+ return;
+ }
+
+ const { type, props } = element;
+
+ switch ( type ) {
+ case StrictMode:
+ case Fragment:
+ return addValuesForElements( props.children, ...args );
+ case RawHTML:
+ return;
+ case InnerBlocks.Content:
+ return addValuesForBlocks( ...args );
+ case Content:
+ const [ values ] = args;
+ values.push( props.value );
+ return;
+ }
+
+ switch ( typeof type ) {
+ case 'string':
+ if ( typeof props.children !== 'undefined' ) {
+ return addValuesForElements( props.children, ...args );
+ }
+ return;
+ case 'function':
+ if (
+ type.prototype &&
+ typeof type.prototype.render === 'function'
+ ) {
+ return addValuesForElement(
+ new type( props ).render(),
+ ...args
+ );
+ }
+
+ return addValuesForElement( type( props ), ...args );
+ }
+}
+
+function addValuesForElements( children, ...args ) {
+ children = Array.isArray( children ) ? children : [ children ];
+
+ for ( let i = 0; i < children.length; i++ ) {
+ addValuesForElement( children[ i ], ...args );
+ }
+}
+
+function addValuesForBlocks( values, blocks ) {
+ for ( let i = 0; i < blocks.length; i++ ) {
+ const { name, attributes, innerBlocks } = blocks[ i ];
+ const saveElement = getSaveElement( name, attributes );
+ addValuesForElement( saveElement, values, innerBlocks );
+ }
+}
+
+export function getRichTextValues( blocks = [] ) {
+ getBlockProps.skipFilters = true;
+ const values = [];
+ addValuesForBlocks( values, blocks );
+ getBlockProps.skipFilters = false;
+ return values;
+}
diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index dd8d2d8ff411f..432312d0ce0aa 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -4,7 +4,7 @@
import * as globalStyles from './components/global-styles';
import { ExperimentalBlockEditorProvider } from './components/provider';
import { lock } from './lock-unlock';
-import { getRichTextValues } from './components/rich-text/content';
+import { getRichTextValues } from './components/rich-text/get-rich-text-values';
import ResizableBoxPopover from './components/resizable-box-popover';
import { ComposedPrivateInserter as PrivateInserter } from './components/inserter';
import { PrivateListView } from './components/list-view';
diff --git a/packages/block-library/src/comment-template/index.php b/packages/block-library/src/comment-template/index.php
index 3a553e802de0e..bb1cfa474e4c3 100644
--- a/packages/block-library/src/comment-template/index.php
+++ b/packages/block-library/src/comment-template/index.php
@@ -35,8 +35,11 @@ function block_core_comment_template_render_comments( $comments, $block ) {
* We set commentId context through the `render_block_context` filter so
* that dynamically inserted blocks (at `render_block` filter stage)
* will also receive that context.
+ *
+ * Use an early priority to so that other 'render_block_context' filters
+ * have access to the values.
*/
- add_filter( 'render_block_context', $filter_block_context );
+ add_filter( 'render_block_context', $filter_block_context, 1 );
/*
* We construct a new WP_Block instance from the parsed block so that
@@ -44,7 +47,7 @@ function block_core_comment_template_render_comments( $comments, $block ) {
*/
$block_content = ( new WP_Block( $block->parsed_block ) )->render( array( 'dynamic' => false ) );
- remove_filter( 'render_block_context', $filter_block_context );
+ remove_filter( 'render_block_context', $filter_block_context, 1 );
$children = $comment->get_children();
diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json
index 0ab992009d123..e021e9c5225da 100644
--- a/packages/block-library/src/footnotes/block.json
+++ b/packages/block-library/src/footnotes/block.json
@@ -11,7 +11,6 @@
"supports": {
"html": false,
"multiple": false,
- "inserter": false,
"reusable": false
},
"style": "wp-block-footnotes"
diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js
index e90a7f82be94a..fdfe7a94039af 100644
--- a/packages/block-library/src/footnotes/edit.js
+++ b/packages/block-library/src/footnotes/edit.js
@@ -1,8 +1,11 @@
/**
* WordPress dependencies
*/
-import { RichText, useBlockProps } from '@wordpress/block-editor';
+import { BlockIcon, RichText, useBlockProps } from '@wordpress/block-editor';
import { useEntityProp } from '@wordpress/core-data';
+import { __ } from '@wordpress/i18n';
+import { Placeholder } from '@wordpress/components';
+import { formatListNumbered as icon } from '@wordpress/icons';
export default function FootnotesEdit( { context: { postType, postId } } ) {
const [ meta, updateMeta ] = useEntityProp(
@@ -12,8 +15,24 @@ export default function FootnotesEdit( { context: { postType, postId } } ) {
postId
);
const footnotes = meta?.footnotes ? JSON.parse( meta.footnotes ) : [];
+ const blockProps = useBlockProps();
+
+ if ( ! footnotes.length ) {
+ return (
+
+
}
+ label={ __( 'Footnotes' ) }
+ instructions={ __(
+ 'Footnotes found in blocks within this document will be displayed here.'
+ ) }
+ />
+
+ );
+ }
+
return (
-
+
{ footnotes.map( ( { id, content } ) => (
*`,
},
value.end,
value.end
diff --git a/packages/block-library/src/footnotes/style.scss b/packages/block-library/src/footnotes/style.scss
index 4debba0560f17..aa7ab8b6951dd 100644
--- a/packages/block-library/src/footnotes/style.scss
+++ b/packages/block-library/src/footnotes/style.scss
@@ -1,17 +1,20 @@
+// These styles are for backwards compatibility with the old footnotes anchors.
+// Can be removed in the future.
.editor-styles-wrapper,
.entry-content {
counter-reset: footnotes;
}
-[data-fn].fn {
+a[data-fn].fn {
vertical-align: super;
font-size: smaller;
counter-increment: footnotes;
- display: inline-block;
+ display: inline-flex;
+ text-decoration: none;
text-indent: -9999999px;
}
-[data-fn].fn::after {
+a[data-fn].fn::after {
content: "[" counter(footnotes) "]";
text-indent: 0;
float: left;
diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js
index 9b7a41cab188d..bdfdca6ee3c4d 100644
--- a/packages/block-library/src/image/deprecated.js
+++ b/packages/block-library/src/image/deprecated.js
@@ -6,355 +6,632 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { RichText, useBlockProps } from '@wordpress/block-editor';
+import {
+ RichText,
+ useBlockProps,
+ __experimentalGetElementClassName as getBorderClassesAndStyles,
+} from '@wordpress/block-editor';
-const blockAttributes = {
- align: {
- type: 'string',
- },
- url: {
- type: 'string',
- source: 'attribute',
- selector: 'img',
- attribute: 'src',
- },
- alt: {
- type: 'string',
- source: 'attribute',
- selector: 'img',
- attribute: 'alt',
- default: '',
- },
- caption: {
- type: 'string',
- source: 'html',
- selector: 'figcaption',
- },
- title: {
- type: 'string',
- source: 'attribute',
- selector: 'img',
- attribute: 'title',
- },
- href: {
- type: 'string',
- source: 'attribute',
- selector: 'figure > a',
- attribute: 'href',
- },
- rel: {
- type: 'string',
- source: 'attribute',
- selector: 'figure > a',
- attribute: 'rel',
- },
- linkClass: {
- type: 'string',
- source: 'attribute',
- selector: 'figure > a',
- attribute: 'class',
- },
- id: {
- type: 'number',
+/**
+ * Deprecation for adding the `wp-image-${id}` class to the image block for
+ * responsive images.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/4898
+ */
+const v1 = {
+ attributes: {
+ url: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'src',
+ },
+ alt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'alt',
+ default: '',
+ },
+ caption: {
+ type: 'array',
+ source: 'children',
+ selector: 'figcaption',
+ },
+ href: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'a',
+ attribute: 'href',
+ },
+ id: {
+ type: 'number',
+ },
+ align: {
+ type: 'string',
+ },
+ width: {
+ type: 'number',
+ },
+ height: {
+ type: 'number',
+ },
},
- width: {
- type: 'number',
+ save( { attributes } ) {
+ const { url, alt, caption, align, href, width, height } = attributes;
+ const extraImageProps = width || height ? { width, height } : {};
+ const image = ;
+
+ let figureStyle = {};
+
+ if ( width ) {
+ figureStyle = { width };
+ } else if ( align === 'left' || align === 'right' ) {
+ figureStyle = { maxWidth: '50%' };
+ }
+
+ return (
+
+ { href ? { image } : image }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+
+ );
},
- height: {
- type: 'number',
+};
+
+/**
+ * Deprecation for adding the `is-resized` class to the image block to fix
+ * captions on resized images.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/6496
+ */
+const v2 = {
+ attributes: {
+ url: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'src',
+ },
+ alt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'alt',
+ default: '',
+ },
+ caption: {
+ type: 'array',
+ source: 'children',
+ selector: 'figcaption',
+ },
+ href: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'a',
+ attribute: 'href',
+ },
+ id: {
+ type: 'number',
+ },
+ align: {
+ type: 'string',
+ },
+ width: {
+ type: 'number',
+ },
+ height: {
+ type: 'number',
+ },
},
- sizeSlug: {
- type: 'string',
+ save( { attributes } ) {
+ const { url, alt, caption, align, href, width, height, id } =
+ attributes;
+
+ const image = (
+
+ );
+
+ return (
+
+ { href ? { image } : image }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+
+ );
},
- linkDestination: {
- type: 'string',
+};
+
+/**
+ * Deprecation for image floats including a wrapping div.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/7721
+ */
+const v3 = {
+ attributes: {
+ url: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'src',
+ },
+ alt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'alt',
+ default: '',
+ },
+ caption: {
+ type: 'array',
+ source: 'children',
+ selector: 'figcaption',
+ },
+ href: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'href',
+ },
+ id: {
+ type: 'number',
+ },
+ align: {
+ type: 'string',
+ },
+ width: {
+ type: 'number',
+ },
+ height: {
+ type: 'number',
+ },
+ linkDestination: {
+ type: 'string',
+ default: 'none',
+ },
},
- linkTarget: {
- type: 'string',
- source: 'attribute',
- selector: 'figure > a',
- attribute: 'target',
+ save( { attributes } ) {
+ const { url, alt, caption, align, href, width, height, id } =
+ attributes;
+
+ const classes = classnames( {
+ [ `align${ align }` ]: align,
+ 'is-resized': width || height,
+ } );
+
+ const image = (
+
+ );
+
+ return (
+
+ { href ? { image } : image }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+
+ );
},
};
-const blockSupports = {
- anchor: true,
- color: {
- __experimentalDuotone: 'img',
- text: false,
- background: false,
- },
- __experimentalBorder: {
- radius: true,
- __experimentalDefaultControls: {
- radius: true,
+/**
+ * Deprecation for removing the outer div wrapper around aligned images.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/38657
+ */
+const v4 = {
+ attributes: {
+ align: {
+ type: 'string',
+ },
+ url: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'src',
+ },
+ alt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'alt',
+ default: '',
+ },
+ caption: {
+ type: 'string',
+ source: 'html',
+ selector: 'figcaption',
+ },
+ title: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'title',
+ },
+ href: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'href',
+ },
+ rel: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'rel',
+ },
+ linkClass: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'class',
+ },
+ id: {
+ type: 'number',
+ },
+ width: {
+ type: 'number',
+ },
+ height: {
+ type: 'number',
+ },
+ sizeSlug: {
+ type: 'string',
+ },
+ linkDestination: {
+ type: 'string',
+ },
+ linkTarget: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'target',
},
},
-};
+ supports: {
+ anchor: true,
+ },
+ save( { attributes } ) {
+ const {
+ url,
+ alt,
+ caption,
+ align,
+ href,
+ rel,
+ linkClass,
+ width,
+ height,
+ id,
+ linkTarget,
+ sizeSlug,
+ title,
+ } = attributes;
-const deprecated = [
- // The following deprecation moves existing border radius styles onto the
- // inner img element where new border block support styles must be applied.
- // It will also add a new `.has-custom-border` class for existing blocks
- // with border radii set. This class is required to improve caption position
- // and styling when an image within a gallery has a custom border or
- // rounded corners.
- //
- // See: https://github.com/WordPress/gutenberg/pull/31366/
- {
- attributes: blockAttributes,
- supports: blockSupports,
- save( { attributes } ) {
- const {
- url,
- alt,
- caption,
- align,
- href,
- rel,
- linkClass,
- width,
- height,
- id,
- linkTarget,
- sizeSlug,
- title,
- } = attributes;
-
- const newRel = ! rel ? undefined : rel;
-
- const classes = classnames( {
- [ `align${ align }` ]: align,
- [ `size-${ sizeSlug }` ]: sizeSlug,
- 'is-resized': width || height,
- } );
-
- const image = (
-
- );
+ const newRel = ! rel ? undefined : rel;
- const figure = (
- <>
- { href ? (
-
- { image }
-
- ) : (
- image
- ) }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
- >
- );
+ const classes = classnames( {
+ [ `align${ align }` ]: align,
+ [ `size-${ sizeSlug }` ]: sizeSlug,
+ 'is-resized': width || height,
+ } );
+
+ const image = (
+
+ );
+
+ const figure = (
+ <>
+ { href ? (
+
+ { image }
+
+ ) : (
+ image
+ ) }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+ >
+ );
+ if ( 'left' === align || 'right' === align || 'center' === align ) {
return (
-
- { figure }
-
+
+ { figure }
+
);
+ }
+
+ return (
+
+ { figure }
+
+ );
+ },
+};
+
+/**
+ * Deprecation for moving existing border radius styles onto the inner img
+ * element where new border block support styles must be applied.
+ * It will also add a new `.has-custom-border` class for existing blocks
+ * with border radii set. This class is required to improve caption position
+ * and styling when an image within a gallery has a custom border or
+ * rounded corners.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/31366
+ */
+const v5 = {
+ attributes: {
+ align: {
+ type: 'string',
+ },
+ url: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'src',
+ },
+ alt: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'alt',
+ default: '',
+ },
+ caption: {
+ type: 'string',
+ source: 'html',
+ selector: 'figcaption',
+ },
+ title: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'img',
+ attribute: 'title',
+ },
+ href: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'href',
+ },
+ rel: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'rel',
+ },
+ linkClass: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'class',
+ },
+ id: {
+ type: 'number',
+ },
+ width: {
+ type: 'number',
+ },
+ height: {
+ type: 'number',
+ },
+ sizeSlug: {
+ type: 'string',
+ },
+ linkDestination: {
+ type: 'string',
+ },
+ linkTarget: {
+ type: 'string',
+ source: 'attribute',
+ selector: 'figure > a',
+ attribute: 'target',
},
},
- {
- attributes: {
- ...blockAttributes,
- title: {
- type: 'string',
- source: 'attribute',
- selector: 'img',
- attribute: 'title',
+ supports: {
+ anchor: true,
+ color: {
+ __experimentalDuotone: 'img',
+ text: false,
+ background: false,
+ },
+ __experimentalBorder: {
+ radius: true,
+ __experimentalDefaultControls: {
+ radius: true,
},
- sizeSlug: {
- type: 'string',
+ },
+ __experimentalStyle: {
+ spacing: {
+ margin: '0 0 1em 0',
},
},
- supports: blockSupports,
- save( { attributes } ) {
- const {
- url,
- alt,
- caption,
- align,
- href,
- rel,
- linkClass,
- width,
- height,
- id,
- linkTarget,
- sizeSlug,
- title,
- } = attributes;
-
- const newRel = ! rel ? undefined : rel;
-
- const classes = classnames( {
- [ `align${ align }` ]: align,
- [ `size-${ sizeSlug }` ]: sizeSlug,
- 'is-resized': width || height,
- } );
-
- const image = (
-
- );
+ },
+ save( { attributes } ) {
+ const {
+ url,
+ alt,
+ caption,
+ align,
+ href,
+ rel,
+ linkClass,
+ width,
+ height,
+ id,
+ linkTarget,
+ sizeSlug,
+ title,
+ } = attributes;
- const figure = (
- <>
- { href ? (
-
- { image }
-
- ) : (
- image
- ) }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
- >
- );
+ const newRel = ! rel ? undefined : rel;
- if ( 'left' === align || 'right' === align || 'center' === align ) {
- return (
-
- { figure }
-
- );
- }
+ const classes = classnames( {
+ [ `align${ align }` ]: align,
+ [ `size-${ sizeSlug }` ]: sizeSlug,
+ 'is-resized': width || height,
+ } );
- return (
-
- { figure }
-
- );
- },
- },
- {
- attributes: blockAttributes,
- save( { attributes } ) {
- const { url, alt, caption, align, href, width, height, id } =
- attributes;
-
- const classes = classnames( {
- [ `align${ align }` ]: align,
- 'is-resized': width || height,
- } );
-
- const image = (
-
- );
+ const image = (
+
+ );
- return (
-
- { href ? { image } : image }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
-
- );
- },
- },
- {
- attributes: blockAttributes,
- save( { attributes } ) {
- const { url, alt, caption, align, href, width, height, id } =
- attributes;
-
- const image = (
-
- );
+ const figure = (
+ <>
+ { href ? (
+
+ { image }
+
+ ) : (
+ image
+ ) }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+ >
+ );
- return (
-
- { href ? { image } : image }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
-
- );
- },
+ return (
+
+ { figure }
+
+ );
},
- {
- attributes: blockAttributes,
- save( { attributes } ) {
- const { url, alt, caption, align, href, width, height } =
- attributes;
- const extraImageProps = width || height ? { width, height } : {};
- const image = (
-
- );
+};
- let figureStyle = {};
+/**
+ * Deprecation for adding width and height as style rules on the inner img.
+ * It also updates the widht and height attributes to be strings instead of numbers.
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/31366
+ */
+const v6 = {
+ save( { attributes } ) {
+ const {
+ url,
+ alt,
+ caption,
+ align,
+ href,
+ rel,
+ linkClass,
+ width,
+ height,
+ aspectRatio,
+ scale,
+ id,
+ linkTarget,
+ sizeSlug,
+ title,
+ } = attributes;
- if ( width ) {
- figureStyle = { width };
- } else if ( align === 'left' || align === 'right' ) {
- figureStyle = { maxWidth: '50%' };
- }
+ const newRel = ! rel ? undefined : rel;
+ const borderProps = getBorderClassesAndStyles( attributes );
- return (
-
- { href ? { image } : image }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
-
- );
- },
+ const classes = classnames( {
+ [ `align${ align }` ]: align,
+ [ `size-${ sizeSlug }` ]: sizeSlug,
+ 'is-resized': width || height,
+ 'has-custom-border':
+ !! borderProps.className ||
+ ( borderProps.style &&
+ Object.keys( borderProps.style ).length > 0 ),
+ } );
+
+ const imageClasses = classnames( borderProps.className, {
+ [ `wp-image-${ id }` ]: !! id,
+ } );
+
+ const image = (
+
+ );
+
+ const figure = (
+ <>
+ { href ? (
+
+ { image }
+
+ ) : (
+ image
+ ) }
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+ >
+ );
+
+ return (
+
+ { figure }
+
+ );
},
-];
+};
-export default deprecated;
+export default [ v6, v5, v4, v3, v2, v1 ];
diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js
index 95e8803dd6785..6fa8c6b2342f3 100644
--- a/packages/block-library/src/image/save.js
+++ b/packages/block-library/src/image/save.js
@@ -58,6 +58,8 @@ export default function save( { attributes } ) {
...borderProps.style,
aspectRatio,
objectFit: scale,
+ width,
+ height,
} }
width={ width }
height={ height }
diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js
index 24d5ead74c47d..7c8c15e05fe87 100644
--- a/packages/block-library/src/list/edit.js
+++ b/packages/block-library/src/list/edit.js
@@ -177,10 +177,12 @@ export default function Edit( { attributes, setAttributes, clientId, style } ) {
{ controls }
{ ordered && (
) }
>
diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js
index b91015313f403..96d6f16926a66 100644
--- a/packages/block-library/src/navigation-link/edit.js
+++ b/packages/block-library/src/navigation-link/edit.js
@@ -181,6 +181,10 @@ export default function NavigationLinkEdit( {
const itemLabelPlaceholder = __( 'Add label…' );
const ref = useRef();
+ // Change the label using inspector causes rich text to change focus on firefox.
+ // This is a workaround to keep the focus on the label field when label filed is focused we don't render the rich text.
+ const [ isLabelFieldFocused, setIsLabelFieldFocused ] = useState( false );
+
const {
innerBlocks,
isAtMaxNesting,
@@ -424,6 +428,8 @@ export default function NavigationLinkEdit( {
} }
label={ __( 'Label' ) }
autoComplete="off"
+ onFocus={ () => setIsLabelFieldFocused( true ) }
+ onBlur={ () => setIsLabelFieldFocused( false ) }
/>
) : (
<>
- { ! isInvalid && ! isDraft && (
- <>
-
- setAttributes( {
- label: labelValue,
- } )
- }
- onMerge={ mergeBlocks }
- onReplace={ onReplace }
- __unstableOnSplitAtEnd={ () =>
- insertBlocksAfter(
- createBlock(
- 'core/navigation-link'
+ { ! isInvalid &&
+ ! isDraft &&
+ ! isLabelFieldFocused && (
+ <>
+
+ setAttributes( {
+ label: labelValue,
+ } )
+ }
+ onMerge={ mergeBlocks }
+ onReplace={ onReplace }
+ __unstableOnSplitAtEnd={ () =>
+ insertBlocksAfter(
+ createBlock(
+ 'core/navigation-link'
+ )
)
- )
- }
- aria-label={ __(
- 'Navigation link text'
- ) }
- placeholder={ itemLabelPlaceholder }
- withoutInteractiveFormatting
- allowedFormats={ [
- 'core/bold',
- 'core/italic',
- 'core/image',
- 'core/strikethrough',
- ] }
- onClick={ () => {
- if ( ! url ) {
- setIsLinkOpen( true );
}
- } }
- />
- { description && (
-
- { description }
-
- ) }
- >
- ) }
- { ( isInvalid || isDraft ) && (
+ aria-label={ __(
+ 'Navigation link text'
+ ) }
+ placeholder={ itemLabelPlaceholder }
+ withoutInteractiveFormatting
+ allowedFormats={ [
+ 'core/bold',
+ 'core/italic',
+ 'core/image',
+ 'core/strikethrough',
+ ] }
+ onClick={ () => {
+ if ( ! url ) {
+ setIsLinkOpen( true );
+ }
+ } }
+ />
+ { description && (
+
+ { description }
+
+ ) }
+ >
+ ) }
+ { ( isInvalid ||
+ isDraft ||
+ isLabelFieldFocused ) && (
diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php
index 3c023c80ed263..b1499d845f39a 100644
--- a/packages/block-library/src/post-template/index.php
+++ b/packages/block-library/src/post-template/index.php
@@ -97,11 +97,13 @@ function render_block_core_post_template( $attributes, $content, $block ) {
$context['postId'] = $post_id;
return $context;
};
- add_filter( 'render_block_context', $filter_block_context );
+
+ // Use an early priority to so that other 'render_block_context' filters have access to the values.
+ add_filter( 'render_block_context', $filter_block_context, 1 );
// Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
// `render_callback` and ensure that no wrapper markup is included.
$block_content = ( new WP_Block( $block_instance ) )->render( array( 'dynamic' => false ) );
- remove_filter( 'render_block_context', $filter_block_context );
+ remove_filter( 'render_block_context', $filter_block_context, 1 );
// Wrap the render inner blocks in a `li` element with the appropriate post classes.
$post_classes = implode( ' ', get_post_class( 'wp-block-post' ) );
diff --git a/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap
index 5b5df918f2bee..65d87d5b0d7bd 100644
--- a/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap
+++ b/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap
@@ -22,6 +22,16 @@ exports[`Quote block transforms to Group block 1`] = `
"
`;
+exports[`Quote block transforms to Paragraph block 1`] = `
+"
+"This will make running your own blog a viable alternative again."
+
+
+
+— Adrian Zumbrunnen
+"
+`;
+
exports[`Quote block transforms to Pullquote block 1`] = `
"
"This will make running your own blog a viable alternative again."
— Adrian Zumbrunnen
diff --git a/packages/block-library/src/quote/test/transforms.native.js b/packages/block-library/src/quote/test/transforms.native.js
index 46c4eb2b6f972..25030e0a018d4 100644
--- a/packages/block-library/src/quote/test/transforms.native.js
+++ b/packages/block-library/src/quote/test/transforms.native.js
@@ -21,7 +21,11 @@ const initialHtml = `
`;
const transformsWithInnerBlocks = [ 'Columns', 'Group' ];
-const blockTransforms = [ 'Pullquote', ...transformsWithInnerBlocks ];
+const blockTransforms = [
+ 'Pullquote',
+ 'Paragraph',
+ ...transformsWithInnerBlocks,
+];
setupCoreBlocks();
diff --git a/packages/block-library/src/quote/transforms.js b/packages/block-library/src/quote/transforms.js
index d4cd77177bf03..4e153a6399029 100644
--- a/packages/block-library/src/quote/transforms.js
+++ b/packages/block-library/src/quote/transforms.js
@@ -109,6 +109,19 @@ const transforms = {
} );
},
},
+ {
+ type: 'block',
+ blocks: [ 'core/paragraph' ],
+ transform: ( { citation }, innerBlocks ) =>
+ citation
+ ? [
+ ...innerBlocks,
+ createBlock( 'core/paragraph', {
+ content: citation,
+ } ),
+ ]
+ : innerBlocks,
+ },
{
type: 'block',
blocks: [ 'core/group' ],
diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js
index da048944f1498..6cc1e021841b4 100644
--- a/packages/core-data/src/entity-provider.js
+++ b/packages/core-data/src/entity-provider.js
@@ -191,9 +191,10 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
const updateFootnotes = useCallback(
( _blocks ) => {
- if ( ! meta ) return;
+ const output = { blocks: _blocks };
+ if ( ! meta ) return output;
// If meta.footnotes is empty, it means the meta is not registered.
- if ( meta.footnotes === undefined ) return {};
+ if ( meta.footnotes === undefined ) return output;
const { getRichTextValues } = unlock( blockEditorPrivateApis );
const _content = getRichTextValues( _blocks ).join( '' ) || '';
@@ -215,7 +216,8 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
: [];
const currentOrder = footnotes.map( ( fn ) => fn.id );
- if ( currentOrder.join( '' ) === newOrder.join( '' ) ) return;
+ if ( currentOrder.join( '' ) === newOrder.join( '' ) )
+ return output;
const newFootnotes = newOrder.map(
( fnId ) =>
@@ -226,6 +228,71 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
}
);
+ function updateAttributes( attributes ) {
+ attributes = { ...attributes };
+
+ for ( const key in attributes ) {
+ const value = attributes[ key ];
+
+ if ( Array.isArray( value ) ) {
+ attributes[ key ] = value.map( updateAttributes );
+ continue;
+ }
+
+ if ( typeof value !== 'string' ) {
+ continue;
+ }
+
+ if ( value.indexOf( 'data-fn' ) === -1 ) {
+ continue;
+ }
+
+ // When we store rich text values, this would no longer
+ // require a regex.
+ const regex =
+ /(]+data-fn="([^"]+)"[^>]*>]*>)[\d*]*<\/a><\/sup>/g;
+
+ attributes[ key ] = value.replace(
+ regex,
+ ( match, opening, fnId ) => {
+ const index = newOrder.indexOf( fnId );
+ return `${ opening }${ index + 1 } `;
+ }
+ );
+
+ const compatRegex =
+ /]+data-fn="([^"]+)"[^>]*>\*<\/a>/g;
+
+ attributes[ key ] = attributes[ key ].replace(
+ compatRegex,
+ ( match, fnId ) => {
+ const index = newOrder.indexOf( fnId );
+ return `${
+ index + 1
+ } `;
+ }
+ );
+ }
+
+ return attributes;
+ }
+
+ function updateBlocksAttributes( __blocks ) {
+ return __blocks.map( ( block ) => {
+ return {
+ ...block,
+ attributes: updateAttributes( block.attributes ),
+ innerBlocks: updateBlocksAttributes(
+ block.innerBlocks
+ ),
+ };
+ } );
+ }
+
+ // We need to go through all block attributs deeply and update the
+ // footnote anchor numbering (textContent) to match the new order.
+ const newBlocks = updateBlocksAttributes( _blocks );
+
oldFootnotes = {
...oldFootnotes,
...footnotes.reduce( ( acc, fn ) => {
@@ -241,6 +308,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
...meta,
footnotes: JSON.stringify( newFootnotes ),
},
+ blocks: newBlocks,
};
},
[ meta ]
@@ -258,7 +326,6 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
// to make sure the edit makes the post dirty and creates
// a new undo level.
const edits = {
- blocks: newBlocks,
selection,
content: ( { blocks: blocksForSerialization = [] } ) =>
__unstableSerializeAndClean( blocksForSerialization ),
@@ -282,7 +349,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
( newBlocks, options ) => {
const { selection } = options;
const footnotesChanges = updateFootnotes( newBlocks );
- const edits = { blocks: newBlocks, selection, ...footnotesChanges };
+ const edits = { selection, ...footnotesChanges };
editEntityRecord( kind, name, id, edits, { isCached: true } );
},
diff --git a/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php b/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php
index ad98354dd45dc..3f24a6e25cfcb 100644
--- a/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php
+++ b/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php
@@ -17,5 +17,18 @@ static function() {
filemtime( plugin_dir_path( __FILE__ ) . 'iframed-enqueue-block-assets/style.css' )
);
wp_add_inline_style( 'iframed-enqueue-block-assets', 'body{padding:20px!important}' );
+ wp_enqueue_script(
+ 'iframed-enqueue-block-assets-script',
+ plugin_dir_url( __FILE__ ) . 'iframed-enqueue-block-assets/script.js',
+ array(),
+ filemtime( plugin_dir_path( __FILE__ ) . 'iframed-enqueue-block-assets/script.js' )
+ );
+ wp_localize_script(
+ 'iframed-enqueue-block-assets-script',
+ 'iframedEnqueueBlockAssetsL10n',
+ array(
+ 'test' => 'Iframed Enqueue Block Assets!',
+ )
+ );
}
);
diff --git a/packages/e2e-tests/plugins/iframed-enqueue-block-assets/script.js b/packages/e2e-tests/plugins/iframed-enqueue-block-assets/script.js
new file mode 100644
index 0000000000000..f0eddd65c70eb
--- /dev/null
+++ b/packages/e2e-tests/plugins/iframed-enqueue-block-assets/script.js
@@ -0,0 +1,3 @@
+window.addEventListener( 'load', () => {
+ document.body.dataset.iframedEnqueueBlockAssetsL10n = window.iframedEnqueueBlockAssetsL10n.test;
+} );
diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js
index c1bd26fe1c761..c29af593abb12 100644
--- a/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js
@@ -32,6 +32,7 @@ describe( 'iframed inline styles', () => {
} );
it( 'should load styles added through enqueue_block_assets', async () => {
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
// Check stylesheet.
expect(
await getComputedStyle( canvas(), 'body', 'background-color' )
@@ -40,5 +41,11 @@ describe( 'iframed inline styles', () => {
expect( await getComputedStyle( canvas(), 'body', 'padding' ) ).toBe(
'20px'
);
+
+ expect(
+ await canvas().evaluate( () => ( { ...document.body.dataset } ) )
+ ).toEqual( {
+ iframedEnqueueBlockAssetsL10n: 'Iframed Enqueue Block Assets!',
+ } );
} );
} );
diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js
index 0c18521b1215f..15bc017900daa 100644
--- a/packages/edit-post/src/components/layout/index.js
+++ b/packages/edit-post/src/components/layout/index.js
@@ -16,10 +16,7 @@ import {
store as editorStore,
} from '@wordpress/editor';
import { useSelect, useDispatch } from '@wordpress/data';
-import {
- BlockBreadcrumb,
- privateApis as blockEditorPrivateApis,
-} from '@wordpress/block-editor';
+import { BlockBreadcrumb } from '@wordpress/block-editor';
import { Button, ScrollLock, Popover } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { PluginArea } from '@wordpress/plugins';
@@ -52,9 +49,6 @@ import WelcomeGuide from '../welcome-guide';
import ActionsPanel from './actions-panel';
import StartPageOptions from '../start-page-options';
import { store as editPostStore } from '../../store';
-import { unlock } from '../../lock-unlock';
-
-const { BlockRemovalWarningModal } = unlock( blockEditorPrivateApis );
const interfaceLabels = {
/* translators: accessibility text for the editor top bar landmark region. */
@@ -69,12 +63,6 @@ const interfaceLabels = {
footer: __( 'Editor footer' ),
};
-const blockRemovalRules = {
- 'core/footnotes': __(
- 'The Footnotes block displays all footnotes found in the content. Note that any footnotes in the content will persist after removing this block.'
- ),
-};
-
function Layout( { styles } ) {
const isMobileViewport = useViewportMatch( 'medium', '<' );
const isHugeViewport = useViewportMatch( 'huge', '>=' );
@@ -214,7 +202,6 @@ function Layout( { styles } ) {
-
{
+ const { canUser } = select( coreStore );
+ const { getEditorSettings } = select( editorStore );
+
+ const isBlockTheme = getEditorSettings().__unstableIsBlockBasedTheme;
+ const defaultUrl = addQueryArgs( 'edit.php', {
+ post_type: 'wp_block',
+ } );
+ const patternsUrl = addQueryArgs( 'site-editor.php', {
+ path: '/patterns',
+ } );
+
+ // The site editor and templates both check whether the user has
+ // edit_theme_options capabilities. We can leverage that here and not
+ // display the manage patterns link if the user can't access it.
+ return canUser( 'read', 'templates' ) && isBlockTheme
+ ? patternsUrl
+ : defaultUrl;
+ }, [] );
+
+ return (
+
+ { __( 'Manage Patterns' ) }
+
+ );
+}
+
registerPlugin( 'edit-post', {
render() {
return (
@@ -22,14 +53,7 @@ registerPlugin( 'edit-post', {
{ ( { onClose } ) => (
<>
-
- { __( 'Manage Patterns' ) }
-
+
diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss
index d33a5ae22c0fd..fac5deef18f71 100644
--- a/packages/edit-post/src/style.scss
+++ b/packages/edit-post/src/style.scss
@@ -41,13 +41,7 @@
}
}
-// In order to use mix-blend-mode, this element needs to have an explicitly set background-color
-// We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations
-html.wp-toolbar {
- background: $white;
-}
-
-body.block-editor-page {
+body.js.block-editor-page {
@include wp-admin-reset( ".block-editor" );
}
diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js
index 34c23cc699bc2..a8bbe75e0261b 100644
--- a/packages/edit-site/src/components/block-editor/editor-canvas.js
+++ b/packages/edit-site/src/components/block-editor/editor-canvas.js
@@ -88,7 +88,7 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) {
enableResizing ? 'min-height:0!important;' : ''
}}body{position:relative; ${
canvasMode === 'view'
- ? 'cursor: pointer; height: 100vh'
+ ? 'cursor: pointer; min-height: 100vh;'
: ''
}}}`
}
diff --git a/packages/edit-site/src/components/create-pattern-modal/index.js b/packages/edit-site/src/components/create-pattern-modal/index.js
index 46d734b86fdd1..753dccfb961dd 100644
--- a/packages/edit-site/src/components/create-pattern-modal/index.js
+++ b/packages/edit-site/src/components/create-pattern-modal/index.js
@@ -14,6 +14,7 @@ import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch } from '@wordpress/data';
+import { serialize } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -21,9 +22,11 @@ import { useDispatch } from '@wordpress/data';
import { SYNC_TYPES, USER_PATTERN_CATEGORY } from '../page-patterns/utils';
export default function CreatePatternModal( {
+ blocks = [],
closeModal,
onCreate,
onError,
+ title,
} ) {
const [ name, setName ] = useState( '' );
const [ syncType, setSyncType ] = useState( SYNC_TYPES.unsynced );
@@ -52,7 +55,7 @@ export default function CreatePatternModal( {
'wp_block',
{
title: name || __( 'Untitled Pattern' ),
- content: '',
+ content: blocks?.length ? serialize( blocks ) : '',
status: 'publish',
meta:
syncType === SYNC_TYPES.unsynced
@@ -76,7 +79,7 @@ export default function CreatePatternModal( {
return (
diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss
index 6281887b13738..d26bbdaf28ff6 100644
--- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss
+++ b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss
@@ -40,6 +40,10 @@
overflow: hidden;
grid-column: 2 / 3;
+ .block-editor-block-icon {
+ min-width: $grid-unit-30;
+ }
+
h1 {
white-space: nowrap;
overflow: hidden;
diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
new file mode 100644
index 0000000000000..d2c14d15f341b
--- /dev/null
+++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js
@@ -0,0 +1,196 @@
+/**
+ * WordPress dependencies
+ */
+import { MenuItem } from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { useDispatch } from '@wordpress/data';
+import { __, sprintf } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { privateApis as routerPrivateApis } from '@wordpress/router';
+
+/**
+ * Internal dependencies
+ */
+import {
+ TEMPLATE_PARTS,
+ PATTERNS,
+ SYNC_TYPES,
+ USER_PATTERNS,
+ USER_PATTERN_CATEGORY,
+} from './utils';
+import {
+ useExistingTemplateParts,
+ getUniqueTemplatePartTitle,
+ getCleanTemplatePartSlug,
+} from '../../utils/template-part-create';
+import { unlock } from '../../lock-unlock';
+
+const { useHistory } = unlock( routerPrivateApis );
+
+function getPatternMeta( item ) {
+ if ( item.type === PATTERNS ) {
+ return { wp_pattern_sync_status: SYNC_TYPES.unsynced };
+ }
+
+ const syncStatus = item.reusableBlock.wp_pattern_sync_status;
+ const isUnsynced = syncStatus === SYNC_TYPES.unsynced;
+
+ return {
+ ...item.reusableBlock.meta,
+ wp_pattern_sync_status: isUnsynced ? syncStatus : undefined,
+ };
+}
+
+export default function DuplicateMenuItem( {
+ categoryId,
+ item,
+ label = __( 'Duplicate' ),
+ onClose,
+} ) {
+ const { saveEntityRecord } = useDispatch( coreStore );
+ const { createErrorNotice, createSuccessNotice } =
+ useDispatch( noticesStore );
+
+ const history = useHistory();
+ const existingTemplateParts = useExistingTemplateParts();
+
+ async function createTemplatePart() {
+ try {
+ const copiedTitle = sprintf(
+ /* translators: %s: Existing template part title */
+ __( '%s (Copy)' ),
+ item.title
+ );
+ const title = getUniqueTemplatePartTitle(
+ copiedTitle,
+ existingTemplateParts
+ );
+ const slug = getCleanTemplatePartSlug( title );
+ const { area, content } = item.templatePart;
+
+ const result = await saveEntityRecord(
+ 'postType',
+ 'wp_template_part',
+ { slug, title, content, area },
+ { throwOnError: true }
+ );
+
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: The new template part's title e.g. 'Call to action (copy)'.
+ __( '"%s" created.' ),
+ title
+ ),
+ {
+ type: 'snackbar',
+ id: 'edit-site-patterns-success',
+ actions: [
+ {
+ label: __( 'Edit' ),
+ onClick: () =>
+ history.push( {
+ postType: TEMPLATE_PARTS,
+ postId: result?.id,
+ categoryType: TEMPLATE_PARTS,
+ categoryId,
+ } ),
+ },
+ ],
+ }
+ );
+
+ onClose();
+ } catch ( error ) {
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : __(
+ 'An error occurred while creating the template part.'
+ );
+
+ createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ id: 'edit-site-patterns-error',
+ } );
+ onClose();
+ }
+ }
+
+ async function createPattern() {
+ try {
+ const isThemePattern = item.type === PATTERNS;
+ const title = sprintf(
+ /* translators: %s: Existing pattern title */
+ __( '%s (Copy)' ),
+ item.title
+ );
+
+ const result = await saveEntityRecord(
+ 'postType',
+ 'wp_block',
+ {
+ content: isThemePattern
+ ? item.content
+ : item.reusableBlock.content,
+ meta: getPatternMeta( item ),
+ status: 'publish',
+ title,
+ },
+ { throwOnError: true }
+ );
+
+ const actionLabel = isThemePattern
+ ? __( 'View my patterns' )
+ : __( 'Edit' );
+
+ const newLocation = isThemePattern
+ ? {
+ categoryType: USER_PATTERNS,
+ categoryId: USER_PATTERN_CATEGORY,
+ path: '/patterns',
+ }
+ : {
+ categoryType: USER_PATTERNS,
+ categoryId: USER_PATTERN_CATEGORY,
+ postType: USER_PATTERNS,
+ postId: result?.id,
+ };
+
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: The new pattern's title e.g. 'Call to action (copy)'.
+ __( '"%s" added to my patterns.' ),
+ title
+ ),
+ {
+ type: 'snackbar',
+ id: 'edit-site-patterns-success',
+ actions: [
+ {
+ label: actionLabel,
+ onClick: () => history.push( newLocation ),
+ },
+ ],
+ }
+ );
+
+ onClose();
+ } catch ( error ) {
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : __( 'An error occurred while creating the pattern.' );
+
+ createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ id: 'edit-site-patterns-error',
+ } );
+ onClose();
+ }
+ }
+
+ const createItem =
+ item.type === TEMPLATE_PARTS ? createTemplatePart : createPattern;
+
+ return { label } ;
+}
diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js
index 7db14e1d37788..441529e1c0583 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -8,87 +8,103 @@ import classnames from 'classnames';
*/
import { BlockPreview } from '@wordpress/block-editor';
import {
+ Button,
__experimentalConfirmDialog as ConfirmDialog,
DropdownMenu,
MenuGroup,
MenuItem,
__experimentalHeading as Heading,
__experimentalHStack as HStack,
- __unstableCompositeItem as CompositeItem,
Tooltip,
Flex,
} from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
-import { useState, useId } from '@wordpress/element';
+import { useState, useId, memo } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import {
Icon,
header,
footer,
- symbolFilled,
+ symbolFilled as uncategorized,
+ symbol,
moreHorizontal,
lockSmall,
} from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { store as reusableBlocksStore } from '@wordpress/reusable-blocks';
-import { DELETE, BACKSPACE } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
-import { PATTERNS, USER_PATTERNS } from './utils';
+import RenameMenuItem from './rename-menu-item';
+import DuplicateMenuItem from './duplicate-menu-item';
+import { PATTERNS, TEMPLATE_PARTS, USER_PATTERNS, SYNC_TYPES } from './utils';
+import { store as editSiteStore } from '../../store';
import { useLink } from '../routes/link';
-const THEME_PATTERN_TOOLTIP = __( 'Theme patterns cannot be edited.' );
+const templatePartIcons = { header, footer, uncategorized };
-export default function GridItem( { categoryId, composite, icon, item } ) {
+function GridItem( { categoryId, item, ...props } ) {
const descriptionId = useId();
const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false );
+ const { removeTemplate } = useDispatch( editSiteStore );
const { __experimentalDeleteReusableBlock } =
useDispatch( reusableBlocksStore );
const { createErrorNotice, createSuccessNotice } =
useDispatch( noticesStore );
+ const isUserPattern = item.type === USER_PATTERNS;
+ const isNonUserPattern = item.type === PATTERNS;
+ const isTemplatePart = item.type === TEMPLATE_PARTS;
+
const { onClick } = useLink( {
postType: item.type,
- postId: item.type === USER_PATTERNS ? item.id : item.name,
+ postId: isUserPattern ? item.id : item.name,
categoryId,
categoryType: item.type,
} );
- const onKeyDown = ( event ) => {
- if ( DELETE === event.keyCode || BACKSPACE === event.keyCode ) {
- setIsDeleteDialogOpen( true );
- }
- };
-
const isEmpty = ! item.blocks?.length;
const patternClassNames = classnames( 'edit-site-patterns__pattern', {
'is-placeholder': isEmpty,
} );
const previewClassNames = classnames( 'edit-site-patterns__preview', {
- 'is-inactive': item.type === PATTERNS,
+ 'is-inactive': isNonUserPattern,
} );
const deletePattern = async () => {
try {
await __experimentalDeleteReusableBlock( item.id );
- createSuccessNotice( __( 'Pattern successfully deleted.' ), {
- type: 'snackbar',
- } );
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: The pattern's title e.g. 'Call to action'.
+ __( '"%s" deleted.' ),
+ item.title
+ ),
+ { type: 'snackbar', id: 'edit-site-patterns-success' }
+ );
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __( 'An error occurred while deleting the pattern.' );
- createErrorNotice( errorMessage, { type: 'snackbar' } );
+ createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ id: 'edit-site-patterns-error',
+ } );
}
};
+ const deleteItem = () =>
+ isTemplatePart ? removeTemplate( item ) : deletePattern();
- const isUserPattern = item.type === USER_PATTERNS;
+ // Only custom patterns or custom template parts can be renamed or deleted.
+ const isCustomPattern =
+ isUserPattern || ( isTemplatePart && item.isCustom );
+ const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file;
const ariaDescriptions = [];
- if ( isUserPattern ) {
+
+ if ( isCustomPattern ) {
// User patterns don't have descriptions, but can be edited and deleted, so include some help text.
ariaDescriptions.push(
__( 'Press Enter to edit, or Delete to delete the pattern.' )
@@ -96,139 +112,173 @@ export default function GridItem( { categoryId, composite, icon, item } ) {
} else if ( item.description ) {
ariaDescriptions.push( item.description );
}
- if ( item.type === PATTERNS ) {
- ariaDescriptions.push( THEME_PATTERN_TOOLTIP );
- }
- let itemIcon = icon;
- if ( categoryId === 'header' ) {
- itemIcon = header;
- } else if ( categoryId === 'footer' ) {
- itemIcon = footer;
- } else if ( categoryId === 'uncategorized' ) {
- itemIcon = symbolFilled;
+ if ( isNonUserPattern ) {
+ ariaDescriptions.push( __( 'Theme patterns cannot be edited.' ) );
}
+ const itemIcon =
+ templatePartIcons[ categoryId ] ||
+ ( item.syncStatus === SYNC_TYPES.full ? symbol : undefined );
+
+ const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' );
+ const confirmPrompt = hasThemeFile
+ ? __( 'Are you sure you want to clear these customizations?' )
+ : sprintf(
+ // translators: %s: The pattern or template part's title e.g. 'Call to action'.
+ __( 'Are you sure you want to delete "%s"?' ),
+ item.title
+ );
+
return (
- <>
-
-
- `${ descriptionId }-${ index }`
- )
- .join( ' ' )
- : undefined
- }
+
+
+ `${ descriptionId }-${ index }`
+ )
+ .join( ' ' )
+ : undefined
+ }
+ >
+ { isEmpty && __( 'Empty pattern' ) }
+ { ! isEmpty && }
+
+ { ariaDescriptions.map( ( ariaDescription, index ) => (
+
- { isEmpty && __( 'Empty pattern' ) }
- { ! isEmpty &&
}
-
- { ariaDescriptions.map( ( ariaDescription, index ) => (
-
- { ariaDescription }
-
- ) ) }
+ { ariaDescription }
+
+ ) ) }
+
-
- { icon && (
-
- ) }
-
- { item.title }
- { item.type === PATTERNS && (
-
+
+
+
+ ) }
+
+ { item.type === PATTERNS ? (
+ item.title
+ ) : (
+
+
-
-
-
-
+ { item.title }
+
+
+ ) }
+ { item.type === PATTERNS && (
+
+
+
+
+
+ ) }
+
+
+
+ { ( { onClose } ) => (
+
+ { isCustomPattern && ! hasThemeFile && (
+
) }
-
-
- { item.type === USER_PATTERNS && (
-
- { () => (
-
-
- setIsDeleteDialogOpen( true )
- }
- >
- { __( 'Delete' ) }
-
-
+
+ { isCustomPattern && (
+
+ setIsDeleteDialogOpen( true )
+ }
+ >
+ { hasThemeFile
+ ? __( 'Clear customizations' )
+ : __( 'Delete' ) }
+
) }
-
+
) }
-
-
+
+
+
{ isDeleteDialogOpen && (
setIsDeleteDialogOpen( false ) }
>
- { __( 'Are you sure you want to delete this pattern?' ) }
+ { confirmPrompt }
) }
- >
+
);
}
+
+export default memo( GridItem );
diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js
index 3f6e5fd01f72f..1902b36982c14 100644
--- a/packages/edit-site/src/components/page-patterns/grid.js
+++ b/packages/edit-site/src/components/page-patterns/grid.js
@@ -1,39 +1,52 @@
/**
* WordPress dependencies
*/
-import {
- __unstableComposite as Composite,
- __unstableUseCompositeState as useCompositeState,
-} from '@wordpress/components';
+import { __experimentalText as Text } from '@wordpress/components';
+import { useRef } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import GridItem from './grid-item';
-export default function Grid( { categoryId, label, icon, items } ) {
- const composite = useCompositeState( { orientation: 'vertical' } );
+const PAGE_SIZE = 100;
+
+export default function Grid( { categoryId, items, ...props } ) {
+ const gridRef = useRef();
if ( ! items?.length ) {
return null;
}
+ const list = items.slice( 0, PAGE_SIZE );
+ const restLength = items.length - PAGE_SIZE;
+
return (
-
- { items.map( ( item ) => (
-
- ) ) }
-
+ <>
+
+ { list.map( ( item ) => (
+
+ ) ) }
+
+ { restLength > 0 && (
+
+ { sprintf(
+ /* translators: %d: number of patterns */
+ __( '+ %d more patterns discoverable by searching' ),
+ restLength
+ ) }
+
+ ) }
+ >
);
}
diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js
new file mode 100644
index 0000000000000..1237b85d6c978
--- /dev/null
+++ b/packages/edit-site/src/components/page-patterns/header.js
@@ -0,0 +1,69 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalVStack as VStack,
+ __experimentalHeading as Heading,
+ __experimentalText as Text,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { store as editorStore } from '@wordpress/editor';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories';
+import {
+ USER_PATTERN_CATEGORY,
+ USER_PATTERNS,
+ TEMPLATE_PARTS,
+ PATTERNS,
+} from './utils';
+
+export default function PatternsHeader( {
+ categoryId,
+ type,
+ titleId,
+ descriptionId,
+} ) {
+ const { patternCategories } = usePatternCategories();
+ const templatePartAreas = useSelect(
+ ( select ) =>
+ select( editorStore ).__experimentalGetDefaultTemplatePartAreas(),
+ []
+ );
+
+ let title, description;
+ if ( categoryId === USER_PATTERN_CATEGORY && type === USER_PATTERNS ) {
+ title = __( 'My Patterns' );
+ description = '';
+ } else if ( type === TEMPLATE_PARTS ) {
+ const templatePartArea = templatePartAreas.find(
+ ( area ) => area.area === categoryId
+ );
+ title = templatePartArea?.label;
+ description = templatePartArea?.description;
+ } else if ( type === PATTERNS ) {
+ const patternCategory = patternCategories.find(
+ ( category ) => category.name === categoryId
+ );
+ title = patternCategory?.label;
+ description = patternCategory?.description;
+ }
+
+ if ( ! title ) return null;
+
+ return (
+
+
+ { title }
+
+ { description ? (
+
+ { description }
+
+ ) : null }
+
+ );
+}
diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js
index 961ed51f39e5d..d90fc74844244 100644
--- a/packages/edit-site/src/components/page-patterns/index.js
+++ b/packages/edit-site/src/components/page-patterns/index.js
@@ -32,7 +32,12 @@ export default function PagePatterns() {
title={ __( 'Patterns content' ) }
hideTitleFromUI
>
-
+
);
diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js
index 545ffdb044275..7bf2a9d506584 100644
--- a/packages/edit-site/src/components/page-patterns/patterns-list.js
+++ b/packages/edit-site/src/components/page-patterns/patterns-list.js
@@ -1,51 +1,87 @@
/**
* WordPress dependencies
*/
-
+import { useState, useDeferredValue, useId } from '@wordpress/element';
import {
SearchControl,
- __experimentalHeading as Heading,
- __experimentalText as Text,
__experimentalVStack as VStack,
Flex,
FlexBlock,
+ __experimentalToggleGroupControl as ToggleGroupControl,
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
+ __experimentalHeading as Heading,
+ __experimentalText as Text,
} from '@wordpress/components';
import { __, isRTL } from '@wordpress/i18n';
-import { symbol, chevronLeft, chevronRight } from '@wordpress/icons';
+import { chevronLeft, chevronRight } from '@wordpress/icons';
import { privateApis as routerPrivateApis } from '@wordpress/router';
-import { useViewportMatch } from '@wordpress/compose';
+import { useViewportMatch, useAsyncList } from '@wordpress/compose';
/**
* Internal dependencies
*/
+import PatternsHeader from './header';
import Grid from './grid';
import NoPatterns from './no-patterns';
import usePatterns from './use-patterns';
import SidebarButton from '../sidebar-button';
import useDebouncedInput from '../../utils/use-debounced-input';
import { unlock } from '../../lock-unlock';
+import { SYNC_TYPES, USER_PATTERN_CATEGORY } from './utils';
const { useLocation, useHistory } = unlock( routerPrivateApis );
+const SYNC_FILTERS = {
+ all: __( 'All' ),
+ [ SYNC_TYPES.full ]: __( 'Synced' ),
+ [ SYNC_TYPES.unsynced ]: __( 'Standard' ),
+};
+
+const SYNC_DESCRIPTIONS = {
+ all: '',
+ [ SYNC_TYPES.full ]: __(
+ 'Patterns that are kept in sync across the site.'
+ ),
+ [ SYNC_TYPES.unsynced ]: __(
+ 'Patterns that can be changed freely without affecting the site.'
+ ),
+};
+
export default function PatternsList( { categoryId, type } ) {
const location = useLocation();
const history = useHistory();
const isMobileViewport = useViewportMatch( 'medium', '<' );
const [ filterValue, setFilterValue, delayedFilterValue ] =
useDebouncedInput( '' );
+ const deferredFilterValue = useDeferredValue( delayedFilterValue );
- const [ patterns, isResolving ] = usePatterns(
- type,
- categoryId,
- delayedFilterValue
- );
+ const [ syncFilter, setSyncFilter ] = useState( 'all' );
+ const deferredSyncedFilter = useDeferredValue( syncFilter );
+ const { patterns, isResolving } = usePatterns( type, categoryId, {
+ search: deferredFilterValue,
+ syncStatus:
+ deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter,
+ } );
- const { syncedPatterns, unsyncedPatterns } = patterns;
- const hasPatterns = !! syncedPatterns.length || !! unsyncedPatterns.length;
+ const id = useId();
+ const titleId = `${ id }-title`;
+ const descriptionId = `${ id }-description`;
+
+ const hasPatterns = patterns.length;
+ const title = SYNC_FILTERS[ syncFilter ];
+ const description = SYNC_DESCRIPTIONS[ syncFilter ];
+ const shownPatterns = useAsyncList( patterns );
return (
-
+
+
+
{ isMobileViewport && (
) }
-
+
setFilterValue( value ) }
@@ -71,42 +107,48 @@ export default function PatternsList( { categoryId, type } ) {
__nextHasNoMarginBottom
/>
+ { categoryId === USER_PATTERN_CATEGORY && (
+ setSyncFilter( value ) }
+ __nextHasNoMarginBottom
+ >
+ { Object.entries( SYNC_FILTERS ).map(
+ ( [ key, label ] ) => (
+
+ )
+ ) }
+
+ ) }
- { isResolving && __( 'Loading' ) }
- { ! isResolving && !! syncedPatterns.length && (
- <>
-
- { __( 'Synced' ) }
-
- { __(
- 'Patterns that are kept in sync across the site'
- ) }
+ { syncFilter !== 'all' && (
+
+
+ { title }
+
+ { description ? (
+
+ { description }
-
-
- >
+ ) : null }
+
) }
- { ! isResolving && !! unsyncedPatterns.length && (
- <>
-
- { __( 'Standard' ) }
-
- { __(
- 'Patterns that can be changed freely without affecting the site'
- ) }
-
-
-
- >
+ { hasPatterns && (
+
) }
{ ! isResolving && ! hasPatterns && }
diff --git a/packages/edit-site/src/components/page-patterns/rename-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-menu-item.js
new file mode 100644
index 0000000000000..938023a62cefd
--- /dev/null
+++ b/packages/edit-site/src/components/page-patterns/rename-menu-item.js
@@ -0,0 +1,115 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ Button,
+ MenuItem,
+ Modal,
+ TextControl,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
+import { TEMPLATE_PARTS } from './utils';
+
+export default function RenameMenuItem( { item, onClose } ) {
+ const [ title, setTitle ] = useState( () => item.title );
+ const [ isModalOpen, setIsModalOpen ] = useState( false );
+
+ const { editEntityRecord, saveEditedEntityRecord } =
+ useDispatch( coreStore );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+
+ if ( item.type === TEMPLATE_PARTS && ! item.isCustom ) {
+ return null;
+ }
+
+ async function onRename( event ) {
+ event.preventDefault();
+
+ try {
+ await editEntityRecord( 'postType', item.type, item.id, { title } );
+
+ // Update state before saving rerenders the list.
+ setTitle( '' );
+ setIsModalOpen( false );
+ onClose();
+
+ // Persist edited entity.
+ await saveEditedEntityRecord( 'postType', item.type, item.id, {
+ throwOnError: true,
+ } );
+
+ createSuccessNotice( __( 'Entity renamed.' ), {
+ type: 'snackbar',
+ } );
+ } catch ( error ) {
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : __( 'An error occurred while renaming the entity.' );
+
+ createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ }
+
+ return (
+ <>
+ {
+ setIsModalOpen( true );
+ setTitle( item.title );
+ } }
+ >
+ { __( 'Rename' ) }
+
+ { isModalOpen && (
+ {
+ setIsModalOpen( false );
+ onClose();
+ } }
+ overlayClassName="edit-site-list__rename_modal"
+ >
+
+
+ ) }
+ >
+ );
+}
diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss
index fdf0aea3431f6..79731999f46ef 100644
--- a/packages/edit-site/src/components/page-patterns/style.scss
+++ b/packages/edit-site/src/components/page-patterns/style.scss
@@ -1,6 +1,13 @@
.edit-site-patterns {
- background: rgba(0, 0, 0, 0.05);
+ background: rgba(0, 0, 0, 0.15);
margin: $header-height 0 0;
+ .components-base-control {
+ width: 100%;
+ @include break-medium {
+ width: auto;
+ }
+ }
+
.components-text {
color: $gray-600;
}
@@ -12,33 +19,81 @@
@include break-medium {
margin: 0;
}
+
+ .edit-site-patterns__search-block {
+ min-width: fit-content;
+ flex-grow: 1;
+ }
+
+ // The increased specificity here is to overcome component styles
+ // without relying on internal component class names.
+ .edit-site-patterns__search {
+ input[type="search"] {
+ height: $button-size-next-default-40px;
+ background: $gray-800;
+ color: $gray-200;
+
+ &:focus {
+ background: $gray-800;
+ }
+ }
+
+ svg {
+ fill: $gray-600;
+ }
+ }
+
+ .edit-site-patterns__sync-status-filter {
+ background: $gray-800;
+ border: none;
+ height: $button-size-next-default-40px;
+ min-width: max-content;
+ width: 100%;
+ max-width: 100%;
+
+ @include break-medium {
+ width: 300px;
+ }
+ }
+ .edit-site-patterns__sync-status-filter-option:active {
+ background: $gray-700;
+ color: $gray-100;
+ }
}
-.edit-site-patterns__grid {
- column-gap: $grid-unit-30;
- @include break-large() {
- column-count: 2;
+.edit-site-patterns__section-header {
+ .screen-reader-shortcut:focus {
+ top: 0;
}
+}
+.edit-site-patterns__grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: $grid-unit-40;
// Small top padding required to avoid cutting off the visible outline
// when hovering items.
padding-top: $border-width-focus-fallback;
margin-bottom: $grid-unit-40;
-
+ @include break-large {
+ grid-template-columns: 1fr 1fr;
+ }
.edit-site-patterns__pattern {
break-inside: avoid-column;
display: flex;
flex-direction: column;
- margin-bottom: $grid-unit-60;
-
.edit-site-patterns__preview {
- border-radius: $radius-block-ui;
+ box-shadow: none;
+ border: none;
+ padding: 0;
+ background-color: unset;
+ box-sizing: border-box;
+ border-radius: 4px;
cursor: pointer;
overflow: hidden;
&:focus {
- box-shadow: inset 0 0 0 2px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
-
+ box-shadow: inset 0 0 0 0 $white, 0 0 0 2px var(--wp-admin-theme-color);
// Windows High Contrast mode will show this outline, but not the box-shadow.
outline: 2px solid transparent;
}
@@ -46,6 +101,10 @@
&.is-inactive {
cursor: default;
}
+ &.is-inactive:focus {
+ box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $gray-800;
+ opacity: 0.8;
+ }
}
.edit-site-patterns__footer,
@@ -68,31 +127,28 @@
}
.edit-site-patterns__preview {
- flex: 1;
+ flex: 0 1 auto;
margin-bottom: $grid-unit-20;
}
}
-// The increased specificity here is to overcome component styles
-// without relying on internal component class names.
-.edit-site-patterns__search {
- {&} input[type="search"] {
- background: $gray-800;
+.edit-site-patterns__load-more {
+ align-self: center;
+}
+
+.edit-site-patterns__pattern-title {
+ color: $gray-200;
+
+ .is-link {
+ text-decoration: none;
color: $gray-200;
+ &:hover,
&:focus {
- background: $gray-800;
+ color: $white;
}
}
- svg {
- fill: $gray-600;
- }
-}
-
-.edit-site-patterns__pattern-title {
- color: $gray-600;
-
.edit-site-patterns__pattern-icon {
border-radius: $grid-unit-05;
background: var(--wp-block-synced-color);
@@ -101,6 +157,10 @@
.edit-site-patterns__pattern-lock-icon {
display: inline-flex;
+
+ svg {
+ fill: currentcolor;
+ }
}
}
diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js
index a394aabf572c4..ea2b8ac976fea 100644
--- a/packages/edit-site/src/components/page-patterns/use-patterns.js
+++ b/packages/edit-site/src/components/page-patterns/use-patterns.js
@@ -4,7 +4,7 @@
import { parse } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
-import { useMemo } from '@wordpress/element';
+import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@@ -15,7 +15,6 @@ import {
SYNC_TYPES,
TEMPLATE_PARTS,
USER_PATTERNS,
- USER_PATTERN_CATEGORY,
filterOutDuplicatesByName,
} from './utils';
import { unlock } from '../../lock-unlock';
@@ -31,9 +30,11 @@ const templatePartToPattern = ( templatePart ) => ( {
blocks: parse( templatePart.content.raw ),
categories: [ templatePart.area ],
description: templatePart.description || '',
+ isCustom: templatePart.source === 'custom',
keywords: templatePart.keywords || [],
+ id: createTemplatePartId( templatePart.theme, templatePart.slug ),
name: createTemplatePartId( templatePart.theme, templatePart.slug ),
- title: templatePart.title.rendered,
+ title: decodeEntities( templatePart.title.rendered ),
type: templatePart.type,
templatePart,
} );
@@ -41,106 +42,64 @@ const templatePartToPattern = ( templatePart ) => ( {
const templatePartHasCategory = ( item, category ) =>
item.templatePart.area === category;
-const useTemplatePartsAsPatterns = (
- categoryId,
- postType = TEMPLATE_PARTS,
- filterValue = ''
+const selectTemplatePartsAsPatterns = (
+ select,
+ { categoryId, search = '' } = {}
) => {
- const { templateParts, isResolving } = useSelect(
- ( select ) => {
- if ( postType !== TEMPLATE_PARTS ) {
- return {
- templateParts: EMPTY_PATTERN_LIST,
- isResolving: false,
- };
- }
-
- const { getEntityRecords, isResolving: _isResolving } =
- select( coreStore );
- const query = { per_page: -1 };
- const rawTemplateParts = getEntityRecords(
- 'postType',
- postType,
- query
- );
- const partsAsPatterns = rawTemplateParts?.map( ( templatePart ) =>
- templatePartToPattern( templatePart )
- );
-
- return {
- templateParts: partsAsPatterns,
- isResolving: _isResolving( 'getEntityRecords', [
- 'postType',
- 'wp_template_part',
- query,
- ] ),
- };
- },
- [ postType ]
+ const { getEntityRecords, getIsResolving } = select( coreStore );
+ const query = { per_page: -1 };
+ const rawTemplateParts =
+ getEntityRecords( 'postType', TEMPLATE_PARTS, query ) ??
+ EMPTY_PATTERN_LIST;
+ const templateParts = rawTemplateParts.map( ( templatePart ) =>
+ templatePartToPattern( templatePart )
);
- const filteredTemplateParts = useMemo( () => {
- if ( ! templateParts ) {
- return EMPTY_PATTERN_LIST;
- }
+ const isResolving = getIsResolving( 'getEntityRecords', [
+ 'postType',
+ 'wp_template_part',
+ query,
+ ] );
- return searchItems( templateParts, filterValue, {
- categoryId,
- hasCategory: templatePartHasCategory,
- } );
- }, [ templateParts, filterValue, categoryId ] );
+ const patterns = searchItems( templateParts, search, {
+ categoryId,
+ hasCategory: templatePartHasCategory,
+ } );
- return { templateParts: filteredTemplateParts, isResolving };
+ return { patterns, isResolving };
};
-const useThemePatterns = (
- categoryId,
- postType = PATTERNS,
- filterValue = ''
-) => {
- const blockPatterns = useSelect( ( select ) => {
- const { getSettings } = unlock( select( editSiteStore ) );
- const settings = getSettings();
- return (
- settings.__experimentalAdditionalBlockPatterns ??
- settings.__experimentalBlockPatterns
- );
+const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+ const settings = getSettings();
+ const blockPatterns =
+ settings.__experimentalAdditionalBlockPatterns ??
+ settings.__experimentalBlockPatterns;
+
+ const restBlockPatterns = select( coreStore ).getBlockPatterns();
+
+ let patterns = [
+ ...( blockPatterns || [] ),
+ ...( restBlockPatterns || [] ),
+ ]
+ .filter(
+ ( pattern ) => ! CORE_PATTERN_SOURCES.includes( pattern.source )
+ )
+ .filter( filterOutDuplicatesByName )
+ .map( ( pattern ) => ( {
+ ...pattern,
+ keywords: pattern.keywords || [],
+ type: 'pattern',
+ blocks: parse( pattern.content ),
+ } ) );
+
+ patterns = searchItems( patterns, search, {
+ categoryId,
+ hasCategory: ( item, currentCategory ) =>
+ item.categories?.includes( currentCategory ),
} );
- const restBlockPatterns = useSelect( ( select ) =>
- select( coreStore ).getBlockPatterns()
- );
-
- const patterns = useMemo(
- () =>
- [ ...( blockPatterns || [] ), ...( restBlockPatterns || [] ) ]
- .filter(
- ( pattern ) =>
- ! CORE_PATTERN_SOURCES.includes( pattern.source )
- )
- .filter( filterOutDuplicatesByName )
- .map( ( pattern ) => ( {
- ...pattern,
- keywords: pattern.keywords || [],
- type: 'pattern',
- blocks: parse( pattern.content ),
- } ) ),
- [ blockPatterns, restBlockPatterns ]
- );
-
- const filteredPatterns = useMemo( () => {
- if ( postType !== PATTERNS ) {
- return EMPTY_PATTERN_LIST;
- }
-
- return searchItems( patterns, filterValue, {
- categoryId,
- hasCategory: ( item, currentCategory ) =>
- item.categories?.includes( currentCategory ),
- } );
- }, [ patterns, filterValue, categoryId, postType ] );
-
- return filteredPatterns;
+ return { patterns, isResolving: false };
};
const reusableBlockToPattern = ( reusableBlock ) => ( {
@@ -154,88 +113,58 @@ const reusableBlockToPattern = ( reusableBlock ) => ( {
reusableBlock,
} );
-const useUserPatterns = (
- categoryId,
- categoryType = PATTERNS,
- filterValue = ''
-) => {
- const postType = categoryType === PATTERNS ? USER_PATTERNS : categoryType;
- const unfilteredPatterns = useSelect(
- ( select ) => {
- if (
- postType !== USER_PATTERNS ||
- categoryId !== USER_PATTERN_CATEGORY
- ) {
- return EMPTY_PATTERN_LIST;
- }
+const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => {
+ const { getEntityRecords, getIsResolving } = select( coreStore );
- const { getEntityRecords } = select( coreStore );
- const records = getEntityRecords( 'postType', postType, {
- per_page: -1,
- } );
+ const query = { per_page: -1 };
+ const records = getEntityRecords( 'postType', USER_PATTERNS, query );
- if ( ! records ) {
- return EMPTY_PATTERN_LIST;
- }
+ let patterns = records
+ ? records.map( ( record ) => reusableBlockToPattern( record ) )
+ : EMPTY_PATTERN_LIST;
+ const isResolving = getIsResolving( 'getEntityRecords', [
+ 'postType',
+ USER_PATTERNS,
+ query,
+ ] );
- return records.map( ( record ) =>
- reusableBlockToPattern( record )
- );
- },
- [ postType, categoryId ]
- );
+ if ( syncStatus ) {
+ patterns = patterns.filter(
+ ( pattern ) => pattern.syncStatus === syncStatus
+ );
+ }
- const filteredPatterns = useMemo( () => {
- if ( ! unfilteredPatterns.length ) {
- return EMPTY_PATTERN_LIST;
- }
-
- return searchItems( unfilteredPatterns, filterValue, {
- // We exit user pattern retrieval early if we aren't in the
- // catch-all category for user created patterns, so it has
- // to be in the category.
- hasCategory: () => true,
- } );
- }, [ unfilteredPatterns, filterValue ] );
-
- const patterns = { syncedPatterns: [], unsyncedPatterns: [] };
-
- filteredPatterns.forEach( ( pattern ) => {
- if ( pattern.syncStatus === SYNC_TYPES.full ) {
- patterns.syncedPatterns.push( pattern );
- } else {
- patterns.unsyncedPatterns.push( pattern );
- }
+ patterns = searchItems( patterns, search, {
+ // We exit user pattern retrieval early if we aren't in the
+ // catch-all category for user created patterns, so it has
+ // to be in the category.
+ hasCategory: () => true,
} );
- return patterns;
+ return { patterns, isResolving };
};
-export const usePatterns = ( categoryType, categoryId, filterValue ) => {
- const blockPatterns = useThemePatterns(
- categoryId,
- categoryType,
- filterValue
- );
-
- const { syncedPatterns = [], unsyncedPatterns = [] } = useUserPatterns(
- categoryId,
- categoryType,
- filterValue
- );
-
- const { templateParts, isResolving } = useTemplatePartsAsPatterns(
- categoryId,
- categoryType,
- filterValue
+export const usePatterns = (
+ categoryType,
+ categoryId,
+ { search = '', syncStatus }
+) => {
+ return useSelect(
+ ( select ) => {
+ if ( categoryType === TEMPLATE_PARTS ) {
+ return selectTemplatePartsAsPatterns( select, {
+ categoryId,
+ search,
+ } );
+ } else if ( categoryType === PATTERNS ) {
+ return selectThemePatterns( select, { categoryId, search } );
+ } else if ( categoryType === USER_PATTERNS ) {
+ return selectUserPatterns( select, { search, syncStatus } );
+ }
+ return { patterns: EMPTY_PATTERN_LIST, isResolving: false };
+ },
+ [ categoryId, categoryType, search, syncStatus ]
);
-
- const patterns = {
- syncedPatterns: [ ...templateParts, ...syncedPatterns ],
- unsyncedPatterns: [ ...blockPatterns, ...unsyncedPatterns ],
- };
-
- return [ patterns, isResolving ];
};
export default usePatterns;
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js
index bcfc540b1f841..f864d48de3383 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js
@@ -9,42 +9,10 @@ import classnames from 'classnames';
import { __, sprintf } from '@wordpress/i18n';
import { dateI18n, getDate, humanTimeDiff } from '@wordpress/date';
import { createInterpolateElement } from '@wordpress/element';
-import { Path, SVG } from '@wordpress/primitives';
-
-const publishedIcon = (
-
-
-
-);
-
-const draftIcon = (
-
-
-
-);
-
-const pendingIcon = (
-
-
-
-);
export default function StatusLabel( { status, date, short } ) {
const relateToNow = humanTimeDiff( date );
let statusLabel = status;
- let statusIcon = pendingIcon;
switch ( status ) {
case 'publish':
statusLabel = date
@@ -57,7 +25,6 @@ export default function StatusLabel( { status, date, short } ) {
{ time: }
)
: __( 'Published' );
- statusIcon = publishedIcon;
break;
case 'future':
const formattedDate = dateI18n(
@@ -77,7 +44,6 @@ export default function StatusLabel( { status, date, short } ) {
break;
case 'draft':
statusLabel = __( 'Draft' );
- statusIcon = draftIcon;
break;
case 'pending':
statusLabel = __( 'Pending' );
@@ -99,7 +65,7 @@ export default function StatusLabel( { status, date, short } ) {
}
) }
>
- { statusIcon } { statusLabel }
+ { statusLabel }
);
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js
index f200382f96311..d3fc15358027b 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js
@@ -35,7 +35,6 @@ function TemplatePartGroup( { areas, currentArea, currentType } ) {
<>
{ __( 'Template parts' ) }
-
{ __( 'Synced patterns for use in template building.' ) }
{ Object.entries( areas ).map(
@@ -64,11 +63,6 @@ function ThemePatternsGroup( { categories, currentCategory, currentType } ) {
<>
{ __( 'Theme patterns' ) }
-
- { __(
- 'For insertion into documents where they can then be customized.'
- ) }
-
{ categories.map( ( category ) => (
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss
index 65790b5e86216..6a6fbc009e0aa 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/style.scss
@@ -1,8 +1,5 @@
.edit-site-sidebar-navigation-screen-patterns__group {
margin-bottom: $grid-unit-40;
- padding-bottom: $grid-unit-30;
- border-bottom: 1px solid $gray-800;
-
&:last-of-type,
&:first-of-type {
border-bottom: 0;
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js
index e3d5cc297164a..37f0b0f8a4e06 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js
@@ -6,18 +6,19 @@ import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
export default function useMyPatterns() {
- const myPatterns = useSelect( ( select ) =>
- select( coreStore ).getEntityRecords( 'postType', 'wp_block', {
- per_page: -1,
- } )
+ const myPatternsCount = useSelect(
+ ( select ) =>
+ select( coreStore ).getEntityRecords( 'postType', 'wp_block', {
+ per_page: -1,
+ } )?.length ?? 0
);
return {
myPatterns: {
- count: myPatterns?.length || 0,
+ count: myPatternsCount,
name: 'my-patterns',
label: __( 'My patterns' ),
},
- hasPatterns: !! myPatterns?.length,
+ hasPatterns: myPatternsCount > 0,
};
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js
index 4c0d9d82f02cb..919e7e92d721d 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js
@@ -78,7 +78,7 @@ export default function SidebarNavigationScreen( {
icon={ icon }
label={
! isPreviewingTheme()
- ? __( 'Go back to the Dashboard' )
+ ? __( 'Go to the Dashboard' )
: __( 'Go back to the theme showcase' )
}
href={
diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js
index d4f68943c46ae..897924a92a12b 100644
--- a/packages/edit-site/src/components/site-hub/index.js
+++ b/packages/edit-site/src/components/site-hub/index.js
@@ -57,7 +57,7 @@ const SiteHub = forwardRef( ( props, ref ) => {
const siteIconButtonProps = isBackToDashboardButton
? {
href: dashboardLink,
- label: __( 'Go back to the Dashboard' ),
+ label: __( 'Go to the Dashboard' ),
}
: {
href: dashboardLink, // We need to keep the `href` here so the component doesn't remount as a `` and break the animation.
diff --git a/packages/edit-site/src/components/template-actions/index.js b/packages/edit-site/src/components/template-actions/index.js
index b4618dcae966d..6e744135df67f 100644
--- a/packages/edit-site/src/components/template-actions/index.js
+++ b/packages/edit-site/src/components/template-actions/index.js
@@ -13,6 +13,7 @@ import {
} from '@wordpress/components';
import { moreVertical } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
+import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@@ -58,7 +59,7 @@ export default function TemplateActions( {
sprintf(
/* translators: The template/part's name. */
__( '"%s" reverted.' ),
- template.title.rendered
+ decodeEntities( template.title.rendered )
),
{
type: 'snackbar',
diff --git a/packages/edit-site/src/components/template-actions/rename-menu-item.js b/packages/edit-site/src/components/template-actions/rename-menu-item.js
index dec7f0bc8bf7d..51959cfe579a1 100644
--- a/packages/edit-site/src/components/template-actions/rename-menu-item.js
+++ b/packages/edit-site/src/components/template-actions/rename-menu-item.js
@@ -14,9 +14,11 @@ import {
} from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
+import { decodeEntities } from '@wordpress/html-entities';
export default function RenameMenuItem( { template, onClose } ) {
- const [ title, setTitle ] = useState( () => template.title.rendered );
+ const title = decodeEntities( template.title.rendered );
+ const [ editedTitle, setEditedTitle ] = useState( title );
const [ isModalOpen, setIsModalOpen ] = useState( false );
const { editEntityRecord, saveEditedEntityRecord } =
@@ -33,11 +35,11 @@ export default function RenameMenuItem( { template, onClose } ) {
try {
await editEntityRecord( 'postType', template.type, template.id, {
- title,
+ title: editedTitle,
} );
// Update state before saving rerenders the list.
- setTitle( '' );
+ setEditedTitle( '' );
setIsModalOpen( false );
onClose();
@@ -67,7 +69,7 @@ export default function RenameMenuItem( { template, onClose } ) {
{
setIsModalOpen( true );
- setTitle( template.title.rendered );
+ setEditedTitle( title );
} }
>
{ __( 'Rename' ) }
@@ -85,8 +87,8 @@ export default function RenameMenuItem( { template, onClose } ) {
diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
index d82f3a86847da..9b4730a7bb1fc 100644
--- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
+++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
@@ -18,6 +18,7 @@ import { __, sprintf } from '@wordpress/i18n';
import {
__EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY,
getBlockType,
+ hasBlockSupport,
} from '@wordpress/blocks';
import { useContext, useMemo, useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
@@ -93,6 +94,8 @@ const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = {
'typography.fontFamily': 'fontFamily',
};
+const SUPPORTED_STYLES = [ 'border', 'color', 'spacing', 'typography' ];
+
function useChangesToPush( name, attributes ) {
const supports = useSupportedStyles( name );
@@ -212,10 +215,14 @@ function PushChangesToGlobalStylesControl( {
const withPushChangesToGlobalStyles = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const blockEditingMode = useBlockEditingMode();
+ const supportsStyles = SUPPORTED_STYLES.some( ( feature ) =>
+ hasBlockSupport( props.name, feature )
+ );
+
return (
<>
- { blockEditingMode === 'default' && (
+ { blockEditingMode === 'default' && supportsStyles && (
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index 5a7f96433518a..a6309ae2a895c 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -47,27 +47,20 @@
@import "./components/resizable-frame/style.scss";
@import "./hooks/push-changes-to-global-styles/style.scss";
-html #wpadminbar {
+body.js #wpadminbar {
display: none;
}
-html #wpbody {
+body.js #wpbody {
padding-top: 0;
}
-// In order to use mix-blend-mode, this element needs to have an explicitly set background-color.
-// We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations.
-html.wp-toolbar {
- background: $white;
- padding-top: 0;
-}
-
-body.appearance_page_gutenberg-template-parts,
-body.site-editor-php {
+body.js.appearance_page_gutenberg-template-parts,
+body.js.site-editor-php {
@include wp-admin-reset(".edit-site");
}
-body.site-editor-php {
+body.js.site-editor-php {
background: $gray-900;
}
@@ -91,6 +84,11 @@ body.site-editor-php {
top: 0;
}
+ .no-js & {
+ min-height: 0;
+ position: static;
+ }
+
.interface-interface-skeleton {
top: 0;
}
diff --git a/packages/edit-site/src/utils/use-activate-theme.js b/packages/edit-site/src/utils/use-activate-theme.js
index 64654318274cd..b476056feb6b0 100644
--- a/packages/edit-site/src/utils/use-activate-theme.js
+++ b/packages/edit-site/src/utils/use-activate-theme.js
@@ -29,7 +29,7 @@ export function useActivateTheme() {
'themes.php?action=activate&stylesheet=' +
currentlyPreviewingTheme() +
'&_wpnonce=' +
- window.BLOCK_THEME_ACTIVATE_NONCE;
+ window.WP_BLOCK_THEME_ACTIVATE_NONCE;
await window.fetch( activationURL );
const { wp_theme_preview: themePreview, ...params } =
location.params;
diff --git a/packages/edit-widgets/src/style.scss b/packages/edit-widgets/src/style.scss
index 2e78903d72c71..ae850c3bb78fe 100644
--- a/packages/edit-widgets/src/style.scss
+++ b/packages/edit-widgets/src/style.scss
@@ -11,14 +11,8 @@
@import "./components/widget-areas-block-editor-content/style.scss";
@import "./components/secondary-sidebar/style.scss";
-// In order to use mix-blend-mode, this element needs to have an explicitly set background-color
-// We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations
-html.wp-toolbar {
- background: $white;
-}
-
-body.appearance_page_gutenberg-widgets,
-body.widgets-php {
+body.js.appearance_page_gutenberg-widgets,
+body.js.widgets-php {
@include wp-admin-reset( ".blocks-widgets-container" );
}
diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js
index 6f33905888511..e3bbef8bf7738 100644
--- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js
+++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js
@@ -18,28 +18,41 @@ import { store as coreStore } from '@wordpress/core-data';
import { store as reusableBlocksStore } from '../../store';
function ReusableBlocksManageButton( { clientId } ) {
- const { canRemove, isVisible, innerBlockCount } = useSelect(
- ( select ) => {
- const { getBlock, canRemoveBlock, getBlockCount } =
- select( blockEditorStore );
- const { canUser } = select( coreStore );
- const reusableBlock = getBlock( clientId );
+ const { canRemove, isVisible, innerBlockCount, managePatternsUrl } =
+ useSelect(
+ ( select ) => {
+ const { getBlock, canRemoveBlock, getBlockCount, getSettings } =
+ select( blockEditorStore );
+ const { canUser } = select( coreStore );
+ const reusableBlock = getBlock( clientId );
+ const isBlockTheme = getSettings().__unstableIsBlockBasedTheme;
- return {
- canRemove: canRemoveBlock( clientId ),
- isVisible:
- !! reusableBlock &&
- isReusableBlock( reusableBlock ) &&
- !! canUser(
- 'update',
- 'blocks',
- reusableBlock.attributes.ref
- ),
- innerBlockCount: getBlockCount( clientId ),
- };
- },
- [ clientId ]
- );
+ return {
+ canRemove: canRemoveBlock( clientId ),
+ isVisible:
+ !! reusableBlock &&
+ isReusableBlock( reusableBlock ) &&
+ !! canUser(
+ 'update',
+ 'blocks',
+ reusableBlock.attributes.ref
+ ),
+ innerBlockCount: getBlockCount( clientId ),
+ // The site editor and templates both check whether the user
+ // has edit_theme_options capabilities. We can leverage that here
+ // and omit the manage patterns link if the user can't access it.
+ managePatternsUrl:
+ isBlockTheme && canUser( 'read', 'templates' )
+ ? addQueryArgs( 'site-editor.php', {
+ path: '/patterns',
+ } )
+ : addQueryArgs( 'edit.php', {
+ post_type: 'wp_block',
+ } ),
+ };
+ },
+ [ clientId ]
+ );
const { __experimentalConvertBlockToStatic: convertBlockToStatic } =
useDispatch( reusableBlocksStore );
@@ -50,9 +63,7 @@ function ReusableBlocksManageButton( { clientId } ) {
return (
-
+
{ __( 'Manage Patterns' ) }
{ canRemove && (
diff --git a/packages/rich-text/src/component/use-select-object.js b/packages/rich-text/src/component/use-select-object.js
index 9ecc7ed9f147c..e5db313494f48 100644
--- a/packages/rich-text/src/component/use-select-object.js
+++ b/packages/rich-text/src/component/use-select-object.js
@@ -25,8 +25,13 @@ export function useSelectObject() {
if ( selection.containsNode( target ) ) return;
const range = ownerDocument.createRange();
+ // If the target is within a non editable element, select the non
+ // editable element.
+ const nodeToSelect = target.isContentEditable
+ ? target
+ : target.closest( '[contenteditable]' );
- range.selectNode( target );
+ range.selectNode( nodeToSelect );
selection.removeAllRanges();
selection.addRange( range );
diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js
index 828e3a4e3f6cb..305eebaf3e4a6 100644
--- a/packages/rich-text/src/to-dom.js
+++ b/packages/rich-text/src/to-dom.js
@@ -57,6 +57,10 @@ function getNodeByPath( node, path ) {
}
function append( element, child ) {
+ if ( child.html !== undefined ) {
+ return ( element.innerHTML += child.html );
+ }
+
if ( typeof child === 'string' ) {
child = element.ownerDocument.createTextNode( child );
}
diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js
index 805b539053a57..8a1c3ff074a55 100644
--- a/packages/rich-text/src/to-tree.js
+++ b/packages/rich-text/src/to-tree.js
@@ -60,7 +60,7 @@ function fromFormat( {
let elementAttributes = {};
- if ( boundaryClass ) {
+ if ( boundaryClass && isEditableTree ) {
elementAttributes[ 'data-rich-text-format-boundary' ] = 'true';
}
@@ -108,7 +108,7 @@ function fromFormat( {
}
return {
- type: formatType.tagName === '*' ? tagName : formatType.tagName,
+ type: tagName || formatType.tagName,
object: formatType.object,
attributes: restoreOnAttributes( elementAttributes, isEditableTree ),
};
@@ -326,7 +326,11 @@ export function toTree( {
} )
);
- if ( innerHTML ) append( pointer, innerHTML );
+ if ( innerHTML ) {
+ append( pointer, {
+ html: innerHTML,
+ } );
+ }
} else {
pointer = append(
getParent( pointer ),
diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js
index 63480bd11521c..376962b5c99ba 100644
--- a/test/e2e/specs/editor/various/footnotes.spec.js
+++ b/test/e2e/specs/editor/various/footnotes.spec.js
@@ -48,7 +48,7 @@ test.describe( 'Footnotes', () => {
{
name: 'core/paragraph',
attributes: {
- content: `second paragraph* `,
+ content: `second paragraph1 `,
},
},
{
@@ -72,13 +72,13 @@ test.describe( 'Footnotes', () => {
{
name: 'core/paragraph',
attributes: {
- content: `first paragraph* `,
+ content: `first paragraph1 `,
},
},
{
name: 'core/paragraph',
attributes: {
- content: `second paragraph* `,
+ content: `second paragraph2 `,
},
},
{
@@ -106,13 +106,13 @@ test.describe( 'Footnotes', () => {
{
name: 'core/paragraph',
attributes: {
- content: `second paragraph* `,
+ content: `second paragraph1 `,
},
},
{
name: 'core/paragraph',
attributes: {
- content: `first paragraph* `,
+ content: `first paragraph2 `,
},
},
{
@@ -138,7 +138,7 @@ test.describe( 'Footnotes', () => {
{
name: 'core/paragraph',
attributes: {
- content: `second paragraph* `,
+ content: `second paragraph1 `,
},
},
{
@@ -182,4 +182,100 @@ test.describe( 'Footnotes', () => {
expect( await getFootnotes( page ) ).toMatchObject( [] );
} );
+
+ test( 'can be inserted in a list', async ( { editor, page } ) => {
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
+ await page.keyboard.type( '* 1' );
+ await editor.clickBlockToolbarButton( 'More' );
+ await page.locator( 'button:text("Footnote")' ).click();
+
+ await page.keyboard.type( 'a' );
+
+ const id1 = await editor.canvas.evaluate( () => {
+ return document.activeElement.id;
+ } );
+
+ expect( await editor.getBlocks() ).toMatchObject( [
+ {
+ name: 'core/list',
+ innerBlocks: [
+ {
+ name: 'core/list-item',
+ attributes: {
+ content: `11 `,
+ },
+ },
+ ],
+ },
+ {
+ name: 'core/footnotes',
+ },
+ ] );
+
+ expect( await getFootnotes( page ) ).toMatchObject( [
+ {
+ content: 'a',
+ id: id1,
+ },
+ ] );
+ } );
+
+ test( 'can be inserted in a table', async ( { editor, page } ) => {
+ await editor.insertBlock( { name: 'core/table' } );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
+ await page.keyboard.type( '1' );
+ await editor.showBlockToolbar();
+ await editor.clickBlockToolbarButton( 'More' );
+ await page.locator( 'button:text("Footnote")' ).click();
+
+ await page.keyboard.type( 'a' );
+
+ const id1 = await editor.canvas.evaluate( () => {
+ return document.activeElement.id;
+ } );
+
+ expect( await editor.getBlocks() ).toMatchObject( [
+ {
+ name: 'core/table',
+ attributes: {
+ body: [
+ {
+ cells: [
+ {
+ content: `11 `,
+ tag: 'td',
+ },
+ {
+ content: '',
+ tag: 'td',
+ },
+ ],
+ },
+ {
+ cells: [
+ {
+ content: '',
+ tag: 'td',
+ },
+ {
+ content: '',
+ tag: 'td',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ name: 'core/footnotes',
+ },
+ ] );
+
+ expect( await getFootnotes( page ) ).toMatchObject( [
+ {
+ content: 'a',
+ id: id1,
+ },
+ ] );
+ } );
} );
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-1.html b/test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-1.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.html
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-1.json b/test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.json
similarity index 86%
rename from test/integration/fixtures/blocks/core__image__deprecated-1.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.json
index e5d1dcba576aa..6b2f1a80f52c6 100644
--- a/test/integration/fixtures/blocks/core__image__deprecated-1.json
+++ b/test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.json
@@ -3,10 +3,10 @@
"name": "core/image",
"isValid": true,
"attributes": {
- "align": "left",
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==",
"alt": "",
- "caption": ""
+ "caption": [],
+ "align": "left"
},
"innerBlocks": []
}
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-1.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.parsed.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-1.parsed.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.parsed.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-1.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.serialized.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-1.serialized.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v1-add-responsive-class.serialized.html
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-2.html b/test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-2.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.html
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-3.json b/test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.json
similarity index 94%
rename from test/integration/fixtures/blocks/core__image__deprecated-3.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.json
index bae213510011a..a0ad3b755587b 100644
--- a/test/integration/fixtures/blocks/core__image__deprecated-3.json
+++ b/test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.json
@@ -3,10 +3,10 @@
"name": "core/image",
"isValid": true,
"attributes": {
- "align": "left",
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==",
"alt": "",
- "caption": "",
+ "caption": [],
+ "align": "left",
"width": 100,
"height": 100
},
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-2.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.parsed.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-2.parsed.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.parsed.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-2.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.serialized.html
similarity index 62%
rename from test/integration/fixtures/blocks/core__image__deprecated-2.serialized.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.serialized.html
index eb06f24974863..7ce56e11fa75e 100644
--- a/test/integration/fixtures/blocks/core__image__deprecated-2.serialized.html
+++ b/test/integration/fixtures/blocks/core__image__deprecated-v2-add-is-resized-class.serialized.html
@@ -1,3 +1,3 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-3.html b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-3.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.html
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-2.json b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-2.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-3.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.parsed.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-3.parsed.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.parsed.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-3.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html
similarity index 62%
rename from test/integration/fixtures/blocks/core__image__deprecated-3.serialized.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html
index eb06f24974863..7ce56e11fa75e 100644
--- a/test/integration/fixtures/blocks/core__image__deprecated-3.serialized.html
+++ b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html
@@ -1,3 +1,3 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.html b/test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.html
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.json b/test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.parsed.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.parsed.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.parsed.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.serialized.html
similarity index 77%
rename from test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.serialized.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.serialized.html
index 5ba1eb754e83f..cfdc52e3cbb6e 100644
--- a/test/integration/fixtures/blocks/core__image__deprecated-align-wrapper.serialized.html
+++ b/test/integration/fixtures/blocks/core__image__deprecated-v4-remove-align-wrapper.serialized.html
@@ -1,3 +1,3 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.html b/test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.html
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.json b/test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.parsed.json
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.parsed.json
rename to test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.parsed.json
diff --git a/test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.serialized.html
similarity index 100%
rename from test/integration/fixtures/blocks/core__image__deprecated-border-radius-5.serialized.html
rename to test/integration/fixtures/blocks/core__image__deprecated-v5-move-border-radius-style.serialized.html