diff --git a/.github/workflows/cherry-pick-wp-release.yml b/.github/workflows/cherry-pick-wp-release.yml new file mode 100644 index 00000000000000..80aa04295be162 --- /dev/null +++ b/.github/workflows/cherry-pick-wp-release.yml @@ -0,0 +1,133 @@ +name: Auto Cherry-Pick + +on: + pull_request: + types: [closed, labeled] + branches: + - trunk + +# Ensure that new jobs wait for the previous job to finish. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + cherry-pick: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - name: Determine if label should trigger cherry-pick + id: label-check + uses: actions/github-script@v7 + with: + script: | + const labels = context.payload.pull_request.labels.map(label => label.name); + console.log(`Labels: ${labels}`); + const regex = /^Backport to WP ([0-9]+\.[0-9]+) Beta\/RC$/; + let matched = false; + for (const label of labels) { + const match = label.match(regex); + if (match) { + const version = match[1]; + console.log(`Matched label: ${label}`); + console.log(`Extracted version: ${version}`); + core.exportVariable('cherry_pick', 'true'); + core.exportVariable('version', version); + matched = true; + break; + } + } + if (!matched) { + core.exportVariable('cherry_pick', 'false'); + } + + - name: Checkout repository + if: env.cherry_pick == 'true' + uses: actions/checkout@v2 + with: + token: ${{ secrets.GUTENBERG_TOKEN }} + fetch-depth: 0 + + - name: Set up Git + if: env.cherry_pick == 'true' + run: | + git config --global user.name "Gutenberg Repository Automation" + git config --global user.email "gutenberg@wordpress.org" + + - name: Cherry-pick the commit + id: cherry-pick + if: env.cherry_pick == 'true' + run: | + TARGET_BRANCH="wp/${{ env.version }}" + COMMIT_SHA=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH") + echo "Target branch: $TARGET_BRANCH" + echo "Commit SHA: $COMMIT_SHA" + git checkout $TARGET_BRANCH + git cherry-pick $COMMIT_SHA || echo "cherry-pick-failed" > result + if [ -f result ] && grep -q "cherry-pick-failed" result; then + echo "conflict=true" >> $GITHUB_ENV + git cherry-pick --abort + else + NEW_COMMIT_SHA=$(git rev-parse HEAD) + echo "conflict=false" >> $GITHUB_ENV + echo "commit_sha=$NEW_COMMIT_SHA" >> $GITHUB_ENV + git push origin $TARGET_BRANCH + fi + + - name: Remove cherry-pick label + if: env.cherry_pick == 'true' && env.conflict == 'false' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.issue.number; + const version = process.env.version; + console.log(`prNumber: ${prNumber}`); + console.log(`version: ${version}`); + const oldLabel = `Backport to WP ${version} Beta/RC`; + const newLabel = `Backported to WP Core`; + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: oldLabel + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [newLabel] + }); + + - name: Comment on the PR + if: env.cherry_pick == 'true' && env.conflict == 'false' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.issue.number; + const commitSha = process.env.commit_sha; + const targetBranch = `wp/${process.env.version}`; + console.log(`prNumber: ${prNumber}`); + console.log(`commitSha: ${commitSha}`); + console.log(`targetBranch: ${targetBranch}`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `I just cherry-picked this PR to the ${targetBranch} branch to get it included in the next release: ${commitSha}` + }); + + - name: Comment on the PR about conflict + if: env.cherry_pick == 'true' && env.conflict == 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.issue.number; + const targetBranch = `wp/${process.env.version}`; + console.log(`prNumber: ${prNumber}`); + console.log(`targetBranch: ${targetBranch}`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `There was a conflict while trying to cherry-pick the commit to the ${targetBranch} branch. Please resolve the conflict manually and create a PR to the ${targetBranch} branch.` + }); diff --git a/changelog.txt b/changelog.txt index 80a932719036b9..38dbcadaa4d9c3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,26 @@ == Changelog == += 18.6.1 = + + +## Changelog + +### Bug Fixes + +#### Block Library +- Navigation block: Fix submenu not opening on macOS Safari. ([62800](https://github.com/WordPress/gutenberg/pull/62800)) + +### Code Quality +- Template Part: Improve how the tag name attribute is handled (#62785)(https://github.com/WordPress/gutenberg/pull/62785) + + +## Contributors + +The following contributors merged PRs in this release: + +@luisherranz @aaronjorbin @gziolo @westonruter @cbravobernal + + = 18.6.0 = ## Changelog diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md index fadbd82350c88b..d92270d73bdb22 100644 --- a/docs/reference-guides/interactivity-api/api-reference.md +++ b/docs/reference-guides/interactivity-api/api-reference.md @@ -73,8 +73,9 @@ The `wp-interactive` directive "activates" the interactivity for the DOM element ``` -> **Note** -> The use of `data-wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `data-wp-interactive` has not been added for the sake of simplicity. Also, the `data-wp-interactive` directive will be injected automatically in the future. +
+ The use of data-wp-interactive is a requirement for the Interactivity API "engine" to work. In the following examples the data-wp-interactive has not been added for the sake of simplicity. Also, the data-wp-interactive directive will be injected automatically in the future. +
### `wp-context` @@ -299,8 +300,9 @@ The returned value is used to change the inner content of the element: `
val ### `wp-on` -> [!NOTE] -> Consider using the more performant [`wp-on-async`](#wp-on-async) instead if your directive code does not need synchronous access to the event object. If synchronous access is required, consider implementing an [async action](#async-actions) which yields to the main thread after calling the synchronous API. +
+ Consider using the more performant wp-on-async instead if your directive code does not need synchronous access to the event object. If synchronous access is required, consider implementing an async action which yields to the main thread after calling the synchronous API. +
This directive runs code on dispatched DOM events like `click` or `keyup`. The syntax is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`). @@ -335,8 +337,9 @@ to run sooner. Use this async version whenever there is no need for synchronous ### `wp-on-window` -> [!NOTE] -> Consider using the more performant [`wp-on-async-window`](#wp-on-async-window) instead if your directive code does not need synchronous access to the event object. If synchronous access is required, consider implementing an [async action](#async-actions) which yields to the main thread after calling the synchronous API. +
+ Consider using the more performant wp-on-async-window instead if your directive code does not need synchronous access to the event object. If synchronous access is required, consider implementing an async action which yields to the main thread after calling the synchronous API. +
This directive allows you to attach global window events like `resize`, `copy`, and `focus` and then execute a defined callback when those happen. @@ -371,8 +374,9 @@ Similar to `wp-on-async`, this is an optimized version of `wp-on-window` that im ### `wp-on-document` -> [!NOTE] -> Consider using the more performant [`wp-on-async-document`](#wp-on-async-document) instead if your directive code does not need synchronous access to the event object. If synchronous access is required, consider implementing an [async action](#async-actions) which yields to the main thread after calling the synchronous API. +
+ Consider using the more performant wp-on-async-document instead if your directive code does not need synchronous access to the event object. If synchronous access is required, consider implementing an async action which yields to the main thread after calling the synchronous API. +
This directive allows you to attach global document events like `scroll`, `mousemove`, and `keydown` and then execute a defined callback when those happen. @@ -759,7 +763,7 @@ const { state, actions } = store("myPlugin", { }); ``` -##### Async actions +
Async actions
Async actions should use generators instead of async/await. @@ -889,9 +893,9 @@ const { state } = store( "myPlugin", { } } ); ``` - -> **Note** -> All `store()` calls with the same namespace return the same references, i.e., the same `state`, `actions`, etc., containing the result of merging all the store parts passed. +
+ All store() calls with the same namespace return the same references, i.e., the same state, actions, etc., containing the result of merging all the store parts passed. +
- To access the context inside an action, derived state, or side effect, you can use the `getContext` function. - To access the reference, you can use the `getElement` function. diff --git a/gutenberg.php b/gutenberg.php index b91cb99062aded..0edf6868f1aa04 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.4 * Requires PHP: 7.2 - * Version: 18.6.0 + * Version: 18.6.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 9762bb610f77b9..e09dc899a24894 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -2326,43 +2326,37 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { * @return array Returns the modified $declarations. */ protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { - if ( null === $properties ) { - $properties = static::PROPERTIES_METADATA; - } - - $declarations = array(); if ( empty( $styles ) ) { - return $declarations; + return array(); } + if ( null === $properties ) { + $properties = static::PROPERTIES_METADATA; + } + $declarations = array(); $root_variable_duplicates = array(); + $root_style_length = strlen( '--wp--style--root--' ); foreach ( $properties as $css_property => $value_path ) { - $value = static::get_property_value( $styles, $value_path, $theme_json ); + if ( ! is_array( $value_path ) ) { + continue; + } - if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { + $is_root_style = str_starts_with( $css_property, '--wp--style--root--' ); + if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { continue; } + + $value = static::get_property_value( $styles, $value_path, $theme_json ); + // Root-level padding styles don't currently support strings with CSS shorthand values. // This may change: https://github.com/WordPress/gutenberg/issues/40132. if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { continue; } - if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) { - $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) ); - } - - // Look up protected properties, keyed by value path. - // Skip protected properties that are explicitly set to `null`. - if ( is_array( $value_path ) ) { - $path_string = implode( '.', $value_path ); - if ( - isset( static::PROTECTED_PROPERTIES[ $path_string ] ) && - _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null - ) { - continue; - } + if ( $is_root_style && $use_root_padding ) { + $root_variable_duplicates[] = substr( $css_property, $root_style_length ); } // Processes background styles. @@ -2377,6 +2371,16 @@ protected static function compute_style_properties( $styles, $settings = array() continue; } + // Look up protected properties, keyed by value path. + // Skip protected properties that are explicitly set to `null`. + $path_string = implode( '.', $value_path ); + if ( + isset( static::PROTECTED_PROPERTIES[ $path_string ] ) && + _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null + ) { + continue; + } + // Calculates fluid typography rules where available. if ( 'font-size' === $css_property ) { /* diff --git a/package-lock.json b/package-lock.json index 54e498ea5bc09f..2440b27e0f08ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "18.6.0", + "version": "18.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "18.6.0", + "version": "18.6.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 96aca6616d425e..9b025db89016ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "18.6.0", + "version": "18.6.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/components/block-mover/index.js b/packages/block-editor/src/components/block-mover/index.js index 24f259613351d2..71009a102e5d68 100644 --- a/packages/block-editor/src/components/block-mover/index.js +++ b/packages/block-editor/src/components/block-mover/index.js @@ -25,7 +25,14 @@ function BlockMover( { isBlockMoverUpButtonDisabled, isBlockMoverDownButtonDisabled, } ) { - const { canMove, rootClientId, isFirst, isLast, orientation } = useSelect( + const { + canMove, + rootClientId, + isFirst, + isLast, + orientation, + isManualGrid, + } = useSelect( ( select ) => { const { getBlockIndex, @@ -33,6 +40,7 @@ function BlockMover( { canMoveBlocks, getBlockOrder, getBlockRootClientId, + getBlockAttributes, } = select( blockEditorStore ); const normalizedClientIds = Array.isArray( clientIds ) ? clientIds @@ -44,6 +52,7 @@ function BlockMover( { normalizedClientIds[ normalizedClientIds.length - 1 ] ); const blockOrder = getBlockOrder( _rootClientId ); + const { layout = {} } = getBlockAttributes( _rootClientId ) ?? {}; return { canMove: canMoveBlocks( clientIds ), @@ -51,6 +60,10 @@ function BlockMover( { isFirst: firstIndex === 0, isLast: lastIndex === blockOrder.length - 1, orientation: getBlockListSettings( _rootClientId )?.orientation, + isManualGrid: + layout.type === 'grid' && + !! layout.columnCount && + window.__experimentalEnableGridInteractivity, }; }, [ clientIds ] @@ -60,8 +73,6 @@ function BlockMover( { return null; } - const dragHandleLabel = __( 'Drag' ); - return ( ); } diff --git a/packages/block-editor/src/components/child-layout-control/index.js b/packages/block-editor/src/components/child-layout-control/index.js index dfc4ee69437f67..c0d90b24b29312 100644 --- a/packages/block-editor/src/components/child-layout-control/index.js +++ b/packages/block-editor/src/components/child-layout-control/index.js @@ -14,6 +14,13 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useEffect } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { useGetNumberOfBlocksBeforeCell } from '../grid/use-get-number-of-blocks-before-cell'; +import { store as blockEditorStore } from '../../store'; function helpText( selfStretch, parentLayout ) { const { orientation = 'horizontal' } = parentLayout; @@ -48,21 +55,46 @@ export default function ChildLayoutControl( { isShownByDefault, panelId, } ) { - const { - selfStretch, - flexSize, - columnStart, - rowStart, - columnSpan, - rowSpan, - } = childLayout; const { type: parentType, default: { type: defaultParentType = 'default' } = {}, - orientation = 'horizontal', } = parentLayout ?? {}; const parentLayoutType = parentType || defaultParentType; + if ( parentLayoutType === 'flex' ) { + return ( + + ); + } else if ( parentLayoutType === 'grid' ) { + return ( + + ); + } + + return null; +} + +function FlexControls( { + childLayout, + onChange, + parentLayout, + isShownByDefault, + panelId, +} ) { + const { selfStretch, flexSize } = childLayout; + const { orientation = 'horizontal' } = parentLayout ?? {}; const hasFlexValue = () => !! selfStretch; const flexResetLabel = orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); @@ -73,6 +105,96 @@ export default function ChildLayoutControl( { } ); }; + useEffect( () => { + if ( selfStretch === 'fixed' && ! flexSize ) { + onChange( { + ...childLayout, + selfStretch: 'fit', + } ); + } + }, [] ); + + return ( + + { + const newFlexSize = value !== 'fixed' ? null : flexSize; + onChange( { + selfStretch: value, + flexSize: newFlexSize, + } ); + } } + isBlock + > + + + + + { selfStretch === 'fixed' && ( + { + onChange( { + selfStretch, + flexSize: value, + } ); + } } + value={ flexSize } + /> + ) } + + ); +} + +export function childLayoutOrientation( parentLayout ) { + const { orientation = 'horizontal' } = parentLayout; + return orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); +} + +function GridControls( { + childLayout, + onChange, + parentLayout, + isShownByDefault, + panelId, +} ) { + const { columnStart, rowStart, columnSpan, rowSpan } = childLayout; + const { columnCount } = parentLayout ?? {}; + const gridColumnNumber = parseInt( columnCount, 10 ) || 3; + const rootClientId = useSelect( ( select ) => + select( blockEditorStore ).getBlockRootClientId( panelId ) + ); + const { moveBlocksToPosition, __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + const getNumberOfBlocksBeforeCell = useGetNumberOfBlocksBeforeCell( + rootClientId, + gridColumnNumber + ); const hasStartValue = () => !! columnStart || !! rowStart; const hasSpanValue = () => !! columnSpan || !! rowSpan; const resetGridStarts = () => { @@ -88,184 +210,127 @@ export default function ChildLayoutControl( { } ); }; - useEffect( () => { - if ( selfStretch === 'fixed' && ! flexSize ) { - onChange( { - ...childLayout, - selfStretch: 'fit', - } ); - } - }, [] ); - return ( <> - { parentLayoutType === 'flex' && ( - + { + onChange( { + columnStart, + rowStart, + rowSpan, + columnSpan: value, + } ); + } } + value={ columnSpan } + min={ 1 } + /> + { + onChange( { + columnStart, + rowStart, + columnSpan, + rowSpan: value, + } ); + } } + value={ rowSpan } + min={ 1 } + /> + + { window.__experimentalEnableGridInteractivity && columnCount && ( + // Use Flex with an explicit width on the FlexItem instead of HStack to + // work around an issue in webkit where inputs with a max attribute are + // sized incorrectly. + - { - const newFlexSize = - value !== 'fixed' ? null : flexSize; - onChange( { - selfStretch: value, - flexSize: newFlexSize, - } ); - } } - isBlock - > - - - - - { selfStretch === 'fixed' && ( - { - onChange( { - selfStretch, - flexSize: value, - } ); - } } - value={ flexSize } - /> - ) } - - ) } - { parentLayoutType === 'grid' && ( - <> - + { onChange( { - columnStart, + columnStart: value, rowStart, + columnSpan, rowSpan, - columnSpan: value, } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ panelId ], + rootClientId, + rootClientId, + getNumberOfBlocksBeforeCell( + value, + rowStart + ) + ); } } - value={ columnSpan } + value={ columnStart } min={ 1 } + max={ + gridColumnNumber + ? gridColumnNumber - ( columnSpan ?? 1 ) + 1 + : undefined + } /> + + { onChange( { columnStart, - rowStart, + rowStart: value, columnSpan, - rowSpan: value, + rowSpan, } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ panelId ], + rootClientId, + rootClientId, + getNumberOfBlocksBeforeCell( + columnStart, + value + ) + ); } } - value={ rowSpan } + value={ rowStart } min={ 1 } + max={ + parentLayout?.rowCount + ? parentLayout.rowCount - + ( rowSpan ?? 1 ) + + 1 + : undefined + } /> - - { window.__experimentalEnableGridInteractivity && ( - // Use Flex with an explicit width on the FlexItem instead of HStack to - // work around an issue in webkit where inputs with a max attribute are - // sized incorrectly. - - - { - onChange( { - columnStart: value, - rowStart, - columnSpan, - rowSpan, - } ); - } } - value={ columnStart } - min={ 1 } - max={ - parentLayout?.columnCount - ? parentLayout.columnCount - - ( columnSpan ?? 1 ) + - 1 - : undefined - } - /> - - - { - onChange( { - columnStart, - rowStart: value, - columnSpan, - rowSpan, - } ); - } } - value={ rowStart } - min={ 1 } - max={ - parentLayout?.rowCount - ? parentLayout.rowCount - - ( rowSpan ?? 1 ) + - 1 - : undefined - } - /> - - - ) } - + + ) } ); } - -export function childLayoutOrientation( parentLayout ) { - const { orientation = 'horizontal' } = parentLayout; - - return orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); -} diff --git a/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js b/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js deleted file mode 100644 index cff5efc5218e10..00000000000000 --- a/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useEffect, forwardRef } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; -import BlockPopoverCover from '../block-popover/cover'; -import { store as blockEditorStore } from '../../store'; -import { getComputedCSS } from './utils'; - -export function GridVisualizer( { clientId, contentRef } ) { - const isDistractionFree = useSelect( - ( select ) => - select( blockEditorStore ).getSettings().isDistractionFree, - [] - ); - const blockElement = useBlockElement( clientId ); - - if ( isDistractionFree || ! blockElement ) { - return null; - } - - return ( - - - - ); -} - -const GridVisualizerGrid = forwardRef( ( { blockElement }, ref ) => { - const [ gridInfo, setGridInfo ] = useState( () => - getGridInfo( blockElement ) - ); - useEffect( () => { - const observers = []; - for ( const element of [ blockElement, ...blockElement.children ] ) { - const observer = new window.ResizeObserver( () => { - setGridInfo( getGridInfo( blockElement ) ); - } ); - observer.observe( element ); - observers.push( observer ); - } - return () => { - for ( const observer of observers ) { - observer.disconnect(); - } - }; - }, [ blockElement ] ); - return ( -
- { Array.from( { length: gridInfo.numItems }, ( _, i ) => ( -
- ) ) } -
- ); -} ); - -function getGridInfo( blockElement ) { - const gridTemplateColumns = getComputedCSS( - blockElement, - 'grid-template-columns' - ); - const gridTemplateRows = getComputedCSS( - blockElement, - 'grid-template-rows' - ); - const numColumns = gridTemplateColumns.split( ' ' ).length; - const numRows = gridTemplateRows.split( ' ' ).length; - const numItems = numColumns * numRows; - return { - numItems, - currentColor: getComputedCSS( blockElement, 'color' ), - style: { - gridTemplateColumns, - gridTemplateRows, - gap: getComputedCSS( blockElement, 'gap' ), - padding: getComputedCSS( blockElement, 'padding' ), - }, - }; -} diff --git a/packages/block-editor/src/components/grid-visualizer/index.js b/packages/block-editor/src/components/grid-visualizer/index.js deleted file mode 100644 index add845d7022030..00000000000000 --- a/packages/block-editor/src/components/grid-visualizer/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { GridVisualizer } from './grid-visualizer'; -export { GridItemResizer } from './grid-item-resizer'; diff --git a/packages/block-editor/src/components/grid-visualizer/style.scss b/packages/block-editor/src/components/grid-visualizer/style.scss deleted file mode 100644 index 2adaf18f52470a..00000000000000 --- a/packages/block-editor/src/components/grid-visualizer/style.scss +++ /dev/null @@ -1,34 +0,0 @@ -// TODO: Specificity hacks to get rid of all these darn !importants. - -.block-editor-grid-visualizer { - z-index: z-index(".block-editor-grid-visualizer") !important; -} - -.block-editor-grid-visualizer .components-popover__content * { - pointer-events: none !important; -} - -.block-editor-grid-visualizer__grid { - display: grid; -} - -.block-editor-grid-visualizer__item { - outline: 1px solid transparent; - border-radius: $radius-block-ui; -} - -.block-editor-grid-item-resizer { - z-index: z-index(".block-editor-grid-visualizer") !important; -} - -.block-editor-grid-item-resizer .components-popover__content * { - pointer-events: none !important; -} - -.block-editor-grid-item-resizer__box { - border: $border-width solid var(--wp-admin-theme-color); - - .components-resizable-box__handle { - pointer-events: all !important; - } -} diff --git a/packages/block-editor/src/components/grid-visualizer/utils.js b/packages/block-editor/src/components/grid-visualizer/utils.js deleted file mode 100644 index a100e596a4e243..00000000000000 --- a/packages/block-editor/src/components/grid-visualizer/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function getComputedCSS( element, property ) { - return element.ownerDocument.defaultView - .getComputedStyle( element ) - .getPropertyValue( property ); -} diff --git a/packages/block-editor/src/components/grid/grid-item-movers.js b/packages/block-editor/src/components/grid/grid-item-movers.js new file mode 100644 index 00000000000000..4f1d3853568fda --- /dev/null +++ b/packages/block-editor/src/components/grid/grid-item-movers.js @@ -0,0 +1,128 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToolbarButton } from '@wordpress/components'; +import { arrowLeft, arrowUp, arrowDown, arrowRight } from '@wordpress/icons'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import BlockControls from '../block-controls'; +import { useGetNumberOfBlocksBeforeCell } from './use-get-number-of-blocks-before-cell'; +import { store as blockEditorStore } from '../../store'; + +export function GridItemMovers( { + layout, + parentLayout, + onChange, + gridClientId, + blockClientId, +} ) { + const { moveBlocksToPosition, __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + + const columnStart = layout?.columnStart ?? 1; + const rowStart = layout?.rowStart ?? 1; + const columnSpan = layout?.columnSpan ?? 1; + const rowSpan = layout?.rowSpan ?? 1; + const columnEnd = columnStart + columnSpan - 1; + const rowEnd = rowStart + rowSpan - 1; + const columnCount = parentLayout?.columnCount; + const rowCount = parentLayout?.rowCount; + + const columnCountNumber = parseInt( columnCount, 10 ); + const rowStartNumber = parseInt( rowStart, 10 ); + const columnStartNumber = parseInt( columnStart, 10 ); + + const getNumberOfBlocksBeforeCell = useGetNumberOfBlocksBeforeCell( + gridClientId, + columnCountNumber + ); + + return ( + + { + onChange( { + rowStart: rowStart - 1, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ blockClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( + columnStartNumber, + rowStartNumber - 1 + ) + ); + } } + /> + = rowCount } + onClick={ () => { + onChange( { + rowStart: rowStart + 1, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ blockClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( + columnStartNumber, + rowStartNumber + 1 + ) + ); + } } + /> + { + onChange( { + columnStart: columnStartNumber - 1, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ blockClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( + columnStartNumber - 1, + rowStartNumber + ) + ); + } } + /> + = columnCount } + onClick={ () => { + onChange( { + columnStart: columnStartNumber + 1, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ blockClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( + columnStartNumber + 1, + rowStartNumber + ) + ); + } } + /> + + ); +} diff --git a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js b/packages/block-editor/src/components/grid/grid-item-resizer.js similarity index 68% rename from packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js rename to packages/block-editor/src/components/grid/grid-item-resizer.js index 21e9bfccee754f..a5847d852e0a96 100644 --- a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js +++ b/packages/block-editor/src/components/grid/grid-item-resizer.js @@ -9,11 +9,17 @@ import { useState, useEffect } from '@wordpress/element'; */ import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; import BlockPopoverCover from '../block-popover/cover'; -import { getComputedCSS } from './utils'; +import { getComputedCSS, getGridTracks, getClosestTrack } from './utils'; -export function GridItemResizer( { clientId, bounds, onChange } ) { +export function GridItemResizer( { + clientId, + bounds, + onChange, + parentLayout, +} ) { const blockElement = useBlockElement( clientId ); const rootBlockElement = blockElement?.parentElement; + const { columnCount } = parentLayout; if ( ! blockElement || ! rootBlockElement ) { return null; @@ -26,6 +32,9 @@ export function GridItemResizer( { clientId, bounds, onChange } ) { blockElement={ blockElement } rootBlockElement={ rootBlockElement } onChange={ onChange } + isManualGrid={ + !! columnCount && window.__experimentalEnableGridInteractivity + } /> ); } @@ -36,6 +45,7 @@ function GridItemResizerInner( { blockElement, rootBlockElement, onChange, + isManualGrid, } ) { const [ resizeDirection, setResizeDirection ] = useState( null ); const [ enableSide, setEnableSide ] = useState( { @@ -171,59 +181,11 @@ function GridItemResizerInner( { onChange( { columnSpan: columnEnd - columnStart + 1, rowSpan: rowEnd - rowStart + 1, + columnStart: isManualGrid ? columnStart : undefined, + rowStart: isManualGrid ? rowStart : undefined, } ); } } /> ); } - -/** - * Given a grid-template-columns or grid-template-rows CSS property value, gets the start and end - * position in pixels of each grid track. - * - * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track - * - * @param {string} template The grid-template-columns or grid-template-rows CSS property value. - * Only supports fixed sizes in pixels. - * @param {number} gap The gap between grid tracks in pixels. - * - * @return {Array<{start: number, end: number}>} An array of objects with the start and end - * position in pixels of each grid track. - */ -function getGridTracks( template, gap ) { - const tracks = []; - for ( const size of template.split( ' ' ) ) { - const previousTrack = tracks[ tracks.length - 1 ]; - const start = previousTrack ? previousTrack.end + gap : 0; - const end = start + parseFloat( size ); - tracks.push( { start, end } ); - } - return tracks; -} - -/** - * Given an array of grid tracks and a position in pixels, gets the index of the closest track to - * that position. - * - * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track - * - * @param {Array<{start: number, end: number}>} tracks An array of objects with the start and end - * position in pixels of each grid track. - * @param {number} position The position in pixels. - * @param {string} edge The edge of the track to compare the - * position to. Either 'start' or 'end'. - * - * @return {number} The index of the closest track to the position. 0-based, unlike CSS grid which - * is 1-based. - */ -function getClosestTrack( tracks, position, edge = 'start' ) { - return tracks.reduce( - ( closest, track, index ) => - Math.abs( track[ edge ] - position ) < - Math.abs( tracks[ closest ][ edge ] - position ) - ? index - : closest, - 0 - ); -} diff --git a/packages/block-editor/src/components/grid/grid-visualizer.js b/packages/block-editor/src/components/grid/grid-visualizer.js new file mode 100644 index 00000000000000..5e639615a062cf --- /dev/null +++ b/packages/block-editor/src/components/grid/grid-visualizer.js @@ -0,0 +1,267 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { useState, useEffect, forwardRef } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import BlockPopoverCover from '../block-popover/cover'; +import { range, GridRect, getGridInfo } from './utils'; +import { store as blockEditorStore } from '../../store'; +import { useGetNumberOfBlocksBeforeCell } from './use-get-number-of-blocks-before-cell'; + +export function GridVisualizer( { clientId, contentRef, parentLayout } ) { + const isDistractionFree = useSelect( + ( select ) => + select( blockEditorStore ).getSettings().isDistractionFree, + [] + ); + const gridElement = useBlockElement( clientId ); + + if ( isDistractionFree || ! gridElement ) { + return null; + } + + const isManualGrid = + parentLayout?.columnCount && + window.__experimentalEnableGridInteractivity; + return ( + + ); +} + +const GridVisualizerGrid = forwardRef( + ( { clientId, gridElement, isManualGrid }, ref ) => { + const [ gridInfo, setGridInfo ] = useState( () => + getGridInfo( gridElement ) + ); + const [ isDroppingAllowed, setIsDroppingAllowed ] = useState( false ); + const [ highlightedRect, setHighlightedRect ] = useState( null ); + + useEffect( () => { + const observers = []; + for ( const element of [ gridElement, ...gridElement.children ] ) { + const observer = new window.ResizeObserver( () => { + setGridInfo( getGridInfo( gridElement ) ); + } ); + observer.observe( element ); + observers.push( observer ); + } + return () => { + for ( const observer of observers ) { + observer.disconnect(); + } + }; + }, [ gridElement ] ); + + useEffect( () => { + function onGlobalDrag() { + setIsDroppingAllowed( true ); + } + function onGlobalDragEnd() { + setIsDroppingAllowed( false ); + } + document.addEventListener( 'drag', onGlobalDrag ); + document.addEventListener( 'dragend', onGlobalDragEnd ); + return () => { + document.removeEventListener( 'drag', onGlobalDrag ); + document.removeEventListener( 'dragend', onGlobalDragEnd ); + }; + }, [] ); + + return ( + +
+ { isManualGrid + ? range( 1, gridInfo.numRows ).map( ( row ) => + range( 1, gridInfo.numColumns ).map( + ( column ) => ( + + + + ) + ) + ) + : Array.from( + { length: gridInfo.numItems }, + ( _, i ) => ( + + ) + ) } +
+
+ ); + } +); + +function GridVisualizerCell( { color, children } ) { + return ( +
+ { children } +
+ ); +} + +function GridVisualizerDropZone( { + column, + row, + gridClientId, + gridInfo, + highlightedRect, + setHighlightedRect, +} ) { + const { getBlockAttributes } = useSelect( blockEditorStore ); + const { + updateBlockAttributes, + moveBlocksToPosition, + __unstableMarkNextChangeAsNotPersistent, + } = useDispatch( blockEditorStore ); + + const getNumberOfBlocksBeforeCell = useGetNumberOfBlocksBeforeCell( + gridClientId, + gridInfo.numColumns + ); + + const ref = useDropZoneWithValidation( { + validateDrag( srcClientId ) { + const attributes = getBlockAttributes( srcClientId ); + const rect = new GridRect( { + columnStart: column, + rowStart: row, + columnSpan: attributes.style?.layout?.columnSpan, + rowSpan: attributes.style?.layout?.rowSpan, + } ); + const isInBounds = new GridRect( { + columnSpan: gridInfo.numColumns, + rowSpan: gridInfo.numRows, + } ).containsRect( rect ); + return isInBounds; + }, + onDragEnter( srcClientId ) { + const attributes = getBlockAttributes( srcClientId ); + setHighlightedRect( + new GridRect( { + columnStart: column, + rowStart: row, + columnSpan: attributes.style?.layout?.columnSpan, + rowSpan: attributes.style?.layout?.rowSpan, + } ) + ); + }, + onDragLeave() { + // onDragEnter can be called before onDragLeave if the user moves + // their mouse quickly, so only clear the highlight if it was set + // by this cell. + setHighlightedRect( ( prevHighlightedRect ) => + prevHighlightedRect?.columnStart === column && + prevHighlightedRect?.rowStart === row + ? null + : prevHighlightedRect + ); + }, + onDrop( srcClientId ) { + setHighlightedRect( null ); + const attributes = getBlockAttributes( srcClientId ); + updateBlockAttributes( srcClientId, { + style: { + ...attributes.style, + layout: { + ...attributes.style?.layout, + columnStart: column, + rowStart: row, + }, + }, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ srcClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( column, row ) + ); + }, + } ); + + const isHighlighted = highlightedRect?.contains( column, row ) ?? false; + + return ( +
+ ); +} + +function useDropZoneWithValidation( { + validateDrag, + onDragEnter, + onDragLeave, + onDrop, +} ) { + const { getDraggedBlockClientIds } = useSelect( blockEditorStore ); + return useDropZone( { + onDragEnter() { + const [ srcClientId ] = getDraggedBlockClientIds(); + if ( srcClientId && validateDrag( srcClientId ) ) { + onDragEnter( srcClientId ); + } + }, + onDragLeave() { + onDragLeave(); + }, + onDrop() { + const [ srcClientId ] = getDraggedBlockClientIds(); + if ( srcClientId && validateDrag( srcClientId ) ) { + onDrop( srcClientId ); + } + }, + } ); +} diff --git a/packages/block-editor/src/components/grid/index.js b/packages/block-editor/src/components/grid/index.js new file mode 100644 index 00000000000000..a1552610102a06 --- /dev/null +++ b/packages/block-editor/src/components/grid/index.js @@ -0,0 +1,4 @@ +export { GridVisualizer } from './grid-visualizer'; +export { GridItemResizer } from './grid-item-resizer'; +export { GridItemMovers } from './grid-item-movers'; +export { useGridLayoutSync } from './use-grid-layout-sync'; diff --git a/packages/block-editor/src/components/grid/style.scss b/packages/block-editor/src/components/grid/style.scss new file mode 100644 index 00000000000000..dfb3e57f84ae0b --- /dev/null +++ b/packages/block-editor/src/components/grid/style.scss @@ -0,0 +1,63 @@ +.block-editor-grid-visualizer { + // Specificity to override the z-index and pointer-events set by .components-popover. + &.block-editor-grid-visualizer.block-editor-grid-visualizer { + z-index: z-index(".block-editor-grid-visualizer"); + + .components-popover__content * { + pointer-events: none; + } + + &.is-dropping-allowed { + .block-editor-grid-visualizer__drop-zone { + pointer-events: all; + } + } + } +} + +.block-editor-grid-visualizer__grid { + display: grid; +} + +.block-editor-grid-visualizer__cell { + align-items: center; + display: flex; + justify-content: center; +} + +.block-editor-grid-visualizer__drop-zone { + background: rgba($gray-400, 0.1); + border: $border-width dotted $gray-300; + width: 100%; + height: 100%; + + // Make drop zone 8x8 at minimum so that it's easier to drag into. This will overflow the parent. + min-width: $grid-unit-10; + min-height: $grid-unit-10; + + &.is-highlighted { + background: var(--wp-admin-theme-color); + } +} + +.block-editor-grid-item-resizer { + // Specificity to override the z-index and pointer-events set by .components-popover. + &.block-editor-grid-item-resizer.block-editor-grid-item-resizer { + z-index: z-index(".block-editor-grid-visualizer"); + + .components-popover__content * { + pointer-events: none; + } + } +} + +.block-editor-grid-item-resizer__box { + border: $border-width solid var(--wp-admin-theme-color); + + .components-resizable-box__handle { + // Specificity to override the pointer-events set by .components-popover. + &.components-resizable-box__handle.components-resizable-box__handle { + pointer-events: all; + } + } +} diff --git a/packages/block-editor/src/components/grid/use-get-number-of-blocks-before-cell.js b/packages/block-editor/src/components/grid/use-get-number-of-blocks-before-cell.js new file mode 100644 index 00000000000000..11e991c432080d --- /dev/null +++ b/packages/block-editor/src/components/grid/use-get-number-of-blocks-before-cell.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export function useGetNumberOfBlocksBeforeCell( gridClientId, numColumns ) { + const { getBlockOrder, getBlockAttributes } = useSelect( blockEditorStore ); + + const getNumberOfBlocksBeforeCell = ( column, row ) => { + const targetIndex = ( row - 1 ) * numColumns + column - 1; + + let count = 0; + for ( const clientId of getBlockOrder( gridClientId ) ) { + const { columnStart, rowStart } = + getBlockAttributes( clientId ).style?.layout ?? {}; + const cellIndex = ( rowStart - 1 ) * numColumns + columnStart - 1; + if ( cellIndex < targetIndex ) { + count++; + } + } + return count; + }; + + return getNumberOfBlocksBeforeCell; +} diff --git a/packages/block-editor/src/components/grid/use-grid-layout-sync.js b/packages/block-editor/src/components/grid/use-grid-layout-sync.js new file mode 100644 index 00000000000000..6a3a05e52fcb9a --- /dev/null +++ b/packages/block-editor/src/components/grid/use-grid-layout-sync.js @@ -0,0 +1,167 @@ +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { GridRect } from './utils'; + +export function useGridLayoutSync( { clientId: gridClientId } ) { + const { gridLayout, blockOrder } = useSelect( + ( select ) => { + const { getBlockAttributes, getBlockOrder } = + select( blockEditorStore ); + return { + gridLayout: getBlockAttributes( gridClientId ).layout ?? {}, + blockOrder: getBlockOrder( gridClientId ), + }; + }, + [ gridClientId ] + ); + + const { getBlockAttributes } = useSelect( blockEditorStore ); + const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + + useEffect( () => { + const updates = {}; + + const { columnCount, rowCount = 2 } = gridLayout; + const isManualGrid = !! columnCount; + + if ( isManualGrid ) { + const rects = []; + let cellsTaken = 0; + + // Respect the position of blocks that already have a columnStart and rowStart value. + for ( const clientId of blockOrder ) { + const attributes = getBlockAttributes( clientId ); + const { + columnStart, + rowStart, + columnSpan = 1, + rowSpan = 1, + } = attributes.style?.layout || {}; + cellsTaken += columnSpan * rowSpan; + if ( ! columnStart || ! rowStart ) { + continue; + } + rects.push( + new GridRect( { + columnStart, + rowStart, + columnSpan, + rowSpan, + } ) + ); + } + + // Ensure there's enough rows to fit all blocks. + const minimumNeededRows = Math.ceil( cellsTaken / columnCount ); + if ( rowCount < minimumNeededRows ) { + updates[ gridClientId ] = { + layout: { + ...gridLayout, + rowCount: minimumNeededRows, + }, + }; + } + + // When in manual mode, ensure that every block has a columnStart and rowStart value. + for ( const clientId of blockOrder ) { + const attributes = getBlockAttributes( clientId ); + const { columnStart, rowStart, columnSpan, rowSpan } = + attributes.style?.layout || {}; + if ( columnStart && rowStart ) { + continue; + } + const [ newColumnStart, newRowStart ] = getFirstEmptyCell( + rects, + columnCount, + minimumNeededRows, + columnSpan, + rowSpan + ); + rects.push( + new GridRect( { + columnStart: newColumnStart, + rowStart: newRowStart, + columnSpan, + rowSpan, + } ) + ); + updates[ clientId ] = { + style: { + ...attributes.style, + layout: { + ...attributes.style?.layout, + columnStart: newColumnStart, + rowStart: newRowStart, + }, + }, + }; + } + } else { + // When in auto mode, remove all of the columnStart and rowStart values. + for ( const clientId of blockOrder ) { + const attributes = getBlockAttributes( clientId ); + const { columnStart, rowStart, ...layout } = + attributes.style?.layout || {}; + // Only update attributes if columnStart or rowStart are set. + if ( columnStart || rowStart ) { + updates[ clientId ] = { + style: { + ...attributes.style, + layout, + }, + }; + } + } + } + + if ( Object.keys( updates ).length ) { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( + Object.keys( updates ), + updates, + /* uniqueByBlock: */ true + ); + } + }, [ + // Actual deps to sync: + gridClientId, + gridLayout, + blockOrder, + // Needed for linter: + __unstableMarkNextChangeAsNotPersistent, + getBlockAttributes, + updateBlockAttributes, + ] ); +} + +function getFirstEmptyCell( + rects, + columnCount, + rowCount, + columnSpan = 1, + rowSpan = 1 +) { + for ( let row = 1; row <= rowCount; row++ ) { + for ( let column = 1; column <= columnCount; column++ ) { + const rect = new GridRect( { + columnStart: column, + rowStart: row, + columnSpan, + rowSpan, + } ); + if ( ! rects.some( ( r ) => r.intersectsRect( rect ) ) ) { + return [ column, row ]; + } + } + } + return [ 1, 1 ]; +} diff --git a/packages/block-editor/src/components/grid/utils.js b/packages/block-editor/src/components/grid/utils.js new file mode 100644 index 00000000000000..fc012c645f0916 --- /dev/null +++ b/packages/block-editor/src/components/grid/utils.js @@ -0,0 +1,178 @@ +export function range( start, length ) { + return Array.from( { length }, ( _, i ) => start + i ); +} + +export class GridRect { + constructor( { + columnStart, + rowStart, + columnEnd, + rowEnd, + columnSpan, + rowSpan, + } = {} ) { + this.columnStart = columnStart ?? 1; + this.rowStart = rowStart ?? 1; + if ( columnSpan !== undefined ) { + this.columnEnd = this.columnStart + columnSpan - 1; + } else { + this.columnEnd = columnEnd ?? this.columnStart; + } + if ( rowSpan !== undefined ) { + this.rowEnd = this.rowStart + rowSpan - 1; + } else { + this.rowEnd = rowEnd ?? this.rowStart; + } + } + + get columnSpan() { + return this.columnEnd - this.columnStart + 1; + } + + get rowSpan() { + return this.rowEnd - this.rowStart + 1; + } + + contains( column, row ) { + return ( + column >= this.columnStart && + column <= this.columnEnd && + row >= this.rowStart && + row <= this.rowEnd + ); + } + + containsRect( rect ) { + return ( + this.contains( rect.columnStart, rect.rowStart ) && + this.contains( rect.columnEnd, rect.rowEnd ) + ); + } + + intersectsRect( rect ) { + return ( + this.columnStart <= rect.columnEnd && + this.columnEnd >= rect.columnStart && + this.rowStart <= rect.rowEnd && + this.rowEnd >= rect.rowStart + ); + } +} + +export function getComputedCSS( element, property ) { + return element.ownerDocument.defaultView + .getComputedStyle( element ) + .getPropertyValue( property ); +} + +/** + * Given a grid-template-columns or grid-template-rows CSS property value, gets the start and end + * position in pixels of each grid track. + * + * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track + * + * @param {string} template The grid-template-columns or grid-template-rows CSS property value. + * Only supports fixed sizes in pixels. + * @param {number} gap The gap between grid tracks in pixels. + * + * @return {Array<{start: number, end: number}>} An array of objects with the start and end + * position in pixels of each grid track. + */ +export function getGridTracks( template, gap ) { + const tracks = []; + for ( const size of template.split( ' ' ) ) { + const previousTrack = tracks[ tracks.length - 1 ]; + const start = previousTrack ? previousTrack.end + gap : 0; + const end = start + parseFloat( size ); + tracks.push( { start, end } ); + } + return tracks; +} + +/** + * Given an array of grid tracks and a position in pixels, gets the index of the closest track to + * that position. + * + * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track + * + * @param {Array<{start: number, end: number}>} tracks An array of objects with the start and end + * position in pixels of each grid track. + * @param {number} position The position in pixels. + * @param {string} edge The edge of the track to compare the + * position to. Either 'start' or 'end'. + * + * @return {number} The index of the closest track to the position. 0-based, unlike CSS grid which + * is 1-based. + */ +export function getClosestTrack( tracks, position, edge = 'start' ) { + return tracks.reduce( + ( closest, track, index ) => + Math.abs( track[ edge ] - position ) < + Math.abs( tracks[ closest ][ edge ] - position ) + ? index + : closest, + 0 + ); +} + +export function getGridRect( gridElement, rect ) { + const columnGap = parseFloat( getComputedCSS( gridElement, 'column-gap' ) ); + const rowGap = parseFloat( getComputedCSS( gridElement, 'row-gap' ) ); + const gridColumnTracks = getGridTracks( + getComputedCSS( gridElement, 'grid-template-columns' ), + columnGap + ); + const gridRowTracks = getGridTracks( + getComputedCSS( gridElement, 'grid-template-rows' ), + rowGap + ); + const columnStart = getClosestTrack( gridColumnTracks, rect.left ) + 1; + const rowStart = getClosestTrack( gridRowTracks, rect.top ) + 1; + const columnEnd = + getClosestTrack( gridColumnTracks, rect.right, 'end' ) + 1; + const rowEnd = getClosestTrack( gridRowTracks, rect.bottom, 'end' ) + 1; + return new GridRect( { + columnStart, + columnEnd, + rowStart, + rowEnd, + } ); +} + +export function getGridItemRect( gridItemElement ) { + return getGridRect( + gridItemElement.parentElement, + new window.DOMRect( + gridItemElement.offsetLeft, + gridItemElement.offsetTop, + gridItemElement.offsetWidth, + gridItemElement.offsetHeight + ) + ); +} + +export function getGridInfo( gridElement ) { + const gridTemplateColumns = getComputedCSS( + gridElement, + 'grid-template-columns' + ); + const gridTemplateRows = getComputedCSS( + gridElement, + 'grid-template-rows' + ); + const numColumns = gridTemplateColumns.split( ' ' ).length; + const numRows = gridTemplateRows.split( ' ' ).length; + const numItems = numColumns * numRows; + return { + numColumns, + numRows, + numItems, + currentColor: getComputedCSS( gridElement, 'color' ), + style: { + gridTemplateColumns, + gridTemplateRows, + gap: getComputedCSS( gridElement, 'gap' ), + padding: getComputedCSS( gridElement, 'padding' ), + }, + }; +} diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 49ab36499692b5..d46f28d1e19f2c 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -267,7 +267,9 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const ref = useMergeRefs( [ props.ref, - __unstableDisableDropZone || isDropZoneDisabled + __unstableDisableDropZone || + isDropZoneDisabled || + ( layout?.columnCount && window.__experimentalEnableGridInteractivity ) ? null : blockDropZoneRef, ] ); diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js index 860b4aaf041794..f5104213729366 100644 --- a/packages/block-editor/src/hooks/layout-child.js +++ b/packages/block-editor/src/hooks/layout-child.js @@ -11,7 +11,11 @@ import { useState } from '@wordpress/element'; import { store as blockEditorStore } from '../store'; import { useStyleOverride } from './utils'; import { useLayout } from '../components/block-list/layout'; -import { GridVisualizer, GridItemResizer } from '../components/grid-visualizer'; +import { + GridVisualizer, + GridItemResizer, + GridItemMovers, +} from '../components/grid'; function useBlockPropsChildLayoutStyles( { style } ) { const shouldRenderChildLayoutStyles = useSelect( ( select ) => { @@ -135,10 +139,12 @@ function useBlockPropsChildLayoutStyles( { style } ) { } function ChildLayoutControlsPure( { clientId, style, setAttributes } ) { + const parentLayout = useLayout() || {}; const { type: parentLayoutType = 'default', allowSizingOnChildren = false, - } = useLayout() || {}; + columnCount, + } = parentLayout; const rootClientId = useSelect( ( select ) => { @@ -154,29 +160,43 @@ function ChildLayoutControlsPure( { clientId, style, setAttributes } ) { return null; } + const isManualGrid = !! columnCount; + + function updateLayout( layout ) { + setAttributes( { + style: { + ...style, + layout: { + ...style?.layout, + ...layout, + }, + }, + } ); + } + return ( <> { allowSizingOnChildren && ( { - setAttributes( { - style: { - ...style, - layout: { - ...style?.layout, - columnSpan, - rowSpan, - }, - }, - } ); - } } + onChange={ updateLayout } + parentLayout={ parentLayout } + /> + ) } + { isManualGrid && window.__experimentalEnableGridInteractivity && ( + ) } diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js index 4528de117c45b0..744441b22608b1 100644 --- a/packages/block-editor/src/layouts/grid.js +++ b/packages/block-editor/src/layouts/grid.js @@ -23,7 +23,7 @@ import { appendSelectors, getBlockGapCSS } from './utils'; import { getGapCSSValue } from '../hooks/gap'; import { shouldSkipSerialization } from '../hooks/utils'; import { LAYOUT_DEFINITIONS } from './definitions'; -import { GridVisualizer } from '../components/grid-visualizer'; +import { GridVisualizer, useGridLayoutSync } from '../components/grid'; const RANGE_CONTROL_MAX_VALUES = { px: 600, @@ -93,7 +93,14 @@ export default { ); }, toolBarControls: function GridLayoutToolbarControls( { clientId } ) { - return ; + return ( + <> + { window.__experimentalEnableGridInteractivity && ( + + ) } + + + ); }, getLayoutStyle: function getLayoutStyle( { selector, @@ -245,9 +252,6 @@ function GridLayoutColumnsAndRowsControl( { return ( <>
- - { __( 'Columns' ) } - + - - onChange( { - ...layout, - columnCount: value, - } ) - } - min={ 1 } - max={ 16 } - withInputField={ false } - label={ __( 'Columns' ) } - hideLabelFromVision - /> + { window.__experimentalEnableGridInteractivity && + allowSizingOnChildren ? ( + { + onChange( { + ...layout, + rowCount: value, + } ); + } } + value={ rowCount } + min={ 1 } + label={ __( 'Rows' ) } + /> + ) : ( + + onChange( { + ...layout, + columnCount: value, + } ) + } + min={ 1 } + max={ 16 } + withInputField={ false } + label={ __( 'Columns' ) } + hideLabelFromVision + /> + ) }
- { allowSizingOnChildren && - window.__experimentalEnableGridInteractivity && ( -
- - { __( 'Rows' ) } - - - - { - onChange( { - ...layout, - rowCount: value, - } ); - } } - value={ rowCount } - min={ 1 } - label={ __( 'Rows' ) } - hideLabelFromVision - /> - - - - onChange( { - ...layout, - rowCount: value, - } ) - } - min={ 1 } - max={ 16 } - withInputField={ false } - label={ __( 'Rows' ) } - hideLabelFromVision - /> - - -
- ) } ); } @@ -366,10 +345,19 @@ function GridLayoutTypeControl( { layout, onChange } ) { return ( ); } + +function GridLayoutSync( props ) { + useGridLayoutSync( props ); +} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index cf4683b02c707d..0772ddf0d31e57 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -27,7 +27,7 @@ @import "./components/duotone-control/style.scss"; @import "./components/font-appearance-control/style.scss"; @import "./components/global-styles/style.scss"; -@import "./components/grid-visualizer/style.scss"; +@import "./components/grid/style.scss"; @import "./components/height-control/style.scss"; @import "./components/image-size-control/style.scss"; @import "./components/inserter-list-item/style.scss"; diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index 740f3e50f84eee..d0f90b93467c9d 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -93,6 +93,7 @@ "__experimentalTextTransform": true, "__experimentalTextDecoration": true, "__experimentalLetterSpacing": true, + "__experimentalWritingMode": true, "__experimentalDefaultControls": { "fontSize": true } diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 6095ef2fee24d1..c96eb4e45117d9 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -426,6 +426,9 @@ export default function Image( { ); + const arePatternOverridesEnabled = + metadata?.bindings?.__default?.source === 'core/pattern-overrides'; + const { lockUrlControls = false, lockHrefControls = false, @@ -470,7 +473,7 @@ export default function Image( { lockHrefControls: // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. - hasParentPattern, + hasParentPattern || arePatternOverridesEnabled, lockCaption: // Disable editing the caption if the image is inside a pattern instance. // This is a temporary solution until we support overriding the caption on the frontend. @@ -971,7 +974,11 @@ export default function Image( { isSelected={ isSingleSelected } insertBlocksAfter={ insertBlocksAfter } label={ __( 'Image caption text' ) } - showToolbarButton={ isSingleSelected && hasNonContentControls } + showToolbarButton={ + isSingleSelected && + hasNonContentControls && + ! arePatternOverridesEnabled + } readOnly={ lockCaption } /> diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 44c4d1410f4bee..99feef98542ed9 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -828,7 +828,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut $tags->set_attribute( 'data-wp-interactive', 'core/navigation' ); $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu" }' ); $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); - $tags->set_attribute( 'data-wp-on-async--focusout', 'actions.handleMenuFocusout' ); + $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); // This is a fix for Safari. Without it, Safari doesn't change the active diff --git a/packages/block-library/src/post-date/index.php b/packages/block-library/src/post-date/index.php index 8e778f4e853997..e185a995f6fc21 100644 --- a/packages/block-library/src/post-date/index.php +++ b/packages/block-library/src/post-date/index.php @@ -44,7 +44,7 @@ function render_block_core_post_date( $attributes, $content, $block ) { */ if ( isset( $attributes['displayType'] ) && 'modified' === $attributes['displayType'] ) { if ( get_the_modified_date( 'Ymdhi', $post_ID ) > get_the_date( 'Ymdhi', $post_ID ) ) { - if ( 'human-diff' === $attributes['format'] ) { + if ( isset( $attributes['format'] ) && 'human-diff' === $attributes['format'] ) { // translators: %s: human-readable time difference. $formatted_date = sprintf( __( '%s ago', 'gutenberg' ), human_time_diff( get_post_timestamp( $post_ID, 'modified' ) ) ); } else { diff --git a/packages/block-library/src/site-tagline/block.json b/packages/block-library/src/site-tagline/block.json index 01ec727eb90531..2b1c0b1cb64c77 100644 --- a/packages/block-library/src/site-tagline/block.json +++ b/packages/block-library/src/site-tagline/block.json @@ -44,6 +44,7 @@ "__experimentalFontStyle": true, "__experimentalFontWeight": true, "__experimentalLetterSpacing": true, + "__experimentalWritingMode": true, "__experimentalDefaultControls": { "fontSize": true } diff --git a/packages/block-library/src/site-title/block.json b/packages/block-library/src/site-title/block.json index 6179452cd15b7a..518b458184e941 100644 --- a/packages/block-library/src/site-title/block.json +++ b/packages/block-library/src/site-title/block.json @@ -55,6 +55,7 @@ "__experimentalFontStyle": true, "__experimentalFontWeight": true, "__experimentalLetterSpacing": true, + "__experimentalWritingMode": true, "__experimentalDefaultControls": { "fontSize": true } diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index b9cae2d48ed17c..be867c4ced1660 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -161,7 +161,7 @@ function render_block_core_template_part( $attributes ) { global $wp_embed; $content = $wp_embed->autoembed( $content ); - if ( empty( $attributes['tagName'] ) ) { + if ( empty( $attributes['tagName'] ) || tag_escape( $attributes['tagName'] ) !== $attributes['tagName'] ) { $area_tag = 'div'; if ( $area_definition && isset( $area_definition['area_tag'] ) ) { $area_tag = $area_definition['area_tag']; diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index 8e52c8bcfa30bb..387ff3dfe17123 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -51,6 +51,7 @@ "__experimentalLetterSpacing": true, "__experimentalTextTransform": true, "__experimentalTextDecoration": true, + "__experimentalWritingMode": true, "__experimentalDefaultControls": { "fontSize": true } diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index 1d88d6d7a09c45..01ad3cf768276b 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -4,10 +4,18 @@ ## 4.44.0 (2024-06-15) +### Bug fix + +- Pin the `@wordpress/scripts` version to a version supported by WordPress 6.5 ([#62234](https://github.com/WordPress/gutenberg/pull/62234)). + ## 4.43.0 (2024-05-31) ## 4.42.0 (2024-05-16) +### Breaking Change + +- Increase the minimum required Node.js version to v20.10.0 matching the support defined for Gutenberg and WordPress core ([#61430](https://github.com/WordPress/gutenberg/pull/61430)). + ## 4.41.0 (2024-05-02) ## 4.40.0 (2024-04-19) diff --git a/packages/dataviews/src/bulk-actions.tsx b/packages/dataviews/src/bulk-actions.tsx index 0382e9d836bc95..fd4d5c390948d4 100644 --- a/packages/dataviews/src/bulk-actions.tsx +++ b/packages/dataviews/src/bulk-actions.tsx @@ -7,7 +7,7 @@ import { Modal, } from '@wordpress/components'; import { __, sprintf, _n } from '@wordpress/i18n'; -import { useMemo, useState, useCallback, useEffect } from '@wordpress/element'; +import { useMemo, useState, useCallback } from '@wordpress/element'; import { useRegistry } from '@wordpress/data'; /** @@ -196,37 +196,16 @@ export default function BulkActions< Item extends AnyItem >( { }, [ data, bulkActions ] ); const numberSelectableItems = selectableItems.length; - const areAllSelected = - selection && selection.length === numberSelectableItems; const selectedItems = useMemo( () => { - return data.filter( ( item ) => - selection.includes( getItemId( item ) ) + return data.filter( + ( item ) => + selection.includes( getItemId( item ) ) && + selectableItems.includes( item ) ); - }, [ selection, data, getItemId ] ); + }, [ selection, data, getItemId, selectableItems ] ); - const hasNonSelectableItemSelected = useMemo( () => { - return selectedItems.some( ( item ) => { - return ! selectableItems.includes( item ); - } ); - }, [ selectedItems, selectableItems ] ); - useEffect( () => { - if ( hasNonSelectableItemSelected ) { - onSelectionChange( - selectedItems.filter( ( selectedItem ) => { - return selectableItems.some( ( item ) => { - return getItemId( selectedItem ) === getItemId( item ); - } ); - } ) - ); - } - }, [ - hasNonSelectableItemSelected, - selectedItems, - selectableItems, - getItemId, - onSelectionChange, - ] ); + const areAllSelected = selectedItems.length === numberSelectableItems; if ( bulkActions.length === 0 ) { return null; @@ -245,15 +224,15 @@ export default function BulkActions< Item extends AnyItem >( { variant="tertiary" size="compact" > - { selection.length + { selectedItems.length ? sprintf( /* translators: %d: Number of items. */ _n( 'Edit %d item', 'Edit %d items', - selection.length + selectedItems.length ), - selection.length + selectedItems.length ) : __( 'Bulk edit' ) } diff --git a/packages/dataviews/src/dataviews.tsx b/packages/dataviews/src/dataviews.tsx index 601ee23304633c..de4deb36659f40 100644 --- a/packages/dataviews/src/dataviews.tsx +++ b/packages/dataviews/src/dataviews.tsx @@ -7,7 +7,7 @@ import type { ComponentType } from 'react'; * WordPress dependencies */ import { __experimentalHStack as HStack } from '@wordpress/components'; -import { useMemo, useState, useCallback, useEffect } from '@wordpress/element'; +import { useMemo, useState, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -92,25 +92,6 @@ export default function DataViews< Item extends AnyItem >( { } const [ openedFilter, setOpenedFilter ] = useState< string | null >( null ); - useEffect( () => { - if ( - selection.length > 0 && - selection.some( - ( id ) => ! data.some( ( item ) => getItemId( item ) === id ) - ) - ) { - const newSelection = selection.filter( ( id ) => - data.some( ( item ) => getItemId( item ) === id ) - ); - setSelection( newSelection ); - onSelectionChange( - data.filter( ( item ) => - newSelection.includes( getItemId( item ) ) - ) - ); - } - }, [ selection, data, getItemId, onSelectionChange ] ); - const onSetSelection = useCallback( ( items: Item[] ) => { setSelection( items.map( ( item ) => getItemId( item ) ) ); @@ -127,6 +108,11 @@ export default function DataViews< Item extends AnyItem >( { actions, data ); + const _selection = useMemo( () => { + return selection.filter( ( id ) => + data.some( ( item ) => getItemId( item ) === id ) + ); + }, [ selection, data, getItemId ] ); return (
( { actions={ actions } data={ data } onSelectionChange={ onSetSelection } - selection={ selection } + selection={ _selection } getItemId={ getItemId } /> ) } @@ -179,7 +165,7 @@ export default function DataViews< Item extends AnyItem >( { isLoading={ isLoading } onChangeView={ onChangeView } onSelectionChange={ onSetSelection } - selection={ selection } + selection={ _selection } setOpenedFilter={ setOpenedFilter } view={ view } /> @@ -193,7 +179,7 @@ export default function DataViews< Item extends AnyItem >( { diff --git a/packages/dataviews/src/single-selection-checkbox.tsx b/packages/dataviews/src/single-selection-checkbox.tsx index 1c87c7f7c52c17..7c61b8e4aaa83c 100644 --- a/packages/dataviews/src/single-selection-checkbox.tsx +++ b/packages/dataviews/src/single-selection-checkbox.tsx @@ -29,7 +29,7 @@ export default function SingleSelectionCheckbox< Item extends AnyItem >( { disabled, }: SingleSelectionCheckboxProps< Item > ) { const id = getItemId( item ); - const isSelected = selection.includes( id ); + const isSelected = ! disabled && selection.includes( id ); let selectionLabel; if ( primaryField?.getValue && item ) { // eslint-disable-next-line @wordpress/valid-sprintf diff --git a/packages/dataviews/src/view-list.tsx b/packages/dataviews/src/view-list.tsx index 51df552564d748..295c3d28856ebe 100644 --- a/packages/dataviews/src/view-list.tsx +++ b/packages/dataviews/src/view-list.tsx @@ -184,7 +184,7 @@ function ListItem< Item extends AnyItem >( {
- { actions?.length > 0 && ( + { eligibleActions?.length > 0 && ( { onSelectionChange: ( items: Item[] ) => void; data: Item[]; actions: Action< Item >[]; + getItemId: ( item: Item ) => string; } interface TableRowProps< Item extends AnyItem > { @@ -249,6 +250,7 @@ function BulkSelectionCheckbox< Item extends AnyItem >( { onSelectionChange, data, actions, + getItemId, }: BulkSelectionCheckboxProps< Item > ) { const selectableItems = useMemo( () => { return data.filter( ( item ) => { @@ -259,13 +261,18 @@ function BulkSelectionCheckbox< Item extends AnyItem >( { ); } ); }, [ data, actions ] ); - const areAllSelected = selection.length === selectableItems.length; + const selectedItems = data.filter( + ( item ) => + selection.includes( getItemId( item ) ) && + selectableItems.includes( item ) + ); + const areAllSelected = selectedItems.length === selectableItems.length; return ( { if ( areAllSelected ) { onSelectionChange( [] ); @@ -293,7 +300,7 @@ function TableRow< Item extends AnyItem >( { data, }: TableRowProps< Item > ) { const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); - const isSelected = selection.includes( id ); + const isSelected = hasPossibleBulkAction && selection.includes( id ); const [ isHovered, setIsHovered ] = useState( false ); @@ -323,6 +330,9 @@ function TableRow< Item extends AnyItem >( { isTouchDevice.current = true; } } onClick={ () => { + if ( ! hasPossibleBulkAction ) { + return; + } if ( ! isTouchDevice.current && document.getSelection()?.type !== 'Range' @@ -495,6 +505,7 @@ function ViewTable< Item extends AnyItem >( { onSelectionChange={ onSelectionChange } data={ data } actions={ actions } + getItemId={ getItemId } /> ) } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index c1eb6ddbdc00b3..f9888be6b27cad 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -39,6 +39,7 @@ import AddNewPageModal from '../add-new-page'; import Media from '../media'; import { unlock } from '../../lock-unlock'; import { useEditPostAction } from '../dataviews-actions'; +import { usePrevious } from '@wordpress/compose'; const { usePostActions } = unlock( editorPrivateApis ); const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -201,43 +202,18 @@ function FeaturedImage( { item, viewType } ) { ); } -function usePostIdLinkInSelection( - selection, - setSelection, - isLoadingItems, - items -) { - const { - params: { postId }, - } = useLocation(); - const [ postIdToSelect, setPostIdToSelect ] = useState( postId ); - useEffect( () => { - if ( postId ) { - setPostIdToSelect( postId ); - } - }, [ postId ] ); - - useEffect( () => { - if ( ! postIdToSelect ) { - return; - } - // Only try to select an item if the loading is complete and we have items. - if ( ! isLoadingItems && items && items.length ) { - // If the item is not in the current selection, select it. - if ( selection.length !== 1 || selection[ 0 ] !== postIdToSelect ) { - setSelection( [ postIdToSelect ] ); - } - setPostIdToSelect( undefined ); - } - }, [ postIdToSelect, selection, setSelection, isLoadingItems, items ] ); +function getItemId( item ) { + return item.id.toString(); } export default function PagePages() { const postType = 'page'; const [ view, setView ] = useView( postType ); const history = useHistory(); - - const [ selection, setSelection ] = useState( [] ); + const { + params: { postId }, + } = useLocation(); + const [ selection, setSelection ] = useState( [ postId ] ); const onSelectionChange = useCallback( ( items ) => { @@ -299,7 +275,19 @@ export default function PagePages() { totalPages, } = useEntityRecords( 'postType', postType, queryArgs ); - usePostIdLinkInSelection( selection, setSelection, isLoadingPages, pages ); + const ids = pages?.map( ( page ) => getItemId( page ) ) ?? []; + const prevIds = usePrevious( ids ) ?? []; + const deletedIds = prevIds.filter( ( id ) => ! ids.includes( id ) ); + const postIdWasDeleted = deletedIds.includes( postId ); + + useEffect( () => { + if ( postIdWasDeleted ) { + history.push( { + ...history.getLocationWithParams().params, + postId: undefined, + } ); + } + }, [ postIdWasDeleted, history ] ); const { records: authors, isResolving: isLoadingAuthors } = useEntityRecords( 'root', 'user', { per_page: -1 } ); @@ -569,7 +557,7 @@ export default function PagePages() { selection={ selection } setSelection={ setSelection } onSelectionChange={ onSelectionChange } - getItemId={ ( item ) => item.id.toString() } + getItemId={ getItemId } /> ); diff --git a/packages/editor/src/components/global-styles-provider/index.js b/packages/editor/src/components/global-styles-provider/index.js index 4250910cc6320a..b7869e3413c3de 100644 --- a/packages/editor/src/components/global-styles-provider/index.js +++ b/packages/editor/src/components/global-styles-provider/index.js @@ -7,17 +7,15 @@ import { isPlainObject } from 'is-plain-object'; /** * WordPress dependencies */ -import { registerBlockStyle, store as blocksStore } from '@wordpress/blocks'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useMemo, useCallback } from '@wordpress/element'; +import { useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; -import setNestedValue from '../../utils/set-nested-value'; const { GlobalStylesContext, cleanEmptyObject } = unlock( blockEditorPrivateApis @@ -32,85 +30,6 @@ export function mergeBaseAndUserConfigs( base, user ) { } ); } -/** - * Resolves shared block style variation definitions from the user origin - * under their respective block types and registers the block style if required. - * - * @param {Object} userConfig Current user origin global styles data. - * @return {Object} Updated global styles data. - */ -function useResolvedBlockStyleVariationsConfig( userConfig ) { - const { getBlockStyles } = useSelect( blocksStore ); - const sharedVariations = userConfig?.styles?.blocks?.variations; - - // Collect block style variation definitions to merge and unregistered - // block styles for automatic registration. - const [ userConfigToMerge, unregisteredStyles ] = useMemo( () => { - if ( ! sharedVariations ) { - return []; - } - - const variationsConfigToMerge = {}; - const unregisteredBlockStyles = []; - - Object.entries( sharedVariations ).forEach( - ( [ variationName, variation ] ) => { - if ( ! variation?.blockTypes?.length ) { - return; - } - - variation.blockTypes.forEach( ( blockName ) => { - const blockStyles = getBlockStyles( blockName ); - const registeredBlockStyle = blockStyles.find( - ( { name } ) => name === variationName - ); - - if ( ! registeredBlockStyle ) { - unregisteredBlockStyles.push( [ - blockName, - { - name: variationName, - label: variationName, - }, - ] ); - } - - const path = [ - 'styles', - 'blocks', - blockName, - 'variations', - variationName, - ]; - setNestedValue( variationsConfigToMerge, path, variation ); - } ); - } - ); - - return [ variationsConfigToMerge, unregisteredBlockStyles ]; - }, [ sharedVariations, getBlockStyles ] ); - - // Automatically register missing block styles from variations. - useEffect( - () => - unregisteredStyles?.forEach( ( unregisteredStyle ) => - registerBlockStyle( ...unregisteredStyle ) - ), - [ unregisteredStyles ] - ); - - // Merge shared block style variation definitions into overall user config. - const updatedConfig = useMemo( () => { - if ( ! userConfigToMerge ) { - return userConfig; - } - - return deepmerge( userConfigToMerge, userConfig ); - }, [ userConfigToMerge, userConfig ] ); - - return updatedConfig; -} - function useGlobalStylesUserConfig() { const { globalStylesId, isReady, settings, styles, _links } = useSelect( ( select ) => { @@ -199,7 +118,7 @@ function useGlobalStylesUserConfig() { options ); }, - [ globalStylesId ] + [ globalStylesId, editEntityRecord, getEditedEntityRecord ] ); return [ isReady, config, setConfig ]; @@ -219,28 +138,26 @@ export function useGlobalStylesContext() { const [ isUserConfigReady, userConfig, setUserConfig ] = useGlobalStylesUserConfig(); const [ isBaseConfigReady, baseConfig ] = useGlobalStylesBaseConfig(); - const userConfigWithVariations = - useResolvedBlockStyleVariationsConfig( userConfig ); const mergedConfig = useMemo( () => { - if ( ! baseConfig || ! userConfigWithVariations ) { + if ( ! baseConfig || ! userConfig ) { return {}; } - return mergeBaseAndUserConfigs( baseConfig, userConfigWithVariations ); - }, [ userConfigWithVariations, baseConfig ] ); + return mergeBaseAndUserConfigs( baseConfig, userConfig ); + }, [ userConfig, baseConfig ] ); const context = useMemo( () => { return { isReady: isUserConfigReady && isBaseConfigReady, - user: userConfigWithVariations, + user: userConfig, base: baseConfig, merged: mergedConfig, setUserConfig, }; }, [ mergedConfig, - userConfigWithVariations, + userConfig, baseConfig, setUserConfig, isUserConfigReady, diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 0034e2b2664c6a..edf67bb1da9244 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -307,34 +307,29 @@ const trashPostAction = { }, }; -function useTrashPostAction( postType ) { +function useCanUserEligibilityCheckPostType( capability, resource, action ) { const registry = useRegistry(); - const { resource, cachedCanUserResolvers } = useSelect( - ( select ) => { - const { getPostType, getCachedResolvers } = select( coreStore ); - return { - resource: getPostType( postType )?.rest_base || '', - cachedCanUserResolvers: getCachedResolvers().canUser, - }; - }, - [ postType ] - ); return useMemo( () => ( { - ...trashPostAction, + ...action, isEligible( item ) { return ( - trashPostAction.isEligible( item ) && + action.isEligible( item ) && registry .select( coreStore ) - .canUser( 'delete', resource, item.id ) + .canUser( capability, resource, item.id ) ); }, } ), - // We are making this use memo depend on cachedCanUserResolvers as a way to make the component using this hook re-render - // when user capabilities are resolved. This makes sure the isEligible function is re-evaluated. - // eslint-disable-next-line react-hooks/exhaustive-deps - [ registry, resource, cachedCanUserResolvers ] + [ action, registry, capability, resource ] + ); +} + +function useTrashPostAction( resource ) { + return useCanUserEligibilityCheckPostType( + 'delete', + resource, + trashPostAction ); } @@ -428,6 +423,14 @@ const permanentlyDeletePostAction = { }, }; +function usePermanentlyDeletePostAction( resource ) { + return useCanUserEligibilityCheckPostType( + 'delete', + resource, + permanentlyDeletePostAction + ); +} + const restorePostAction = { id: 'restore', label: __( 'Restore' ), @@ -535,6 +538,14 @@ const restorePostAction = { }, }; +function useRestorePostAction( resource ) { + return useCanUserEligibilityCheckPostType( + 'update', + resource, + restorePostAction + ); +} + const viewPostAction = { id: 'view-post', label: __( 'View' ), @@ -694,6 +705,14 @@ const renamePostAction = { }, }; +function useRenamePostAction( resource ) { + return useCanUserEligibilityCheckPostType( + 'update', + resource, + renamePostAction + ); +} + const useDuplicatePostAction = ( postType ) => { const { userCanCreatePost } = useSelect( ( select ) => { @@ -1038,20 +1057,36 @@ export const duplicateTemplatePartAction = { }; export function usePostActions( { postType, onActionPerformed, context } ) { - const { defaultActions, postTypeObject } = useSelect( + const { + defaultActions, + postTypeObject, + userCanCreatePostType, + resource, + cachedCanUserResolvers, + } = useSelect( ( select ) => { - const { getPostType } = select( coreStore ); + const { getPostType, canUser, getCachedResolvers } = + select( coreStore ); const { getEntityActions } = unlock( select( editorStore ) ); + const _postTypeObject = getPostType( postType ); + const _resource = _postTypeObject?.rest_base || ''; return { - postTypeObject: getPostType( postType ), + postTypeObject: _postTypeObject, defaultActions: getEntityActions( 'postType', postType ), + userCanCreatePostType: canUser( 'create', _resource ), + resource: _resource, + cachedCanUserResolvers: getCachedResolvers()?.canUser, }; }, [ postType ] ); const duplicatePostAction = useDuplicatePostAction( postType ); - const trashPostActionForPostType = useTrashPostAction( postType ); + const trashPostActionForPostType = useTrashPostAction( resource ); + const permanentlyDeletePostActionForPostType = + usePermanentlyDeletePostAction( resource ); + const renamePostActionForPostType = useRenamePostAction( resource ); + const restorePostActionForPostType = useRestorePostAction( resource ); const isTemplateOrTemplatePart = [ TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE, @@ -1073,15 +1108,20 @@ export function usePostActions( { postType, onActionPerformed, context } ) { ! isPattern && duplicatePostAction : false, - isTemplateOrTemplatePart && duplicateTemplatePartAction, - isPattern && duplicatePatternAction, - supportsTitle && renamePostAction, + isTemplateOrTemplatePart && + userCanCreatePostType && + duplicateTemplatePartAction, + isPattern && userCanCreatePostType && duplicatePatternAction, + supportsTitle && renamePostActionForPostType, isPattern && exportPatternAsJSONAction, - isTemplateOrTemplatePart ? resetTemplateAction : restorePostAction, + isTemplateOrTemplatePart + ? resetTemplateAction + : restorePostActionForPostType, isTemplateOrTemplatePart || isPattern ? deletePostAction : trashPostActionForPostType, - ! isTemplateOrTemplatePart && permanentlyDeletePostAction, + ! isTemplateOrTemplatePart && + permanentlyDeletePostActionForPostType, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -1139,17 +1179,25 @@ export function usePostActions( { postType, onActionPerformed, context } ) { } return actions; + // We are making this use memo depend on cachedCanUserResolvers as a way to make the component using this hook re-render + // when user capabilities are resolved. This makes sure the isEligible functions of actions dependent on capabilities are re-evaluated. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ defaultActions, + userCanCreatePostType, isTemplateOrTemplatePart, isPattern, postTypeObject?.viewable, duplicatePostAction, trashPostActionForPostType, + restorePostActionForPostType, + renamePostActionForPostType, + permanentlyDeletePostActionForPostType, onActionPerformed, isLoaded, supportsRevisions, supportsTitle, context, + cachedCanUserResolvers, ] ); } diff --git a/packages/editor/src/hooks/pattern-overrides.js b/packages/editor/src/hooks/pattern-overrides.js index 485cc3725d8d71..36a67bb9c5d244 100644 --- a/packages/editor/src/hooks/pattern-overrides.js +++ b/packages/editor/src/hooks/pattern-overrides.js @@ -34,9 +34,8 @@ const { */ const withPatternOverrideControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { - const isSupportedBlock = Object.keys( - PARTIAL_SYNCING_SUPPORTED_BLOCKS - ).includes( props.name ); + const isSupportedBlock = + !! PARTIAL_SYNCING_SUPPORTED_BLOCKS[ props.name ]; return ( <> diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 173ef1b1c6a982..3544e9dde459be 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -1,7 +1,9 @@ ## Unreleased +### Enhancements +- Export `splitTask` function from `@wordpress/interactivity` package to facilitate yielding to the main thread. See example in [async actions](https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/interactivity-api/api-reference.md#async-actions) documentation. ([#62665](https://github.com/WordPress/gutenberg/pull/62665)) ## 6.1.0 (2024-06-15) ## 6.0.0 (2024-05-31) diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js index 9869c5b072c856..e2c74e76965839 100644 --- a/packages/patterns/src/components/pattern-overrides-controls.js +++ b/packages/patterns/src/components/pattern-overrides-controls.js @@ -31,7 +31,11 @@ function addBindings( bindings ) { }; } -function PatternOverridesControls( { attributes, setAttributes } ) { +function PatternOverridesControls( { + attributes, + setAttributes, + name: blockName, +} ) { const controlId = useId(); const [ showAllowOverridesModal, setShowAllowOverridesModal ] = useState( false ); @@ -71,15 +75,25 @@ function PatternOverridesControls( { attributes, setAttributes } ) { return null; } + const hasUnsupportedImageAttributes = + blockName === 'core/image' && + ( !! attributes.caption?.length || !! attributes.href?.length ); + + const helpText = hasUnsupportedImageAttributes + ? __( + `Overrides currently don't support image captions or links. Remove the caption or link first before enabling overrides.` + ) + : __( + 'Allow changes to this block throughout instances of this pattern.' + ); + return ( <>